├── .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 | Clerk Logo for light background 6 | 7 | 8 |
9 |

10 |
11 |

12 | Clerk Organizations Demo 13 |

14 | 15 | Downloads 16 | 17 | 18 | Clerk Documentation 19 | 20 | 21 | Discord 22 | 23 | 24 | Twitter 25 | 26 |
27 |
28 | Clerk Hero Image 29 |
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 |
13 | 14 | 15 | 16 |
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 | 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 && } 22 | {canDelete && } 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 | 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 | 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 |
44 |
45 | 46 | 53 |
54 |
55 | 58 | 66 |
67 | 70 |
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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {memberships?.data?.map((mem) => ( 35 | 36 | 40 | 41 | 52 | 62 | 63 | ))} 64 | 65 |
UserJoinedRoleActions
37 | {mem.publicUserData.identifier}{" "} 38 | {mem.publicUserData.userId === user?.id && "(You)"} 39 | {mem.createdAt.toLocaleDateString()} 42 | { 45 | await mem.update({ 46 | role: e.target.value as OrganizationCustomRoleKey, 47 | }) 48 | await memberships?.revalidate() 49 | }} 50 | /> 51 | 53 | 61 |
66 | 67 |
68 | 75 | 76 | 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 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {invitations?.data?.map((inv) => ( 111 | 112 | 113 | 114 | 115 | 128 | 129 | ))} 130 | 131 |
UserInvitedRoleActions
{inv.emailAddress}{inv.createdAt.toLocaleDateString()}{inv.role} 116 | 127 |
132 | 133 |
134 | 141 | 142 | 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 | 169 | 170 | 171 | 172 | 173 | 174 | {membershipRequests?.data?.map((mem) => ( 175 | 176 | 177 | 178 | 179 | 180 | ))} 181 | 182 |
UserRequested AccessActions
{mem.publicUserData.identifier}{mem.createdAt.toLocaleDateString()}
183 | 184 |
185 | 195 | 196 | 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 |
    220 | {domains?.data?.map((domain) => ( 221 |
  • 222 | {domain.name} 223 |
    224 | 232 |
    233 |
  • 234 | ))} 235 |
236 | 237 | 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 |
284 | setEmailAddress(e.target.value)} 290 | /> 291 | 292 | 293 | 296 | 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 | 32 | 44 |
    45 |
  • 46 | ))} 47 |
48 | 49 | 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 |
    78 | {userInvitations.data?.map((mem) => ( 79 |
  • 80 | {mem.publicOrganizationData.name} 81 |
    82 | 91 |
    92 |
  • 93 | ))} 94 |
95 | 96 | 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 |
    121 | {userSuggestions.data?.map((mem) => ( 122 |
  • 123 | {mem.publicOrganizationData.name} 124 |
    125 | 134 |
    135 |
  • 136 | ))} 137 |
138 | 139 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------