├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── (auth)
│ ├── layout.tsx
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.tsx
├── (demo)
│ ├── create-organization
│ │ └── page.tsx
│ ├── discover
│ │ └── page.tsx
│ ├── organization
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── page.tsx
│ └── route-handlers
│ │ └── page.tsx
├── api
│ ├── admin-editor
│ │ └── route.ts
│ ├── authTest
│ │ └── route.ts
│ └── editor
│ │ └── route.ts
├── authorization-playground
│ ├── layout.tsx
│ ├── page.tsx
│ ├── post-actions.tsx
│ ├── post-button.tsx
│ └── settings
│ │ └── page.tsx
└── layout.tsx
├── clerk.d.ts
├── components
├── CreateOrganization.tsx
├── CustomOrganizationProfile.tsx
├── OrganizationList.tsx
├── RequireActiveOrganization.tsx
├── SelectRole.tsx
└── endpoint-tests.tsx
├── docs
├── clerk-logo-dark.png
└── clerk-logo-light.png
├── middleware.ts
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
├── dark-logo.png
├── favicon.ico
├── hero.png
├── light-logo.png
└── vercel.svg
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
└── utils
├── cn.ts
└── organizations.ts
/.env.example:
--------------------------------------------------------------------------------
1 | # -----------------------------------------------------------------------------
2 | # App
3 | # -----------------------------------------------------------------------------
4 | NEXT_PUBLIC_APP_URL=http://localhost:3000
5 |
6 | # -----------------------------------------------------------------------------
7 | # Authentication (Clerk)
8 | # -----------------------------------------------------------------------------
9 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_******
10 | CLERK_SECRET_KEY=sk_test_*****
11 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
12 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
13 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
14 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss", "unused-imports"],
10 | "rules": {
11 | "tailwindcss/classnames-order": "error",
12 | "unused-imports/no-unused-imports": "error"
13 | },
14 | "settings": {
15 | "tailwindcss": {
16 | "callees": ["cn"],
17 | "config": "tailwind.config.js"
18 | },
19 | "next": {
20 | "rootDir": true
21 | }
22 | },
23 | "overrides": [
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "parser": "@typescript-eslint/parser"
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | .yalc
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | .pnpm-debug.log*
29 |
30 | # local env files
31 | .env*.local
32 | .env
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 |
41 | .vscode
42 | .contentlayer
43 | .idea
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
30 |
31 | ---
32 |
33 | ## Introduction
34 |
35 | Clerk is a developer-first authentication and user management solution. It provides pre-built React components and hooks for sign-in, sign-up, user profile, and organization management. Clerk is designed to be easy to use and customize, and can be dropped into any React or Next.js application.
36 |
37 | Clerk organizations are a way of grouping your application's users. Organizations are shared accounts, useful for project and team leaders — think of GitHub Teams, or the different departments of a company.
38 |
39 | This starter project shows how to use [Clerk](https://www.clerk.com/?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter) with Next.js to build a rather lean interface, showcasing the power of the [organizations feature](https://clerk.com/docs/organizations/overview?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter).
40 |
41 | ## Live Demo
42 |
43 | [https://organizations-demo.clerk.app](https://organizations-demo.clerk.app)
44 |
45 | ## Running the demo
46 |
47 | ### Prerequisites
48 |
49 | - React v18
50 |
51 | - Node.js v18+
52 |
53 | ### Setup
54 |
55 | ```bash
56 | gh repo clone clerk/organizations-demo
57 | ```
58 |
59 | To run the example locally, you need to:
60 |
61 | 1. Sign up at [Clerk.com](https://www.clerk.com/?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter).
62 |
63 | 2. Go to your [Clerk dashboard](https://dashboard.clerk.com/?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter) and enable the Organizations feature.
64 |
65 | 3. Set up your Publishable key and other variables, as shown in our [Getting started tutorial](https://clerk.com/docs/quickstarts/get-started-with-nextjs#install-clerk-s-sdk?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter).
66 |
67 | 4. `npm install` to install the required dependencies.
68 |
69 | 5. `npm run dev` to launch the development server.
70 |
71 | ## Learn more
72 |
73 | To learn more about Clerk and Next.js, and the organizations feature, check out the following resources:
74 |
75 | - [Clerk's organizations feature](https://clerk.com/docs/organizations/overview?utm_source=github&utm_medium=starter_repos&utm_campaign=organizations_starter)
76 |
77 | - [Quickstart: Get started with Next.js and Clerk](https://clerk.com/docs/quickstarts/nextjs?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-nextjs-pages-quickstart)
78 |
79 | - [Clerk Documentation](https://clerk.com/docs?utm_source=DevRel&utm_medium=docs&utm_campaign=templates&utm_content=10-24-2023&utm_term=clerk-nextjs-pages-quickstart)
80 |
81 | - [Next.js Documentation](https://nextjs.org/docs)
82 |
83 | ## Found an issue?
84 |
85 | If you have found an issue with the quickstart, please create an [issue](https://github.com/clerkinc/clerk-nextjs-pages-quickstart/issues).
86 |
87 | If it's a quick fix, such as a misspelled word or a broken link, feel free to skip creating an issue.
88 | Go ahead and create a [pull request](https://github.com/clerkinc/clerk-nextjs-pages-quickstart/pulls) with the solution. :rocket:
89 |
90 | ## Want to leave feedback?
91 |
92 | Feel free to create an [issue](https://github.com/clerkinc/clerk-nextjs-pages-quickstart/issues) with the **feedback** label. Our team will take a look at it and get back to you as soon as we can!
93 |
94 | ## Connect with us
95 |
96 | You can discuss ideas, ask questions, and meet others from the community in our [Discord](https://discord.com/invite/b5rXHjAg7A).
97 |
98 | If you prefer, you can also find support through our [Twitter](https://twitter.com/ClerkDev), or you can [email](mailto:support@clerk.dev) us!
99 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react"
2 |
3 | export default function AuthLayout({ children }: PropsWithChildren) {
4 | return (
5 |
6 |
7 | {children}
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import { SignIn } from "@clerk/nextjs"
3 |
4 | export const metadata: Metadata = {
5 | title: "Sign In | Clerk Organizations Demo",
6 | description: "Login to your account",
7 | }
8 |
9 | export default function SignInPage() {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs"
2 |
3 | import { Metadata } from "next"
4 |
5 | export const metadata: Metadata = {
6 | title: "Sign In | Clerk Organizations Demo",
7 | description: "Create an account to get started.",
8 | }
9 |
10 | export default function SignUpPage() {
11 | return
12 | }
13 |
--------------------------------------------------------------------------------
/app/(demo)/create-organization/page.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ClerkLoading, CreateOrganization } from "@clerk/nextjs"
3 | import { CustomCreateOrganizationForm } from "@/components/CreateOrganization"
4 | import { MyMemberships } from "@/components/OrganizationList"
5 |
6 | export default function CreateOrganizationPage() {
7 | return (
8 |
13 |
14 |
UI Component
15 | Loading ...
16 |
17 | Custom UI
18 |
19 | List your memberships
20 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/app/(demo)/discover/page.tsx:
--------------------------------------------------------------------------------
1 | import { shouldGate } from "@/utils/organizations"
2 | import {
3 | ClerkLoading,
4 | OrganizationList,
5 | OrganizationSwitcher,
6 | } from "@clerk/nextjs"
7 | import {
8 | MyInvitations,
9 | MyMemberships,
10 | MySuggestions,
11 | } from "@/components/OrganizationList"
12 |
13 | export default function DiscoverPage() {
14 | return (
15 |
20 |
21 |
Pre-built OrganizationList
22 | Loading ...
23 |
30 |
31 | Pre-built OrganizationSwitcher
32 | Loading ...
33 |
34 | List my memberships
35 |
36 | List my invitations
37 |
38 | List my suggestions
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/(demo)/organization/layout.tsx:
--------------------------------------------------------------------------------
1 | import { RequireActiveOrganization } from "@/components/RequireActiveOrganization"
2 |
3 | export default RequireActiveOrganization
4 |
--------------------------------------------------------------------------------
/app/(demo)/organization/page.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkLoading, OrganizationProfile } from "@clerk/nextjs"
2 | import {
3 | OrgInvitations,
4 | OrgInviteMemberForm,
5 | OrgMembers,
6 | OrgMembershipRequests,
7 | OrgVerifiedDomains,
8 | } from "@/components/CustomOrganizationProfile"
9 |
10 | export default function OrganizationPage() {
11 | return (
12 |
13 |
14 |
UI Component
15 | Loading ...
16 |
17 | Custom List Domains
18 |
19 | Custom List Invitations
20 |
21 | Custom List Membership Requests
22 |
23 | Custom List Memberships
24 |
25 | Custom Invite Form
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/(demo)/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | export const metadata = {
4 | title: "Clerk | Organization Demo",
5 | }
6 |
7 | export default function DashboardPage() {
8 | return (
9 |
10 |
14 | Authorization with Custom UI
15 |
16 |
17 |
18 |
22 | Discover Organizations
23 |
24 |
25 |
26 |
30 | Organization Profile
31 |
32 |
33 |
34 |
38 | Create Organization
39 |
40 |
41 |
42 |
46 | Test endpoints
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/app/(demo)/route-handlers/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AdminOrEditor,
3 | AuthTest,
4 | EditorTest,
5 | } from "@/components/endpoint-tests"
6 |
7 | export default function RouteHandlersPage() {
8 | return (
9 |
10 |
11 |
Route Handlers
12 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/api/admin-editor/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server"
2 |
3 | export function GET() {
4 | const { userId, has } = auth()
5 |
6 | if (!userId) {
7 | return new Response(null, {
8 | status: 401,
9 | })
10 | }
11 |
12 | if (!has({ role: "org:admin" }) && !has({ role: "org:editor" })) {
13 | return new Response(null, {
14 | status: 403,
15 | })
16 | }
17 |
18 | return new Response(
19 | JSON.stringify({
20 | userId,
21 | })
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/app/api/authTest/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server"
2 |
3 | export function GET() {
4 | const { userId, orgRole } = auth()
5 | return new Response(JSON.stringify({ userId, orgRole }))
6 | }
7 |
--------------------------------------------------------------------------------
/app/api/editor/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server"
2 |
3 | export function GET() {
4 | const { userId } = auth().protect({ role: "org:editor" })
5 |
6 | return new Response(
7 | JSON.stringify({
8 | userId,
9 | })
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/authorization-playground/layout.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react"
2 | import { auth, OrganizationList } from "@clerk/nextjs"
3 | import Link from "next/link"
4 |
5 | export default function AuthorizationPlaygroundLayout(
6 | props: PropsWithChildren
7 | ) {
8 | const { orgId, has } = auth().protect({ redirectUrl: "/sign-in" })
9 |
10 | if (!orgId) {
11 | return (
12 |
13 |
14 |
15 |
Welcome
16 |
17 | This part of the application requires the user to select an
18 | organization in order to proceed. If you are not part of an
19 | organization, you can accept an invitation or create your own
20 | organization
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 | This is a dummy app which mimics a content management platform and
34 | allows users within the organization to create posts.
35 |
36 |
37 |
38 | Visit this page with different roles in order to see how the layout
39 | changes.
40 |
41 |
42 | Supported roles for this app are "Editor",
43 | "Admin", "Viewer".
44 |
45 |
46 |
47 |
51 | Posts
52 |
53 |
54 |
59 | Settings
60 |
61 |
62 | {props.children}
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/app/authorization-playground/page.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Protect } from "@clerk/nextjs"
3 | import { PostActions } from "@/app/authorization-playground/post-actions"
4 | import { PostButton } from "@/app/authorization-playground/post-button"
5 |
6 | interface PostItemProps {
7 | id: string
8 | title: string
9 | description: string
10 | }
11 |
12 | function PostItem(props: PostItemProps) {
13 | return (
14 |
15 |
{props.title}
16 |
17 |
18 |
has({ permission: "org:posts:manage" })}>
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | function Posts() {
27 | const posts = [
28 | {
29 | id: "one",
30 | title: "Post One",
31 | description: "This is the first post",
32 | },
33 | {
34 | id: "two",
35 | title: "Post One",
36 | description: "This is the first post",
37 | },
38 | ]
39 |
40 | return (
41 |
42 | {posts.map((post) => (
43 |
44 | ))}
45 |
46 | )
47 | }
48 |
49 | export default function CreateOrganizationPage() {
50 | return (
51 |
56 |
57 |
POSTS
58 |
59 |
Access not granted}
62 | >
63 |
64 |
65 |
66 |
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/app/authorization-playground/post-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useAuth } from "@clerk/nextjs"
4 |
5 | interface PostItemProps {
6 | id: string
7 | title: string
8 | description: string
9 | }
10 |
11 | export function PostActions(props: PostItemProps) {
12 | const { has, isLoaded } = useAuth()
13 | if (!isLoaded) return null
14 |
15 | const canEdit = has({ permission: "org:posts:manage" })
16 | const canDelete = has({ permission: "org:posts:delete" })
17 |
18 | return (
19 |
20 |
21 | {canEdit && Edit }
22 | {canDelete && Delete }
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/authorization-playground/post-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Protect } from "@clerk/nextjs"
4 |
5 | export function PostButton() {
6 | return (
7 |
8 |
9 | New Post
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/authorization-playground/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkLoading, OrganizationProfile, auth } from "@clerk/nextjs"
2 |
3 | export default function CustomAppSettings() {
4 | auth().protect({
5 | permission: "org:posts:manage",
6 | })
7 | return (
8 |
13 |
14 |
Settings
15 |
Clerk Organization Settings
16 |
Loading ...
17 |
25 |
26 | {/*TODO: Add role checks as well*/}
27 |
28 |
Post Organization Settings
29 | {/*TODO: Add a fake form*/}
30 | {/*
Access not granted}*/}
33 | {/*>*/}
34 | {/* */}
35 | {/* */}
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css"
2 | import { PropsWithChildren } from "react"
3 | import {
4 | ClerkLoading,
5 | ClerkProvider,
6 | OrganizationSwitcher,
7 | UserButton,
8 | } from "@clerk/nextjs"
9 | import { cn } from "@/utils/cn"
10 | import Link from "next/link"
11 |
12 | export default function RootLayout({ children }: PropsWithChildren) {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Loading ...
20 |
27 |
28 |
29 | Home
30 |
34 | Github
35 |
36 |
37 |
38 |
46 |
47 | {children}
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/clerk.d.ts:
--------------------------------------------------------------------------------
1 | interface ClerkAuthorization {
2 | permission: "org:posts:delete" | "org:posts:manage" | "org:posts:read"
3 | role: "org:admin" | "org:editor" | "org:viewer"
4 | }
5 |
--------------------------------------------------------------------------------
/components/CreateOrganization.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { useOrganizationList } from "@clerk/nextjs"
3 | import { FormEventHandler, useState } from "react"
4 | import { UserMembershipParams } from "@/utils/organizations"
5 |
6 | export function CustomCreateOrganizationForm() {
7 | const { isLoaded, createOrganization, setActive, userMemberships } =
8 | useOrganizationList(UserMembershipParams)
9 | const [isSubmitting, setSubmitting] = useState(false)
10 |
11 | const handleSubmit: FormEventHandler = async (e) => {
12 | e.preventDefault()
13 | if (!isLoaded) {
14 | return null
15 | }
16 | setSubmitting(true)
17 |
18 | const submittedData = Object.fromEntries(
19 | new FormData(e.currentTarget).entries()
20 | ) as { organizationName: string | undefined; asActive?: "on" }
21 |
22 | if (!submittedData.organizationName) {
23 | return
24 | }
25 |
26 | try {
27 | const organization = await createOrganization({
28 | name: submittedData.organizationName,
29 | })
30 | void userMemberships?.revalidate()
31 | if (submittedData.asActive === "on") {
32 | await setActive({ organization })
33 | }
34 | } finally {
35 | if (e.target instanceof HTMLFormElement) {
36 | e.target.reset()
37 | }
38 | setSubmitting(false)
39 | }
40 | }
41 |
42 | return (
43 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/components/CustomOrganizationProfile.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useOrganization, useUser } from "@clerk/nextjs"
4 | import {
5 | OrgDomainParams,
6 | OrgInvitationsParams,
7 | OrgMembershipRequestsParams,
8 | OrgMembersParams,
9 | } from "@/utils/organizations"
10 | import { useState } from "react"
11 | import { OrganizationCustomRoleKey } from "@clerk/types"
12 | import { SelectRole } from "@/components/SelectRole"
13 |
14 | export const OrgMembers = () => {
15 | const { user } = useUser()
16 | const { isLoaded, memberships } = useOrganization(OrgMembersParams)
17 |
18 | if (!isLoaded) {
19 | return <>Loading>
20 | }
21 |
22 | return (
23 | <>
24 |
25 |
26 |
27 | User
28 | Joined
29 | Role
30 | Actions
31 |
32 |
33 |
34 | {memberships?.data?.map((mem) => (
35 |
36 |
37 | {mem.publicUserData.identifier}{" "}
38 | {mem.publicUserData.userId === user?.id && "(You)"}
39 |
40 | {mem.createdAt.toLocaleDateString()}
41 |
42 | {
45 | await mem.update({
46 | role: e.target.value as OrganizationCustomRoleKey,
47 | })
48 | await memberships?.revalidate()
49 | }}
50 | />
51 |
52 |
53 | {
55 | await mem.destroy()
56 | await memberships?.revalidate()
57 | }}
58 | >
59 | Remove
60 |
61 |
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 | memberships?.fetchPrevious?.()}
72 | >
73 | Previous
74 |
75 |
76 | memberships?.fetchNext?.()}
80 | >
81 | Next
82 |
83 |
84 | >
85 | )
86 | }
87 |
88 | export const OrgInvitations = () => {
89 | const { isLoaded, invitations, memberships } = useOrganization({
90 | ...OrgInvitationsParams,
91 | ...OrgMembersParams,
92 | })
93 |
94 | if (!isLoaded) {
95 | return <>Loading>
96 | }
97 |
98 | return (
99 | <>
100 |
101 |
102 |
103 | User
104 | Invited
105 | Role
106 | Actions
107 |
108 |
109 |
110 | {invitations?.data?.map((inv) => (
111 |
112 | {inv.emailAddress}
113 | {inv.createdAt.toLocaleDateString()}
114 | {inv.role}
115 |
116 | {
118 | await inv.revoke()
119 | await Promise.all([
120 | memberships?.revalidate,
121 | invitations?.revalidate,
122 | ])
123 | }}
124 | >
125 | Revoke
126 |
127 |
128 |
129 | ))}
130 |
131 |
132 |
133 |
134 | invitations?.fetchPrevious?.()}
138 | >
139 | Previous
140 |
141 |
142 | invitations?.fetchNext?.()}
146 | >
147 | Next
148 |
149 |
150 | >
151 | )
152 | }
153 |
154 | export const OrgMembershipRequests = () => {
155 | const { isLoaded, membershipRequests } = useOrganization(
156 | OrgMembershipRequestsParams
157 | )
158 |
159 | if (!isLoaded) {
160 | return <>Loading>
161 | }
162 |
163 | return (
164 | <>
165 |
166 |
167 |
168 | User
169 | Requested Access
170 | Actions
171 |
172 |
173 |
174 | {membershipRequests?.data?.map((mem) => (
175 |
176 | {mem.publicUserData.identifier}
177 | {mem.createdAt.toLocaleDateString()}
178 |
179 |
180 | ))}
181 |
182 |
183 |
184 |
185 | membershipRequests?.fetchPrevious?.()}
192 | >
193 | Previous
194 |
195 |
196 | membershipRequests?.fetchNext?.()}
202 | >
203 | Next
204 |
205 |
206 | >
207 | )
208 | }
209 |
210 | export const OrgVerifiedDomains = () => {
211 | const { isLoaded, domains } = useOrganization(OrgDomainParams)
212 |
213 | if (!isLoaded) {
214 | return <>Loading>
215 | }
216 |
217 | return (
218 | <>
219 |
236 |
237 | domains?.fetchNext?.()}
241 | >
242 | {domains?.hasNextPage ? "Load more" : "No more to load"}
243 |
244 | >
245 | )
246 | }
247 |
248 | export const OrgInviteMemberForm = () => {
249 | const { isLoaded, organization, invitations } =
250 | useOrganization(OrgInvitationsParams)
251 | const [emailAddress, setEmailAddress] = useState("")
252 | const [disabled, setDisabled] = useState(false)
253 |
254 | if (!isLoaded || !organization) {
255 | return <>Loading>
256 | }
257 |
258 | const onSubmit = async (e) => {
259 | e.preventDefault()
260 |
261 | const submittedData = Object.fromEntries(
262 | new FormData(e.currentTarget).entries()
263 | ) as {
264 | email: string | undefined
265 | role: OrganizationCustomRoleKey | undefined
266 | }
267 |
268 | if (!submittedData.email || !submittedData.role) {
269 | return
270 | }
271 |
272 | setDisabled(true)
273 | await organization.inviteMember({
274 | emailAddress: submittedData.email,
275 | role: submittedData.role,
276 | })
277 | await invitations?.revalidate?.()
278 | setEmailAddress("")
279 | setDisabled(false)
280 | }
281 |
282 | return (
283 |
297 | )
298 | }
299 |
--------------------------------------------------------------------------------
/components/OrganizationList.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useOrganizationList } from "@clerk/nextjs"
4 | import {
5 | UserInvitationsParams,
6 | UserMembershipParams,
7 | UserSuggestionsParams,
8 | } from "@/utils/organizations"
9 | import { useRouter } from "next/navigation"
10 |
11 | export const MyMemberships = () => {
12 | const { push } = useRouter()
13 | const { isLoaded, setActive, userMemberships } =
14 | useOrganizationList(UserMembershipParams)
15 |
16 | if (!isLoaded) {
17 | return <>Loading>
18 | }
19 |
20 | return (
21 | <>
22 |
23 | {userMemberships.data?.map((mem) => (
24 |
25 | {mem.organization.name}
26 |
27 | setActive({ organization: mem.organization.id })}
29 | >
30 | Select
31 |
32 |
34 | setActive({
35 | organization: mem.organization.id,
36 | beforeEmit: () => {
37 | push("/organization")
38 | },
39 | })
40 | }
41 | >
42 | Redirect
43 |
44 |
45 |
46 | ))}
47 |
48 |
49 | userMemberships.fetchNext()}
53 | >
54 | {userMemberships.isFetching
55 | ? "Loading"
56 | : userMemberships.hasNextPage
57 | ? "Load more"
58 | : "No more to load"}
59 |
60 | >
61 | )
62 | }
63 |
64 | export const MyInvitations = () => {
65 | const { isLoaded, setActive, userInvitations, userMemberships } =
66 | useOrganizationList({
67 | ...UserInvitationsParams,
68 | ...UserMembershipParams,
69 | })
70 |
71 | if (!isLoaded) {
72 | return <>Loading>
73 | }
74 |
75 | return (
76 | <>
77 |
95 |
96 | userInvitations.fetchNext()}
100 | >
101 | {userInvitations.hasNextPage ? "Load more" : "No more to load"}
102 |
103 | >
104 | )
105 | }
106 |
107 | export const MySuggestions = () => {
108 | const { isLoaded, setActive, userSuggestions, userMemberships } =
109 | useOrganizationList({
110 | ...UserSuggestionsParams,
111 | ...UserMembershipParams,
112 | })
113 |
114 | if (!isLoaded) {
115 | return <>Loading>
116 | }
117 |
118 | return (
119 | <>
120 |
138 |
139 | userSuggestions.fetchNext()}
143 | >
144 | {userSuggestions.hasNextPage ? "Load more" : "No more to load"}
145 |
146 | >
147 | )
148 | }
149 |
--------------------------------------------------------------------------------
/components/RequireActiveOrganization.tsx:
--------------------------------------------------------------------------------
1 | import { auth, OrganizationList } from "@clerk/nextjs"
2 | import { PropsWithChildren } from "react"
3 |
4 | export const RequireActiveOrganization = (props: PropsWithChildren) => {
5 | const { orgId } = auth()
6 |
7 | if (orgId) {
8 | return props.children
9 | }
10 | return (
11 |
12 |
13 |
14 |
Welcome
15 |
16 | This part of the application requires the user to select an
17 | organization in order to proceed. If you are not part of an
18 | organization, you can accept an invitation or create your own
19 | organization
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/SelectRole.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEventHandler, useEffect, useRef, useState } from "react"
2 | import { OrganizationCustomRoleKey } from "@clerk/types"
3 | import { useOrganization } from "@clerk/nextjs"
4 |
5 | type SelectRoleProps = {
6 | fieldName?: string
7 | isDisabled?: boolean
8 | onChange?: ChangeEventHandler
9 | defaultRole?: string
10 | }
11 |
12 | export const SelectRole = ({
13 | fieldName,
14 | isDisabled = false,
15 | onChange,
16 | defaultRole,
17 | }: SelectRoleProps) => {
18 | const { organization } = useOrganization()
19 | const [fetchedRoles, setRoles] = useState([])
20 | const isPopulated = useRef(false)
21 |
22 | useEffect(() => {
23 | if (isPopulated.current) return
24 | organization
25 | ?.getRoles({
26 | pageSize: 20,
27 | initialPage: 1,
28 | })
29 | .then((res) => {
30 | isPopulated.current = true
31 | setRoles(
32 | res.data.map((roles) => roles.key as OrganizationCustomRoleKey)
33 | )
34 | })
35 | }, [organization?.id])
36 |
37 | if (fetchedRoles.length === 0) return null
38 |
39 | return (
40 |
47 | {fetchedRoles?.map((roleKey) => (
48 |
49 | {roleKey}
50 |
51 | ))}
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/components/endpoint-tests.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState, useCallback } from "react"
4 |
5 | const useFetch = (path: string) => {
6 | const [state, setState] = useState<{ data: any; error: any } | null>(null)
7 | const request = useCallback(async () => {
8 | setState(null)
9 | try {
10 | /* Clerk already stores the active organization on the JWT claims */
11 | const res = await fetch(path)
12 | if (res.ok) {
13 | const data = await res.json()
14 | setState({
15 | data,
16 | error: null,
17 | })
18 | } else {
19 | setState({
20 | data: null,
21 | error: res.status,
22 | })
23 | }
24 | } catch (error) {
25 | setState({
26 | data: null,
27 | error,
28 | })
29 | }
30 | }, [setState])
31 |
32 | return { request, state }
33 | }
34 |
35 | export function EditorTest() {
36 | const { request, state } = useFetch(`/api/editor`)
37 |
38 | return (
39 |
40 |
Editor test
41 |
42 | This should fail with 404 if user is not an editor
43 | /api/editor
44 |
45 |
46 | Test
47 |
48 | {state?.data &&
{JSON.stringify(state.data, null, 2)} }
49 | {state?.error &&
{JSON.stringify(state.error, null, 2)} }
50 |
51 | )
52 | }
53 |
54 | export function AuthTest() {
55 | const { request, state } = useFetch(`/api/authTest`)
56 |
57 | return (
58 |
59 |
Backend test
60 |
61 | Read your user ID and role for this org from{" "}
62 | /api/testAuth
63 |
64 |
65 | Test
66 |
67 | {state?.data &&
{JSON.stringify(state.data, null, 2)} }
68 |
69 | )
70 | }
71 |
72 | export function AdminOrEditor() {
73 | const { request, state } = useFetch(`/api/admin-editor`)
74 |
75 | return (
76 |
77 |
Admin Or Editor test
78 |
79 | This should fail with 403 if user is not an editor or editor
80 | /api/admin-editor
81 |
82 |
83 | Test
84 |
85 | {state?.data &&
{JSON.stringify(state.data, null, 2)} }
86 | {state?.error &&
{JSON.stringify(state.error, null, 2)} }
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/docs/clerk-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/docs/clerk-logo-dark.png
--------------------------------------------------------------------------------
/docs/clerk-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/docs/clerk-logo-light.png
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server"
2 | import { shouldGate } from "@/utils/organizations"
3 | import { authMiddleware, redirectToSignIn } from "@clerk/nextjs/server"
4 |
5 | export default authMiddleware({
6 | publicRoutes: ["/", /^(\/(sign-in|sign-up|authorization-playground)\/*).*$/],
7 |
8 | afterAuth(auth, req) {
9 | // handle users who aren't authenticated
10 | if (!auth.userId && !auth.isPublicRoute) {
11 | return redirectToSignIn({ returnBackUrl: req.url })
12 | }
13 |
14 | if (shouldGate && req.nextUrl.pathname === "/") {
15 | const orgSelection = new URL("/discover", req.url)
16 | return NextResponse.redirect(orgSelection)
17 | }
18 |
19 | // redirect them to organization selection page
20 | if (
21 | shouldGate &&
22 | auth.userId &&
23 | !auth.orgId &&
24 | !auth.isApiRoute &&
25 | req.nextUrl.pathname !== "/discover"
26 | ) {
27 | const orgSelection = new URL("/discover", req.url)
28 | return NextResponse.redirect(orgSelection)
29 | }
30 | },
31 | })
32 |
33 | export const config = {
34 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
35 | }
36 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | export default nextConfig
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clerk-organizations-demo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/backend": "0.36.0",
13 | "@clerk/nextjs": "^4.29.3",
14 | "@clerk/types": "3.60.0",
15 | "clsx": "^2.0.0",
16 | "next": "^14.0.3",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "tailwind-merge": "^1.14.0"
20 | },
21 | "devDependencies": {
22 | "@ianvs/prettier-plugin-sort-imports": "^3.7.2",
23 | "@tailwindcss/typography": "^0.5.9",
24 | "@types/node": "^18.16.0",
25 | "@types/react": "^18.2.39",
26 | "@types/react-dom": "^18.2.17",
27 | "autoprefixer": "^10.4.16",
28 | "eslint": "^8.39.0",
29 | "eslint-config-next": "^14.0.3",
30 | "eslint-config-prettier": "^8.8.0",
31 | "eslint-plugin-tailwindcss": "^3.11.0",
32 | "eslint-plugin-unused-imports": "^3.0.0",
33 | "postcss": "^8.4.31",
34 | "prettier": "^2.8.8",
35 | "prettier-plugin-tailwindcss": "^0.1.13",
36 | "tailwindcss": "^3.3.5",
37 | "typescript": "^4.9.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/env(.*)$",
15 | "^@/types/(.*)$",
16 | "^@/config/(.*)$",
17 | "^@/lib/(.*)$",
18 | "^@/hooks/(.*)$",
19 | "^@/components/ui/(.*)$",
20 | "^@/components/(.*)$",
21 | "^@/styles/(.*)$",
22 | "^@/app/(.*)$",
23 | "",
24 | "^[./]",
25 | ],
26 | importOrderSeparation: false,
27 | importOrderSortSpecifiers: true,
28 | importOrderBuiltinModulesToTop: true,
29 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
30 | importOrderMergeDuplicateImports: true,
31 | importOrderCombineTypeAndValueImports: true,
32 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
33 | }
34 |
--------------------------------------------------------------------------------
/public/dark-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/public/dark-logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/public/favicon.ico
--------------------------------------------------------------------------------
/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/public/hero.png
--------------------------------------------------------------------------------
/public/light-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clerk/organizations-demo/b74ba0f7c998697564323aef12b50e133cdef5de/public/light-logo.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | @layer base {
7 | body {
8 | font-feature-settings: "rlig" 1, "calt" 1;
9 | @apply bg-slate-50;
10 | }
11 |
12 | h1 {
13 | @apply text-3xl font-bold
14 | }
15 |
16 | h2 {
17 | @apply text-2xl font-bold
18 | }
19 |
20 | h2 {
21 | @apply text-xl font-bold
22 | }
23 |
24 | label:not(.cl-rootBox) {
25 | @apply block text-xs font-medium text-gray-700;
26 | }
27 |
28 | input:not([type='checkbox']):not(.cl-rootBox input) {
29 | @apply mt-1 w-full rounded-md border-gray-200 border shadow-sm sm:text-sm;
30 | }
31 |
32 | form {
33 | @apply flex flex-col gap-4 items-start;
34 | }
35 |
36 | button:not(.cl-rootBox button) {
37 | @apply py-2 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none
38 | }
39 |
40 | input:where(:not([type])),
41 | select,
42 | textarea,
43 | input:not(.cl-rootBox input) {
44 | @apply px-2 py-2
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{ts,tsx}",
5 | "./components/**/*.{ts,tsx}",
6 | "./app/layout.tsx",
7 | ],
8 | darkMode: ["class"],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [require("@tailwindcss/typography")],
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": [
24 | "./*"
25 | ]
26 | },
27 | "plugins": [
28 | {
29 | "name": "next"
30 | }
31 | ],
32 | "strictNullChecks": true
33 | },
34 | "include": [
35 | "next-env.d.ts",
36 | "clerk.d.ts",
37 | "**/*.ts",
38 | "**/*.tsx",
39 | ".next/types/**/*.ts"
40 | ],
41 | "exclude": [
42 | "node_modules",
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/utils/organizations.ts:
--------------------------------------------------------------------------------
1 | export const shouldGate = process.env.NEXT_PUBLIC_GATING === "true"
2 |
3 | export const UserMembershipParams = {
4 | userMemberships: {
5 | infinite: true,
6 | },
7 | }
8 |
9 | export const UserInvitationsParams = {
10 | userInvitations: {
11 | infinite: true,
12 | },
13 | }
14 |
15 | export const UserSuggestionsParams = {
16 | userSuggestions: {
17 | infinite: true,
18 | },
19 | }
20 |
21 | export const OrgMembersParams = {
22 | memberships: {
23 | pageSize: 5,
24 | keepPreviousData: true,
25 | },
26 | }
27 |
28 | export const OrgInvitationsParams = {
29 | invitations: {
30 | pageSize: 5,
31 | // TODO: Seems like keepPreviousData is broken
32 | keepPreviousData: true,
33 | },
34 | }
35 | export const OrgMembershipRequestsParams = {
36 | membershipRequests: {
37 | pageSize: 5,
38 | keepPreviousData: true,
39 | },
40 | }
41 |
42 | export const OrgDomainParams = {
43 | domains: {
44 | infinite: true,
45 | },
46 | }
47 |
--------------------------------------------------------------------------------