# Clerk Blog — Guides — Page 4

# Build a modern authenticated chat application with Next.js, Ably, and Clerk
URL: https://clerk.com/blog/authenticated-next-chat-app-with-ably-and-clerk.md
Date: 2024-06-04
Category: Guides
Description: Learn how to build a modern, authenticated chat application using Next.js, Ably, and Clerk. This comprehensive guide covers everything from setting up real-time messaging and user authentication to implementing roles and message history.

This 6000-word mega-post teaches you how to code an authenticated chat application with roles and permissions from scratch!

You will learn how to implement message transmission over WebSockets and a dynamic "who's online?" list that updates in realtime.

We'll also implement a secure moderator role that enables select users to access restricted channels like "#moderators-only" and delete unwanted messages.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./1.png)

As for the technology stack, it's React on the frontend using Tailwind and shadcn/ui for styling. We'll integrate [Ably](https://ably.com) for serverless WebSockets and [Clerk](https://clerk.com) for authentication and user management.

## Just want the code?

I feel that! You can find the complete source code on my [GitHub](https://github.com/bookercodes/authenticated-next-chat-app-with-ably-and-clerk).

If you want to run the project locally, I've included the minimal necessary steps in the repository README for you to reference.

## Before you begin

Before delving in, it would be good if you had an understanding of the following technologies and concepts:

- React
  - Components and how to [think in React](https://react.dev/learn/thinking-in-react)
  - A grasp of `useState` and how to, for example, implement a form component from scratch
  - Familiarity with `useEffect`, `useRef`, and `useReducer`
- Next.js
  - How to define routes with App Router
  - Awareness of client components vs server components
- CSS
  - An awareness of Flexbox and Grid

## Start here

First, let's create a blank Next.js App Router project called `chat-tutorial`.

The easiest way is with `create-next-app`:

```bash {{ filename: 'terminal' }}
npx create-next-app chat-tutorial --javascript --tailwind --eslint --app --src-dir --no-import-alias
```

Run the above command then `cd` into the newly-created directory:

```bash {{ filename: 'terminal' }}
cd chat-tutorial
```

### Create the chat layout

Before we go too far, let's first create the layout for our chat application.

Open the `chat-tutorial` directory in your editor of choice and add a `nav` to `src/app/layout.js`:

```jsx {{ filename: 'src/app/layout.js', ins: [[15, 17]] }}
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav className="flex border-b border-gray-200 p-5">
          <h1 className="font-bold">Clover Corp</h1>
        </nav>
        {children}
      </body>
    </html>
  )
}
```

Next, create `src/app/chat/[[...channelName]]/page.js`:

```jsx {{ filename: 'src/app/chat/[[...channelName]]/page.js' }}
'use client'

const Page = ({ params }) => {
  return (
    <div className="grid h-[calc(100vh-72.8px)] grid-cols-4">
      <div className="border-r border-gray-200 p-5"></div>
      <div className="col-span-2"></div>
      <div className="border-l border-gray-200 p-5"></div>
    </div>
  )
}
export default Page
```

Run the development server with `npm run dev`, open your browser, then navigate to `/chat` to observe the three-column skeleton:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./skeleton.png)

In the upcoming steps, we'll populate each column with the channel list, chat, and online list components respectively.

Note that `/chat` is a dynamic route with an optional route segment called `channelName` .

We'll reference this segment by `params.channelName` and use the value do determine what channel the user is currently participating in.

| Route               | `params.channelName` |
| ------------------- | -------------------- |
| /chat/announcements | `"announcements"`    |
| /chat/general       | `"general"`          |
| /chat/random        | `"random"`           |
| /chat/mods-only     | `"mods-only"`        |
| /chat               | `null`               |

### Install shadcn/ui

If you haven't come across [shadcn/ui](https://ui.shadcn.com) before, this tool enables design-impaired JavaScript developers like me to pick and choose from an assortment of beautiful React components that are also accessible and easy to customize. 😅

Still inside the `chat-tutorial` directory, run:

```bash {{ filename: 'terminal' }}
npx shadcn-ui@latest init
```

You'll be presented with 3 prompts. Answer like this:

- **Which style would you like to use?** Default
- **Which color would you like to use as base color?** Slate
- **Would you like to use CSS variables for colors?** Yes

### Build the chat React components

In this section, we'll build the `MessageInput` and `MessageList` components.

To make them look sleek without writing much code, let's lean on the shadcn/ui's ready-made `Input` and `Avatar` components.

To download them, run the following commands:

```bash {{ filename: 'terminal' }}
npx shadcn-ui@latest add input
npx shadcn-ui@latest add avatar
```

Create `src/app/[[...channel]]/chat/message-input.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/message-input.js' }}
import { useState } from 'react'
import { Input } from '@/components/ui/input'

const MessageInput = ({ onSubmit, disabled }) => {
  const [input, setInput] = useState('')

  const handleChange = (e) => {
    setInput(e.target.value)
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    onSubmit(input)
    setInput('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="text"
        value={input}
        onChange={handleChange}
        disabled={disabled}
        placeholder={disabled ? 'This input has been disabled.' : 'Your message here'}
      />
    </form>
  )
}
export default MessageInput
```

Finally, create `src/app/[[...channel]]/chat/message-list.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/message-list.js' }}
import { Avatar, AvatarImage } from '@/components/ui/avatar'

const MessageList = ({ messages }) => {
  const createLi = (message) => (
    <li key={message.id} className="group my-2 flex justify-between bg-slate-50 p-3">
      <div className="flex items-center">
        <Avatar className="mr-2">
          <AvatarImage src={message.data.avatarUrl} />
        </Avatar>
        <p>{message.data.text}</p>
      </div>
    </li>
  )

  return <ul>{messages.map(createLi)}</ul>
}
export default MessageList
```

## Realtime messaging with Ably

To enable realtime messaging and store message history, we'll use a serverless WebSockets platform called Ably.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./ably.png)

Because Ably manages the WebSockets, the connection is highly reliable. Additionally, using a hosted WebSocket platform allows us to benefit from realtime communication, even when deploying to serverless platforms such as Vercel that don't typically support long-lived WebSocket connections.

With Ably, events are published to "topics" (named logical channels). Subscribers receive all messages published to the topics to which they subscribe.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./pub-sub.png)

Conceptually, this maps very tidily in chat application where we utilize an Ably topic per chat channel.

When the user navigates routes, the `channelName` segment is updated. We can reference `channelName` to connect to an Ably topic by the same name prefixed with `chat:`, like so:

| Route               | `params.channelName` | Ably topic             |
| ------------------- | -------------------- | ---------------------- |
| /chat/announcements | `"announcements"`    | `"chat:announcements"` |
| /chat/general       | `"general"`          | `"chat:general"`       |
| /chat/random        | `"random"`           | `"chat:random"`        |
| /chat/mods-only     | `"mods-only"`        | `"chat:mods-only"`     |
| /chat               | `null`               | `null`                 |

The prefix part of the Ably topic is called a [namespace](https://ably.com/docs/channels#namespaces). Namespaces allow us to logically group related channels. This grouping will come in handy later when we apply Ably access rules for all `chat:*` channels.

The prefix `chat:` is one I chose arbitrarily and doesn't mean anything specific in Ably.

### Implement basic Ably chat messaging

If you haven't already, sign-up to Ably then **Create new app** with the following options:

- **App name:** Call your app something meaningful
- **Select your preferred language(s):** JavaScript
- **What type of app are you building?** Live Chat

![Authenticated Next Chat App With Ably And Clerk guide illustration](./create-ably-app.png)

In the next screen, copy or otherwise note your Ably API key - we'll need it momentarily:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./copy-ably-key.png)

Back in the terminal, install the Ably React SDK:

```bash {{ filename: 'terminal' }}
npm install ably
```

Create `src/app/[[...channel]]/chat/chat.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/chat.js' }}
import MessageInput from './message-input'
import MessageList from './message-list'

import { useReducer } from 'react'
import { useChannel } from 'ably/react'

const ADD = 'ADD'

const reducer = (prev, event) => {
  switch (event.name) {
    // 👉 Append the message to messages
    case ADD:
      return [...prev, event]
  }
}

const Chat = ({ channelName }) => {
  // 👉 Placeholder user to be replaced with the authenticated user later
  const user = {
    imageUrl: 'https://ui-avatars.com/api/?name=Alex',
  }
  const [messages, dispatch] = useReducer(reducer, [])
  // 👉 useChannel accepts the channel name and a function to invoke when
  //    new messages are received. We pass dispatch.
  const { channel, publish } = useChannel(channelName, dispatch)

  const publishMessage = (text) => {
    // 👉 Publish event through Ably
    publish({
      name: ADD,
      data: {
        text,
        avatarUrl: user.imageUrl,
      },
    })
  }

  return (
    <>
      <div className="overflow-y-auto p-5">
        <MessageList messages={messages} />
      </div>
      <div className="mt-auto p-5">
        <MessageInput onSubmit={publishMessage} />
      </div>
    </>
  )
}
export default Chat
```

Finally, update `src/app/[[...channel]]/chat/page.js`.

Take care to replace `YOUR_ABLY_API_KEY` with the API key you noted a moment ago:

```jsx {{ filename: 'src/app/[[...channel]]/chat/page.js', ins: [[3, 5], [8, 17], [18, 19], 23, 27, 28] }}
'use client'

import Chat from './chat'
import { Realtime } from 'ably'
import { AblyProvider, ChannelProvider } from 'ably/react'

const Page = ({ params }) => {
  // 👉 Instantiate Ably client
  const client = new Realtime({
    key: 'YOUR_ABLY_API_KEY',
    clientId: 'Alex',
  })
  const channelName = `chat:${params.channel}`

  return (
    // 👉 Wrap chat app in AblyProvider and ChannelProvider necessary to
    // use Ably hooks
    <AblyProvider client={client}>
      <ChannelProvider channelName={channelName}>
        <div className="grid h-[calc(100vh-72.8px)] grid-cols-4">
          <div className="border-r border-gray-200 p-5"></div>
          <div className="col-span-2">
            <Chat channelName={channelName} />
          </div>
          <div className="border-l border-gray-200 p-5"></div>
        </div>
      </ChannelProvider>
    </AblyProvider>
  )
}
export default Page
```

With that, the basic chat is ready to test!

Open /chat/general in a couple of localhost browser windows to send and receive chat messages over WebSockets:

Before we go any further, there is something very important to note about the code above!

In the excerpt, we instantiate an Ably `Realtime` instance with the API key. We also hard-coded the `clientId`.

```jsx {{ filename: 'src/app/[[...channel]]/chat/page.js' }}
const client = new Realtime({
  key: 'SlyWSw.jiu7RA:A64U3y3ty-9QLGB4Tn4NbBtlYYsx60qCI0ooT4WhNq0',
  clientId: 'Alex',
})
const channelName = `chat:${params.channel}`
```

This is problematic:

- An Ably API key is top secret and should never (ever) go in production client-side code as a malicious actor could easily find it and exploit your application. I have done so here *only* for demonstration purposes.
- Every client should have a unique identifier based on the user's name.

In an upcoming section, we will address these concerns by implementing a secure backend endpoint to generate an Ably-compatible JWT token which includes verified information about the user's identity and their capabilities.

### Implement a chat channel-switcher

To help users discover channels and navigate them more easily, let's create a `ChannelList` component and slot it into the left-hand column of our layout.

First, run this command to install `clsx`, a handy module for conditionally applying Tailwind styles:

```bash {{ filename: 'terminal' }}
npm install clsx
```

Next, create `src/app/[[...channel]]/chat/channel-list.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/channel-list.js' }}
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { clsx } from 'clsx'

const ChannelList = ({ channels }) => {
  const currentPath = usePathname()

  const createLi = (channel) => {
    return (
      <li key={channel.path}>
        <Link
          href={channel.path}
          className={clsx('flex items-center', {
            'font-bold': currentPath === channel.path,
          })}
        >
          {channel.label}
        </Link>
      </li>
    )
  }

  return <ul>{channels.map(createLi)}</ul>
}

export default ChannelList
```

Finally, update `src/app/[[...channel]]/chat/page.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/page.js', ins: [3, [8, 13], 26] }}
'use client'
import Chat from './chat'
import ChannelList from './channel-list'
import { Realtime } from 'ably'
import { AblyProvider, ChannelProvider } from 'ably/react'

const Page = ({ params }) => {
  const channels = [
    { path: '/chat/announcements', label: '# Announcements' },
    { path: '/chat/general', label: '# General' },
    { path: '/chat/random', label: '# Random' },
    { path: '/chat/mods-only', label: '# Mods-only', modOnly: true },
  ]

  const client = new Realtime({
    key: 'SlyWSw.jiu7RA:A64U3y3ty-9QLGB4Tn4NbBtlYYsx60qCI0ooT4WhNq0',
    clientId: 'Alex',
  })
  const channelName = `chat:${params.channel}`

  return (
    <AblyProvider client={client}>
      <ChannelProvider channelName={channelName}>
        <div className="grid h-[calc(100vh-72.8px)] grid-cols-4">
          <div className="border-r border-gray-200 p-5">
            <ChannelList channels={channels} />
          </div>
          <div className="col-span-2">
            <Chat channelName={channelName} />
          </div>
          <div className="border-l border-gray-200 p-5"></div>
        </div>
      </ChannelProvider>
    </AblyProvider>
  )
}
export default Page
```

Using `clsx`, we conditionally bolden the current channel to convey the channel they're currently chatting in.

Here, I hardcoded a `channels` list but this same data structure could come from a database should you want to enable users to create channels dynamically.

Return to the browser and check it out - the user can now switch channels without fumbling with the URL in the address bar:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./channel-switcher.png)

## Authentication with Clerk

With basic realtime messaging in place, it's time to authenticate Next.js users with Clerk.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./clerk.png)

[Clerk](/) is an authentication and user management platform with readyade React components and seamless [Next.js authentication middleware](/nextjs-authentication).

With Clerk, you get an assortment of beautifully-designed React components. Some are UI components like `SignInButton` and `UserButton`, while others are "helper components" that conditionally render children based on some authentication or authorization state.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./clerk-feature-grid.png)

It's typical to use both kinds of components, and they make adding fully-featured authentication to Next.js a lot smoother than you might be expecting if you have experience rolling your own auth.

### User management

In addition to authentication, Clerk handles user management. This means when a user signs up to your application, Clerk stores and manages a user record with information such as the their full name, avatar, and metadata.

In the next sections, we'll utilize this information from Clerk to identify the Ably client by the user's unique identifier instead of the hardcoded value of "Alex" we have been relying on until now.

The Clerk dashboard UI provides a convenient user management backoffice to  manage user's information such as their role, without having to implement commands or a custom backend UI.

For the purposes of this tutorial, we'll update the user's public metadata with an `isMod` property that denotes the user's role in the chat application (more on that soon).

![Authenticated Next Chat App With Ably And Clerk guide illustration](./clerk-dashboard-preview.png)

### Configure Clerk

If you haven't already, [create a Clerk account](https://dashboard.clerk.com/sign-up). Clerk doesn't ask for a credit card and you get 10,000 monthly active users for free.

Next, create a Clerk application with **username**, **phone**, and **email** sign-in options:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./create-clerk-app.png)

Still in the Clerk dashboard, under **User & Authentication** and **Email, Phone, Username**, scroll to **Personal information** and enable **Name:**

![Authenticated Next Chat App With Ably And Clerk guide illustration](./enable-name.png)

Under **Customization** and **Avatars**, enable **Initials**:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./enable-initials.png)

If a user doesn't have an avatar, Clerk will generate one based on their initials and the styling options you choose. I set the background to **Solid** and chose the color **#BDBDBD** but you can make this your own!

> \[!NOTE]
> If you enabled social sign-in options like GitHub earlier and sign-in with them, Clerk will pull the avatar from the there by default.
>
> Users can also upload a custom avatar through the `<UserButton />`.

Next, under **API Keys**, take advantage of the handy **Quick Copy** box to copy your Clerk environment variables.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./quick-copy-envs.png)

Create a file called `./.env.local` and paste them in.

It'll look something like this:

```sh {{ filename: '.env.local' }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
```

### Implement authentication with Clerk

To start authenticating Next.js users with Clerk, we'll first need to install the Clerk Next SDK:

```bash {{ filename: 'terminal' }}
npm install @clerk/nextjs
```

Next, create `src/middleware.js`:

```jsx {{ filename: 'src/middleware.js' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isChatRoute = createRouteMatcher(['/chat(.*)'])

export default clerkMiddleware((auth, req) => {
  if (isChatRoute(req)) auth().protect()
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
```

This middleware makes it so that only authenticated users can access routes starting with `/chat`.

If the user isn't authenticated, they'll be redirected to the [Clerk account portal](/docs/account-portal/overview) to sign-up or sign-in.

Next, update `src/app/layout.js`:

```jsx {{ filename: 'src/app/layout.js', ins: [2, 14, [19, 24], 28, 31, 34], del: [27] }}
import { Inter } from 'next/font/google'
import {
  ClerkLoaded,
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs'

import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Comet',
}

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body className={inter.className}>
          <nav className="flex justify-between border-b border-gray-200 p-5">
            <h1 className="font-bold">Comet</h1>
            <SignedOut>
              <SignInButton mode="modal" />
            </SignedOut>
            <SignedIn>
              <UserButton showName afterSignOutUrl="/" />
            </SignedIn>
          </nav>

          {children}
          <ClerkLoaded>{children}</ClerkLoaded>
        </body>
      </html>
    </ClerkProvider>
  )
}
```

Here, we utilize some of those Clerk React components I mentioned before:

- `<ClerkProvider />` wraps your React app to provide session and user context.
- `<SignInButton />` links to the sign-in page or shows the sign-in modal.
- `<UserButton />` renders the user's avatar with a dropdown for account management.
- `<SignedIn />` renders children only if a user is signed in.
- `<SignedOut />` renders children only if no user is signed in.
- `<ClerkLoaded />` ensures the Clerk object is loaded and accessible.

Let's see it all in action.

Open /chat/general and you'll be redirected to the Clerk account portal to sign-in.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./sign-in.png)

Sign-in successfully and Clerk will redirect the user back to /chat/general where they'll have full access to the chat!

Note the `<UserButton />` in the top-right corner, designed to resemble the user button UI popularized by Google:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./user-btn.png)

## Using Ably and Clerk together

So far, we have a functioning chat application and we have a way to authenticate users but these two parts of the app aren't really connected:

- Even though the user's signed in under their own name, every message is still technically coming from "Alex" with the "AL" avatar we hard-coded earlier. Now that we know the identity of the user, we'll need to pass that on to Ably instead of the hard-coded value.
- Even though the Next.js pages are protected behind a sign-in form, the underlying Ably topic is technically accessible to anyone. That isn't necessarily a problem for public channels like "general", but the "mods-only" channel includes sensitive messages and access should be carefully restricted. Now that we have a means to authenticate users with Clerk, let's authorize their ability to connect to restricted channels.

### Understanding tokens

As a reminder, earlier, we hard-coded the Ably API key in the client-side code:

```jsx {{ filename: 'src/app/[[...channel]]/chat/page.js' }}
const client = new Realtime({
  key: 'SlyWSw.jiu7RA:A64U3y3ty-9QLGB4Tn4NbBtlYYsx60qCI0ooT4WhNq0',
  clientId: 'Alex',
})
```

This made it convenient to connect to Ably for the purposes of this tutorial, but it's neither secure nor flexible.

In the upcoming section, we'll remove the hard-coded values. Instead, we'll point Ably at an authentication backend endpoint poised to return an Ably-compatible JWT token.

Before we write any code on that front, let's take a moment to understand Ably-compatible JWT tokens and the token flow at high level. It will make the code in the next section a lot easier to understand.

An Ably-compatible JWT token contains three crucial pieces of information:

1. The identity (`clientId`) of the authenticated user (according to Clerk and the Clerk Next.js middleware).
2. The capabilities this user has in which Ably channels, dependent on their role.
3. Additional claims, such as `isMod`.

Instead of passing a hard-coded `clientId` and `key` to Ably, we'll point the Ably JavaScript SDK to the backend endpoint we're about to implement.

Equipped with the backend authentication endpoint, the Ably SDK will automatically request the token before using it to connect to the service.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./ably-token-flow.png)

Ably uses this token for two discrete purposes:

- **Capabilities:** A token contains information about the user's Ably capabilities such as their capability to subscribe to messages in a topic named `"chat:mods-only"`. If the user doesn't have the capability to a restricted topic like `"chat:mods-only"`, Ably rejects the connection and the Ably client propagates a connection error.
- **Claims:** A token also includes claims. When Ably learns of a client's claims, the service will automatically add those claims to the metadata of events published by the authenticated client. These claims can be read by the client to authorize actions such as deleting or editing a message.

While capabilities and claims can sound similar, they are two discrete ideas in the Ably world.

We use capabilities to allow or disallow native Ably operations like subscribing or publishing to/on a specific topic or set of topics.

Capabilities, on the other hand, are metadata encoded in the token and associated with the connected client and any events they publish. Capabilities are used to authorize bespoke functionality like message deletions or edits.

### Create an Ably token Next endpoint

First things first, copy the Ably API key from earlier into .env.local.

You can copy the key over from `src/app/[[...channel]]/chat/page.js` or fetch it from the Ably dashboard again:

```sh {{ filename: '.env.local', ins: [3] }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret}}
ABLY_SECRET_KEY=XXXXXXX
```

Going forward, we'll only reference the environment variable (we'll remove it from the client-side code in due course).

Next, install `jose`, a JWT helper library:

```bash {{ filename: 'terminal' }}
npm install jose
```

Once `jose` installs, create `src/app/api/ably/route.js`:

```jsx {{ filename: 'src/app/api/ably/route.js' }}
import { currentUser } from '@clerk/nextjs/server'
import { SignJWT } from 'jose'

const createToken = (clientId, apiKey, claim, capability) => {
  const [appId, signingKey] = apiKey.split(':', 2)
  const enc = new TextEncoder()
  const token = new SignJWT({
    'x-ably-capability': JSON.stringify(capability),
    'x-ably-clientId': clientId,
    'ably.channel.*': JSON.stringify(claim),
    // 'ably.limits.publish.perAttachment.maxRate.chat': 0.1,
  })
    .setProtectedHeader({ kid: appId, alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('24h')
    .sign(enc.encode(signingKey))
  return token
}

const generateCapability = (claim) => {
  if (claim.isMod) {
    return { '*': ['*'] }
  } else {
    return {
      'chat:general': ['subscribe', 'publish', 'presence', 'history'],
      'chat:random': ['subscribe', 'publish', 'presence', 'history'],
      'chat:announcements': ['subscribe', 'presence', 'history'],
    }
  }
}

export const GET = async () => {
  const user = await currentUser()

  const userClaim = user.publicMetadata
  const userCapability = generateCapability(userClaim)

  const token = await createToken(user.id, process.env.ABLY_SECRET_KEY, userClaim, userCapability)

  return Response.json(token)
}
```

This is the backend authentication endpoint. It utilizes Clerk's `currentUser` function to access information about the [currently-authenticated user](https://clerk.com/blog/read-user-data-guide), including their ID. This user ID is used for the Ably `clientId`, ensuring every Ably client has a unique identifier.

If the user is a moderator according to the user's `publicMetadata`, we give them unrestricted Ably capabilities with `*`. Otherwise, they are limited to 3 channels:

```jsx {{ filename: 'src/app/api/ably/route.js' }}
if (claim.isMod) {
  return { '*': ['*'] }
} else {
  return {
    'chat:general': ['subscribe', 'publish', 'presence', 'history'],
    'chat:random': ['subscribe', 'publish', 'presence', 'history'],
    'chat:announcements': ['subscribe', 'presence', 'history'],
  }
}
```

What's more, we encode the `claim` in the token:

```jsx {{ prettier: false, filename: 'src/app/api/ably/route.js' }}
const token = new SignJWT({
  'ably.channel.*': JSON.stringify(claim),
})
```

Ably will include this claim in any events published by this client in topics with a name matching `*` . The `*` character is a wildcard, which Ably understands to mean the claim should be included in any event on any topic.

It's important to note that, after building up the token, we ultimately `sign` the token with `ABLY_SECRET_KEY` before returning it to the client.

Ably uses this same key to cryptographically verify the integrity of the token on the receiving end, ensuring the token was issued by a secure server (yours) and hasn't been tampered with. Coincidentally, this highlights just how important it is to keep your secret keys secret!

Next, update `src/app/chat/[[...channelName]]/page.js` to replace the hard-coded values with a link to the backend `authUrl`:

```jsx {{ filename: 'src/app/chat/[[...channelName]]/page.js', ins: [19, 20], del: [17, 18] }}
'use client'
import Chat from './chat'
import ChannelList from './channel-list'
import { Realtime } from 'ably'
import { AblyProvider, ChannelProvider } from 'ably/react'

const Page = ({ params }) => {
  const channels = [
    { path: '/chat/announcements', label: '# Announcements' },
    { path: '/chat/general', label: '# General' },
    { path: '/chat/random', label: '# Random' },
    { path: '/chat/mods-only', label: '# Mods-only', modOnly: true },
  ]

  const client = new Realtime({
    key: 'SlyWSw.jiu7RA:A64U3y3ty-9QLGB4Tn4NbBtlYYsx60qCI0ooT4WhNq0',
    clientId: 'Alex',
    authUrl: '/api/ably',
    autoConnect: typeof window !== 'undefined',
  })
  const channelName = `chat:${params.channel}`

  return (
    <AblyProvider client={client}>
      <ChannelProvider channelName={channelName}>
        <div className="grid h-[calc(100vh-72.8px)] grid-cols-4">
          <div className="border-r border-gray-200 p-5">
            <ChannelList channels={channels} />
          </div>
          <div className="col-span-2">
            <Chat channelName={channelName} />
          </div>
          <div className="border-l border-gray-200 p-5"></div>
        </div>
      </ChannelProvider>
    </AblyProvider>
  )
}
export default Page
```

Finally, update `src/app/[[...channel]]/chat/chat.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/chat.js', ins: [5, 20], del: [[17, 19]] }}
import MessageInput from './message-input'
import MessageList from './message-list'
import { useReducer } from 'react'
import { useChannel } from 'ably/react'
import { useUser } from '@clerk/nextjs'

const ADD = 'ADD'

const reducer = (prev, event) => {
  switch (event.name) {
    case ADD:
      return [...prev, event]
  }
}

const Chat = ({ channelName }) => {
  const user = {
    imageUrl: '',
  }
  const { user } = useUser()
  const [messages, dispatch] = useReducer(reducer, [])
  const { channel, publish } = useChannel(channelName, dispatch)

  const publishMessage = (text) => {
    publish({
      name: ADD,
      data: {
        text,
        avatarUrl: user.imageUrl,
      },
    })
  }

  return (
    <>
      <div className="overflow-y-auto p-5">
        <MessageList messages={messages} />
      </div>
      <div className="mt-auto p-5">
        <MessageInput onSubmit={publishMessage} />
      </div>
    </>
  )
}
export default Chat
```

Reload the page and instead of the "AB" initials I hard-coded earlier, you should see your own!

![Authenticated Next Chat App With Ably And Clerk guide illustration](./authenticated-chat.png)

## Lock moderator-only channels

The moderator-only channel isn't accessible to unauthorized users, yet, the user interface makes it look clickable.

It would be a confusing experience if your user clicks the channel only to get an Ably error because they don't have the necessary capabilities based on their role.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./err.png)

In this section, let's quickly disable moderator-only channels and show a lock icon if the user isn't authorized to participate.

First, install the `lucide-react` icon library by running the following command:

```bash {{ filename: 'terminal' }}
npm install lucide-react
```

This is where we'll get the lock icon from.

Next, update `src/app/[[...channel]]/chat/channel-list.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/channel-list.js', ins: [4, 5, 10, 11, 13, 20, 24] }}
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { clsx } from 'clsx'
import { useUser } from '@clerk/nextjs'
import { Lock } from 'lucide-react'

const ChannelList = ({ channels }) => {
  const currentPath = usePathname()
  const { user } = useUser()
  const userIsMod = user?.publicMetadata.isMod

  const createLi = (channel) => {
    const locked = channel.modOnly && !userIsMod
    return (
      <li key={channel.path}>
        <Link
          href={channel.path}
          className={clsx('flex items-center', {
            'font-bold': currentPath === channel.path,
            'pointer-events-none': locked,
          })}
        >
          {channel.label}
          {locked && <Lock className="m-1" size={16} />}
        </Link>
      </li>
    )
  }

  return <ul> {channels.map(createLi)} </ul>
}

export default ChannelList
```

Reload the page and you should see that the mods-only channel is "locked" and unclickable:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./mods-only.png)

To really test if this works, we'll need to grant our user moderator permissions. If all is working well, the channel should become "unlocked" and accessible.

## Managing moderator roles with Clerk

To promote your user to a moderator, open the Clerk **Users** tab in the dashboard and find your user.

If you've been making lots of test accounts like I sometimes do, a good tip is to sort by **Last signed in**:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./clerk-user-list.png)

Scroll to the bottom and **Edit** the public metadata to look like this:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./clerk-public-metadata.png)

Hit **Save**, then reload your app for good measure.

Your user and fellow moderators will now have access to the channel:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./mods-only-unlocked.png)

As an exercise, I like the idea of adding a `/promote {user}` command, similar to what you might find in Discord, say.

That would be totally doable on the backend using [`updateUserMetadata`](/docs/users/metadata), but I'll leave it as a stretch goal for you should you like the sound of this challenge!

## Implement message deleting

Next, let's enable users to delete messages.

Our requirements are as follows:

- Any user should be able to delete their own message (we all make typos).
- Meanwhile, moderators should have permission to delete anyone's messages.

Ably doesn't support deleting messages in the traditional sense, however, the service supports [message interactions](https://ably.com/docs/channels/messages?lang=javascript#interactions), which allow you to associate metadata such as "deleted" with a previously-sent message. This is known as a soft delete.

In the next section, we'll add a "delete message" button but, before implementing the code, I should explain how we plan to authorize message deletions. This will make the code in the next section a lot easier to understand.

At a high level, a message deletion is a special type of Ably event called a message interaction.

When the user click the "delete message" button, we'll publish an Ably message interaction called "delete" with an `extras` property that references the message they're trying to delete's identifier called a `timeserial.`

Here's a preview of code (you'll see where to slot it in the next section):

```jsx {{ title: 'Publishes a delete Ably message interaction' }}
const deleteMessage = (timeSerial) => {
  publish({
    name: DELETE,
    extras: {
      ref: {
        timeserial: timeSerial,
      },
    },
  })
}
```

Subscribers receive this event, at which point, we will execute some logic to process the message deletion:

```jsx {{ title: 'Subscribes to a delete Ably message interaction' }}
const reducer = (prev, event) => {
  switch (event.name) {
    case ADD:
      return [...prev, event]
    case DELETE:
      const isMod = JSON.parse(event.extras.userClaim).isMod
      return prev.filter((msg) => {
        const match = msg.extras.timeserial === event.extras.ref.timeserial
        const ownMsg = msg.clientId === event.clientId
        if (match && (ownMsg || isMod)) {
          return false
        }
        return true
      })
  }
}
```

- If the message identifier matches the deleted messages identifier, we move on to check if the user is allowed to delete it.
- If the message-to-delete was sent by the same client that published the delete event, we know they're allowed to delete it so we remove it from the `messages` state.
- Otherwise, they might be a moderator, so we check if the delete event has the `isMod` claim and, if so, proceed to remove the message from the messages state.

As a reminder, if the user is a moderator, Ably will include the `isMod` claim in any event they publish.

It's important to note that, while anyone can technically publish a delete message, we only process the deletion if the user owns the message or has the `isMod` claim. For anyone else who might publish a delete message, nothing happens, it has no effect.

With the theory out of the way (and a preview of the code), let's tie it all together in the next section.

### Implement a delete message button

First, create the shadcn/ui `menubar` component by running the following command, we'll need it in a moment:

```bash {{ filename: 'terminal' }}
npx shadcn-ui@latest add menubar
```

Next, enable message interactions for your Ably app.

To do this, open your app, click **Settings**, then **Add new rule**. Enter the namespace "chat", tick **Message interactions enabled**, then click **Create channel rule**.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./enable-message-interaction.png)

Once you've done that, update `src/app/[[...channel]]/chat/message-list.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/message-list.js', ins: [2, 3, 6, 7, 8, [21, 32]] }}
import { Avatar, AvatarImage } from '@/components/ui/avatar'
import { EllipsisVertical } from 'lucide-react'
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarTrigger,
} from '@/components/ui/menubar'
import { useUser } from '@clerk/nextjs'

const userCanDelete = (message, user) => {
  return user.publicMetadata.isMod || message.clientId === user.id
}

const MessageList = ({ messages, onDelete }) => {
  const { user } = useUser()
  const createLi = (message) => (
    <li key={message.id} className="group my-2 flex justify-between bg-slate-50 p-3">
      <div className="flex items-center">
        <Avatar className="mr-2">
          <AvatarImage src={message.data.avatarUrl} />
        </Avatar>
        <p>{message.data.text}</p>
      </div>

      <Menubar>
        <MenubarMenu>
          <MenubarTrigger className="cursor-pointer">
            <EllipsisVertical size={16} />
          </MenubarTrigger>
          <MenubarContent>
            <MenubarItem
              disabled={!userCanDelete(message, user)}
              onClick={() => onDelete(message.extras.timeserial)}
            >
              Delete
            </MenubarItem>
          </MenubarContent>
        </MenubarMenu>
      </Menubar>
    </li>
  )

  return <ul> {messages.map(createLi)} </ul>
}
export default MessageList
```

Finally, update `src/app/[[...channel]]/chat/chat.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/chat.js', ins: [8, [14, 23], [43, 52], 58], del: [57] }}
import MessageInput from './message-input'
import MessageList from './message-list'
import { useReducer } from 'react'
import { useChannel } from 'ably/react'
import { useUser } from '@clerk/nextjs'

const ADD = 'ADD'
const DELETE = 'DELETE'

const reducer = (prev, event) => {
  switch (event.name) {
    case ADD:
      return [...prev, event]
    case DELETE:
      const isMod = JSON.parse(event.extras.userClaim).isMod
      return prev.filter((msg) => {
        const match = msg.extras.timeserial === event.extras.ref.timeserial
        const ownMsg = msg.clientId === event.clientId
        if (match && (ownMsg || isMod)) {
          return false
        }
        return true
      })
  }
}

const Chat = ({ channelName }) => {
  const { user } = useUser()
  const [messages, dispatch] = useReducer(reducer, [])
  const { channel, publish } = useChannel(channelName, dispatch)

  const publishMessage = (text) => {
    console.log('user', user)
    publish({
      name: ADD,
      data: {
        text,
        avatarUrl: user.imageUrl,
      },
    })
  }

  const deleteMessage = (timeSerial) => {
    publish({
      name: DELETE,
      extras: {
        ref: {
          timeserial: timeSerial,
        },
      },
    })
  }

  return (
    <>
      <div className="overflow-y-auto p-5">
        <MessageList messages={messages} />
        <MessageList messages={messages} onDelete={deleteMessage} />
      </div>
      <div className="mt-auto p-5">
        <MessageInput onSubmit={publishMessage} />
      </div>
    </>
  )
}
export default Chat
```

Reload the application. Assuming you're logged into the same account and still have `isMod` , you will have the ability to **Delete** any message.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./can-delete-messages.png)

Remove the `isMod` role from your user via the Clerk dashboard and reload the page for good measure, and the **Delete** button will now be greyed out:

![Authenticated Next Chat App With Ably And Clerk guide illustration](./cant-delete-messages.png)

## Implement chat message history

Ably is not a database, but it does hold on to events history for 24-72 hours (free accounts are limited to 24 hours).

You should ideally store chat messages in your own databases for long-term processing (either using an [Ably queue](https://ably.com/docs/general/queues) or by subscribing to events on your server), but Ably history allows us to conveniently populate the UI with recent messages so that users have some context about the conversation they're joining.

To enable message history, open your Ably app in the Ably dashboard, click **Settings**, spot the rule you created in the previous step for message interactions, click **Edit,** then tick **Persist all messages**. Finally, click **Save**.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./enable-message-hist.png)

Next, update `src/app/[[...channel]]/chat/chat.js` to fetch message history. While we're here, we'll also create a React hook that automatically scrolls messages into view:

```jsx {{ filename: 'src/app/[[...channel]]/chat/chat.js', ins: [4, 32, [55, 69], 75], del: [3] }}
import MessageInput from './message-input'
import MessageList from './message-list'
import { useReducer } from 'react'
import { useReducer, useEffect, useRef } from 'react'
import { useChannel } from 'ably/react'
import { useUser } from '@clerk/nextjs'

const ADD = 'ADD'
const DELETE = 'DELETE'

const reducer = (prev, event) => {
  switch (event.name) {
    case ADD:
      return [...prev, event]
    case DELETE:
      const isMod = JSON.parse(event.extras.userClaim).isMod
      return prev.filter((msg) => {
        const match = msg.extras.timeserial === event.extras.ref.timeserial
        const ownMsg = msg.clientId === event.clientId
        if (match && (ownMsg || isMod)) {
          return false
        }
        return true
      })
  }
}

const Chat = ({ channelName }) => {
  const { user } = useUser()
  const [messages, dispatch] = useReducer(reducer, [])
  const { channel, publish } = useChannel(channelName, dispatch)
  const scrollRef = useRef(null)

  const publishMessage = (text) => {
    console.log('user', user)
    publish({
      name: ADD,
      data: {
        text,
        avatarUrl: user.imageUrl,
      },
    })
  }

  const deleteMessage = (timeSerial) => {
    publish({
      name: DELETE,
      extras: {
        ref: {
          timeserial: timeSerial,
        },
      },
    })
  }
  useEffect(() => {
    let ignore = false
    const fetchHist = async () => {
      const history = await channel.history({ limit: 100, direction: 'forwards' })
      if (!ignore) history.items.forEach(dispatch)
    }
    fetchHist()
    return () => {
      ignore = true
    }
  }, [channel])

  useEffect(() => {
    scrollRef.current.scrollIntoView()
  }, [messages.length])

  return (
    <>
      <div className="overflow-y-auto p-5">
        <MessageList messages={messages} onDelete={deleteMessage} />
        <div ref={scrollRef} />
      </div>
      <div className="mt-auto p-5">
        <MessageInput onSubmit={publishMessage} />
      </div>
    </>
  )
}
export default Chat
```

Send a test message or two and reload the page. Whereas before, the chat start completely empty, the chat now loads with recent messages, allowing your users to find their place in the conversation.

## Implement an online list

An online list creates a sense of togetherness among your users and subtly communicates who's online and likely to respond.

![Authenticated Next Chat App With Ably And Clerk guide illustration](./u-up.png)

Using Ably, we can implement such functionality using a feature called presence.

Presence provides information and realtime updates about who's "present" on a particular topic.

First, create `/src/app/chat[[...channel]]/whos-online-list.js`:

```jsx {{ filename: '/src/app/chat[[...channel]]/whos-online-list.js' }}
'use client'
import { usePresence, usePresenceListener } from 'ably/react'
import { useUser } from '@clerk/nextjs'
import { Circle } from 'lucide-react'

const WhosOnlineList = ({ channelName }) => {
  const { user } = useUser()
  const { presenceData } = usePresenceListener(channelName)
  usePresence(channelName, { fullName: user.fullName })
  const users = presenceData
  const color = '#01FE19'

  const createLi = (user) => {
    return (
      <li key={user.id} className="flex items-center">
        <Circle className="mr-1" size={8} fill={color} color={color} />
        {user.data.fullName}
      </li>
    )
  }

  return (
    <div>
      <h2 className="mb-2.5">Present and together right now with you in {channelName}:</h2>
      <ul>{users.map(createLi)}</ul>
    </div>
  )
}
export default WhosOnlineList
```

Now, for the final time in this tutorial, update `src/app/[[...channel]]/chat/page.js`:

```jsx {{ filename: 'src/app/[[...channel]]/chat/page.js', ins: [4, 33] }}
'use client'
import Chat from './chat'
import ChannelList from './channel-list'
import WhosOnlineList from './whos-online-list'
import { Realtime } from 'ably'
import { AblyProvider, ChannelProvider } from 'ably/react'

const Page = ({ params }) => {
  const channels = [
    { path: '/chat/announcements', label: '# Announcements' },
    { path: '/chat/general', label: '# General' },
    { path: '/chat/random', label: '# Random' },
    { path: '/chat/mods-only', label: '# Mods-only', modOnly: true },
  ]

  const client = new Realtime({
    authUrl: '/api/ably',
    autoConnect: typeof window !== 'undefined',
  })
  const channelName = `chat:${params.channel}`

  return (
    <AblyProvider client={client}>
      <ChannelProvider channelName={channelName}>
        <div className="grid h-[calc(100vh-72.8px)] grid-cols-4">
          <div className="border-r border-gray-200 p-5">
            <ChannelList channels={channels} />
          </div>
          <div className="col-span-2">
            <Chat channelName={channelName} />
          </div>
          <div className="border-l border-gray-200 p-5">
            <WhosOnlineList channelName={channelName} />
          </div>
        </div>
      </ChannelProvider>
    </AblyProvider>
  )
}
export default Page
```

As users come and go, so does the green light that indicates their online status.

Here, we use presence on the topic for the user's current chat channel, which means the online status is on a per-channel basis.

Alternatively, you could introduce a new topic like "clover-corp" (or whatever meaningful name you gave your app) and enable presence on a per-app basis instead. Remember, whenever you introduce a new Ably topic, you'll need to introduce another `ChannelProvider`.

## Conclusion

In this tutorial, we fleshed out a highly-featured authenticated Next.js chat application, featuring message deletion, online presence, and a moderator role!

If you found this guide useful, tell us how you used it in your app on X, and be sure to tag @clerk to let us know!

---

# Build a waitlist with Clerk user metadata
URL: https://clerk.com/blog/build-a-waitlist-with-clerk-user-metadata.md
Date: 2024-05-28
Category: Guides
Description: Learn how to use Clerk user metadata to build a waitlist for your application, as well as an admin dashboard to toggle user access.

As of Nov 2024, Clerk now has a built-in waitlist feature that you can use instead of the one outlined in this article. [Learn more](/docs/guides/waitlist).

Fast feedback when building a software-as-a-service application is critical.

This is especially true in the early days of building. The quicker you can get a working version of your product in the hands of users, the faster you can collect input and make decisions based on that input. Doing so can make an incredible difference in the success of your online business. One option is to use a platform to collect emails and notify them that the application is ready to test, but wouldn't it be nice to have them sign up for the application directly first?

In this article, you'll learn how to configure Clerk to allow users to sign up for your application but restrict their access until you explicitly allow it. You'll also learn how to create a page to interact with the user's info in Clerk to grant them access to the application.

> 💡 To follow along with this article, you'll need a [free Clerk account](https://dashboard.clerk.com/sign-up), as well as Node.js installed on your computer.

## Follow along using the Cooking with Clerk repository

Cooking with Clerk is an open-source web application built with Clerk and Next.js that will be used to apply the techniques outlined in this article. The application is an AI-powered recipe generator that uses OpenAI's API as part of the generative process. During development, we don't want to allow anyone to use it since it can easily start increasing our cost to use the OpenAI API.

If you want to follow along, clone the repository to your computer and follow the steps outlined in the `readme.md` file to get your local environment set up. The source code can be found at [https://github.com/bmorrisondev/cooking-with-clerk](https://github.com/bmorrisondev/cooking-with-clerk).

The remainder of this article assumes you will be following along using the `waitlist-article` branch, however this is entirely optional. It also assumes you've already signed in with your own account.

To build the waitlist functionality, we'll be performing the following actions:

- Configure session tokens and user metadata to flag users in and out of the waitlist.
- Set up the Clerk middleware to redirect users based on those flags.
- Design an admin dashboard that allows administrators to enable/disable users.

## Configure session tokens and user metadata

Users in Clerk can be configured with [various types of metadata](/docs/users/metadata) that can store information about that user in JSON format.

We can take advantage of this storage mechanism to assign the various flags to users of our application:

- `isBetaUser` can be used to determine if the user has access to test the application while in early development.
- `isAdmin` can be used to determine if the user has access to the admin dashboard that will be created to allow users into the beta.

Let's start by setting the `isAdmin` flag on our own account. Open the Clerk dashboard and navigate to **"Users"** from the left navigation.

![The Clerk Dashboard Users section](./the-clerk-dashboard-users-section.png)

Select the user you want to allow admin access to, then scroll to the bottom and locate the **Metadata** section. Click the first **"Edit"** button to edit the users' public metadata.

![Editing the user's public metadata in the Clerk Dashboard](./the-clerk-dashboard-edit-public-metadata.png)

Paste the following into the JSON editor and click Save.

```json
{
  "isBetaUser": true
}
```

Now even though public metadata is accessible from the front end, we'll be modifying the Clerk middleware to determine where to redirect the user once they've signed in. This means we'll need to add the public metadata to the claims so we have access to it before the user is fully loaded in the front end.

To do this, select **"Sessions"** from the left navigation, then click **"Edit"** in the **Customize session token** section.

![The sessions tab of the Clerk Dashboard](./the-sessions-tab-of-the-clerk-dashboard.png)

Paste the following into the JSON editor and click Save.

```json
{
  "metadata": "{{user.public_metadata}}"
}
```

Every token minted from now on will contain the JSON that is saved to the user's public metadata within the claims of the token.

It's worth noting that the total size of the authentication object (including custom session claims) cannot exceed 4kb.

## Route users using Clerk middleware

The Clerk middleware runs on every page load to determine if the user is authenticated and is allowed to access the requested resource using the `isProtectedRoute` helper. For example, the following middleware configuration will protect every page that starts with the `/app` route and the `/api` route:

```tsx {{ filename: 'src/middleware.ts' }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher(['/app(.*)', '/api(.*)'])

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect()
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}
```

Clerk will automatically parse the session claims (where the public metadata is) within the `auth()` function, which means it's accessible to us during this process like so:

```tsx
const { sessionClaims } = auth()
```

Using this, we can determine if the session claims contain our `isBetaUser` flag. Update the `src/middleware.ts` file to match the following:

```tsx {{ filename: 'src/middleware.ts', ins: [[11, 13], [20, 25]] }}
// 👉 Update the imports
import { ClerkMiddlewareAuth, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isProtectedRoute = createRouteMatcher(['/app(.*)', '/api(.*)'])

// 👉 Create a type to define the metadata
type UserMetadata = {
  isBetaUser?: boolean
}

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect()

    // 👉 Use `auth()` to get the sessionClaims, which includes the public metadata
    const { sessionClaims } = auth()
    const { isBetaUser } = sessionClaims?.metadata as UserMetadata
    if (!isBetaUser) {
      // 👉 If the user is not a beta user, redirect them to the waitlist
      return NextResponse.redirect(new URL('/waitlist', req.url))
    }
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}
```

From now on, any user that does not have `isBetaUser` defined in their public metadata will instead be redirected to a page that simply tells them that they are on the waitlist. It's also worth noting that since this check is performed after `auth().protect()`, this function will only run if the user is logged in with a Clerk account, preventing it from running when not needed.

To see this in action, start the project on your computer by running `npm run dev` in your terminal and navigate to the URL displayed in the terminal (the default is `http://localhost:3000`, but may differ if another process is using port 3000).

![Cooking with Clerk homepage](./cooking-with-clerk-homepage.png)

Click **"Sign In"** in the upper right and log in with the user account you used during setup. You should be able to access and test the app with no issues.

![Cooking with Clerk with recipes generated](./cooking-with-clerk-recipes.png)

Now sign out using the user menu, and sign in again with a different account. You'll notice that instead of accessing the application, you are redirected to `/waitlist`. This is the middleware at work!

![Cooking with Clerk waitlist](./cooking-with-clerk-waitlist.png)

## Creating the admin area

Now that we've built the capability into the app to require the `isBetaUser` flag to be set, we need a way to set this for users interested in testing the app. Sure, it can be done from within the Clerk dashboard, but we can also take advantage of the Clerk SDK to create a page that allows us to perform this action within the app. Start by creating the `src/app/admin/page.tsx` file and paste the following code into it. This will create a page at `/admin` that displays an empty table.

```tsx {{ filename: 'src/app/admin/page.tsx' }}
import React from 'react'
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import UserRow from './UserRow'
import { clerkClient } from '@clerk/nextjs/server'

export const fetchCache = 'force-no-store'

async function Admin() {
  // 👉 Gets the users from the Clerk application
  let res = await clerkClient.users.getUserList()
  let users = res.data

  return (
    <main>
      <h1 className="my-2 text-2xl font-bold">Admin</h1>
      <h2 className="my-2 text-xl">Users</h2>
      <Table className="rounded-lg border border-gray-200">
        <TableHeader>
          <TableRow>
            <TableHead className="w-[100px]">Name</TableHead>
            <TableHead>Email</TableHead>
            <TableHead className="text-right">Beta enabled?</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>{/* 👉 User records will be displayed here */}</TableBody>
      </Table>
    </main>
  )
}

export default Admin
```

Next, we're going to create a client component that will display a row for each user within the table named `UserRow`. Before we do that, however, we need a server action that the `UserRow` component can use to interact with the Clerk Backend SDK to toggle the `isBetaUser` flag within a user's public metadata. Create the `src/app/admin/actions.ts` file and populate it with the following:

```tsx {{ filename: 'src/app/admin/actions.ts' }}
'use server'

import { clerkClient } from '@clerk/nextjs/server'

export async function setBetaStatus(userId: string, status: boolean) {
  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      isBetaUser: status,
    },
  })
}
```

Now create the `src/app/admin/UserRow.tsx` file with the following contents. This will be used to render each user in a row on the admin page.

```tsx {{ filename: 'src/app/admin/UserRow.tsx' }}
'use client'
import React, { useState } from 'react'
import { TableCell, TableRow } from '@/components/ui/table'
import { Switch } from '@/components/ui/switch'
import { setBetaStatus } from './actions'

// 👉 Define the necessary props we need to render the component
type Props = {
  name: string
  id: string
  emailAddress?: string
  metadata?: UserPublicMetadata
}

function UserRow({ name, id, metadata, emailAddress }: Props) {
  // 👉 Set the initial state of `isBetaUser` based on the metadata
  const [isBetaUser, setIsBetaUser] = useState(metadata?.isBetaUser || false)

  // 👉 Calls the server action defined earlier and sets the state on change
  async function onToggleBetaStatus() {
    try {
      await setBetaStatus(id, !isBetaUser)
      setIsBetaUser(!isBetaUser)
    } catch (err) {
      console.error(err)
    }
  }

  return (
    <TableRow>
      <TableCell className="flex flex-col">
        <span>{name}</span>
        <span className="text-xs text-gray-600 italic">{id}</span>
      </TableCell>

      <TableCell>{emailAddress}</TableCell>

      <TableCell className="text-right">
        <Switch onCheckedChange={onToggleBetaStatus} checked={isBetaUser} aria-readonly />
      </TableCell>
    </TableRow>
  )
}

export default UserRow
```

Finally, update `src/app/admin/page.tsx` by importing the new component and adding it to the table:

```tsx {{ filename: 'src/app/admin/page.tsx', ins: [10, [31, 37]] }}
import React from 'react'
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { clerkClient } from '@clerk/nextjs/server'
import UserRow from './UserRow'

export const fetchCache = 'force-no-store'

async function Admin() {
  let res = await clerkClient.users.getUserList()
  let users = res.data

  return (
    <main>
      <h1 className="my-2 text-2xl font-bold">Admin</h1>
      <h2 className="my-2 text-xl">Users</h2>
      <Table className="rounded-lg border border-gray-200">
        <TableHeader>
          <TableRow>
            <TableHead className="w-[100px]">Name</TableHead>
            <TableHead>Email</TableHead>
            <TableHead className="text-right">Beta enabled?</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {users?.map((u) => (
            <UserRow
              key={u.id}
              id={u.id}
              name={`${u.firstName} ${u.lastName}`}
              metadata={u.publicMetadata}
              emailAddress={u.emailAddresses[0]?.emailAddress}
            />
          ))}
        </TableBody>
      </Table>
    </main>
  )
}

export default Admin
```

Open the app in your browser again and navigate to `/admin`, you should see a list of the users from your Clerk application displayed in a table. Notice how only the account you manually added `isBetaUser` to during the first part of this guide has the toggle enabled under the Beta enabled? column.

![Cooking with Clerk admin panel](./cooking-with-clerk-admin-panel.png)

Now, if you toggle another user on and log in again with that account, you should be redirected to `/app` instead of `/waitlist`! Furthermore, if you open the application in the Clerk dashboard and review the user's public metadata, you should see that `isBetaUser` has been enabled via the dashboard.

## Securing the admin page

At this point, we've effectively built the waitlist functionality, as well as created a polished experience for controlling the flags enabled on the user account. The problem is that the middleware is set up only to protect `/app` and not `/admin`, so anyone with the beta flag could technically access the admin panel. With a few minor tweaks to `middleware.ts`, we can also prevent users from accessing the admin panel:

```tsx {{ filename: 'src/middleware.ts', ins: [7, 12, [21, 29]], del: [20] }}
import { ClerkMiddlewareAuth, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isProtectedRoute = createRouteMatcher(['/app(.*)', '/api(.*)', '/admin(.*)'])

type UserMetadata = {
  isBetaUser?: boolean
  isAdmin?: boolean
}

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect()

    const { sessionClaims } = auth()
    const { isBetaUser } = sessionClaims?.metadata as UserMetadata
    const { isAdmin, isBetaUser } = sessionClaims?.metadata as UserMetadata
    if (isAdmin) {
      // 👉 If the user is an admin, let them proceed to anything
      return
    }
    if (!isAdmin && req.nextUrl.pathname.startsWith('/admin')) {
      // 👉 If the user is not an admin and they try to access the admin panel, return an error
      return NextResponse.error()
    }
    if (!isBetaUser) {
      return NextResponse.redirect(new URL('/waitlist', req.url))
    }
  }
})

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}
```

Now whenever someone tries to access `/admin` without the `isAdmin` flag set in their Clerk user metadata, they'll get a 404 page instead of the admin panel.

## Conclusion

Clerk user metadata can be extremely useful for storing various information about the user.

This is simply one example of how to use metadata. If you need some more inspiration, we also have a blog post showing how to build an onboarding flow using a similar approach that I recommend reading!

Do you have an interesting way you've used user metadata in your application? Share it on X and let us know by tagging [@clerk](https://x.com/clerk)!

---

# How to use Clerk with PostHog Identify in Next.js
URL: https://clerk.com/blog/how-to-use-clerk-with-posthog-identify-in-nextjs.md
Date: 2024-05-09
Category: Guides
Description: Learn how to configure your Next.js applications to send Clerk user data to PostHog for analytics and data analysis.

The ability to gather analytics and trace actions on your website is important, but data analysis can be tricky.

[PostHog](https://posthog.com) is an open-source platform that enables developers to easily add features to their web apps such as product analysis, session replay, feature flags, and more. By default, PostHog will automatically start gathering data about your web application and how it's being used. When properly configured, you can also tie data about your Clerk users to the events that are captured, whether this is using default events like the `$pageview` event, or custom events you program directly into your code.

In this article, you'll learn how to link Clerk users with data in PostHog to confidently identify what specific users are doing in your Next.js application.

## How PostHog works

When PostHog is first installed, it will automatically start creating sessions for users who visit your web applications.

These sessions are identified by [universally unique identifiers](https://www.uuidtools.com/what-is-uuid) (UUIDs) and each ID is specific to the browser that is accessing the site. PostHog will then start gathering data about the usage of the web app such as page views, elements clicked, etc. While this functionality in itself is super powerful, we live in a world where users often interact with their favorite services from a number of different devices.

This is where the [`identify`](https://posthog.com/docs/product-analytics/identify#how-identify-works) function comes in.

### Identifying users with `identify`

The `identify` function in PostHog allows you to add your own user IDs to sessions, along with any custom metadata you'd like to include.

Doing so allows you to enrich the data gathered by PostHog, and allows the PostHog dashboard to display the captured events with the user profile along with any additional details you associated with the session. And one of the great outcomes of using `identify` is you can trace the actions of your users across different devices, as PostHog will link those sessions based on the user ID you configured.

This can be incredibly useful when it comes to debugging user issues or gathering intel about specific features.

## How to configure a Clerk and PostHog to work together

Now that you understand how `identify` can be used to associate individual users with PostHog events that span across devices, let's look at how to configure this in your Next.js application. The demo app is a relatively simple app that uses a single input and button to generate recipe suggestions using OpenAI's developer platform and render them in a grid. This project already has [Clerk installed and configured](/docs/quickstarts/nextjs), a PostHog project created, and the library configured as directed in the [Next.js guide in their docs](https://posthog.com/docs/libraries/next-js).

![The 'Cooking with Clerk' home page with a list of recipes based on 'Ground beef, garlic, asian themed' query](./cooking-with-clerk.png)

By default, PostHog will capture certain actions as they're performed in the web app, such as typing in a field or clicking a button.

![The PostHog dashboard with a list of events from an anonymous user](./posthog-dashboard-1.png)

Notice, however, that the Person column shows a UUID instead of a user name. This will be the case even if I am signed into my app using Clerk. This can be fixed by using the `identify` function from PostHog along with the information Clerks Next.js library makes available in a few hooks.

The first we'll use from Clerk is `useAuth`, which provides some basic information about the session such as the login status and user ID. The second hook that will be used is `useUser` which returns more detailed information about the user who's logged in. By adding another `useEffect` the `PostHogPageView` component created in their onboarding guide, we can check to make sure the user is logged in, that the info we need is available, and the `identify` function has not already been called before linking the user's data to the session. The `posthog._isIdentified()` function lets us know if `identify` has been run previously, so this can be used to prevent the function from running when it doesn't need to.

```tsx {{ filename: 'app/PostHogPageView.tsx' }}
'use client'

import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
import { usePostHog } from 'posthog-js/react'

// 👉 Import the necessary Clerk hooks
import { useAuth, useUser } from '@clerk/nextjs'

export default function PostHogPageView(): null {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const posthog = usePostHog()

  // 👉 Add the hooks into the component
  const { isSignedIn, userId } = useAuth()
  const { user } = useUser()

  // Track pageviews
  useEffect(() => {
    if (pathname && posthog) {
      let url = window.origin + pathname
      if (searchParams.toString()) {
        url = url + `?${searchParams.toString()}`
      }
      posthog.capture('$pageview', {
        $current_url: url,
      })
    }
  }, [pathname, searchParams, posthog])

  useEffect(() => {
    // 👉 Check the sign-in status and user info,
    //    and identify the user if they aren't already
    if (isSignedIn && userId && user && !posthog._isIdentified()) {
      // 👉 Identify the user
      posthog.identify(userId, {
        email: user.primaryEmailAddress?.emailAddress,
        username: user.username,
      })
    }
  }, [posthog, user])

  return null
}
```

Now by signing in and performing those same actions, my account info is also displayed in the PostHog dashboard!

![The PostHog dashboard with a list of events from an identified user](./posthog-dashboard-2.png)

Now the one thing that needs to be addressed is logging out. If the info passed into `identify` is not cleared out and the person is using a shared computer, there is a chance that you'd get some bad data since your app might think the previous user is still logged in. By adding another few lines of code to the `useEffect` created, you can configure your app to clear the data when the user logs out:

```tsx
useEffect(() => {
  // 👉 Check the sign in status and user info,
  //    and identify the user if they aren't already
  if (isSignedIn && userId && user && !posthog._isIdentified()) {
    // 👉 Identify the user
    posthog.identify(userId, {
      email: user.primaryEmailAddress?.emailAddress,
      username: user.username,
    })
  }

  // 👉 Reset the user if they sign out
  if (!isSignedIn && posthog._isIdentified()) {
    posthog.reset()
  }
}, [posthog, user])
```

## Conclusion

With a small modification to the default `PostHogPageView` component, you can dramatically increase user traceability in your web app analytics! This method enables you to not only trace user flows within a single session, but with every session they create over a number of devices.

If you found this guide useful, tell us how you used it in your app on X, and be sure to tag [@clerk](https://x.com/clerk) to let us know!

---

# How to secure API Gateway using JWT and Lambda Authorizers with Clerk
URL: https://clerk.com/blog/how-to-secure-api-gateway-using-jwt-and-lambda-authorizers-with-clerk.md
Date: 2024-04-29
Category: Guides
Description: Learn what API Gateway authorizers are, how they work, and how to use them with Clerk to secure your API endpoints using JWT and Lambda authorizers.

One of the common ways to access AWS services over HTTP is through API Gateway.

API Gateway acts as a centralized entry point for many of the services offered through AWS. For example, you can configure serverless Lambda functions that are capable of accepting HTTP events from API Gateway for processing, enabling you to build a completely serverless backend for your application or service. Allowing any traffic into an AWS account should be done securely, otherwise, you run the risk of services being taken advantage and causing issues such as data exfiltration or a surprise AWS bill. Luckily, API Gateway offers a feature called *authorizers* that can be used to secure your endpoints before traffic ever reaches the service on the other side.

In this article, you'll learn what API Gateway authorizers are, how they work, and [how to use them with Clerk](#using-clerk-with-api-gateway-authorizers).

## What are API Gateway Authorizers?

API Gateway authorizers are a feature of API Gateway that allows you to lock down your API endpoints so that only authorized requests are permitted.

API Gateway is compatible with a wide array of AWS services, allowing you to mix and match multiple services behind a single domain to precisely craft the service that your users need. While services such as Lambda or EC2 can have built-in logic to verify the request, something like DynamoDB does not have that capability. When an authorizer is attached to an endpoint, API Gateway will first use the authorizer to verify that the request being sent in is by an authorized party before passing it through to the service, or denying the request if it's unauthorized.

To better explain how an authorizer works, I'll use the example of the serverless API in the introduction of this article.

## How Authorizers Work

Let's assume you have a simple serverless API that combines API Gateway and a series of Lambda functions.

When a request comes into the API, the request will be proxied to one of the associated Lambda functions for processing before sending a response back to the caller. Without authorizers, the request is sent directly to the Lambda function, regardless of who sent it or where it comes from. This means that the code for each Lambda would need to individually verify that the request is valid.

![A diagram showing API Gateway passing through a request](./api-gateway-diagram.jpg)

Now let's look at the same example with an authorizer attached to each endpoint. When a request comes into an endpoint that is protected with an authorizer, API Gateway will first send the request to the authorizer to verify and deny the request depending on if the it passes the checks defined by the authorizer.

![A diagram showing API Gateway using an authorizer](./api-gateway-authorizer-diagram.jpg)

Authorizers enable you to centralize your authorization logic, protect services that would otherwise be difficult to protect, and utilise caching to reduce your AWS bill.

## Understanding JWT and Lambda Authorizers

There are several types of authorizers available depending on the type of API Gateway configuration you have, but we're going to focus on the two that are compatible with Clerk: JWT and Lambda authorizers

### JWT Authorizers

A JWT authorizer uses an [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) endpoint to validate tokens based on the included JSON Web Key Set (JWKS).

The OpenID Connect Discovery endpoint contains information about the identity provider (also known as the IdP) that can be used when configuring services to support authenticating with the IdP. This endpoint contains information such as who is issuing the JWT (or token), what authorization scopes are supported, what information is included in the tokens, etc.

One of the things that is often included in the Discovery endpoint is the URI of the JWKS, a collection of public keys that can be used to verify the signature on a token. If the signature of a token is valid, the information about the user that is included within it can be considered trustworthy. When a request comes in that is protected by a JWT authorizer, AWS will use the publically available JWKS, along with an audience (`aud`) value in the claims, to validate the token and pass the request on if it's valid.

JWT Authorizers are a simple way of verifying requests using the JWKS but are only available on the HTTP-type of API Gateway instances.

### Lambda Authorizers

Lambda authorizers allow you to create a custom Lambda function using the language of your choice to validate inbound requests.

They offer the most flexibility but are also relatively complex when compared to JWT authorizers. When a Lambda authorizer is executed, the configured authorization header is passed along to a Lambda function in the event parameter, but it's up to you to write the code that validates the event and responds with an IAM policy. However, because you are essentially writing code, you can even parse the claims and conditionally permit the request based on more than just an audience value, something we'll explore later in this article.

Lambda authorizers are compatible with both REST and HTTP API Gateway types.

## Using Clerk with API Gateway Authorizers

Clerk's use of [JWTs](/blog/how-we-roll-sessions) makes our service compatible with API Gateway authorizers when configured as explained in this article.

When a user signs into an application with Clerk, a short-lived token is created and stored in the browser cookies. Clerk's libraries allow you to easily extract this token and use it however you need, including adding it to the authorization header of an HTTP request:

```jsx
'use client'
import { useSession } from '@clerk/nextjs'

export default function Home() {
  const { session } = useSession()

  async function callApi() {
    const token = await session?.getToken()
    await fetch(url, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    })
  }

  // Code removed for brevity...
}
```

Now let's take a look at how to configure both a JWT and Lambda authorizer to work with Clerk.

### Prerequisites

To follow along with this portion of the article, you'll need the following configured:

- An AWS account, and familiarity with Lambda and API Gateway
- A [Clerk account](/sign-up)
- Node and NPM installed on your computer

> While everything discussed attempts to be covered as part of the AWS free tier, be aware that we will be creating resources that may cost real money.

### Using Clerk with JWT Authorizers

As mentioned earlier, JWT Authorizers require you to know the OpenID Connect Discovery endpoint, as well as an `aud` value in the claims of the token being checked, so let's start by gathering this info.

In the Clerk Dashboard, select **"API Keys"** from the navigation, then click **"Show API URLs"**. This will show you URLs for the Frontend and Backend API. Take note of the value value in the **Frontend API URL** field, this will be used as the OpenID Connect Discovery endpoint as Clerk automatically sets up this endpoint for each application created in your account.

Next, you'll need to configure the `aud` claim value since Clerk tokens do not contain this by default. The value can be any arbitrary string, it just needs to match what's specified in the authorizer configuration, which we'll do in the next step. This example uses "ClerkJwtAuthorizer" as the value, but you're free to use something else.

To add a static value to all tokens, select **"Sessions"** from the navigation, then the **"Edit"** button under **Customize session token**. In the modal that appears, modify the **Claims** to include an `aud` value. If you don't have any other custom claims defined, it should look like this:

```json
{
  "aud": "ClerkJwtAuthorizer"
}
```

In an HTTP-type API Gateway instance in AWS, you can create an authorizer by selecting **"Authorization"** in the left navigation, then the **"Manage authorizers"**, and finally the **"Create"** button.

![Create an HTTP JWT authorizer](./1-http-create-authorizer.png)

JWT Authorizer is selected by default, but you'll need to populate the following values:

- **Name** - A friendly name for the authorizer.
- **Identity source** - Where the token should be referenced in the request. This can be left with the default value of “*$request.header.Authorization*” which will use the `Authorization` header of the inbound request.
- **Issuer URL** - This should be set to the Frontend API URL value from earlier.
- **Audience** - The value set earlier in the `aud` field of the session claims (you may have to click **"Add Audience"** for this input to appear).

![The settings view when creating an HTTP JWT authorizer](./2-http-authorizer-settings.png)

Once you click **"Create"**, you'll be returned to the previous screen. From here, select the **"Attach authorizers to routes"** tab. This will display a list of the routes configured in your API. Choose a route from the list, then use the **"Select existing authorizer dropdown"** to select the authorizer you created earlier, then **"Attach authorizer"**.

![A screenshot of the AWS UI showing how to attach an authorizer to a route](./3-http-attach-authorizer.png)

Repeat this process for every route you want to protect, and any request that are executed against these routes will automatically be protected using the user's Clerk session token.

> One caveat to using JWT authorizers is that they are incompatible with the `ANY` method available in AWS if the API will be called from the browser. This is due to the fact that CORS preflight requests will not include the Authorization header, which will cause the authorizer to deny the preflight request.

### Using a Lambda Authorizer with Clerk

While Lambda authorizers are compatible with HTTP-type API Gateways, they are more common in REST types, so this section of the guide will move over to a REST API. Since Lambda authorizers are limited to a short execution window, we'll be using [Clerk networkless verification](/docs/references/nodejs/token-verification) to make sure the request is authorized. Essentially we'll be embedding the public key of the key set into the code to eliminate unnecessary network requests, making the code as efficient as possible.

Start in the Clerk dashboard and navigate back to the **"API Keys"** section. Now select **"Show JWT public key."** From the modal, copy the block of text under **PEM Public Key** for later use.

Now let's create a Lambda function in AWS that will serve as the authorizer. As stated earlier, you can use any supported language, but I'll be using JavaScript for this demo. Start a terminal session on your workstation and run the following commands to initialize a new Node project and install the `jsonwebtoken` library.

```bash
# Initialize a new NPM project
npm init -y

# Install the `jsonwebtoken` dependency
npm install jsonwebtoken
```

In the root of the project, create an `index.js` file and populate it with the following code, replacing the contents of the `publicKey` variable with the **PEM Public Key** string from earlier (it should be a multiline string in the code). Notice how we're also checking the user's metadata included in the claims to make sure they have the role of “admin”, something that is not possible with JWT authorizers.

```jsx
import jwt from 'jsonwebtoken'

const publicKey = `{PASTE YOUR PEM KEY HERE}`

export async function handler(event, context, callback) {
  // Extract the token from the Authorization header
  const token = event.authorizationToken.split(' ')[1]

  // Verifies and decodes the JWT
  const claims = jwt.verify(token, publicKey)

  // Check if the user is an admin
  if (claims.metadata.role === 'admin') {
    callback(null, generatePolicy(claims.metadata, claims.sub, 'Allow', event.methodArn))
  } else {
    callback(null, generatePolicy(null, claims.sub, 'Deny', event.methodArn))
  }
}

function generatePolicy(metadata, principalId, effect, resource) {
  const authResponse = {
    principalId: principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: effect,
          Resource: resource,
        },
      ],
    },
  }
  if (metadata) {
    authResponse.context = metadata
  }
  return authResponse
}
```

> The above code is a modified version of the sample provided in the [AWS docs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html).

Now create a zip folder that contains the following files and folders:

- The entire `node_modules` folder.
- `package.json`
- `index.js`

Now we can upload this zip folder into a new AWS Lambda function. Navigate to the Lambda section of AWS and create a new Lambda with the following settings:

- **Name**: ClerkLambdaAuthorizer
- **Runtime**: Node.js 20.x

The rest of the values can be left at their defaults. Scroll to the bottom and click **"Create function"**. After the function is created, scroll down to the **Code source section** and click the **"Upload from"** button, then **".zip file"**. Select the zip file you created and upload it to Lambda.

![The view of AWS Lambda that shows where to upload a .zip file to Lambda](./4-rest-upload-lambda.png)

Now navigate to a REST-type API Gateway instance. Authorizers can be created by selecting the **"Authorizers"** item from the left navigation, then clicking **"Create an authorizer"**.

![A screenshot of the AWS dashboard showing where where to create an authorizer for a REST API](./5-rest-create-authorizer.png)

Give the authorizer a name and select the Lambda function that was created in the previous section.

![The configuration view with in AWS when creating a Lambda authorizer](./6-rest-create-authorizer-view.png)

Scroll down a bit and enter “*Authorization*” in the **"Token source field"** then click **"Create authorizer"**.

![Where the Authorization header is configured for a Lambda authorizer](./7-rest-create-authorizer-view-2.png)

To attach the authorizer to a request, click **"Resources"** in the navigation, select the route and method you want to add the authorizer to, then scroll down and click **"Edit"** in the Method request settings section.

![The AWS UI showing where to edit a route to attach an authorizer](./8-rest-edit-route.png)

Use the dropdown under **"Authorization"** to select the authorizer you just created, then click **"Save"** at the bottom of the screen.

![The AWS UI showing where to attach a Lambda authorizer to a route](./9-rest-attach-authorizer.png)

At this point, the authorizer is configured, however, API Gateway will still deny the request if the Clerk session token hasn't been customized to include the user metadata. To do this, go to the Clerk dashboard and select **"Sessions"** from the left navigation. Then click **"Edit"** in the **Customize session token** and update the Claims text input to match the following:

```json
{
  "metadata": "{{user.public_metadata}}"
}
```

Now every user that has `"role": "admin"` in their public metadata will be allowed to make requests to endpoints secured with the authorizer we created. When this role is set in the public metadata, AWS will follow a flow that matches the original depiction of an authorizer:

![A diagram showing the Lambda authorizer allowing a request after checking a role](./10-diagram-showing-role-allowed.jpg)

1. A request is made to the API Gateway endpoint.
2. The token is sent to the Lambda authorizer first.
3. The Lambda authorizer checks the role of the user.
4. If the role is "admin", the request is allowed to pass through to the backend Lambda function.

Conversely, if the role is not "admin", the request will be denied:

![A diagram showing the Lambda authorizer denying a request after checking a role](./11-diagram-showing-role-denied.jpg)

1. A request is made to the API Gateway endpoint.
2. The token is sent to the Lambda authorizer first.
3. The Lambda authorizer checks the role of the user.
4. If the role is "admin", the request is allowed to pass through to the backend Lambda function.

## Conclusion

Authorizers act as a first line of defense for API Gateway endpoints. Thankfully, Clerk supports this approach to securing your API Gateway with a few simple steps. With the flexibilty of modifying session tokens in the platform, you can easily add additional claims and fine-tune the access control of your API Gateway endpoints.

In this guide, we covered how to use JWT Authorizers to protect endpoints using the public keys of a Clerk instance, as well as Lambda Authorizers for more fine-grained control of who can access your API Gateway endpoints.

---

# Create Your Own Custom User Menu with Radix - Part 2
URL: https://clerk.com/blog/create-custom-user-menu-radix-pt-2.md
Date: 2024-01-29
Category: Guides
Description: Extend your Radix powered custom User Menu to turn it into a Sign In or User Profile component

Code samples are from @clerk/nextjs 4.29.5, @radix-ui/react-dropdown-menu 2.0.6, class-variance-authority 0.7.0 and
@heroicons/react 2.1.1

Welcome to the second part of building a custom user menu! [In the first part,](/blog/create-custom-user-menu-radix) we built a replacement for Clerk’s `<UserButton /> `using the Radix Dropdown primitive and some of Clerk’s hooks. Now we’ll be upgrading our user button with sign-in functionality when the user is not logged in as well as improve the behavior of the component in several ways.

## Refactoring the component

The first step is to refactor the component so it's ready to build upon. Take the contents of the `return` and create a new component in the file above the exported component. Call the new component `<UserButtonAndMenu />` and paste the copied code. We will need to add in the destructures for the user method from the `useUser()` hook, `signOut()`, and `openUserProfile()` from the `userClerk()` hook, and the router method from the `userRouter()` hook.

The first step is to refactor the component so it's ready to build upon.

- Create a new component in the file called `<UserButtonAndMenu />`
- Move the JSX from the original `<UserButton />` into the `<UserButtonAndMenu />` component
- Move `user` method from `useUser()` hook to the new component
- Move `signOut()`, and `openUserProfile()` from `userClerk()` hook to the new component
- Move `router` method from `userRouter()` hook to the new component\\

In the `<UserButton />` component, we’re going to leave the check for `isLoaded` and add a `user.id` check using `if ( !user.id ) {}` and return the `<SignInButton />` component from Clerk if there is no user.

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useUser, useClerk, SignInButton } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import Link from 'next/link'

// Create a new UserButtonandMenu component and move the old return into this
const UserButtonAndMenu = () => {
  const { signOut, openUserProfile } = useClerk()
  const router = useRouter()
  const { user } = useUser()

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        {/* Render a button using the image and email from `user` */}
        <button className="flex flex-row rounded-xl border border-gray-200 bg-white px-4 py-3 text-black drop-shadow-md">
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl}
            width={30}
            height={30}
            className="mr-2 rounded-full border border-gray-200 drop-shadow-sm"
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="mt-4 w-52 rounded-xl border border-gray-200 bg-white px-6 py-4 text-black drop-shadow-2xl">
          <DropdownMenu.Label />
          <DropdownMenu.Group className="py-3">
            <DropdownMenu.Item asChild>
              {/* Create a button with an onClick to open the User Profile modal */}
              <button onClick={() => openUserProfile()} className="pb-3">
                Profile
              </button>
            </DropdownMenu.Item>
            <DropdownMenu.Item asChild>
              {/* Create a fictional link to /subscriptions */}
              <Link href="/subscriptions" passHref className="py-3">
                Subscription
              </Link>
            </DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator className="my-1 h-px bg-gray-500" />
          <DropdownMenu.Item asChild>
            {/* Create a Sign Out button -- signOut() takes a call back where the user is redirected */}
            <button onClick={() => signOut(() => router.push('/'))} className="py-3">
              Sign Out{' '}
            </button>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

// Refactor to show the default <SignInButton /> if the user is logged out
// Show the UserButtonAndMenu if the user is logged in
export const UserButton = () => {
  const { isLoaded, user } = useUser()

  if (!isLoaded) return null

  if (!user?.id) return <SignInButton />

  return <UserButtonAndMenu />
}
```

## Improving the component

If you saved your work and tested it, you will see that the refactoring has already accomplished the base goal — it is now both a Sign-In button and a User Button/User Menu. We can still improve the component and provide a better user experience, so let’s do a few more refactors.

The first step is moving the button to its component. We’ll also add in a `forwardRef` to plan for the later improvements.

Tip: You might have your own custom button already created or available from a library you are using. If so then you
can use that in place of this one.

```tsx
// Add import
import * as React from 'react'

// Create a new <Button /> component using the same classes
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <button ref={ref} className={className} {...props}>
        {children}
      </button>
    )
  },
)
```

The second step is to refactor part of the `<UserButtonAndMenu /`> component. We want to take advantage of the new `<Button />`

```tsx
const UserButtonAndMenu = () => {
  const { user } = useUser()
  const { signOut, openUserProfile } = useClerk()
  const router = useRouter()

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        {/* Swap <button /> to the new <Button /> */}
        <Button>
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl!}
            width={30}
            height={30}
            className="mr-2 rounded-full border border-gray-200 drop-shadow-sm"
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </Button>
      </DropdownMenu.Trigger>
    </DropdownMenu.Root>

    // rest of the component remains the same
  )
}
```

The third step is to refactor the top-level `<UserButton /> `component to also use the new `<Button />` component. This button will use `openSignIn() `from `useClerk()` to programmatically open the sign-in modal. This results in a custom button and removal of the Clerk `<SignInButton />`

```tsx
// Update @clerk/nextjs imports
import { useUser, useClerk } from '@clerk/nextjs'

export const UserButton = () => {
  const { isLoaded, user } = useUser()
  // Bring in openSignIn
  const { openSignIn } = useClerk()

  if (!isLoaded || !user?.id) {
    /* Use the new <Button /> component for the sign-in button */
    return <Button onClick={() => openSignIn()}>Sign In</Button>
  }

  return <UserButtonAndMenu />
}
```

You can see that we left the user profile image inside of `<UserMenuAndButton />` and passed it to the `<Button />` as a child. Depending on your need you could hoist the image handling into the `<Button />` — that’s totally up to the needs of what you’re building.

## Refining the component

Everything is working nicely at this point, and the structure is in a great place to build on. That said, we can add a few refinements to elevate the user experience. Let’s start by installing an icon package and the class-variance-authority package.

This is using `@heroicons/react` as it's a simple, one-stop icon package. Use other icons that might suit your design
or needs better.

```bash
pnpm install @heroicons/react class-variance-authority
```

With that installed, let’s do another refactor of the Button. We’ll use `class-variance-authority` to expand on what the button can do. This is a very structured approach and provides TypeScript support. We will set up a `variant` and a `size`, use the resulting `primary` and `regular` for the main button, and `menu` and `small` for the buttons in the dropdown menu.

```tsx
// Add imports
import { VariantProps, cva } from 'class-variance-authority'

// Configure the styles for the Button and its variants and sizes
const button = cva(['flex', 'flex-row', 'items-center', 'rounded-xl'], {
  variants: {
    variant: {
      primary: [
        'border',
        'border-gray-200',
        'bg-white',
        'text-black',
        'drop-shadow-md',
        'hover:bg-stone-100',
        'hover:text-stone-800',
      ],
      menu: ['bg-transparent', 'text-gray-800/70', 'hover:text-gray-900'],
    },
    size: {
      regular: ['px-4', 'py-3'],
      small: ['py-3', 'py-2'],
    },
  },
  defaultVariants: {
    variant: 'primary',
    size: 'regular',
  },
})

// Extend the default Button types with props created by create-variance-authority
interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof button> {}

// Create a new <Button /> component using the same classes
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, size, children, className, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className="flex flex-row rounded-xl border border-gray-200 bg-white px-4 py-3 text-black drop-shadow-md"
        {...props}
      >
        {children}
      </button>
    )
  },
)
```

This `<Button />` component can be used anywhere in your application — if you don’t have a `<Button />` you’ve built
already or one from a library then you can move this out of the `<UserButton />` and instead use it application-wide.

Now that our `<Button />` has reached its final form, let’s import some icons.

```typescript
// Add import
import {
  ArrowRightCircleIcon,
  ArrowRightEndOnRectangleIcon,
  CurrencyDollarIcon,
  UserIcon,
} from '@heroicons/react/24/solid'
```

Let’s modify the button that serves as the trigger for the User Menu. We’ll use Clerk’s `hasImage` value from the `user` return of `useUser()`. Using them will let us display the `UserIcon` we just imported when the user hasn’t set a profile image, but use their image when they have. We will also will move the logic we have for the label for the button up.

```tsx
const UserButtonAndMenu = () => {
  const { signOut, openUserProfile } = useClerk()
  const router = useRouter()
  const { user } = useUser()
  // Use the firstname if there is on, otherwise provide a label */
  const label = user?.firstName ? user.firstName : 'Profile'

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <Button>
          {/* Render a button using the image and email from `user` */}
          {user?.hasImage ? (
            <Image
              alt={label}
              src={user?.imageUrl}
              width={30}
              height={30}
              className="mr-2 rounded-full border border-gray-200 drop-shadow-sm"
            />
          ) : (
            <>
              {/* Display the icon is there is no profile image */}
              <UserIcon className="mr-2 h-6 w-auto" />
            </>
          )}
          {label}
        </Button>
      </DropdownMenu.Trigger>
    </DropdownMenu.Root>

    // Rest of the component here
  )
}
```

We can use the `ArrowRightCircleIcon` to add a little flare to the Sign-In button.

```tsx
export const UserButton = () => {
  const { isLoaded, user } = useUser()
  const { openSignIn } = useClerk()

  if (!isLoaded) return null

  if (!user?.id) {
    return (
      <Button onClick={() => openSignIn()}>
        Sign In
        {/* Add an icon to the Sign-in button */}
        <ArrowRightCircleIcon className="ml-2 h-6 w-auto" />
      </Button>
    )
  }

  return <UserButtonAndMenu />
}
```

Lastly, we will use `UserIcon`, `ArrowRightEndOnTectangleIcon`, and `CurrencyDollarIcon` to add icons to the drop-down menu. At the same time we will add the variant and size to the buttons so they are using the new configuration.

```tsx
<DropdownMenu.Portal>
  <DropdownMenu.Content className="mt-4 w-52 rounded-xl border border-gray-200 bg-white px-2 py-2 text-black drop-shadow-2xl">
    <DropdownMenu.Label />
    <DropdownMenu.Group className="py-1">
      <DropdownMenu.Item asChild>
        <Button onClick={() => openUserProfile()} className="pb-3" variant="menu" size="small">
          <UserIcon className="mr-2 h-6 w-auto" />
          Profile
        </Button>
      </DropdownMenu.Item>
      <DropdownMenu.Item asChild>
        <Link href="/subscriptions" passHref>
          <Button className="py-2" variant="menu" size="small">
            <CurrencyDollarIcon className="mr-2 h-6 w-auto" />
            Subscription
          </Button>
        </Link>
      </DropdownMenu.Item>
    </DropdownMenu.Group>
    <DropdownMenu.Separator className="my-1 h-px bg-gray-200" />
    <DropdownMenu.Item asChild>
      <Button
        onClick={() => signOut(() => router.push('/'))}
        className="py-3"
        variant="menu"
        size="small"
      >
        <ArrowRightEndOnRectangleIcon className="mr-2 h-5 w-auto" /> Sign Out
      </Button>
    </DropdownMenu.Item>
  </DropdownMenu.Content>
</DropdownMenu.Portal>
```

## Finishing Touches

We can add a few finishing touches to the component to flesh it out some more with a few smaller tweaks and improvements:

- add an `accent` variant, and then use that for the user button.
- give `menu` variant buttons unique styling.
- add `className="min-w-[192px]"` to the sign-in and user button to help give them a more consistent width.
- add `ArrowPathIcon` to the icon import create a button for `!isLoaded`, and give it a loading/spanning state.
- use the `outline-none` class to remove the focus ring from the menu items

```tsx
'use client'

import * as React from 'react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useUser, useClerk } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import Link from 'next/link'
import {
  ArrowPathIcon,
  ArrowRightCircleIcon,
  ArrowRightEndOnRectangleIcon,
  CurrencyDollarIcon,
  UserIcon,
} from '@heroicons/react/24/solid'
import { VariantProps, cva } from 'class-variance-authority'

const button = cva(['flex', 'flex-row', 'items-center', 'rounded-xl'], {
  variants: {
    variant: {
      primary: [
        'border',
        'border-gray-200',
        'bg-white',
        'text-black',
        'drop-shadow-md',
        'hover:bg-stone-100',
        'hover:text-stone-800',
        'justify-center',
      ],
      accent: [
        'border',
        'border-stone-950',
        'bg-stone-800/70',
        'hover:bg-stone-950',
        'text-stone-200',
        'justify-center',
      ],
      menu: [
        'w-full',
        'justify-start',
        'bg-transparent',
        'hover:bg-stone-800/70',
        'text-gray-800/70',
        'hover:text-stone-100',
        'px-4',
        'rounded-sm',
      ],
    },
    size: {
      regular: ['px-4', 'py-3'],
      small: ['py-3', 'py-2'],
    },
  },
  defaultVariants: {
    variant: 'primary',
    size: 'regular',
  },
})

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof button> {}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, size, children, className, ...props }, ref) => {
    return (
      <button ref={ref} className={button({ variant, size, className })} {...props}>
        {children}
      </button>
    )
  },
)

// Create a new UserButtonandMenu component and move the old return into this
const UserButtonAndMenu = () => {
  const { user } = useUser()
  const { signOut, openUserProfile } = useClerk()
  const router = useRouter()
  const label = user?.firstName ? user.firstName : 'Profile'

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild className="outline-none">
        <Button variant="accent" className="min-w-[192px]">
          {user?.hasImage ? (
            <Image
              alt={label ? label : 'Profile image'}
              src={user?.imageUrl}
              width={30}
              height={30}
              className="mr-2 rounded-full border border-stone-950 drop-shadow-sm"
            />
          ) : (
            <UserIcon className="mr-2 h-6 w-auto" />
          )}
          {label}
        </Button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="mt-4 w-52 rounded-xl border border-gray-200 bg-white px-2 py-2 text-black drop-shadow-2xl">
          <DropdownMenu.Label />
          <DropdownMenu.Group className="py-1">
            <DropdownMenu.Item asChild className="outline-none">
              <Button
                onClick={() => openUserProfile()}
                className="pb-3"
                variant="menu"
                size="small"
              >
                <UserIcon className="mr-2 h-6 w-auto" />
                Profile
              </Button>
            </DropdownMenu.Item>
            <DropdownMenu.Item asChild className="outline-none">
              <Link href="/subscriptions" passHref>
                <Button className="py-2" variant="menu" size="small">
                  <CurrencyDollarIcon className="mr-2 h-6 w-auto" />
                  Subscription
                </Button>
              </Link>
            </DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator className="my-1 h-px bg-gray-200" />
          <DropdownMenu.Item asChild className="outline-none">
            <Button
              onClick={() => signOut(() => router.push('/'))}
              className="py-3"
              variant="menu"
              size="small"
            >
              <ArrowRightEndOnRectangleIcon className="mr-2 h-5 w-auto" /> Sign Out
            </Button>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

export const UserButton = () => {
  const { isLoaded, user } = useUser()
  const { openSignIn } = useClerk()

  if (!isLoaded)
    return (
      <Button onClick={() => openSignIn()} className="w-48">
        <ArrowPathIcon className="ml-2 h-6 w-auto animate-spin" />
      </Button>
    )

  if (!user?.id)
    return (
      <Button onClick={() => openSignIn()} className="w-48">
        Sign In
        <ArrowRightCircleIcon className="ml-2 h-6 w-auto" />
      </Button>
    )

  return <UserButtonAndMenu />
}
```

Example repository: [https://github.com/royanger/clerk-custom-user-menu](https://github.com/royanger/clerk-custom-user-menu)

## Explore the Powerful Customization Options Clerk Offers!

With this component you have the building blocks to build out your own user button and menu. Add the new entries to the dropdown that you need for your application, and use the tools provided by Radix and `cva` to design and style your component so it matches your application's design language!

Take a look at our [Custom Flows](/docs/custom-flows/overview) documentation to explore more ways to customize your application using the many hooks and methods Clerks provides. The ability to add the pieces you need from Clerk to fully custom and unique UI provides flexibility to projects.

For more in-depth technical inquiries or to engage with our community, feel free to [join our Discord](https://clerk.com/discord). Stay in the loop with the latest Clerk features, enhancements, and sneak peeks by following our X account, [@clerk](https://x.com/clerk). Your journey to seamless user management starts here!

---

# Build a Movie Emoji Quiz App with Remix, Fauna, and Clerk
URL: https://clerk.com/blog/build-movie-emoji-quiz-with-remix-fauna-and-clerk.md
Date: 2024-01-02
Category: Guides
Description: Test the emoji game of all the movie buffs you know by building a Movie Emoji Quiz app with Remix, Fauna, and Clerk.

> \[!WARNING]
> Fauna’s service [ended on May 30, 2025](https://web.archive.org/web/20250319171743/https://fauna.com/blog/the-future-of-fauna), but they’ve [open-sourced their codebase](https://faunadb.org).

## Project setup

A fun challenge coming out of the modern smartphone world is to identify a movie based only on a sequence of emoji. Sometimes the emoji “spell out” the words in the title, while other times they identify key plot themes. Test the emoji game of all the movie buffs you know by building a Movie Emoji Quiz app.

In this tutorial, we will build the app using the Remix full-stack web framework, a Fauna database, and Clerk for authentication and user management. Remix is a relatively new, open-source framework for React that has been gaining traction. Fauna is a developer-friendly, cloud-based database platform that offers modern features, such as real-time streaming and GraphQL. Clerk, an authentication provider built for the modern web, has a first-party [Remix authentication package](/solutions/remix-authentication) and integrates with Fauna through its JWT templates feature.

A brief web search didn't come up with any existing Remix and Fauna tutorials. And then I found this tweet from Ryan Florence ([@ryanflorence](https://twitter.com/ryanflorence)), the co-founder of Remix:

> I really want to build some demos with FaunaDB (I love the direction they're going) and AWS DynamoDB (I mean, come on, it's solid) as well. — Ryan Florence (@ryanflorence) [April 6, 2021](https://twitter.com/ryanflorence/status/1379289891226316801?ref_src=twsrc%5Etfw)

That sold me on moving forward in building with this stack.

If you would like to skip ahead, you can see the [completed
codebase](https://github.com/clerkinc/remix-fauna-tutorial) here.

### Assumptions

This tutorial makes the following assumptions...

- Basic command line usage
- Node.js (≥ v14) installed with `npm` (≥ v7)
- Experience with React components and hooks
- [Clerk](https://dashboard.clerk.com/sign-in) and [Fauna](https://dashboard.fauna.com/accounts/login) accounts already set up (if you haven’t done so, do it now\... we’ll wait)

### Set up a Clerk application

The first step is to create a new application from the Clerk dashboard. We’ll name this application **Movie Emoji Quiz** and leave the default **Password** authentication strategy selected. We’re also going to choose **Google** and **Twitter** as the social login providers, but feel free to select whichever ones you and your friends use.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./0a352efb7eacb52a6221f618c1fcd705126aae31-567x882.png)

Click the **Add application** button and your Clerk development instance will be created.

### Create Remix project

The next step is to create the Remix project. Run the following command in your terminal:

```bash
npx create-remix movie-emoji-quiz
```

It may prompt you to install the create-remix package. Then respond with the following:

```text
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Remix App Server
? Do you want me to run `npm install`? Yes
```

It will then ask “TypeScript or JavaScript?”, we’re going with **JavaScript** on this one, but that’s up to your personal preference.

As the instructions state, change into the app directory with `cd movie-emoji-quiz/`

Now install the two dependencies we need for this application:

```bash
npm install @clerk/remix faunadb
```

Next, touch `.env` to create an environment variables file and replace `<YOUR_FRONTEND_API>` and `<YOUR_CLERK_API_KEY> `with their respective values from your Clerk instance, which you can get from the [API keys](https://dashboard.clerk.com/last-active?path=api-keys) page.

```yaml {{ title: '.env' }}
CLERK_FRONTEND_API=<YOUR_FRONTEND_API>
CLERK_API_KEY=<YOUR_CLERK_API_KEY>
```

Once those environment variables are set, spin up the Remix dev server:

```bash
npm run dev
```

### Set up Clerk authentication

To share authentication state with Remix routes, we need to make three modifications to the `app/root.jsx` file:

1. Export `rootAuthLoader` as `loader`
2. Export `ClerkCatchBoundary` as `CatchBoundary`
3. Wrap the default export with `ClerkApp`

```jsx {{ title: 'app/root.jsx' }}
import { rootAuthLoader } from '@clerk/remix/ssr.server'
import { ClerkApp, ClerkCatchBoundary } from '@clerk/remix'

export const loader = (args) => rootAuthLoader(args) /* 1 */
export const CatchBoundary = ClerkCatchBoundary() /* 2 */

function App() {
  return <html lang="en">{/*...*/}</html>
}

export default ClerkApp(App) /* 3 */
```

Clerk supports Remix SSR out-of-the-box.

### Set up Fauna database

From the Fauna dashboard, create a new database. We named ours **emoji-movie-quiz** and chose the **Classic** Region Group.

We only need to create one Collection for this application. You can do so either in the Dashboard UI or in the interactive query shell.

To do it in the shell, type the following:

```javascript
CreateCollection({ name: 'challenge' })
```

Then press the **Run query** button. If it was successful, you should see similar output.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./e653aadad75fe1c67e048c30db7c942f332dd09f-972x932.png)

We’re going to seed the database with a few examples to get started. If you’ve never used the Fauna Query Language (FQL) before, the syntax may look a little funny.

```javascript
Map(
  [
    { emoji: '🔕🐑🐑🐑', title: 'Silence of the Lambs' },
    { emoji: '💍💍💍💍⚰️', title: 'Four Weddings and a Funeral' },
    { emoji: '🏝🏐', title: 'Castaway' },
    { emoji: '👽📞🏡', title: 'E.T.' },
    { emoji: '👂👀👃👅✋6️⃣', title: 'The Sixth Sense' },
  ],
  Lambda('data', Create(Collection('challenge'), { data: Var('data') })),
)
```

This will map over the examples array and create a new document with challenge data for each item in the Challenge collection.

To learn more about FQL, check out the [Fauna docs](https://docs.fauna.com/fauna/current) and this handy [cheat
sheet](https://docs.fauna.com/fauna/current/api/fql/cheat_sheet).

You can navigate to the **Collections** tab to validate that the data is all set. You can even make edits directly from the user interface if you prefer.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./3ff4a1aba4c5fe26181d70d9257ded3cdccfc59e-2674x1620.png)

While we’re here, let’s navigate over to the **Functions** tab and write a couple FQL functions we will need later.

Click the **New Function** button, name it `getChallenges`, and then paste the following function body:

```javascript
Query(
  Lambda(
    [],
    Map(
      Paginate(Documents(Collection('challenge'))),
      Lambda(
        'challenge',
        Let(
          {
            challengeRef: Get(Var('challenge')),
            data: Select('data', Var('challengeRef')),
            refId: Select(['ref', 'id'], Var('challengeRef')),
          },
          Merge(Var('data'), { id: Var('refId') }),
        ),
      ),
    ),
  ),
)
```

This function will get all the challenges and include the unique document ID inside each data object.

You can test out the functionality in the Shell by running:

```javascript
Call('getChallenges')
```

The second function we’re going to define is called `getChallengeById` and will take the unique document ID parameter and return the respective challenge data or `null` if it doesn’t exist.

```javascript
Query(
  Lambda(
    'id',
    Let(
      {
        challengeRef: Ref(Collection('challenge'), Var('id')),
        exists: Exists(Var('challengeRef')),
        challenge: If(Var('exists'), Get(Var('challengeRef')), null),
      },
      Select('data', Var('challenge'), null),
    ),
  ),
)
```

That’s all of the custom functions we’ll need here.

### Authentication integration

Although Fauna offers built-in identity and basic password authentication, it requires that you manage the user data yourself and does not provide features like prebuilt UI components, `<UserProfile />` access, and other auth strategies such as OAuth social login and [magic links](/blog/magic-links). Clerk provides these features and more without the hassle of managing your own user and identity service.

The Clerk integration with Fauna enables you to authenticate queries to your Fauna database using a JSON Web Token (JWT) created with a JWT template.

From your Clerk dashboard, navigate to the [JWT Templates](https://dashboard.clerk.com/last-active?path=jwt-templates) screen. Click the **New template** button and choose the **Fauna** template.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./46108295ebfce65e7c5446b2abe9e80b8959d5df-1386x1246.png)

Take note of the default template name of fauna (as this will come up later). You can leave the default settings and optionally add your own custom claims using convenient shortcodes.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./7e8a7f7f9821b7cb54e49094faf4e62c84a34e41-1519x1096.png)

While keeping this tab open, go back to the Fauna dashboard and navigate to the **Security** page.

Click on the **Roles** tab and then create a **New Custom Role**. Name this role `user` and give it **Read** and **Create** access to the `challenge` collection as well as **Call** permission for both `getChallenges` and `getChallengeById` functions.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./1c3605a3b7bb4cbf0b12803f1a9004a3db0834f5-1519x1039.png)

If everything looks correct, click **Save** to create the user role.

Next, click on the **Providers** tab and click on the **New Access Provider** button.

Enter `Clerk` as the name to identify this access provider.

We’re going to play a little pattycake back-and-forth between Fauna and Clerk, but bear with me and we’ll get through it together.

1. **Fauna**: Copy the **Audience** URL and go back to the Clerk JWT template tab.
2. **Clerk**: Paste the Audience URL as the value for the `aud` claim.
3. **Clerk**: Copy the **Issuer** URL.
4. **Fauna**: Paste into Issuer field.
5. **Clerk**: Copy the **JWKS Endpoint** URL.
6. **Fauna**: Paste into the JWKS endpoint field.
7. **Fauna**: Select the **user** role for access.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./7cafa54652ac1e960b91ec6faac5cf40746b526b-786x695.png)

After those fields have been set, you can save both the Clerk JWT template and Fauna access provider. Whew! That was fun.

### Add Clerk auth to Remix

Now we can get back into building our application. Open the Remix project in your code editor of choice.

In Remix, `app/root.jsx` wraps your entire application in both server and browser contexts.

Clerk requires a few modifications to this file so the authentication state can be shared with your Remix routes. First, add these imports to `app/root.jsx`:

```jsx
import { rootAuthLoader } from '@clerk/remix/ssr.server'
import { ClerkApp, ClerkCatchBoundary } from '@clerk/remix'
```

Second, export `rootAuthLoader` as `loader` taking in the args parameter.

```javascript
export const loader = (args) => rootAuthLoader(args)
```

Next, we need to export the `ClerkCatchBoundary` as `CatchBoundary` to handle expired authentication tokens. If you want your own custom boundary, you can pass it in as the first argument.

```javascript
export const CatchBoundary = ClerkCatchBoundary()
```

And finally, we need to wrap the default export with the `ClerkApp` higher-order component.

```jsx
export default ClerkApp(function App() {
  return <html lang="en">{/*[...]*/}</html>
})
```

That’s all that’s needed to install and configure Clerk for authentication. The next step will include adding sign in functionality.

### Add SignIn to index route

We’re going to use the Clerk hosted `<SignIn />` component to render the sign in form.

Update `app/routes/index.jsx` with the following:

```jsx {{ title: 'app/routes/index.jsx' }}
import { SignIn } from '@clerk/remix'
import styles from '~/styles/index.css'

export const links = () => {
  return [{ rel: 'stylesheet', href: styles }]
}

export default function Index() {
  return (
    <div>
      <main>
        <div className="content">
          <SignIn />
        </div>
      </main>
    </div>
  )
}
```

We’re using Remix’s [route styles](https://remix.run/docs/en/v1/guides/styling#route-styles) functionality to dynamically add a stylesheet to this route.

Create a `styles` directory inside of the `app` folder and save the following as `index.css`:

app/styles/index.css

```css
@font-face {
  font-family: 'color-emoji';
  src:
    local('Apple Color Emoji'), local('Segoe UI Emoji'), local('Segoe UI Symbol'),
    local('Noto Color Emoji');
}

:root {
  --font-body:
    'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif,
    color-emoji;
  --color-error: #c10500;
  --color-success: #15750b;
}

body {
  margin: 0;
  font-family: var(--font-body);
}

header {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  background-color: #fd890f;
  box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.2);
}

.actions {
  display: flex;
  align-items: center;
}

.actions > a {
  margin: 0 20px;
  color: #fff;
  font-size: 14px;
  text-decoration: none;
}

.actions > a:hover,
.actions > a:focus {
  color: rgba(255, 255, 255, 0.8);
}

.logo {
  color: #fff;
  font-size: 22px;
  font-weight: 600;
}

main {
  display: flex;
  height: calc(100vh - 56px);
}

aside {
  width: 280px;
  height: 100%;
  overflow-y: scroll;
  flex-shrink: 0;
  background-color: #f5f5f4;
  border-right: 1px solid #d8d8d4;
}

aside h2 {
  margin: 20px 20px 10px;
  font-size: 18px;
}

aside ul {
  list-style: none;
  padding: 0;
}

aside li {
  font-size: 28px;
}

aside li:not(:last-child) {
  border-bottom: 1px solid #d8d8d4;
}

aside li > a {
  display: block;
  padding: 8px 20px;
  text-decoration: none;
}

aside li > a:hover,
aside li > a:focus,
aside li > a.active {
  background-color: #ececea;
}

.content {
  width: 100%;
  height: calc(100vh - 40px);
  padding: 40px 20px 0;
  background-color: #e5e6e4;
  text-align: center;
}

.content h1 {
  margin-bottom: 8px;
}

@font-face {
  font-family: 'color-emoji';
  src:
    local('Apple Color Emoji'), local('Segoe UI Emoji'), local('Segoe UI Symbol'),
    local('Noto Color Emoji');
}

:root {
  --font-body:
    'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif,
    color-emoji;
  --color-error: #c10500;
  --color-success: #15750b;
}

body {
  margin: 0;
  font-family: var(--font-body);
}

header {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  background-color: #fd890f;
  box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.2);
}

.actions {
  display: flex;
  align-items: center;
}

.actions > a {
  margin: 0 20px;
  color: #fff;
  font-size: 14px;
  text-decoration: none;
}

.actions > a:hover,
.actions > a:focus {
  color: rgba(255, 255, 255, 0.8);
}

.logo {
  color: #fff;
  font-size: 22px;
  font-weight: 600;
}

main {
  display: flex;
  height: calc(100vh - 56px);
}

aside {
  width: 280px;
  height: 100%;
  overflow-y: scroll;
  flex-shrink: 0;
  background-color: #f5f5f4;
  border-right: 1px solid #d8d8d4;
}

aside h2 {
  margin: 20px 20px 10px;
  font-size: 18px;
}

aside ul {
  list-style: none;
  padding: 0;
}

aside li {
  font-size: 28px;
}

aside li:not(:last-child) {
  border-bottom: 1px solid #d8d8d4;
}

aside li > a {
  display: block;
  padding: 8px 20px;
  text-decoration: none;
}

aside li > a:hover,
aside li > a:focus,
aside li > a.active {
  background-color: #ececea;
}

.content {
  width: 100%;
  padding: 40px 20px 0;
  background-color: #e5e6e4;
  text-align: center;
}

.content h1 {
  margin-bottom: 8px;
}

.emoji {
  display: block;
  font-size: 80px;
  line-height: 1.2;
  margin: 20px auto 10px;
}

.error > p {
  color: var(--color-error);
  font-size: 18px;
}
```

Your app should now look something like:

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./dc28c45c9b45ff09ce76b659a41a7841ddf9e1a2-2258x1618.png)

If you see a blank screen, you may be already signed in. We will handle that case momentarily.

Inside of the `app` directory, create a folder called `components` and a file called `header.jsx`.

Drop in the following code:

```jsx {{ title: 'app/components/header.jsx' }}
import { SignedIn, UserButton } from '@clerk/remix'

export default function Header() {
  return (
    <header>
      <span className="logo">Movie Emoji Quiz</span>
      <SignedIn>
        <UserButton />
      </SignedIn>
    </header>
  )
}
```

Here we’re making use of the `<SignedIn>` control flow component and the `<UserButton />` which will allow us to edit our profile and sign out.

Go back to `app/routes/index.jsx` and import the `<Header />` component and add it just above the `<main>` element:

```jsx {{ title: 'app/routes/index.jsx' }}
import Header from '../components/header'

export default function Index() {
  return (
    <div>
      <Header />
      <main>
        <div className="content">
          <SignIn />
        </div>
      </main>
    </div>
  )
}
```

If you sign in, you should now see your avatar. Click on it to see the user profile menu.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./4b528fa3e84cf5949c2047dfa5bfba9e13a0186f-1988x1488.png)

You can now sign in, sign out, and manage your account. Clerk makes it super easy with their hosted components.

### Authenticate with the Fauna client

In order to start fetching data from our Fauna database, we need to set up the Fauna client.

Create a `utils` folder inside of `app` and a file named `db.server.js`.

The `.server` naming convention is a hint to the compiler to ignore this file in the browser bundle.

Add the following code:

```javascript {{ title: 'app/utils/db.server.js' }}
import { getAuth } from '@clerk/remix/ssr.server'
import faunadb from 'faunadb'

export const getClient = async (request) => {
  const { userId, getToken } = await getAuth(request)

  if (!userId) {
    return null
  }

  const secret = await getToken({ template: 'fauna' })

  return new faunadb.Client({ secret })
}

export const q = faunadb.query
```

Here we are using the `getAuth` function from Clerk to check if we have a `userId` (e.g. if the user is signed in) and to get access to the `getToken` function, which when called with our Fauna JWT template name (it was simply “fauna” if you remember), will be passed to the Fauna client as the authentication secret.

If the user is signed in and we get access to the Fauna JWT template, we should then be able to make queries against our Fauna database.

We are also exporting `q` here, which is a convention when using `faunadb.query`. This way all our database helper functions are kept in the same place.

## Display movie challenges

Now it’s time to display some of these challenges.

First let’s create a new route at `/challenges`. We do this by creating a file `app/routes/challenges.jsx` and populating it with the following:

```jsx {{ title: 'app/routes/challenges.jsx' }}
import { Outlet } from '@remix-run/react'
import Header from '../components/header'
import styles from '~/styles/index.css'

export const links = () => {
  return [{ rel: 'stylesheet', href: styles }]
}

export default function Challenges() {
  return (
    <div>
      <Header />
      <main>
        <div className="content">
          <Outlet />
        </div>
      </main>
    </div>
  )
}
```

This should look the same as the index route, with the exception being `<SignIn />` is replaced with the Remix `<Outlet />` component.

Next, import the database utils we previous created and export a `loader` function with the below code:

```javascript
import { getClient, q } from '../utils/db.server'

export const loader = async ({ request }) => {
  const client = await getClient(request)

  if (!client) {
    return null
  }

  const response = await client.query(q.Call('getChallenges'))

  // Check your terminal for response data
  console.log(response)

  return json(response)
}
```

You can see we’re using the Fauna client to perform a FQL query to call the `getChallenges` function we created. If all went well, check your terminal window (not the browser console) and you should see the response data.

Create a new file `app/components/sidebar.jsx` that will loop over this data and create links to each challenge based on its ID.

```jsx {{ title: 'app/components/sidebar.jsx' }}
import { NavLink } from '@remix-run/react'

export default function Sidebar({ data }) {
  return (
    <aside>
      <h2>Guess these movies...</h2>
      <ul>
        {data?.map((movie) => (
          <li key={movie.id}>
            <NavLink to={`/challenges/${movie.id}`}>{movie.emoji}</NavLink>
          </li>
        ))}
      </ul>
    </aside>
  )
}
```

Import the `<Sidebar />` component in the index route directly under the `main` element.

To access the data from the loader, we will use the aptly named `useLoaderData` hook from Remix and pass the `data` property to the `Sidebar` component:

```jsx
export default function Challenges() {
  const { data } = useLoaderData()

  return (
    <div>
      <Header />
      <main>
        <Sidebar data={data} />
        <div className="content">
          <Outlet />
        </div>
      </main>
    </div>
  )
}
```

If you visit [http://localhost:3000/challenges](http://localhost:3000/challenges), you should now see the emoji challenges rendered as links in the sidebar:

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./22b986b5e553c075663f766a040df434cfde6167-2192x1488.png)

If you click on the links, they will cause an error because we haven’t created those individual challenge routes. Let’s do that now.

### Create challenge route

Let’s create a new stylesheet that will hold the challenge styles at `app/styles/challenge.css`.

app/styles/index.css

```css
.emoji {
  display: block;
  font-size: 80px;
  line-height: 1.2;
  margin: 20px auto 10px;
}

.author {
  padding-bottom: 10px;
  color: #8a8a8a;
  font-size: 14px;
}

form {
  display: flex;
  flex-flow: column wrap;
  align-items: center;
  justify-content: center;
  margin-top: 20px;
}

label {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 20px;
}

input[type='text'] {
  min-width: 280px;
  padding: 8px;
  border: 1px solid #ccc;
  font-family: var(--font-body);
  font-size: 16px;
}

.submit-btn {
  appearance: none;
  margin-top: 40px;
  padding: 12px 24px;
  background-color: #7180ac;
  border: 1px solid #6575a4;
  border-radius: 4px;
  cursor: pointer;
  color: #fff;
  font-family: var(--font-body);
  font-size: 14px;
}

.submit-btn:hover,
.submit-btn:focus {
  background-color: #6575a4;
}

.form-field {
  display: flex;
  align-items: baseline;
  margin-top: 20px;
  text-align: left;
}

.form-field label {
  display: block;
  min-width: 50px;
  margin: 0 16px 0 0;
}

.form-validation-error {
  color: var(--color-error);
  font-size: 14px;
  margin-top: 4px;
  margin-bottom: 0;
}

.message {
  margin: 8px 0 0;
  font-size: 16px;
}

.message--correct {
  color: var(--color-success);
}

.message--incorrect {
  color: var(--color-error);
}

.reveal {
  position: relative;
}

.reveal-btn {
  position: relative;
  z-index: 2;
  appearance: none;
  width: 300px;
  margin: 24px auto;
  padding: 16px 24px;
  background: #f5f5f4;
  border: 1px solid #d8d8d4;
  transition: opacity 1.5s ease;
  cursor: help;
  font-family: var(--font-body);
  font-size: 14px;
}

.reveal-btn:hover {
  opacity: 0;
}

.reveal-text {
  position: absolute;
  top: 40px;
  left: 0;
  width: 100%;
  font-size: 14px;
}
```

Next, create a file inside a folder at the path `app/routes/challenges/$id.jsx`

The `$` prefix is important here as it creates a [dynamic segment](https://remix.run/docs/en/v1/guides/routing#dynamic-segments).

Add the following code:

```jsx {{ title: 'app/routes/challenges/$id.jsx' }}
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { getClient, q } from '../../utils/db.server'
import styles from '~/styles/challenge.css'

export const links = () => {
  return [{ rel: 'stylesheet', href: styles }]
}

export const loader = async ({ params, request }) => {
  const client = await getClient(request)

  if (isNaN(params.id)) {
    throw new Response('Challenge not found', {
      status: 404,
    })
  }

  const challenge = await client.query(q.Call('getChallengeById', params.id))

  if (!challenge) {
    throw new Response('Challenge not found', {
      status: 404,
    })
  }

  return json(challenge)
}

export default function Challenge() {
  const { emoji } = useLoaderData()

  return (
    <div>
      <span className="emoji">{emoji}</span>
    </div>
  )
}
```

This code follows a similar pattern to what we did in the sidebar with the `loader` and the `useLoaderData` hook. The difference here is we’re calling the `getChallengeById` FQL function and passing it the `id` parameter, which comes from the `$id` dynamic route.

We are also using a Remix convention of throwing `Response` objects for error scenarios (e.g. invalid ID, challenge not found).

If you click on the links in the sidebar, you should now see each emoji challenge rendered in its full glory (a large type size).

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./91eb7782b4592ebde7e96f1c0023f4fea6e576d5-2090x1572.png)

### Challenge form

Now it’s time to add the form to submit guesses for the emoji challenges. Form handling is one area where Remix really shines.

Add the following form markup below the emoji:

```jsx
<form method="post" autoComplete="off">
  <label htmlFor="guess">What movie is this?</label>
  <input id="guess" type="text" name="guess" placeholder="Enter movie title..." required />
  <button className="submit-btn">Submit guess</button>
</form>
```

This is pretty standard JSX form markup. Nothing too fancy going on here.

Let’s import our DB utils as well as the `json` Response helper from Remix.

```javascript
import { json } from '@remix-run/node'
import { getClient, q } from '~/utils/db.server'
```

Then export the following action function:

```javascript
export const action = async ({ params, request }) => {
  const form = await request.formData()
  const guess = form.get('guess')
  const client = await getClient(request)
  const challenge = await client.query(q.Call('getChallengeById', params.id))
  const isCorrect = guess.toLowerCase() === challenge.title.toLowerCase()

  return json({
    guessed: isCorrect ? 'correct' : 'incorrect',
    message: isCorrect ? 'Correct! ✅' : 'Incorrect! ❌',
    answer: challenge.title,
  })
}
```

Here we’re using native [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) methods to read the guess input and comparing it against the challenge title we get by calling our FQL `getChallengeById` function with the challenge ID from the route params.

Based on whether the guess matches the title (case insensitive), we return an appropriate JSON response. We can access the action data using the `useActionData` hook (also aptly named).

```javascript
import { useActionData, useLoaderData } from '@remix-run/react'
```

Now we can update the UI to display the correct message based on the guess submitted.

```jsx
export default function Challenge() {
  const { emoji } = useLoaderData()
  const data = useActionData()

  return (
    <div>
      <span className="emoji">{emoji}</span>
      <form method="post" autoComplete="off">
        <label htmlFor="guess">What movie is this?</label>
        <input id="guess" type="text" name="guess" placeholder="Enter movie title..." required />
        {data?.guessed ? (
          <p className={`message message--${data.guessed}`}>{data.message}</p>
        ) : null}
        <button className="submit-btn">Submit guess</button>
        {data?.guessed === 'incorrect' ? (
          <div className="reveal">
            <button className="reveal-btn" type="button">
              Reveal answer
            </button>
            <span className="reveal-text">{data?.answer}</span>
          </div>
        ) : null}
      </form>
    </div>
  )
}
```

If an incorrect guess is submit, we provide the user a way to reveal the answer.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./125715f68cfbb3512476f88b2e28ef96d0154475-2090x1572.png)

The form should now work to submit correct and incorrect guesses.

### Handling transitions

Because the form is shared between different challenge routes, if you enter text in one input and go to another challenge, you will see the input value is not being cleared.

We can fix this by using the `useTransition` hook, putting a ref on the form element, and then resetting the form when the transition is a normal page load.

```jsx
export default function Challenge() {
  const { emoji } = useLoaderData()
  const data = useActionData()
  const transition = useTransition()
  const ref = useRef()

  useEffect(() => {
    if (transition.type == 'normalLoad') {
      // Reset form on route change
      ref.current && ref.current.reset()
    }
  }, [transition])

  return (
    <div>
      <span className="emoji">{emoji}</span>
      <form ref={ref} method="post" autoComplete="off">
        <label htmlFor="guess">What movie is this?</label>
        <input id="guess" type="text" name="guess" placeholder="Enter movie title..." required />
        {data?.guessed ? (
          <p className={`message message--${data.guessed}`}>{data.message}</p>
        ) : null}
        <button className="submit-btn">Submit guess</button>
        {data?.guessed === 'incorrect' ? (
          <div className="reveal">
            <button className="reveal-btn" type="button">
              Reveal answer
            </button>
            <span className="reveal-text">{data?.answer}</span>
          </div>
        ) : null}
      </form>
    </div>
  )
}
```

You will need to add the necessary hook imports from React and Remix.

```javascript
import { useEffect, useRef } from 'react'
import { useActionData, useLoaderData, useTransition } from '@remix-run/react'
```

After that is added, the form should clear when choosing a different challenge.

## Submit new challenges

This app is not much so much fun if users can’t submit their own movie emoji challenges. So that’s the functionality we’re going to add now.

As with most things in Remix, the first thing we need to do is create a new route.

Create `app/routes/challenges/new.jsx` with the following:

```jsx {{ title: 'app/routes/challenges/new.jsx' }}
import { getAuth } from '@clerk/remix/ssr.server'
import { json, redirect } from '@remix-run/node'
import { Form, useActionData } from '@remix-run/react'
import { getClient, q } from '~/utils/db.server'
import styles from '~/styles/challenge.css'

export const links = () => {
  return [{ rel: 'stylesheet', href: styles }]
}

const badRequest = (data) => json(data, { status: 400 })

const validateEmoji = (emoji) =>
  !emoji.trim() || /\p{L}|\p{N}(?!\uFE0F)|\p{Z}/gu.test(emoji)
    ? 'Please enter only emoji'
    : undefined

const validateTitle = (title) =>
  title && title.length > 1 ? undefined : 'Please enter a movie title'

export const action = async ({ request }) => {
  const form = await request.formData()
  const emoji = form.get('emoji')
  const title = form.get('title')

  if (typeof emoji !== 'string' || typeof title !== 'string') {
    return badRequest({
      formError: 'Form not submitted correctly.',
    })
  }

  const fieldErrors = {
    emoji: validateEmoji(emoji),
    title: validateTitle(title),
  }

  if (Object.values(fieldErrors).some(Boolean)) {
    return badRequest({
      fieldErrors,
      fieldValues: {
        emoji,
        title,
      },
    })
  }

  const { userId } = await getAuth(request)
  const client = await getClient(request)
  const data = {
    emoji,
    title,
    userId,
  }

  const response = await client.query(q.Create('challenge', { data }))

  return redirect(`/challenges/${response.ref.value.id}`)
}

export default function NewRoute() {
  const actionData = useActionData()

  return (
    <div>
      <h1>Create new challenge</h1>
      <Form method="post" autoComplete="off">
        <div className="form-field">
          <label htmlFor="emoji">Emoji</label>
          <input id="emoji" type="text" name="emoji" />
        </div>
        {actionData?.fieldErrors?.emoji ? (
          <p className="form-validation-error" role="alert" id="name-error">
            {actionData.fieldErrors.emoji}
          </p>
        ) : null}
        <div className="form-field">
          <label htmlFor="title">Movie</label>
          <input id="title" type="text" name="title" />
        </div>
        {actionData?.fieldErrors?.title ? (
          <p className="form-validation-error" role="alert" id="name-error">
            {actionData.fieldErrors.title}
          </p>
        ) : null}
        {actionData?.formError ? (
          <p className="form-validation-error" role="alert">
            {actionData.formError}
          </p>
        ) : null}
        <button className="submit-btn">Submit challenge</button>
      </Form>
    </div>
  )
}
```

This time we’re using the `Form` component provided by Remix, which helps with automatically serializing the values. There’s validation logic to check for valid emoji and titles. If the validation passes, we create a new challenge. We use the `getAuth` function from Clerk to send in the user ID along with the emoji and title. These values form the data for a new challenge document in Fauna. On a successful document creation, the user is redirected to the new challenge page.

Let’s add a link to the header so we can get to this page. It’s wrapped with `<div className="actions">` to provide the necessary styling.

```jsx {{ title: 'app/components/header.jsx' }}
import { SignedIn, UserButton } from '@clerk/remix'
import { Link } from '@remix-run/react'

export default function Header() {
  return (
    <header>
      <span className="logo">Movie Emoji Quiz</span>
      <SignedIn>
        <div className="actions">
          <Link to="/challenges/new">Submit challenge</Link>
          <UserButton />
        </div>
      </SignedIn>
    </header>
  )
}
```

Clicking the link should take you to the page where you can create a new challenge. You should see validation errors if you don’t fill out the form properly.

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./96ba5c89aa2784720ee94206c12557645c742168-2482x1550.png)

Once submitted, you will be redirected to the new challenge page. Because the action created a new mutation, Remix will automatically update the data in the sidebar. Neat!

![Build Movie Emoji Quiz With Remix Fauna And Clerk guide illustration](./0a87785674c2ec965c2c6b54bcf92e8a8e98f967-2482x1550.png)

You now have a working Movie Emoji Quiz app. If you’re ready to share it with your family and friends, Remix makes deployment very easy and has support for various [deployment targets](https://remix.run/docs/en/v1/guides/deployment). Using the [Vercel CLI,](https://vercel.com/cli) all you need to deploy your Remix app is run:

```bash
npm i -g vercel
vercel
```

And your app will be live within minutes!

## Next steps

To take this app even further, you can do the following:

- Add [catch boundaries](https://remix.run/docs/en/v1/guides/not-found) and redirects to handle error cases and invalid challenge routes
- Create Remix routes for `/sign-in` and `/sign-out` to use [mounted Clerk components](/docs/references/javascript/clerk/sign-in#sign-in-component)
- Use the [Clerk User API](/docs/reference/backend-api) to query the username for each user user-submitted challenge so you can give them credit for their cleverness
- Keep track of correct guesses and display an indicator in the UI so the user knows which challenges they have yet to tackle

If you enjoyed this tutorial or have any questions, feel free to reach out to me ([**@devchampian**](https://x.com/devchampian)) on X, follow [**@clerk**](https://x.com/clerk), or join our [**Discord community**](https://clerk.com/discord) to connect with other developers. Happy coding!

---

# Ultimate Guide to Magic Link Authentication
URL: https://clerk.com/blog/magic-links.md
Date: 2023-12-20
Category: Guides
Description: In this post, we discuss the benefits of email magic links, show examples of how they work, and explain why they meet the requirements for secure, passwordless authentication.

Data breaches and password overload have made companies and their users wary of using a traditional username/password authentication system. Companies know that handling user passwords is both technically challenging ***and*** costly, as it requires stringent security measures, robust infrastructure for storage, and continuous monitoring to prevent unauthorized access. Users find it cumbersome to manage multiple complex passwords for various accounts, especially with the prevalence of methods like SSO, and often end up reusing passwords across different platforms, as a matter of convenience; this not only leaves the individual vulnerable, but also makes companies jobs of securing data that much harder, highlighting the critical requirements for improved security measures.

Luckily, email magic links have emerged as an elegant solution to secure user sessions in a [passwordless](/docs/authentication/configuration/sign-up-sign-in-options#authentication-strategy) context. Their rising popularity is underpinned by their dual advantages:

- An augmented security posture through the minimization of phishing opportunities and password theft.
- An optimized user experience devoid of the need to memorize and manage many login credentials.

## What are Magic Links?

Magic links are a token-based authentication (TBA) strategy that uses a unique, time-sensitive URL, which leverages a securely-generated token to serve as a credential for user authentication.

The links are sent directly to the user's registered email or phone number, providing a straightforward, secure authentication method. When a user clicks on a magic link, the embedded token is validated against the server to authenticate the user's identity. This process, by design, eliminates the traditional risks associated with password-based systems and simplifies the login experience for the user.

In this guide, we will show you why you should consider authentication with magic links and how they work at a high level, before going through [a Next.js App Router implementation](/blog/secure-authentication-nextjs-email-magic-links) to show how you can add magic links to your application.

## The Benefits of Magic Links

Magic links bolster the security architecture of authentication systems by adopting a token-based, stateless interaction model. Each link is cryptographically unique and typically accompanied by an expiration timestamp, making it resilient against replay attacks. Given their ephemeral nature, even if a magic link were to be intercepted or exposed, its short-lived validity constrains the window of opportunity for malicious exploitation.

But there are a few other benefits to magic links for companies and users:

- **Streamlined user experience**. Authentication with magic links eliminate the cognitive burden of remembering and managing many complex passwords, by providing a single-click authentication pathway. This frictionless access modality can enhance user engagement and satisfaction, reducing abandonment rates during the sign-in or sign-up processes.
- **Compliance and data protection**. By using magic links to minimize passwords, organizations can reduce the risk of breaches involving personal data and avoid the consequences of compromised credentials, which can include heavy fines and reputational damage.
- **Reduction in credential exposure**. With magic links, the threat of credential stuffing attacks—where compromised credentials are used to gain unauthorized access to multiple user accounts—is mitigated. Since magic links do not rely on reusable passwords, the standard vector of credential exposure is eliminated, enhancing overall system security.
- **Improved accessibility**. For users with disabilities or those less familiar with technology, magic links offer a more accessible authentication mode. The simplification of the login process—replacing typing and memory demands with a single action—can be particularly advantageous for individuals facing physical or cognitive challenges.

## How Magic Links Work

A magic link is structurally composed of two critical elements: the URL, which provides the link for the user's web interaction, and the embedded token, a cryptographically-generated string serving as the temporary credential.

In essence, a typical magic link may resemble the following structure:

`https://example.com/authenticate?token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`

In this example, the `token` query parameter carries the weight of authentication, substantiating the user's claim without revealing identity until verified by the server.

Here’s an example of how the process works:

![Example Magic Link Authentication Process](./387a0a12d7a188ea6c83f3b15c54bf37e63e6462-2400x1392.png)

### Token Validity

Magic links are designed to become invalid under certain conditions for security purposes:

- **Expiration**: The token embedded in the link has a short lifespan and is often configurable based on application security requirements.
- **Usage Limitation**: Once a magic link is used, it becomes invalid, preventing multiple uses, which could lead to unauthorized access.
- **Revocation**: The system can programmatically revoke a token, thus invalidating the magic link in response to specific triggers or anomalies.

### Token Generation

The lifecycle of a magic link commences with the generation of a unique token. This process employs cryptographic algorithms to ensure each token is a random, high-entropy string, making it virtually impossible to predict or reproduce through brute force or other cryptographic attacks. Typically, the token generation utilizes [HMAC](https://en.wikipedia.org/wiki/HMAC) or AES combined with a [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) to guarantee the robustness of the token against collision and preimage attacks.

Once generated, the token is stored on the server along with metadata that includes the user's identifier, the token's expiration time, and any other relevant session data. The magic link is then composed by appending the token to a predetermined URL structure, forming a complete, ready-to-use hyperlink.

This link is dispatched to the user's email address or phone number via SMTP or SMS protocols. The communication channel must be secure, leveraging TLS for email and similarly secure protocols for SMS to safeguard the link during transit.

### User Interaction and Verification

A user typically interacts with the magic link by clicking on it, which initiates a secure request to the service's endpoint. The service extracts the token from the URL and verifies it against the stored data. This verification process involves several checks:

- **Authenticity**: Validates that the token matches a recently generated and stored token.
- **Integrity**: Ensures the token has not been tampered with during transit.
- **Timeliness**: Confirms the token has not expired based on the predefined validity period.
- **Non-reuse**: Ensures the token has not been used before, adhering to the principle of one-time use.

The server considers the authentication request legitimate if the token passes these checks.

### Session Initialization

Post-verification, the server establishes a session for the user. This session is typically stateless, with a new session token or cookie generated to maintain the user's authenticated state in the application. This session token is separate from the magic link token. It has its own security considerations, such as being HttpOnly and Secure, to prevent access via client-side scripts and ensure transmission over HTTPS only.

## Building a Magic Link System

Let’s create our own email with magic link authentication. In this example, we’ll use [Next.js](https://nextjs.org) 13 with the App Router. We’ll also use [Supabase](https://supabase.com) for our backend database to store users and tokens.

First, let’s create a new Next.js project:

```sh
npx create-next-app@latest
```

Follow the prompts to select how you want to configure your app, but be sure to select “Yes” for “Would you like to
use App Router? (recommended)”.

Once you have created and configured your project, `cd` into the created directory and run `npm run dev` to start it. You’ll see just the default Next.js homepage when you load `localhost:3000`.

Before we start building out our project, we need to install a few dependencies that we’ll use. Install them using:

```sh
npm install nodemailer jsonwebtoken @supabase/supabase-js
```

What do these do?

- [nodemailer](https://www.npmjs.com/package/nodemailer): A library that allows for easy email sending.
- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken): A library to implement JSON Web Tokens for secure data transmission.
- [@supabase/supabase-js](https://www.npmjs.com/package/@supabase/supabase-js): The Supabase client library for Javascript.

With those installed, let’s open up the project in an IDE. In total, we’re going to have two pages, two API routes, and two helper libraries:

- `page.js` will be our homepage. It will have a simple email field, and will call our `requestMagicLink` API reroute.
- `requestMagicLink.js` will be the API route that will create our magic link, send it to the email address passed from `page.js`, and save the token on our Supabase database.
- `verify.js` will be the API route called when the user clicks on the magic link in their email. It will verify the token and redirect the user to the protected dashboard page.
- `dashboard/page.js` will be a simple mock “protected page” (that would require the user to be logged in to view it).
- `lib/database.js` will be a number of database helper functions to save, load, and delete Supabase data.
- `lib/supabaseClient.js` will set up our Supabase client.

### page.js

Let’s start with what the user will see, `page.js`:

```jsx
// app/page.js
'use client'
import { useState } from 'react'

export default function RequestMagicLink() {
  const [email, setEmail] = useState('')
  const [message, setMessage] = useState('')
  const [isLoading, setIsLoading] = useState(false)

  const handleSubmit = async (event) => {
    event.preventDefault()
    setIsLoading(true)
    setMessage('')

    try {
      const response = await fetch('/api/auth/requestMagicLink', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      })

      const data = await response.json()

      if (response.ok) {
        setMessage('Magic link sent! Check your email to log in.')
      } else {
        setMessage(data.error || 'An error occurred. Please try again.')
      }
    } catch (error) {
      setMessage('An error occurred. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Sending...' : 'Send Magic Link'}
      </button>
      {message && <p>{message}</p>}
    </form>
  )
}
```

In the code example above, we are requesting the `RequestMagicLink` component, which is responsible for handling the functionality of requesting a magic link via an email address. This component provides a UI for users to request a magic link. Users enter their email and submit the form, triggering a request to the server. Feedback is given to the user through messages and button state changes during the process.

First, we set up the state variables for the page. In the Next.js App Router, you can only use `useState` in client-side components. As all components default to server-side, we have to use the `“use client”` directive at the top of the file to show this page has to be rendered on the client. We have three state variables:

- `email`: Stores the user's email address. It's updated every time the user types into the email input field.
- `message`: Used to display messages to the user, like confirmation or error messages.
- `isLoading`: Indicates whether the request is being processed. It's used to disable the submit button and change the button text while the request is in progress.

After that, we have the `handleSubmit` function. This is triggered when the form is submitted, and initially prevents the default form submission action with `event.preventDefault()`. It then sets `isLoading` to `true` to indicate the start of an asynchronous operation and clears any previous messages stored in `message`.

Then, comes the core part of the component. We make an async `POST` request to the `/api/auth/requestMagicLink` endpoint with the user's email in the request body. We then update the `message` state based on the success or failure of the request:

- If the request is successful (`response.ok` is true), it sets a success message.
- If the request fails, it displays an error message, either from the response or a generic error message.

We have some basic error catching, and then set `isLoading` to `false`.

The actual form presented to the user is basic, with just an email input field that updates the `email` state variable on change and a submit button that is disabled and changes its text based on the `isLoading` state. We have an `onSubmit` event handler linked to `handleSubmit` to send the form details and a paragraph that displays any messages stored in the `message` state.

This `page.js` file calls `requestMagicLink.js`; let’s dig into that, next.

### requestMagicLink.js

```javascript
// app/api/auth/requestMagicLink.js
import jwt from 'jsonwebtoken'
import nodemailer from 'nodemailer'
import { headers } from 'next/headers'

import { saveToken, getUserByEmail } from '../../../../lib/database'

export async function POST(req, res) {
  const { email } = await req.json()

  const user = await getUserByEmail(email)

  if (!user) {
    return Response.json({ message: 'User note found' })
  }

  // Create a magic link token
  const token = jwt.sign({ email }, process.env.JWT_SECRET, {
    expiresIn: '1h',
  })
  const headersList = headers()
  const host = headersList.get('host')
  const magicLink = `http://${host}/api/auth/verify?token=${token}`

  // Save token in your database with an expiration time
  await saveToken(user.id, token)

  // Set up email transporter and send the magic link
  const transporter = nodemailer.createTransport({
    host: '<email-host>',
    port: 587,
    secure: false, // upgrade later with STARTTLS
    auth: {
      user: '<email-username>',
      pass: '<email-password>',
    },
  })

  await transporter.sendMail({
    from: '<from-address>',
    to: email,
    subject: 'Your Magic Link',
    text: `Click here to log in: ${magicLink}`,
  })

  return Response.json({ message: 'Magic link sent!' })
}
```

This example code handles the API requests related to generating and sending the magic link for user authentication. Let's go through the major components and functions of the code.

The function takes `req` (request) and `res` (response) objects as parameters, and then extracts the `email` from the request's JSON body. We then use `getUserByEmail` from `lib/database.js` to search for a user in the database using the provided email. If the user doesn’t exist we send a JSON response indicating the user was not found.

The function then creates the token using `jsonwebtoken` to create a JWT with the user's email, signing it with a secret from environment variables and setting an expiration of 1 hour. With that token we can create our magic link. We retrieve the host from the request headers and construct a URL with the generated token as a query parameter.

The generated token is then saved in the database with an associated user ID using `saveToken`.

After that, we configure a `nodemailer` transporter with SMTP settings (host, port, security, and authentication credentials) and send an email to the user with the magic link authentication.

Finally, we send back a JSON response indicating that the magic link was sent.

Here, we using one of our helper libraries, `database.js`. Let’s go through that next.

### database.js

```javascript
// lib/database.js

import { supabase } from './supabaseClient'

export const saveToken = async (userId, token) => {
  const { data, error } = await supabase.from('magic_tokens').insert([
    {
      user_id: userId,
      token: token,
      expires_at: new Date(Date.now() + 3600000),
    }, // expires in 1 hour
  ])

  if (error) throw new Error(error.message)
  return data
}

export const getUserByEmail = async (email) => {
  const { data, error } = await supabase.from('users').select('*').eq('email', email).single()

  console.log(data)
  if (error) throw new Error(error.message)
  return data
}

export const getTokenData = async (token) => {
  const { data, error } = await supabase
    .from('magic_tokens')
    .select('*')
    .eq('token', token)
    .single()

  if (error) throw new Error(error.message)
  if (new Date(data.expires_at) < new Date()) {
    throw new Error('Token expired')
  }
  return data
}

export const deleteUserToken = async (token) => {
  const { data, error } = await supabase.from('magic_tokens').delete().match({ token: token })

  if (error) throw new Error(error.message)
  return data
}
```

This is a collection of utility functions designed to interact with Supabase. Each function is designed to interact with specific tables in a Supabase database, which each handle different aspects like token generation, user lookup, token validation, and cleanup. Let's break down each function:

### saveToken

- **Purpose**: To save a newly generated token in the database, fulfilling the requirements for secure token management.
- **Parameters**: Accepts `userId` (the user's ID) and `token` (the magic link token).
- **Functionality**:
  - Inserts a new record into the `magic_tokens` table in Supabase with the user's ID, the token, and an expiration time (set to 1 hour ahead of the current time).
  - If an error occurs during the database operation, it throws an error with the message received from Supabase.
  - Returns the data received from Supabase if the operation is successful.

### getUserByEmail

- **Purpose**: To fetch a user's data from the database using their email address.
- **Parameters**: Accepts `email`, which is the email address of the user.
- **Functionality**:
  - Queries the `users` table in Supabase for a record matching the provided email address.
  - Logs the email and the data received for debugging purposes.
  - If an error occurs, it throws an error with the message from Supabase.
  - Returns the user data if a matching record is found.

### getTokenData

- **Purpose**: To retrieve token data from the database.
- **Parameters**: Accepts `token`, the magic link token.
- **Functionality**:
  - Queries the `magic_tokens` table for a record with a token matching the provided one.
  - Checks if the token has expired by comparing the `expires_at` field with the current time. If the token is expired, it throws an error.
  - If an error occurs during the query, it throws an error with the message from Supabase.
  - Returns the token data if a matching and non-expired token is found.

### deleteUserToken

- **Purpose**: To delete a token from the database, complying with the requirements for token lifecycle management.
- **Parameters**: Accepts `token`, the magic link token to be deleted.
- **Functionality**:
  - Deletes the record from the `magic_tokens` table that matches the provided token.
  - If an error occurs during this operation, it throws an error with the message from Supabase.
  - Returns the data received from Supabase upon successful deletion.

This file, in turn, is calling on the other helper library `supabaseClient.js`.

### supabaseClient.js

```javascript
// lib/supabaseClient.js

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.SUPABASE_URL
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)
```

This is a straightforward setup for initializing a client instance of Supabase in a Javascript application.

`export const supabase = createClient(supabaseUrl, supabaseAnonKey);`creates and exports an instance of the Supabase client, which is used to interact with your Supabase project. The `createClient` function takes the Supabase URL and the anonymous key as arguments and returns the initialized client.

`SUPABASE_URL` and `SUPABASE_ANON_KEY` (along with `JWT_SECRET`) are pulled from our environment variables file, `.env.local`.

### .env.local

```sh
SUPABASE_URL=<supabase-url>
SUPABASE_ANON_KEY=<supabase-anon-key>
JWT_SECRET=<jwt-secret>
```

You get `SUPABASE_URL` and `SUPABASE_ANON_KEY` from your Supabase dashboard.

You’ll see from above, we also need to set up two tables in Supabase. We need a `users` table with an `email` field, and a `magic_tokens` table with these fields:

- `user_id`, which comes from the users table.
- `token`, which is the generated token.
- `expires_at`, to add an expiry time to the token.

Let’s get back to the main code. If we fill out the email field on the homepage and click “Send Magic Link,” `requestMagicLink` will be called and an email sent to the entered email address. Clicking on that link calls the `verify.js` endpoint.

### verify.js

```javascript
// app/api/auth/verify.js
import jwt from 'jsonwebtoken'

import { getTokenData, deleteUserToken } from '../../../../lib/database'

export async function GET(req) {
  const token = req.url.split('=')[1]

  console.log(token) // Logs the token value
  const tokenData = await getTokenData(token)

  if (!tokenData) {
    return Response.json({ error: 'Invalid or expired token' })
  }

  const { email } = jwt.verify(token, process.env.JWT_SECRET)

  // Delete or invalidate the token
  await deleteUserToken(token)

  return Response.redirect('/dashboard') // Or wherever you want to redirect the user after login
}
```

This defines the API route for verifying the magic link token as part of an authentication process.

The code is designed to:

- Extract a token from the request URL.
- Verify the token's validity.
- Perform actions based on the token's validity (such as user redirection or error response).

The code retrieves the token from the URL query string by splitting the URL at the `=` character and taking the second part (`req.url.split("=")[1]`).

The `getTokenData` function is called to fetch the token data from the database. If no data is found (implying the token is invalid or expired), it returns a JSON response with an error message. Using `jwt.verify`, we validate the token against the secret key stored in `process.env.JWT_SECRET`. This also extracts the payload (`email`) from the token.

We then call `deleteUserToken` to remove the token from the database, ensuring it cannot be reused. Finally, it redirects the user to the `/dashboard` route upon successful token verification.

### dashboard/page.js

There isn’t much to `dashboard/page.js`:

```javascript
// app/dashboard/page.js
'use client'

export default function Dashboard() {
  return <h1>A verified page</h1>
}
```

In a production application, this is the page you would build out in your application.

## Final Thoughts

There is a lot to think about with email magic links. The above example doesn’t go into robust authentication checking with the user, nor does it add rate-limiting or have significant error handling. An unfortunate truth of authentication is that there is no silver bullet. While magic links take away the headache of managing user credentials, you still have to manage token generation, storage and expiry. Plus, you are still managing and storing user data.

Any good developer is going to take the time to understand, at least, the basic strategies leveraged by the tools they use to speed up their processes (good work understanding magic links!). With that being said, a simpler and more secure alternative to all the code above is to use [Clerk](/). We built Clerk to make it quick and easy to add advanced authentication techniques into your application. To learn more about magic links, visit our [documentation](/docs/custom-flows/magic-links) or this article on [implementing magic links with Next.js and Clerk](/blog/secure-authentication-nextjs-email-magic-links).

---

# Create Your Own Custom User Menu with Radix
URL: https://clerk.com/blog/create-custom-user-menu-radix.md
Date: 2023-12-14
Category: Guides
Description: Quickly and easily build a custom user menu for your application leveraging Clerk's hooks and methods and building on Radix primitives for a custom UI.

> \[!IMPORTANT]
> `<UserButton />` might be more flexible than you think! As of August 2024, you can now customize the component with [custom menu items](/docs/customization/user-button). Should you need even more customization, this post and [part 2](/blog/create-custom-user-menu-radix-pt-2) are still here to help you build the experience you desire from scratch.

Clerk’s components were created with you in mind. Components do most of the functional work for you, allowing you to get your auth flows working in minutes, and support customization to fit your app’s style. That said, sometimes a component like the [`<UserButton />`](/docs/components/user/user-button) doesn’t suit the needs of your application. The good news? Clerk provides hooks and functions that make building your custom UI components easy. Let’s take a quick look at how to create your custom user menu.

Code samples are from @clerk/nextjs 4.27.2 and @radix-ui/react-dropdown-menu 2.0.6

## Getting Started

The hardest part of building a custom user menu is often the dropdown menu itself. You need the button to trigger the menu opening, a way to close it, a way to track open/closed states, a way to handle ‘click off’ to close, logic to close if the user hits the `Esc` button, a way to handle keyboard input, and the list goes on. We’re going to save ourselves some time and use a great library while doing so. Radix provides world-class, accessible, unstyled [primitives](https://www.radix-ui.com/primitives/docs/overview/introduction) that you can use to quickly and efficiently build your UI. Let’s start by installing the primitive we need — the dropdown menu.

```bash
pnpm install @radix-ui/react-dropdown-menu
```

Once the installation finishes, let’s create the scaffolding for our new component. The following will be the foundation of the menu. The trigger will hold the User button that will open the menu, and each item will hold one of the menu entries. Remember that Radix Primitives are unstyled and there is no content so this will be blank.

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'

export const UserButton = () => {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger></DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content>
          <DropdownMenu.Label />
          <DropdownMenu.Group>
            <DropdownMenu.Item></DropdownMenu.Item>
            <DropdownMenu.Item></DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator />
          <DropdownMenu.Item></DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
```

## Create the User Button

The first content we will add is the User button. This will show that the user is logged in, and will be the trigger to open the menu. For the sake of this post we will assume that you have marked email as required in the Clerk Dashboard, in the [User & Authentication → Email, Password, Username → Email](https://dashboard.clerk.com/last-active?path=user-authentication/email-phone-username) section to ensure every user has an email. You could change this to a first name or username easily enough. Just make sure that whatever option you choose is something that will exist for all users, for the sake of rendering. You could also conditionally render different information depending on what the user has provided — show the first name if available; if not, show the email.

To build the button we will leverage the [`useUser()`](/docs/references/react/use-user#use-user) hook. This gives us access to information about the user, such as profile image, email, name, and more. We will also make sure that Clerk and the user have loaded, and that there is valid user data, before rendering the User button.

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
// Import useUser()
import { useUser } from '@clerk/nextjs'
// Import the Image element
import Image from 'next/image'

export const UserButton = () => {
  // Grab the `isLoaded` and `user` from useUser()
  const { isLoaded, user } = useUser()

  // Make sure that the useUser() hook has loaded
  if (!isLoaded) return null
  // Make sure there is valid user data
  if (!user?.id) return null

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        {/* Render a button using the image and email from `user` */}
        <button>
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl}
            width={30}
            height={30}
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content>
          <DropdownMenu.Label />
          <DropdownMenu.Group>
            <DropdownMenu.Item></DropdownMenu.Item>
            <DropdownMenu.Item></DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator />
          <DropdownMenu.Item></DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
```

## Add the Sign-Out and Manage Account Buttons

With the User button in place, we can now add Sign Out and Manage Account buttons. The [`useClerk()`](/docs/references/react/use-clerk) hook provides the two methods we will need for this — the [`signOut()`](/docs/custom-flows/sign-out#custom-sign-out) method and the [`openUserProfile()`](/docs/references/javascript/clerk/user-profile#open-user-profile) method. The User Profile will open as a modal. You could, instead, mount the [`<UserProfile />`](/docs/components/user/user-profile) component to its own route, and then link it to the route. For the Sign Out button, we will also need to use the Next.js router to handle the redirect.

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
// Import useClerk()
import { useUser, useClerk } from '@clerk/nextjs'
// Import the Next.js router
import { useRouter } from 'next/navigation'
import Image from 'next/image'

export const UserButton = () => {
  const { isLoaded, user } = useUser()
  // Grab the signOut and openUserProfile methods
  const { signOut, openUserProfile } = useClerk()
  // Get access to the Next.js router
  const router = useRouter()

  if (!isLoaded) return null
  if (!user?.id) return null

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button>
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl}
            width={30}
            height={30}
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content>
          <DropdownMenu.Label />
          <DropdownMenu.Group>
            <DropdownMenu.Item asChild>
              {/* Create a button with an onClick to open the User Profile modal */}
              <button onClick={() => openUserProfile()}>Profile</button>
            </DropdownMenu.Item>
            <DropdownMenu.Item></DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator />
          <DropdownMenu.Item asChild>
            {/* Create a Sign Out button -- signOut() takes a call back where the user is redirected */}
            <button onClick={() => signOut(() => router.push('/'))}>Sign Out </button>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
```

## Extending the Custom User Menu

The custom button has now replicated the behavior of the `<UserButton />`, though it is very much still unstyled. We’re going to do one more thing — add one more menu entry to mimic expanding the menu. This will use the [`<Link />`](https://nextjs.org/docs/app/api-reference/components/link) component from Next.js to link to a fictional `/subscriptions` route.

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useUser, useClerk } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
// Import Link to add more buttons to the menu
import Link from 'next/link'

export const UserButton = () => {
  const { isLoaded, user } = useUser()
  const { signOut, openUserProfile } = useClerk()
  const router = useRouter()

  if (!isLoaded) return null
  if (!user?.id) return null

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button>
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl}
            width={30}
            height={30}
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="border border-gray-200 bg-white text-black drop-shadow-md">
          <DropdownMenu.Label />
          <DropdownMenu.Group>
            <DropdownMenu.Item asChild>
              <button onClick={() => openUserProfile()}>Profile</button>
            </DropdownMenu.Item>
            <DropdownMenu.Item asChild>
              {/* Create a fictional link to /subscriptions */}
              <Link href="/subscriptions">Subscription</Link>
            </DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator />
          <DropdownMenu.Item asChild>
            <button onClick={() => signOut(() => router.push('/'))}>Sign Out </button>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
```

## The Last Step!

Your new component is almost ready — it just needs some styling. Let’s add a little bit to get started. The code below is ready to drop right into your app, and then you can import the new `<UserButton />` into your header.

### The Custom User Menu

![Create Custom User Menu Radix tutorial illustration](./7d5be406f59f3f30aaecd91a4f4dab6393a7a0ff-366x385.png)

### Final code

```tsx
'use client'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
// Import useUser() and useClerk()
import { useUser, useClerk } from '@clerk/nextjs'
// Import Next's router
import { useRouter } from 'next/navigation'
// Import the Image element
import Image from 'next/image'
// Import Link to add more buttons to the menu
import Link from 'next/link'

export const UserButton = () => {
  // Grab the `isLoaded` and `user` from useUser()
  const { isLoaded, user } = useUser()
  // Grab the signOut and openUserProfile methods
  const { signOut, openUserProfile } = useClerk()
  // Get access to Next's router
  const router = useRouter()

  // Make sure that the useUser() hook has loaded
  if (!isLoaded) return null
  // Make sure there is valid user data
  if (!user?.id) return null

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        {/* Render a button using the image and email from `user` */}
        <button className="flex flex-row rounded-xl border border-gray-200 bg-white px-4 py-3 text-black drop-shadow-md">
          <Image
            alt={user?.primaryEmailAddress?.emailAddress!}
            src={user?.imageUrl}
            width={30}
            height={30}
            className="mr-2 rounded-full border border-gray-200 drop-shadow-sm"
          />
          {user?.username ? user.username : user?.primaryEmailAddress?.emailAddress!}
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="mt-4 w-52 rounded-xl border border-gray-200 bg-white px-6 py-4 text-black drop-shadow-2xl">
          <DropdownMenu.Label />
          <DropdownMenu.Group className="py-3">
            <DropdownMenu.Item asChild>
              {/* Create a button with an onClick to open the User Profile modal */}
              <button onClick={() => openUserProfile()} className="pb-3">
                Profile
              </button>
            </DropdownMenu.Item>
            <DropdownMenu.Item asChild>
              {/* Create a fictional link to /subscriptions */}
              <Link href="/subscriptions" passHref className="py-3">
                Subscription
              </Link>
            </DropdownMenu.Item>
          </DropdownMenu.Group>
          <DropdownMenu.Separator className="my-1 h-px bg-gray-500" />
          <DropdownMenu.Item asChild>
            {/* Create a Sign Out button -- signOut() takes a call back where the user is redirected */}
            <button onClick={() => signOut(() => router.push('/'))} className="py-3">
              Sign Out{' '}
            </button>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
```

Example repository:
[https://github.com/royanger/clerk-custom-user-menu](https://github.com/royanger/clerk-custom-user-menu)

## Explore the Powerful Customization Options Clerk Offers!

Take a look at our [Custom Flows](/docs/custom-flows/overview) documentation to explore more ways to customize your application using the many hooks and methods Clerks provides.

For more in-depth technical inquiries or to engage with our community, feel free to [join our Discord](https://clerk.com/discord). Stay in the loop with the latest Clerk features, enhancements, and sneak peeks by following our X account, [@clerk](https://x.com/clerk). Your journey to seamless user management starts here!

---

# Clerk Webhooks: Data Sync with Convex
URL: https://clerk.com/blog/webhooks-data-sync-convex.md
Date: 2023-11-14
Category: Guides
Description: This post covers how to synchronize user data from Clerk into a Convex database using Webhooks.

Composing an application out of multiple data sources can be challenging, and while Clerk’s Backend APIs work great for most use cases, Webhooks offer the next level of integration that enables you to take full advantage of your existing stack. This post will cover how to synchronize user data from Clerk into your own backend using Webhooks.

## Data Sync with Convex

[Convex](https://convex.dev) is a real time [Backend-as-a-Service](https://en.wikipedia.org/wiki/Backend_as_a_service) Provider that makes building highly dynamic and interactive experiences with React easier than ever.

Clerk offers a first-class integration with Convex. The queries and mutations living in your Convex instance can be authenticated with a token that was created by Clerk. This integration is covered in more detail [in the documentation](/docs/integrations/databases/convex?utm_source=DevRel\&utm_medium=blog\&utm_campaign=docslanding\&utm_content=webhooks-blog\&utm_term=convex-post).

Convex is also a great target for synchronizing user data from Clerk using webhooks, since you can take full advantage of Convex’s realtime capabilities for user data.

If you are not sure what webhooks are and how to use webhooks with Clerk, it’s recommended to read [this getting started post](/blog/webhooks-getting-started?utm_source=DevRel\&utm_medium=blog\&utm_campaign=blog-to-blog\&utm_content=webhooks-blog\&utm_term=convex-post) first.

To demonstrate data synchronization with Convex, we can use [Convex’s Clerk starter repo](https://github.com/thomasballinger/convex-clerk-users-table). You can clone and run this repo locally by following the instructions in the repo.

This post will:

1. Explain how the demo uses data sync with reactive queries together
2. Show code examples of the webhooks handler implementation in the demo

## The Demo

The demo application is a chat room where users can send and receive messages. To access the room, a user needs to sign up for an account using the Clerk integration. Once signed in, the user is redirected to a chat room, which establishes a reactive query to the Convex database. This reactive query looks for the user in the database, and returns a “No Clerk User” if the user is not found, and a “Logged In” if the user is found.

Initially after signing up, the user will not be found in the database. Since the signup process happened with Clerk, Convex has no record of this user.

![Webhooks Data Sync Convex setup guide](./1e2c045dab59119f46ab6a0767ce42e6e766e9c8-3600x1431.png)

This is where webhooks come into play. After the user signs up with Clerk, Clerk will send an http request to a pre-defined URL with data about the sign up. This URL will point to a Convex HTTP Action, which will parse the data and create a record in the Convex database for the user. Now the client can query for this user again and receive the expected response.

![Webhooks Data Sync Convex setup guide](./878c4fda4bc4fdfc93ba7e78ba644c1543666e6a-3600x1023.png)

But since Convex queries are reactive, the client doesn’t need to retry the query, instead it will automatically receive an update from the server with the user data. This will trigger React to re-render the UI, providing the user access to the chat room.

![Webhooks Data Sync Convex setup guide](./41c813e18fefb8873b6df2952569d7417acca319-3600x1023.png)

## The Code

To look at the webhook implementation, we can open the `convex/http.ts` file in this repo.

```typescript {{ title: 'convex/http.ts', prettier: false }}
// define the webhook handler
const handleClerkWebhook = httpAction(async (ctx, request) => {
  const event = await validateRequest(request)
  if (!event) {
    return new Response('Error occured', { status: 400 })
  }
  switch (event.type) { /* ... */ }
  return new Response(null, { status: 200 })
});

// define the http router
const http = httpRouter()

// define the webhook route
http.route({
  path: '/clerk-users-webhook',
  method: 'POST',
  handler: handleClerkWebhook,
})
```

The `handleClerkWebhook` function is responsible for… you guessed it… handling the Clerk webhook. It’s a function decorated with `httpAction` and receives the HTTP request and a context object to access other Convex resources.

This function runs security validation on the request to ensure that it came from a trusted source, and runs some database mutations based on the type of event received. The mutations simply mirror the event - creating, updating, or deleting a user in Clerk simply results in creating, updating, or deleting the same user in Convex. It forms a copy of the Clerk user table within Convex.

```typescript {{ title: 'convex/http.ts' }}
switch (event.type) {
  case 'user.created':
  case 'user.updated': {
    await ctx.runMutation(internal.users.updateOrCreateUser, {
      clerkUser: event.data,
    })
    break
  }
  case 'user.deleted': {
    const id = event.data.id!
    await ctx.runMutation(internal.users.deleteUser, { id })
    break
  }
  default: {
    console.log('ignored Clerk webhook event', event.type)
  }
}
```

This allows the most powerful feature of Convex - Queries, to access the user data directly, which means the application doesn’t need to make additional requests to Clerk to fetch the user data (also called the n+1 problem), and the user interface can get notified if the data changes (e.g. if the user changes their name or profile picture).

```typescript {{ title: 'convex/users.ts' }}
export const getUser = internalQuery({
  args: { subject: v.string() },
  async handler(ctx, args) {
    return ctx.db
      .query('users')
      .withIndex('by_clerk_id', (q) => q.eq('clerkUser.id', args.subject))
      .unique()
  },
})
```

## Full Potential

Webhooks are a powerful feature that enable integrations with existing backend tools, allowing you to use their technical potential to the fullest, while also using Clerk's user management capabilities to the fullest. They help glue disjointed parts of the backend together so that you can pick whatever technology suits your use case for storing and processing data.

## Ready to Integrate Authentication in Your App?

You can explore the [Clerk Webhooks docs](/docs/integrations/webhooks/overview?utm_source=DevRel\&utm_medium=blog\&utm_campaign=docslanding\&utm_content=webhooks-blog\&utm_term=convex-post) to learn more about the webhook events and data that is exposed by Clerk and build your own integrations today!

For more in-depth technical inquiries or to engage with our community, feel free to [join our Discord](https://clerk.com/discord). Stay in the loop with the latest Clerk features, enhancements, and sneak peeks by following our X account, [@clerk](https://x.com/clerk). Your journey to seamless User Management starts here!

---

# Exploring Clerk Metadata with Stripe Webhooks
URL: https://clerk.com/blog/exploring-clerk-metadata-stripe-webhooks.md
Date: 2023-11-09
Category: Guides
Description: Utilize Clerk Metadata & Stripe Webhooks for efficient user data management and enhanced SaaS experiences with our step-by-step tutorial.

## Introduction to User Metadata

By putting Clerk’s user metadata types to work, developers can proficiently handle user data, making their SaaS integrations run smoother, and work harder. It's like adding a turbocharger to your product's engine, enhancing functionality and improving the user experience, for a more comprehensive, customizable, and synchronizing systems ready to build any SaaS product out there.

A great feature in the wild world of SaaS product development to power integrations powering integrations with other powerful products. We're talking about a sturdy, malleable means for handling user data.

You've got three types of User Metadata – [public, private, and unsafe](/docs/users/metadata#user-metadata). Each one has its own unique access level and use case.

- **Public**: It’s an accessible from both the frontend and backend. It's like the town bulletin board where you post things everyone can see but can't change. Think membership levels, user roles, stuff like that.
- **Private**: It’s like the secret stash of user data only reachable from the backend. Perfect for things like account identifiers or subscription details, you know, the stuff you don't want out in the open.
- **Unsafe**: It might sound a bit ominous, but it is super flexible; treat it like form data and validate any user inputs. It can be modified and accessed from both frontend and backend. Great for things like user preferences, setting or just any of the nitty-gritty details that make a user's experience unique.

## User Metadata Meets Stripe Webhooks

Harnessing the power of User Metadata in tandem with Stripe’s webhooks offers significant advantages in SaaS product development. Clerk Metadata's flexible user data management paired with Stripe webhooks' real-time transaction updates creates a robust, efficient system. This combination ensures both comprehensive user data handling and prompt responsiveness to transaction events. Utilizing Clerk Metadata alongside Stripe’s webhooks lends itself well for streamlined and user-friendly SaaS development.

Utilizing Clerk's public User Metadata offers significant advantages for managing user data and transactions in your SaaS product. It allows for real-time updates, such as including a "paid" field after a transaction, offering a clear snapshot of payment statuses. This use of public metadata improves transparency, boosts data management efficiency, and enhances the overall user experience.

## Tutorial: Implementing Stripe Webhook with Clerk User Metadata

The first step will be setting up accounts at [Clerk](https://dashboard.clerk.com/sign-up?utm_source=DevRel\&utm_medium=blog\&utm_campaign=devrel-blog-signups) & [Stripe](https://dashboard.stripe.com/register). Once you have those accounts you will follow the well documented [Clerk’s Next.js Quickstart Guide](/docs/quickstarts/nextjs). To have access to the correct data from the Clerk session you will need to access the [custom session data](/docs/backend-requests/making/custom-session-token#click-the-edit-button) on the Dashboard, we will edit the session data to look like this:

```json
{
  "publicMetadata": "{{user.public_metadata}}"
}
```

The last part will be setting up from the Stripe quickstart, the basic [Stripe webhook](https://stripe.com/docs/webhooks/quickstart). We will modify it later for our own needs, and there will also be a repo for you to grab afterwards! By the end of the quickstarts, you should have something in your **.env.local** that looks like this.

```bash {{ title: 'Environment Variables' }}
# CLERK
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_***
CLERK_SECRET_KEY=sk_test_***

# STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_***
STRIPE_SECRET_KEY=sk_test_***
STRIPE_WEBHOOK_SECRET=whsec_***
```

## Auth Middleware Setup

Once we have everything installed we are going to jump into a very basic app, with one private route **/members**, and the homepage route will serve as our public route where all the fun stuff will happen. Our [Middleware](/docs/references/nextjs/auth-middleware) is going to be handling the access.

```typescript {{ title: 'Middleware Routes' }}
export default authMiddleware({
  // Making sure homepage route and API, especially the webhook, are both public!
  publicRoutes: ['/', '/api/(.*)'],
  afterAuth: async (auth, req) => {
    // Nice try, you need to sign-in
    if (!auth.userId && !auth.isPublicRoute) {
      return redirectToSignIn({ returnBackUrl: req.url })
    }
    // Hey! Members is for members 😆
    if (
      auth.userId &&
      req.nextUrl.pathname === '/members' &&
      auth.sessionClaims.publicMetadata?.stripe?.payment !== 'paid'
    ) {
      return NextResponse.redirect(new URL('/', req.url))
    }
    // Welcome paid member! 👋
    if (
      auth.userId &&
      req.nextUrl.pathname === '/members' &&
      // How we get payment value "paid" is next, in the webhook section!
      auth.sessionClaims.publicMetadata?.stripe?.payment === 'paid'
    ) {
      return NextResponse.next()
    }
    // If we add more public routes, signed-in people can access them
    if (auth.userId && req.nextUrl.pathname !== '/members') {
      return NextResponse.next()
    }
    // Fallthrough last-ditch to allow access to a public route explicitly
    if (auth.isPublicRoute) {
      return NextResponse.next()
    }
  },
})
```

We can simplify the Middleware access logic for this app, but this explicit example can show how you can have far more complex access handling. Where do we get **paid** from!? That is coming up next.

### Webhook Endpoint

Since this app is our SaaS with member access, we need to provide a way for the user to pay and gain access. Let’s start with setting up the tokens for Clerk & instantiating Stripe.

```typescript
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  apiVersion: '2023-10-16',
})
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string
```

After we have the credentials, the rest of the code should look very similar to the Stripe default logic for webhooks.

```typescript
export async function POST(req: NextRequest) {
  if (req === null) throw new Error(`Missing userId or request`, { cause: { req } })
  // Stripe sends this for us 🎉
  const stripeSignature = req.headers.get('stripe-signature')
  // If we don't get it, we can't do anything else!
  if (stripeSignature === null) throw new Error('stripeSignature is null')

  let event
  try {
    event = stripe.webhooks.constructEvent(await req.text(), stripeSignature, webhookSecret)
  } catch (error) {
    if (error instanceof Error)
      return NextResponse.json(
        {
          error: error.message,
        },
        {
          status: 400,
        },
      )
  }
  // If we dont have the event, we can't do anything again
  if (event === undefined) throw new Error(`event is undefined`)
  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object
      console.log(`Payment successful for session ID: ${session.id}`)
      break
    default:
      console.warn(`Unhandled event type: ${event.type}`)
  }

  return NextResponse.json({ status: 200, message: 'success' })
}
```

So what is next!? We need a way to know when a Clerk User has "paid." Well, let's extract that switch statement and add the secret sauce. That'll make this all work when we are done!

```typescript {{ title: 'Adding Clerk User Metadata to Event' }}
switch (event.type) {
  case 'checkout.session.completed':
    const session = event.data.object
    console.log(`Payment successful for session ID: ${session.id}`)
    // That's it? Yep, that's it. We love UX 🎉
    clerkClient.users.updateUserMetadata(event.data.object.metadata?.userId as string, {
      publicMetadata: {
        stripe: {
          status: session.status,
          // This is where we get "paid"
          payment: session.payment_status,
        },
      },
    })
    break
  default:
    console.warn(`Unhandled event type: ${event.type}`)
}
```

Some of you may have noticed **event.data.object.metadata?.userId** (where did that come from!?). We will get to that one too. The reason for this is that we can’t access Clerk’s session in the webhook, so we will get a little creative.

### Session Endpoint

We will now need to create an endpoint that will generate our Stripe session that will be used to make our payment and turn our Clerk User into a paid **Member**. This is where the **userId** in the webhook will also be coming from! Instantiate **stripe** the same as before, it will again be a Next.js POST endpoint.

```typescript {{ title: 'Stripe Session' }}
// This is our Clerk function for session User
const { userId } = auth()
// We are receiving this from the Client request, thats next!
const { unit_amount, quantity } = await req.json()

try {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: {
            name: 'Membership',
          },
          unit_amount,
        },
        quantity,
      },
    ],
    // This is where "event.data.object.metadata?.userId" is defined!
    metadata: {
      userId,
    },
    mode: 'payment',
    success_url: `${req.headers.get('origin')}/members`,
    cancel_url: `${req.headers.get('origin')}/`,
  })

  return NextResponse.json({ session }, { status: 200 })
} catch (error) {
  // ...
}
```

We have now laid the groundwork for a SaaS leveraging Clerk’s User Metadata to manage User specific data! So, to really focus on the versatility and potential of this feature, the UI portion has been kept really simple. We have the Homepage with a button to navigate to **/members** page and to become a paid member, let’s take a look at the homepage.

```tsx {{ title: 'Homepage Implementation' }}
export default function Home() {
  const { isSignedIn } = useAuth()

  return (
    <main>
      {!isSignedIn ? (
        <div className="...">
          <SignIn redirectUrl="/" />
        </div>
      ) : (
        <div>
          <div className="...">You are signed in!</div>
          <div className="...">
            <CheckoutButton />
            <a className="..." href="/members">
              Go to members page
            </a>
          </div>
        </div>
      )}
    </main>
  )
}
```

## Wrap up!

This pattern can be used with any other transactions or user specific data you would like to handle in the backend and then utilize in the client. This keeps your User Management pragmatic & versatile, offloading the burden across multiple systems. This is only the beginning with what we can do with Clerk’s toolset, this time we only leveraged User Metadata! What should we do next? Let us know in the [Discord](https://clerk.com/discord) and on [X(Twitter)](https://x.com/clerk)!

Not forgetting, you will want the [complete codebase](https://github.com/JacobMGEvans/stripe-with-webhooks-and-metadata) to check out, and learn from!

---

# The Ultimate Guide to Next.js Authentication
URL: https://clerk.com/blog/nextjs-authentication.md
Date: 2023-11-02
Category: Guides
Description: In this guide, you will learn best practices for implementing secure authentication in your Next.js app.

[Next.js](https://nextjs.org) has become the go-to framework for JavaScript and React development. It has become so popular because of the superior developer experience, performance optimization features, and its ability to facilitate different rendering options and serverless functions with ease. The combination of these attributes makes it an appealing choice for both new and experienced developers.

In Next.js 13, the Next.js team significantly upgraded the capabilities of the framework. Moving to what they call the “App Router,” Next.js is now based on [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) which boast performance upgrades over the previous client-server architecture.

But this shift has also led to a change in how authentication works in Next.js. In this article we want to provide an up-to-date guide on how authentication now works on Next.js so you can easily understand the concepts involved and **easily add secure authentication to your Next.js applications**.

## The Next.js App Router

The [App Router paradigm was introduced in Next.js 13](https://nextjs.org/docs/app) and is a significant shift in how the Next.js framework works.

Next.js was initially built as a React framework to facilitate server-side rendered and statically generated sites. But as the framework grew, more options became available:

1. **Client-Side Rendering (CSR)** is where content is rendered on the client side, after the initial page load. This is ideal for pages that don’t require SEO or initial content served by the server. Common for user dashboards or pages behind authentication. You use React state and effects (`useState`, `useEffect`, etc.) as you would in a regular React app.
2. **Static Site Generation (SSG)** is where pages are pre-rendered at build time and served statically. Each request receives the same HTML. This is good for content that doesn’t change often and doesn’t need to be updated with every request, such as blogs, marketing pages, and documentation, etc. Here, you use `getStaticProps` to fetch the required data at build time.
3. **Incremental Static Regeneration (ISR)** lets you statically generate pages with the option to update them in the background. Once updated, subsequent requests get the new version. This is useful for content that updates periodically but doesn’t need real-time updates. Like with SSG, you can use `getStaticProps`, but this time you add a `revalidate` property. The `revalidate` interval (in seconds) determines how often the page should be updated.
4. **Server-Side Rendering (SSR)** means each request is rendered on-the-fly on the server, providing fresh data. It is ideal for pages that need real-time data or when content changes frequently. Also beneficial for SEO. You use `getServerSideProps` to fetch data on each request.

You could have some pages work with CSR, some SSG, and some SSR. But this also led to confusion and performance issues with sites not optimally built.

Next.js 13 and the App Router are designed to simplify the developer experience within Next.js, and rebuild the framework around [React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components). With RSC, the default for the entire site is server-side rendering, with each page being opted-in to CSR as needed. This leads to performance gains, as rendering on the server is faster, and you don’t have to send heavy JavaScript payloads over the network for hydration. It also means more sites can work on the ‘edge’ network, where sites are served from multiple servers close to users.

But this does mean that the way developers have been using Next.js thus far has now changed. The old, Pages Router continues to work, and can be used side-by-side with the App Router, but significant changes in how data is served, such as no request/response objects being passed means developers will need to rethink how they work, and how their authentication works.

Here, we’re going to go through:

1. How authentication works with the pages router in Next.js
2. How authentication works with the App Router in Next.js

This will give you the ability to use either as you need, and to see the differences in each paradigm when it comes to authentication.

## Authentication With the Next.js Pages Router

Before we get into authentication with the App Router, let’s look at how authentication works with the Pages Router in Next.js.

Let’s create a Next.js project. To do this, we use:

```sh
npx create-next-app@latest
```

You will be prompted to choose different options, but given that we are explicitly using the Pages Router, you’ll want to select “no” when asked if you would like to use the App Router.

With that done, we can install Clerk:

```sh
npm install @clerk/nextjs
```

Next, we want to add our API keys as environmental variables in an `.env.local` file. To get these, sign up for a free [Clerk account](/):

```sh
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_*****
CLERK_SECRET_KEY=sk_test_*****
```

Before we go any further, we’ll also make some small changes to our homepage, at `pages/index.js`:

```jsx
import Head from 'next/head'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <Head>
        <title>Hello Pages Router with Next.js & Clerk</title>
        <meta
          name="description"
          content="A simple Hello World homepage using Next.js and TailwindCSS"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, Pages Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>
      </div>
    </div>
  )
}
```

If we now run `npm run dev`, we’ll get this homepage:

![Nextjs Authentication tutorial illustration](./e6980a8e3bf380cb0bdf8cac5a4ea4e503f0411e-2000x1264.png)

We’re now ready to start adding Clerk to our code. The first thing we need to do is mount the [ClerkProvider](/docs/components/clerk-provider#clerk-provider). The `ClerkProvider` is the core component of Clerk. It is what handles all the active session data and the user context. Whenever a Clerk hook (or any other components) needs authentication data, it is getting it from the `ClerkProvider`.

The `ClerkProvider` needs needs to access [headers](https://nextjs.org/docs/app/api-reference/functions/headers) to authenticate any user. This is important because:

`headers()` is a Dynamic Function whose returned values cannot be known ahead of time. Using it in a layout or page
will opt a route into dynamic rendering at request time.

To protect your entire application, it is recommended to wrap our main Component in the `_app.tsx|jsx` file:

```tsx
import { ClerkProvider } from '@clerk/nextjs'
import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ClerkProvider {...pageProps}>
      <Component {...pageProps} />
    </ClerkProvider>
  )
}

export default MyApp
```

This will protect every page within the application and will make the user context accessible anywhere within the app, but will also opt every page into dynamic rendering.

If you have statically served pages or are using incremental static regeneration, you can wrap route groups further into your application. For example, you might want to leave your marketing site unprotected, but wrap your dashboard components in the `ClerkProvider`.

Clerk [requires middleware](/docs/references/nextjs/auth-middleware#auth-middleware) to determine the routes that need to be protected. This is in `middleware.js` in our root directory. This matcher will put every page and API route on the site behind authentication:

```jsx
import { authMiddleware } from '@clerk/nextjs'

// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware
export default authMiddleware({})

export const config = {
  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
}
```

The entire application is now protected. Accessing any page while signed out will redirect you to the sign up page. If we go to the homepage again, we get redirected:

![Nextjs Authentication tutorial illustration](./f1c7e0f565e3b201cff729570d3fef5862539c02-2000x1264.png)

Usually, this isn’t what you want. Your homepage, product, and marketing pages aren’t very helpful if they are behind authentication!

If you navigate to the homepage now you’ll also get this warning:

The request to / is being redirected because there is no signed-in user, and the path is not included in ignoredRoutes
or publicRoutes.

To prevent this behavior, choose one of:

1. To make the route accessible to both signed in and signed out users, pass `publicRoutes: ["/"]` to `authMiddleware`
2. To prevent Clerk authentication from running at all, pass `ignoredRoutes: ["/((?!api|trpc))(_next.*|.+\\.[\\w]+$)", "/"]` to `authMiddleware`
3. Pass a custom [afterAuth](/docs/references/nextjs/auth-middleware#using-after-auth-for-fine-grained-control) to `authMiddleware`, and replace Clerk’s default behavior of redirecting unless a route is included in public routes

So let’s make a change to add a public route:

```jsx
import { authMiddleware } from '@clerk/nextjs'

export default authMiddleware({
  publicRoutes: ['/'],
})

export const config = {
  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
}
```

Now, when we head to the homepage, it is available again:

![Nextjs Authentication tutorial illustration](./e6980a8e3bf380cb0bdf8cac5a4ea4e503f0411e-2000x1264.png)

Great. But now there is no way for us to sign up or sign in. Clerk’s [Next.js Authentication SDK](/nextjs-authentication) provides helpers for determining whether a user is signed in or note, and conditionally changing the text. Let’s wrap our text in these components:

```jsx
import { SignedOut } from '@clerk/nextjs'
import Head from 'next/head'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <Head>
        <title>Hello Pages Router with Next.js & Clerk</title>
        <meta
          name="description"
          content="A simple Hello World homepage using Next.js and TailwindCSS"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, Pages Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>

        <SignedOut>
          <Link href="/sign-up">
            <div>
              <h3 className="mb-4 text-xl font-bold text-blue-500">
                Sign in or sign up for an account
              </h3>
            </div>
          </Link>
        </SignedOut>
      </div>
    </div>
  )
}
```

Here, if a user is signed out, they’ll see a link to a sign up page. This page doesn’t exist yet, so lets create it. It will live at `pages/sign-up/[[...index]].jsx`:

```jsx
import { SignUp } from '@clerk/nextjs'

export default function Page() {
  return <SignUp />
}
```

Not much to it. If you try to go that page, you’ll be redirected to sign in/sign up, so it will look like its working. But you are actually being redirected because that page hasn’t been defined as a public route in the middleware, so it itself is behind authentication. To get around this, we’re actually going to add it to our environment variables as a special page:

```sh
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
```

Now, if we go to that sign up page, we’ll get the modal where we can sign up:

![Nextjs Authentication tutorial illustration](./98d8928b270f39bf6cbc462d88899e026b7f96c1-2000x1264.png)

Then, as our environment variables say, we’re redirected to the home page:

![Nextjs Authentication tutorial illustration](./d52c43bd5ba929652ae9b6563b8ea9cf9882c950-2000x1264.png)

As you can see, the link to sign in as gone, as it was wrapped in the component that only shows it if you are signed out.

We now have a problem. We are now signed in (great!), but we can’t sign out (uh oh!). We could create a specific link for this as well, but Clerk provides a [UserButton component](/docs/references/javascript/clerk/user-button#user-button-component) that allows us to do this easily. Let’s add that to the homepage:

```jsx
import { SignedOut } from '@clerk/nextjs'
import Head from 'next/head'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <Head>
        <title>Hello Pages Router with Next.js & Clerk</title>
        <meta
          name="description"
          content="A simple Hello World homepage using Next.js and TailwindCSS"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, Pages Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>

        <SignedOut>
          <Link href="/sign-up">
            <div>
              <h3 className="mb-4 text-xl font-bold text-blue-500">
                Sign in or sign up for an account
              </h3>
            </div>
          </Link>
        </SignedOut>
        <SignedIn>
          <div>
            <UserButton afterSignOutUrl="/" />
          </div>
        </SignedIn>
      </div>
    </div>
  )
}
```

We’ve wrapped this `UserButton` within the [SignedIn component](/docs/components/control/signed-in#signed-in) so it’ll only show when the user is signed in:

![Nextjs Authentication tutorial illustration](./9ff4051f631c059104c26f01fc4cbb304f7a6f99-2000x1264.png)

Using that button, the user can easily adjust their profile as well as sign out of the application.

Now the user is signed in, they can visit other pages. Let’s create a `pages/protected.jsx` page:

```jsx
import { clerkClient } from '@clerk/nextjs'
import { getAuth, buildClerkProps } from '@clerk/nextjs/server'

const ProtectedPage = ({ user }) => {
  if (!user) {
    return (
      <div>
        <p>Please log in to view this content.</p>
        {/* Optionally add a login button or redirect logic here */}
      </div>
    )
  }

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>This is your protected page.</p>
      {/* Other user-specific JSX components/data can be added here */}
    </div>
  )
}

export default ProtectedPage

export const getServerSideProps = async (ctx) => {
  const { userId } = getAuth(ctx.req)

  if (!userId) {
    return { props: {} } // This will pass an empty props object and the component will handle the "not logged in" state
  }

  const userFromClerk = userId ? await clerkClient.users.getUser(userId) : null
  const user = userFromClerk
    ? {
        id: userFromClerk.id,
        firstName: userFromClerk.firstName,
        lastName: userFromClerk.lastName,
        // ... Add other necessary fields here
      }
    : null

  return { props: { user, ...buildClerkProps(ctx.req) } }
}
```

The code uses the `clerkClient`, `getAuth`, and `buildClerkProps` utilities from the `@clerk/nextjs` library to handle user authentication. If a user is not authenticated (determined by the absence of a `user` prop), the page prompts the user to log in. If authenticated, the page displays a personalized welcome message.

The server-side function `getServerSideProps` checks for the `userId` in the incoming request using the `getAuth` function; if the `userId` is present (indicating authentication), it fetches detailed user information from Clerk via `clerkClient.users.getUser`. The fetched data is then streamlined to a simpler format, extracting only necessary fields like the user’s first name and last name.

The `getServerSideProps` function concludes by returning the user data, if any, along with additional props sourced from `buildClerkProps`, ensuring the frontend receives the necessary data to render the page appropriately.

This renders as:

![Nextjs Authentication tutorial illustration](./7247464b939968d7317dd044b2e85c4301b073a6-2000x1264.png)

Here, the authentication is checked on the server before the page is rendered via `getServerSideProps`. We can also check whether the user is authenticated on the client. Let’s create a `pages/protected-client.jsx` page:

```jsx
import { useUser } from '@clerk/nextjs'

export default function Example() {
  const { isLoaded, isSignedIn, user } = useUser()

  if (!isLoaded || !isSignedIn) {
    return null
  }

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>This is your protected page on the client.</p>
      {/* Other user-specific JSX components/data can be added here */}
    </div>
  )
}
```

Which renders this page:

![Nextjs Authentication tutorial illustration](./8fd94b0e65832bf268c37a8aa92c170f6b27de49-2000x1264.png)

One final part of Next.js we can protect with authentication–[API routes](/docs/references/nextjs/get-auth#protecting-api-routes). With the pages router, any route within pages/api is returned as an API. Let’s protect an API route at `pages/api/auth.js`:

```jsx
import { clerkClient } from '@clerk/nextjs'
import { getAuth } from '@clerk/nextjs/server'

export default async function handler(req, res) {
  const { userId } = getAuth(req)

  if (!userId) {
    return res.status(401).json({ error: 'Unauthorized' })
  }

  const user = userId ? await clerkClient.users.getUser(userId) : null

  return res.status(200).json({ user })
}
```

This uses the `getAuth` helper that retrieves the authentication state to protect the API route. If we call this within the browser, we’ll get the user JSON:

![Nextjs Authentication tutorial illustration](./19003492bcb5eb596cd8de307703043ed506f2b6-2000x1264.png)

You now have a protected Next.js application using the pages router. Let’s move on to the App Router.

## Authentication with the Next.js App Router

We’re now going to work through an entire example with [the Next.js App Router.](/docs/quickstarts/nextjs#app-router) Some of this is the same as above, particularly, the set up, but once we get into the actual authentication, there are going to be some differences.

Let’s create a new Next.js project:

```sh
npx create-next-app@latest
```

This time, when you are prompted to use the App Router, select “yes”.

Then install Clerk into this project:

```
npm install @clerk/nextjs
```

And then add our API keys in an `.env.local` file:

```sh
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_*****
CLERK_SECRET_KEY=sk_test_*****
```

We’ll spool up another bare-bones homepage, this time at `app/page.js`:

```jsx
import Head from 'next/head'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <Head>
        <title>Hello Pages Router with Next.js & Clerk</title>
        <meta name="description" content="A simple Hello World homepage using Next.js and Clerk" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, Pages Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>
      </div>
    </div>
  )
}
```

If we now run `npm run dev`, we’ll get this homepage:

![Nextjs Authentication tutorial illustration](./b25b2a50a76078b8517b027d0e5efed705d6576d-2000x1264.png)

Looks like we have some nicer default styling with the App Router.

Next up is to add the `ClerkProvider`. This is the first point at which there is a difference between the pages router and the App Router. With the App Router, we’re going to add the `ClerkProvider` to our `layout.js`:

```jsx
import { ClerkProvider } from '@clerk/nextjs'

export const metadata = {
  title: 'Next.js 13 with Clerk',
}

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}
```

The App Router doesn’t use the `_app.js` file. Instead, layout.js lets you set a global layout for your application. Therefore you can add the `ClerkProvider` to e.g. the `dashboard/layout.js` file, and only the files within that route group would require a login. To repeat from above, this is important because the `ClerkProvider` requires dynamic rendering. If you nest the `ClerkProvider` at a lower level, this allows you to serve the landing and marketing pages statically.

The root layout is a server component. If you plan to use the `ClerkProvider` outside the root layout, it will need to
be a server component as well.

Next step is the middleware. Again, this is in `middleware.js` in our root directory. This time, we’ll start with our homepage as a public route:

```jsx
import { authMiddleware } from '@clerk/nextjs'

export default authMiddleware({
  publicRoutes: ['/'],
})

export const config = {
  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
}
```

Clerk is now protecting the entire application. Let’s change our homepage to use the [SignedOut component](/docs/components/control/signed-out#signed-out) so we can sign in to the app:

```jsx
import { SignedOut } from '@clerk/nextjs'
import Head from 'next/head'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <Head>
        <title>Hello Pages Router with Next.js & Clerk</title>
        <meta
          name="description"
          content="A simple Hello World homepage using Next.js and TailwindCSS"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, Pages Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>

        <SignedOut>
          <Link href="/sign-up">
            <div>
              <h3 className="mb-4 text-xl font-bold text-blue-500">
                Sign in or sign up for an account
              </h3>
            </div>
          </Link>
        </SignedOut>
      </div>
    </div>
  )
}
```

The link now shows on the page:

![Nextjs Authentication tutorial illustration](./53272617b60538ee8c19d0a49d97c4f756342902-2000x1264.png)

Next we’ll add the two other components that will be important for signing up/in/out. First, the sign up page, which will be at `app/sign-up/[[...sign-up]]/page.jsx`:

```jsx
import { SignUp } from '@clerk/nextjs'

export default function Page() {
  return <SignUp />
}
```

Remember to also add these new endpoints to your `.env.local`:

```sh
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
```

Second, we need the user button:

```jsx
import { SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-500 to-purple-500">
      <div className="rounded-xl bg-white p-8 shadow-lg">
        <h1 className="mb-4 text-2xl font-bold text-blue-500">Hello, App Router!</h1>
        <p className="text-gray-600">This is a simple homepage built with Next.js and Clerk</p>

        <SignedOut>
          <Link href="/sign-up">
            <div>
              <h3 className="mb-4 text-xl font-bold text-blue-500">
                Sign in or sign up for an account
              </h3>
            </div>
          </Link>
        </SignedOut>
        <SignedIn>
          <div>
            <UserButton afterSignOutUrl="/" />
          </div>
        </SignedIn>
      </div>
    </div>
  )
}
```

Now, when we click the sign up/in link we again go through to the Clerk modal:

![Nextjs Authentication tutorial illustration](./a018d5cc5de01bc8cbfed9e160a38a96c5808f5b-2000x1264.png)

Once we sign in we are redirected back to the homepage, now with the user button:

![Nextjs Authentication tutorial illustration](./d4a7445cd988fc8f664da1720c1ff78222daf6f8-2000x1264.png)

With the user signed in, let’s do the same as with the pages router: create a protected page and a protected API.

Again, for protected pages, we can check for authentication on the server or on the client. Let’s quickly do the client side as that is similar to the pages router:

```jsx
'use client'

import { useUser } from '@clerk/nextjs'

export default function Example() {
  const { isLoaded, isSignedIn, user } = useUser()

  if (!isLoaded || !isSignedIn) {
    return null
  }

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>This is your protected page on the client.</p>
      {/* Other user-specific JSX components/data can be added here */}
    </div>
  )
}
```

The only difference between the App Router and the pages router version here is the “use client” directive. By default, all components in the App Router are server components, so will render on the server. The “use client” directive opts this component into client-side rendering, so we can then use Clerk hooks.

This renders as:

![Nextjs Authentication tutorial illustration](./e3266ebf4b05b8bf25e26f448f46a325ff460408-2000x1264.png)

For server rendering, Clerk has two App Router-specific helpers that you can use with your Server Components:

1. [auth()](/docs/references/nextjs/auth) will return the [Authentication](/docs/references/nextjs/authentication-object) object of the currently active user. One of the fundamental differences with the App Router is that you don’t have to inspect the request object constantly for authentication headers. Instead they are available in the global scope through Next.js’s [headers()](https://nextjs.org/docs/app/api-reference/functions/headers) and [cookies()](https://nextjs.org/docs/app/api-reference/functions/cookies) functions, that ClerkProvider gives you access to.
2. [currentUser()](/docs/references/nextjs/current-user) will return the [User](/docs/references/javascript/user/user) object of the currently active user so you can render information, like their first and last name, directly from the server.

Here, we’re going to use both to get information about the user on a protected page:

```jsx
import { auth, currentUser } from '@clerk/nextjs'

export default async function Page() {
  // Get the userId from auth() -- if null, the user is not logged in
  const { userId } = auth()

  if (userId) {
    // Query DB for user specific information or display assets only to logged in users
  }

  // Get the User object when you need access to the user's information
  const user = await currentUser()

  // Use `user` to render user details or create UI elements
  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>This is your protected page.</p>
      {/* Other user-specific JSX components/data can be added here */}
    </div>
  )
}
```

Which gives us a page with user information rendered:

![Nextjs Authentication tutorial illustration](./43cb7ef9a02b2d0a7c846269d2eb6e0214e64e92-3346x2114.png)

For API routes, the routing is different, but the code is similar. Let’s build an API route under `app/api/user/route.jsx`:

```jsx
import { NextResponse } from 'next/server'
import { auth, currentUser } from '@clerk/nextjs'

export async function GET() {
  const { userId } = auth()

  if (!userId) {
    return new NextResponse('Unauthorized', { status: 401 })
  }

  const user = await currentUser()

  // perform your Route Handler's logic

  return NextResponse.json({ user }, { status: 200 })
}
```

We don’t have the request and response objects with the App Router, so get all our authentication data from our Clerk helpers (which get it from the ClerkProvider which gets it from the global scope via the [Next.js headers() function](https://nextjs.org/docs/app/api-reference/functions/headers)). We can return a JSON of the user’s information:

![Nextjs Authentication tutorial illustration](./4503ac653efdf0ba31a88581dad9641ea6cff2e7-2000x1264.png)

And that’s it. You have server-side, client-side, and API route authentication using the Next.js App Router.

## Authenticating with the App Router

The Next.js App Router offers significant performance gains compared to the traditional pages router. However, you can absolutely continue leveraging the pages router in your Next.js applications – Clerk supports authentication with both routing approaches.

To learn more about the App Router, check out the official [Next.js documentation](https://nextjs.org/docs/app). If you would like to get started with Clerk, [sign up for a free account](https://dashboard.clerk.com/sign-up) and follow along with our [Next.js Quickstart](/docs/quickstarts/nextjs).

---

# Empower Your Support Team With User Impersonation
URL: https://clerk.com/blog/empower-support-team-user-impersonation.md
Date: 2023-10-18
Category: Guides
Description: User impersonation enables support teams to assist customers without compromising privacy and security, essential for delivering great CX.

Unfortunately, no product is perfect. Once in a while, a user will have a problem with your application and need some help from the support team.

But if the problem is specific to the user–something about their data or their permissions within the product–how can customer support help? They could screenshare with the user, but that requires a lot of setup. They could just get the user to describe their problem and then walk them through the steps to fix it, but that is open to all kinds of errors. They could share their credentials with the support team, but that is a huge security risk!

The answer is to implement **user impersonation** in the product.

User impersonation is an admin feature that allows one user, usually an admin or the support team, to take on the identity of another user without knowing their password or other authentication credentials.

It is one of those features that gets missed in an initial roadmap but is critical to the product's long-term success. Without it, your success team flies blind when trying to help customers. Let’s explain why it’s important and how you can implement user impersonation in your application.

## The Importance of User Impersonation

The core reason to use user impersonation is troubleshooting and support. If a user reports an issue specific to their account, admins or the support team can impersonate the user to experience the application exactly as the user does. This helps in identifying and resolving the issue more efficiently.

But user impersonation can go beyond just straightforward support. User impersonation can be important for:

- **User Experience Testing**: Developers can use impersonation to see how the application or platform appears to users with different roles or permissions. This can be particularly useful for platforms with different user tiers, allowing for testing the user experience at each level.
- **Training and Onboarding**: In some cases, particularly within enterprise applications, user impersonation can be helpful for training. A trainer can impersonate a user role to guide new users through specific functionalities of the platform.
- **Data Access and Recovery**: If a user cannot access specific data or files, a support team member might use impersonation to access and recover that data on the user's behalf.
- **Auditing and Compliance**: In certain regulated industries, there might be a need to audit user actions or verify data accuracy. Impersonation can allow an auditor to access the system as a specific user to ensure compliance with regulations.

Of course, impersonating users is fraught with risks. Impersonation can easily lead to privacy breaches if not handled carefully. Admins and support can see personal or sensitive information. Also, if not implemented securely, impersonation features can be a potential attack vector for malicious actors. Ensuring that only authorized personnel can use impersonation and that all impersonation activities are logged for auditing is essential.

Because of these concerns, any system implementing user impersonation should have stringent security controls, logging, and auditing. It's also essential to have clear policies about when and why impersonation is used.

## Implementing User Impersonation

Implementing user impersonation is tricky. Literally, you are tricking your application into thinking that you are someone else. Here are the steps to consider through as you add user impersonation to your product:

1. **Ensure Strong Permissions and Auditing**: Only certain roles, such as admins and the support team, should be allowed to impersonate. As we said above, you should log every impersonation attempt, including the user doing the impersonation, the impersonated user, and the timestamp.
2. **Authentication & Authorization**: Established libraries, like [Clerk](/docs/custom-flows/user-impersonation#user-impersonation), have user impersonation built in. Use these wherever possible to lessen the likelihood of mistakes–get this wrong, and you can have serious privacy breaches. You should store roles in JWT or sessions to check user permissions.
3. **Impersonation**: When an authorized user requests to impersonate another user, you should store the original user's ID somewhere safe (e.g., in their session or another token), then update the user session/token to reflect the impersonated user's ID. When the user finishes impersonation, you must restore their session/token using the stored original user ID.
4. **UI Indication**: To avoid confusion, the UI should indicate when impersonation is active. A simple banner or color change can be used to show that the admin is in impersonation mode.

Let’s show how this would work with a basic JavaScript application. We’re going to set up two core components to this:

1. A backend Express server that will handle the actual impersonation.
2. A frontend React application that will provide the UI for the support team to use.

### The backend server

The backend is where we will incorporate the logic for our impersonation.

```javascript
const express = require('express')
const cors = require('cors')

const PORT = 3000

const app = express()
app.use(express.json()) // Middleware to parse JSON requests

const users = [
  { id: 1, username: 'admin', password: 'pass', roles: ['admin'] },
  // ... other users
]

// Use the cors middleware and set the frontend origin
app.use(
  cors({
    origin: '<http://localhost:3001>',
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  }),
)

function ensureAdmin(req, res, next) {
  const roles = req.body.roles
  if (roles && roles.includes('admin')) {
    return next()
  } else {
    return res.status(403).json({ message: 'Access forbidden.' })
  }
}

app.post('/impersonate/:userId', ensureAdmin, (req, res) => {
  console.log(req)
  const userIdToImpersonate = req.params.userId
  const user = users.find((u) => u.id == userIdToImpersonate)
  if (!user) {
    return res.status(404).json({ message: 'User not found.' })
  }
  res.json({ message: 'Impersonation started.' })
})

app.post('/stop-impersonation', ensureAdmin, (req, res) => {
  res.json({ message: 'Impersonation stopped.' })
})

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
})
```

We start by importing the two modules needed:

- Express for the web server.
- cors for Cross-Origin Resource Sharing (useful when the frontend and backend are on different origins).

We then set up the Express server. We want to use the built-in `express.json()` middleware, enabling the server to parse incoming JSON payloads in requests.

After that, we set up a user array. This array simulates a database of users. Here, you would call your database to check users and roles. We then have to set up some CORS middleware. Without this, our server wouldn’t want to receive requests from our separate frontend.

ensureAdmin is the integral function of the server. This middleware checks if the user role includes "admin." If so, it allows the request to proceed. Otherwise, it sends back a forbidden status. This ensures the user is an admin before allowing them to impersonate another user.

We then have our two API Endpoints:

- `/impersonate/:userId`: An admin can attempt to impersonate another user by providing their ID.
- `/stop-impersonation`: An admin can stop the impersonation.

Finally, we start our server. Save this as `app.js` and you can run it with

```sh
node app.js
```

With that up and running, we can set up the frontend.

### The frontend application

The frontend will mimic the version of the application that an admin would see. When an admin chooses to impersonate, it makes a request to the `/impersonate/:userId` endpoint. It then updates the UI based on the response to show that impersonation is active. To stop impersonation, it makes a request to `/stop-impersonation`.

```jsx
import React, { useState } from 'react'
import './App.css'

function App() {
  const [isImpersonating, setIsImpersonating] = useState(false)
  const [message, setMessage] = useState('')

  const handleImpersonate = async (userId) => {
    try {
      const response = await fetch(`/impersonate/${userId}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ roles: ['admin'] }),
      })

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`)
      }

      const data = await response.json()
      setIsImpersonating(true)
      setMessage(data.message)
    } catch (err) {
      console.error('Impersonation failed:', err)
      setMessage('Impersonation failed. Check console for details.')
    }
  }

  const handleStopImpersonating = async () => {
    try {
      const response = await fetch('/stop-impersonation>', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ roles: ['admin'] }),
      })

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`)
      }

      const data = await response.json()
      setIsImpersonating(false)
      setMessage(data.message)
    } catch (err) {
      console.error('Failed to stop impersonation:', err)
      setMessage('Failed to stop impersonation. Check console for details.')
    }
  }

  return (
    <div className="App">
      {isImpersonating ? (
        <div className="impersonation-banner">You are impersonating another user!</div>
      ) : null}
      <button onClick={() => handleImpersonate(1)}>Impersonate User with ID 1 (admin)</button>
      {isImpersonating ? (
        <button onClick={handleStopImpersonating}>Stop Impersonating</button>
      ) : null}
      <p>{message}</p>
    </div>
  )
}

export default App
```

After importing React and our CSS, the core App function starts by declaring two state variables:

- `isImpersonating`: A boolean state that tracks whether the admin is currently impersonating another user.
- `message`: A string state to store and display messages from server responses or errors.

We then have our impersonation functions. `handleImpersonate` is an async function to impersonate a user by sending a POST request to `/impersonate/:userId`. If successful, it sets the state `isImpersonating` to `true` and displays a message from the server.

`handleStopImpersonating` is also an async function, this time to stop impersonation by sending a POST request to `/stop-impersonation`. If successful, it sets the state `isImpersonating` to false and displays a message from the server.

We then render our component. A button allows the admin to impersonate a user with ID 1 , and then a banner is displayed when the admin is impersonating another user. If the admin is impersonating, another button appears to stop the impersonation.

So when an admin logs in initially, they see this:

![Empower Support Team User Impersonation guide illustration](./cde8309564132cecdf8143f73c286562746e24f2-2000x1243.png)

Once they press the button, they start the impersonation:

![Empower Support Team User Impersonation guide illustration](./4de7c80017e36f443145441e7398316b59faa87a-2000x1264.png)

Then, they can stop the impersonation again:

![Empower Support Team User Impersonation guide illustration](./461c595a3f3254daeb412a146c6649070e15a1c0-2000x1264.png)

## Using Clerk for User Impersonation

What are the problems with the solution above? They are myriad.

First, we don’t have any authentication. This not only means the whole system is insecure, but it also means the impersonation is insecure. We’re just sending a plain text ‘admin’ message to the backend to tell them we can impersonate a user. The correct way to do this is using JWT. The token would contain the role information and be authenticated by the backend.

We also don’t have the logic incorporated to actually do anything with the impersonation. Neither do we have all the security checks in place to securely audit and log any impersonation.

We’re missing all this because impersonation is difficult to get right. You are coding an entirely different way to access your application, and you have to do it perfectly to avoid any security or privacy issues.

This is where using a platform like Clerk becomes essential. [User impersonation](/docs/custom-flows/user-impersonation) is incorporated directly into Clerk. All you have to do is call our [backend API](/docs/reference/backend-api/tag/actor-tokens/POST/actor_tokens) with your user ID (`user_id`) and the user ID of the user to impersonate (`sub`):

```javascript
const url = 'https://api.clerk.com/v1/actor_tokens'

const data = {
  user_id: 'user_1o4qfak5AdI2qlXSXENGL05iei6',
  expires_in_seconds: 600,
  actor: {
    sub: 'user_21Ufcy98STcA11s3QckIwtwHIES',
  },
}

async function postData() {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    })

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`)
    }

    const responseData = await response.json()
    console.log(responseData)
  } catch (error) {
    console.log('There was a problem with the fetch operation:', error.message)
  }
}

// Invoke the function to execute the fetch operation
postData()
```

This returns a token you can then use to impersonate the user for 10 minutes:

```json
{
  "sub": "user_1o4qfak5AdI2qlXSXENGL05iei6",
  "act": {
    "sub": "user_21Ufcy98STcA11s3QckIwtwHIES"
  }
}
```

You can then use this with the Clerk SDK for impersonation:

```javascript
import express from 'express'
import { ClerkExpressWithAuth } from '@clerk/clerk-sdk-node'

const app = express()

// Apply the Clerk express middleware
app.get(
  '/protected-endpoint',
  ClerkExpressWithAuth({
    // ...options
  }),
  (req, res) => {
    // The request object is augmented with the
    // Clerk authentication context.
    const { userId, actor } = req.auth

    res.json({ userId, actor })
  },
)

app.listen(3000, () => {
  console.log('Booted.')
})
```

There is an even easier way to use Clerk to impersonate a user–through your dashboard. Go to Users in your dashboard:

![Empower Support Team User Impersonation guide illustration](./b80a50270c2e867a35dd4822fa4c0fe576c3857e-2000x1090.png)

Then click to open the menu for the user you want to impersonate and choose "Impersonate user":

![Empower Support Team User Impersonation guide illustration](./a677bff1ae96063af5851ab123c37e66bd2aacfa-2000x1090.png)

Then, your support team is ready to impersonate a user straight away.

## More Support for Your Support Team

Adding user impersonation is a must for a well-functioning support team. It gives them to the tools they need to help your customers with their support needs without sacrificing privacy and security.

Getting it right in your application is a big challenge. Using an authentication solution such as Clerk with user impersonation built-in means you can easily have this embedded in your app, and you don’t have to worry about incorporating errors that lead to security issues. Check out the [Clerk user impersonation docs](/docs/custom-flows/user-impersonation#user-impersonation) to learn more about setting this up quickly with Clerk.

---

# Clerk Webhooks: Getting Started
URL: https://clerk.com/blog/webhooks-getting-started.md
Date: 2023-09-29
Category: Guides
Description: Learn how to get started with Webhooks to build integrations in a Nextjs application with Clerk's fully-featured authentication.

In an age of software that is composed of decoupled services built by specialized (often outsourced) teams, webhooks play an important architectural role. Webhooks enable us to outsource important user interactions and services to external platforms that are specifically designed for those purposes, and still have our applications be able to respond to those interactions.

For example, Clerk is specifically designed to handle authentication and user management related interactions on behalf of your applications. Webhooks allow Clerk to notify your application of these interactions so that you can implement custom follow-up logic. These interactions include (but are not limited to) signing in, changing profile information, creating an organization, or getting invited to an organization. Clerk ensures that all these important interactions are handled securely and responsively, while giving you a way to implement follow-up logic.

In this blog post we will build a simple webhook integration in a Next.js project. We will also explore the use cases of webhooks with Clerk and discuss how to best handle webhooks for production-ready applications.

This post assumes basic familiarity with Next.js and Clerk.

## Setting up Webhooks in Next.js

For this walkthrough we will start with a Next.js project that is already setup with Clerk. However, you can create a webhook subscription in any server-side web framework like Express or Fastify.

If you don’t already have a Next.js project setup with Clerk, you can start by cloning the Clerk Next.js App Router template.

```bash
git clone https://github.com/clerkinc/clerk-next-app.git
```

This will create a minimal Next.js project fully setup with Clerk’s embedded components and a secured dashboard page.

Create a `.env.local` file in the root of the project and add your keys here from the Clerk dashboard.

```env {{ title: '.env.local' }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_pub_key
CLERK_SECRET_KEY=your_clerk_secret_key
```

Let’s create an API route that will handle the Clerk webhook. This handler will live in `app/api/clerk/route.ts` and will have the URL of `your-app.com/api/clerk`.

Webhooks are POST requests by default, so let’s create a handler for receiving POST requests.

```typescript {{ title: 'app/api/clerk/route.ts' }}
export async function POST(request: Request) {}
```

The webhook event data is available as the request payload. Let’s parse it out and log it to the console.

```typescript {{ title: 'app/api/clerk/route.ts' }}
import { WebhookEvent } from '@clerk/nextjs/server'

export async function POST(request: Request) {
  const payload: WebhookEvent = await request.json()
  console.log(payload)
}
```

Clerk provides a `WebhookEvent` type for strong typescript inference. For now we will simply assert the webhook payload to be of this type.

Let’s also create a simple GET handler to test if we can access this URL.

```typescript {{ title: 'app/api/clerk/route.ts' }}
import { WebhookEvent } from '@clerk/nextjs/server'

export async function POST(request: Request) {
  const payload: WebhookEvent = await request.json()
  console.log(payload)
}

export async function GET() {
  return Response.json({ message: 'Hello World!' })
}
```

Add this endpoint as a public route to the Clerk middleware

```typescript {{ title: 'middleware.ts' }}
import { authMiddleware } from '@clerk/nextjs'

export default authMiddleware({
  publicRoutes: ['/', '/api/clerk'],
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/'],
}
```

Start the development server.

```bash
npm run dev
```

Now we can navigate to this endpoint in the browser.

![Endpoint GET response screenshot](./972a73b2d937d644c6bb5a9b2f0134f8d8106f22-4000x1256.png)

That’s all we need to setup a simple webhook endpoint! Now we can provide Clerk with a URL to this endpoint, and we can start receiving events.

However, we are not ready to test this locally yet. While the application is accessible on `localhost` in our machine, it’s not accessible to anything outside our local network. Since webhook requests are originated from Clerk’s servers, the `localhost` URL will just point back to the Clerk server instead of our machine.

![Local network firewall access diagram](./b791b1246f3bf158efb58bbe197a82e9c49f7481-4000x2400.png)

While we could provide Clerk with the public IP address of our machine, any incoming requests to our local home/work networks will be blocked by firewalls.

![Firewall blocking external access diagram](./9ed5794a6cf6a053731e4eb9154f320829866cf6-4000x2400.png)

To work around this problem, we can use a tunneling service like [localtunnel](https://theboroer.github.io/localtunnel-www), which creates a secure tunnel to our local machine from the cloud.

![Localtunnel architecture diagram](./5016844b43d4fc00cf02cd6daf6b815f3ce48f5f-4000x2400.png)

Let’s setup localtunnel in a new terminal window.

```bash
npm install -g localtunnel
```

Once the installation is complete, we can create a tunnel to our local server.

```bash
lt --port 3000
```

You should see an output in your terminal like this.

![Localtunnel terminal output screenshot](./1d3c3bc2a40be4e3231b8a4017a53fb5418915af-4000x1256.png)

Localtunnel will generate a random URL on the `loca.lt` domain that will point to your locally running server on port 3000. We can navigate to this new URL in our browser.

Localtunnel has a security mechanism built in to prevent abuse from malicious actors and phishing links. It asks for the public IP address of your local machine to ensure that it’s really you trying to access your application. Follow the instructions on this page to get your public IP address, and submit it on this page.

Once that’s done, we can access our application on this URL. We should also be able to access our webhook endpoint.

If the application doesn’t show up, make sure both the nextjs server and localtunnel are running on separate terminal windows.

Now we are ready to plug our endpoint into Clerk’s dashboard. Go to the Webhooks page, and click Add Endpoint. Enter the localtunnel URL here. You can also add a description for this endpoint.

![Add webhook endpoint in Clerk dashboard screenshot](./f593b54494ffd98bd34a4c372921c8a898a92199-4000x2596.png)

Here you can select which specific events you want to receive at this endpoint. Let’s leave these unselected for now so that we can receive webhooks for all events. Hit Create.

Once the webhook endpoint is created, we can navigate to the Testing tab. Here we can trigger an example event manually to test our webhook endpoint. Select any event from the dropdown, and hit Send Example.

![Send example webhook event screenshot](./4006a769624fd7e31d9e62b3e6381ca8ca55019f-4000x3972.png)

If your application is running locally and tunneling is set up correctly, you should see a message in your console with the example event.

![Webhook payload logged in console screenshot](./97d57b6839a70569bf7698ee2a479568f7ff92a2-4000x1904.png)

You will also see a log entry in the Testing tab that shows the status of the webhook.

![Webhook delivery status log screenshot](./7dbbcb838ce8602bce5cce8fb1610b0512a1de01-4000x1412.png)

Even though the webhook was correctly received and logged to the console, the dashboard marks the webhook delivery as failed. To ensure that the delivery is marked as succeeded, we need to return a success response from our endpoint.

```typescript {{ title: 'app/api/clerk/route.ts' }}
export async function POST(request: Request) {
  const payload: WebhookEvent = await request.json()
  console.log(payload)
  return Response.json({ message: 'Received' })
}
```

Let’s send another example event. This time it the webhook should be marked as succeeded.

![Successful webhook delivery screenshot](./d1f7fd540f886be2559a152cd905f86e7a7bb64b-4000x1636.png)

Now we can test the webhook with real events. Navigate to your application and sign up with a new account. This should log two webhook events in the console - a `user.created` and a `session.created` event.

Congratulations, we now have a functioning webhook receiver in our application! We can now further explore what events are triggered by Clerk. For example,

- Updating the user profile sends a `user.updated` event
- Signing out of the application sends a `session.ended` event
- Remotely signing out a different device will result in a `session.revoked` event
- Creating a new organization will trigger an `organization.created` and an `organizationMembership.created` event

We can also see logs for real webhook events in the dashboard.

![Clerk dashboard webhook logs screenshot](./08a50b64eb3680e584d89b53cb1fd01c34db2fcf-4000x2636.png)

When you deploy this app to a cloud environment like Vercel, you will need to create a new endpoint in the Clerk dashboard that points to the deployed application instead of a localtunnel URL.

Note that localtunnel will generate a random URL everytime we start the tunnel, which means everytime we are testing with webhooks locally, we will need to create a new endpoint in the Clerk dashboard. To solve this, we can ask localtunnel to provision a specific URL by adding a `--subdomain` argument.

```bash
lt --port 3000 --subdomain unique-url-name
```

If the requested URL is available, it will be provisioned for us instead of a randomly generated URL. This will allow us to reuse the same endpoint in the Clerk dashboard everytime we are testing locally.

## Webhook Use Cases

So now that we are receiving webhook events from Clerk, what can we do with it?

### Data Synchronization

While Clerk’s own database acts as the primary source of truth for authentication and user management, you can create a copy of this data in your own database which gets updated asynchronously through webhooks. This allows you to query your own database for user data instead of relying on Clerk’s APIs, along with flexibility with the data model, complex queries, transactional guarantees, and real time behavior. This mechanism is called **data synchronization**.

### Event Driven Systems

Some advanced use cases might rely on asynchronous workflows and background jobs, such as sending notifications to users or other external systems. These workflows can be triggered to execute when a webhook event is received, for example, sending new users an onboarding email to your application, or subscribing them to mailing lists for software updates and promotional campaigns. Such systems are called **event driven systems**.

### Bring Your Own Email/SMS

Clerk allows you to bring your own Email and/or SMS servers for delivering auth-related messages which would usually be delivered by Clerk. Whenever an auth-related message is created, Clerk sends a webhook event with that message to your application. You can configure Clerk to not deliver these messages in the Clerk dashboard, and instead handle the delivery yourself by listening to the webhook events. (add links to HWR and docs)

We will walk through implementing some of these use-cases in future articles.

## Webhook Best Practices

While setting up a simple webhook receiver for testing purposes is straightforward, operating a webhook-driven system in production requires some additional work. Clerk implements webhook deliveries through [Svix](https://www.svix.com), a service that specializes in handling webhook deliveries. Svix manages concerns like scalability, logging, retries, security, and delivery guarantees. However, we still need to ensure that our application is setup to receive and process webhooks correctly. Let’s explore some best practices for implementing webhook endpoints for production-ready applications.

### Error Handling

Webhook deliveries are marked as successful or failed depending on the HTTP response returned by our webhook endpoint. A failed webhook delivery is retried with exponential backoff until it succeeds, ensuring that our application will never miss an event. The exponential backoff ensures that our application servers are not overloaded with retries and explode server resources.

Our webhook endpoints should be setup to return a successful response (20x) if everything went well. But in case of an error during the processing of a webhook, we can simply throw an error response (40x or 50x) and expect a retry, which often succeeds. We can also monitor the webhook status in the Clerk dashboard, which provides observability into failed webhook deliveries and help discover issues.

```typescript {{ title: 'app/api/clerk/route.ts' }}
import { WebhookEvent } from '@clerk/nextjs/server'

export async function POST(request: Request) {
  try {
    const payload: WebhookEvent = await request.json()
    console.log(payload)

    // process the event

    // everything went well
    return Response.json({ message: 'Received' })
  } catch (e) {
    // something went wrong
    // no changes were made to the database
    return Response.error()
  }
}
```

### Security

Since webhooks requests can originate on any remote server, webhook endpoints are vulnerable to attacks from malicious actors. Securing webhook endpoints is important to ensure malicious actors cannot make unwanted changes to your application.

Svix adds security measures to webhook requests by adding a secure hash in the request headers created using a secret key. We can use the `svix` library in our webhook endpoint to validate that the request was sent by a legitimate Svix server and not a malicious actor.

```typescript {{ title: 'app/api/clerk/route.ts' }}
import { WebhookEvent } from '@clerk/nextjs/server'
import { headers } from 'next/headers'
import { Webhook } from 'svix'

const webhookSecret = process.env.CLERK_WEBHOOK_SECRET || ``

async function validateRequest(request: Request) {
  const payloadString = await request.text()
  const headerPayload = headers()

  const svixHeaders = {
    'svix-id': headerPayload.get('svix-id')!,
    'svix-timestamp': headerPayload.get('svix-timestamp')!,
    'svix-signature': headerPayload.get('svix-signature')!,
  }
  const wh = new Webhook(webhookSecret)
  return wh.verify(payloadString, svixHeaders) as WebhookEvent
}

export async function POST(request: Request) {
  const payload = await validateRequest(request)
  console.log(payload)
  return Response.json({ message: 'Received' })
}
```

We can get the webhook signing secret from the Clerk dashboard and store in a `CLERK_WEBHOOK_SECRET` environment variable.

![Webhook signing secret screenshot](./cb560fdd36109e0abd0ee80c2cdbc83ff625a859-4000x2596.png)

Now our webhook endpoint is fully secure and ready to be deployed to production servers.

### Selective Events

In our walkthrough, we configured Clerk to trigger the webhook endpoint for all events. While this was useful for testing, we want to minimize the use of server resources by only processing events that we actually care about.

For production endpoints, we can apply event filters in the Clerk dashboard by selecting the events that we are processing in the application, and leaving everything else unselected.

![Select webhook events screenshot](./0d52d69df0819bc7ac254509dddc9864c0764a63-4000x2176.png)

## Summary

Webhooks play an important role in creating unified user experiences through integrations like data sync and event driven systems. In this article we setup a simple webhook endpoint, explored some use cases, and addressed concerns like security and error handling.

We will dive deep into specific use cases around data sync and event driven systems in future blog posts and walk through implementing them into our projects.

To learn more about webhooks, check out [the ultimate webhooks guide](/blog/what-are-webhooks), or to learn more about the webhook events exposed by Clerk, [read the documentation](/docs/integrations/webhooks/overview).