# Clerk Blog — Page 4

# Implementing multi-tenancy into a Supabase app with Clerk
URL: https://clerk.com/blog/multitenancy-clerk-supabase-b2b.md
Date: 2025-03-31
Category: Guides
Description: Learn how to build B2B applications with Clerk and Supabase.

Collaborative software is on track to become a nearly $53 billion\* industry by 2032. Needless to say, adding collaborative, multitenant features into your application will position you to capture a piece of that revenue.

In recent articles, we’ve covered how Supabase’s architecture relies on Postgres RLS to secure data within the database. While team-based features can be complex to implement, Clerk’s B2B tools integrate seamlessly with Supabase, enabling multi-tenancy with minimal configuration changes. In this article, you'll learn exactly how the integration works.

To follow along, you should have a general understanding of Supabase and ideally have built an application with Clerk and Supabase. If you want to learn more about the integration, we have an [article explaining how Supabase Auth works and how Clerk can be used with Supabase](/blog/how-clerk-integrates-with-supabase-auth).

## Clerk Organizations

Clerk simplifies authentication and user management implementation, including team structures and granular permissions.

By enabling the Organizations feature in the Clerk dashboard, you empower your users to create teams where they can invite other team members to collaborate. Admin users can also manage roles and permissions at the individual user level, ensuring each user has the proper level of access.

Clerk applies its signature component-driven design to organizations management, offering pre-built UI elements that streamline team permission workflows.

One such component is the `<OrganizationSwitcher />`. By adding a single line to your codebase, you get a beautiful drop-down that allows users to create and manage their organizations associated members (if they have the administrator role).

![Organization Switcher](./org-switcher.png)

## Using organizations with Supabase

Supabase identifies the party making a request by parsing the claims of the incoming JWT.

For example, the following JSON represents the claims of a Clerk user making a request to Supabase, with the `sub` value representing the user ID:

```json
{
  "azp": "http://localhost:3000",
  "exp": 1748881855,
  "fea": "u:ai_assistant",
  "fva": [7142, -1],
  "iat": 1748881795,
  "iss": "https://modest-hog-24.clerk.accounts.dev",
  "jti": "27bb27d6f16174d6a556",
  "nbf": 1748881785,
  "pla": "u:pro",
  "role": "authenticated",
  "sid": "sess_2xjZ6z85O9Uu2mHhE73Z2JVkh1i",
  "sub": "user_2s2XJgQ2iQDUAsTBpem9QTu8Zf7",
  "v": 2
}
```

By setting an active organization for that user (using the `<OrganizationSwitcher />` or [any other method](https://clerk.com/docs/organizations/overview#active-organization)), that user’s claims are modified to include information about the active organization in the `o` object, including the organization ID. The below claims represent a user with an active organization to compare how it differs from the above claims:

```json
{
  "azp": "http://localhost:3000",
  "exp": 1748881940,
  "fea": "o:articles",
  "fva": [7143, -1],
  "iat": 1748881880,
  "iss": "https://modest-hog-24.clerk.accounts.dev",
  "jti": "fb41714162af8da77347",
  "nbf": 1748881870,
  "o": {
    "id": "org_2rxR3osThxAoZXaE7mWeSj065IB",
    "rol": "admin",
    "slg": "echoes"
  },
  "pla": "o:free_org",
  "role": "authenticated",
  "sid": "sess_2xjZ6z85O9Uu2mHhE73Z2JVkh1i",
  "sub": "user_2s2XJgQ2iQDUAsTBpem9QTu8Zf7",
  "v": 2
}
```

## Parsing the Organization ID value

[Our Supabase integration guide](/docs/guides/development/integrations/databases/supabase) walks you through using the `auth.jwt()` function to extract the `sub` value from the JWT claims of the user making the request. This same function can be used to access the `id` value of the `o` object as well.

Consider the following RLS policy that restricts users to accessing their own records in the `articles` table:

```sql
create policy "Users can view their own articles"
	on public.articles
	for select
using((auth.jwt() ->> 'sub'::text) = user_id);
```

Updating the `using` statement as follows will reference both the `sub` and `o`.`id` values.  When combined with `coalesce`, the policy will first check the `o`.`id` claim and fail back to the `sub` claim if `o` is unavailable (indicating that the user doesn’t have an active organization selected):

```sql
create policy "Users can view their own articles"
	on public.articles
	for select
using(
	coalesce(
    (auth.jwt() -> 'o'::text) ->> 'id'::text,
    (auth.jwt() ->> 'sub'::text)
	) = owner_id
);
```

### Using a dedicated function

If you need to create a number of RLS policies, constantly duplicating code to check for both the `sub` and `o`.`id` values can leave some room for human error. An alternate approach to simplify your policies would be to create a dedicated function that checks both values, returning the first available value.

The following snippet can be pasted in the Supabase SQL Editor to create that function:

```sql
-- Add requesting_owner_id function
create or replace function requesting_owner_id()
returns text as $$
    select coalesce(
        (auth.jwt() -> 'o'::text) ->> 'id'::text,
        (auth.jwt() ->> 'sub'::text)
    )::text;
$$ language sql stable;
```

Once created, the function can be used in your RLS policies like so:

```sql
create policy "Users can view their own articles"
  on public.articles
  for select
	using (requesting_owner_id() = user_id);
```

> \[!NOTE]
> If you are using the legacy method of integrating Clerk with Supabase, update the `requesting_user_id()` function body to achieve the same results.

You are welcome to modify the name of the function and relevant column names (I’m partial to `owner_id`) but this small change allows Supabase to automatically use the active organization, if set, with no further changes to the database while falling back to the user ID.

## A practical example

To demonstrate, let’s walk through an example using Quillmate, an open-source writing tool built with Next.js, Clerk, and Supabase. If you want to follow along, clone the [`article-add-orgs`](https://github.com/bmorrisondev/quillmate/tree/article-add-orgs) branch from GitHub and step through the readme to get the project running on your computer. Otherwise, feel free to continue reading.

### Enabling Organizations in the dashboard

Start by enabling organizations within the Clerk Dashboard by heading to **Configure** > **Settings**, then toggling **Enable organizations**. This simple toggle that enables the use of the organization components with this application.

![Enabling organizations in the Clerk dashboard](./enable-orgs.png)

### Add the `<OrganizationSwitcher />` to the sidebar

Next, add the `<OrganizationSwitcher />` to the sidebar near the bottom. This example also includes the `showOrgSwitcher` flag to the component props to indicate that the sidebar is used within a route for organizations. This will allow the sidebar to be used with URL-based organization switching, which is a method of setting the active organization by [using the organization slug directly within the URL](/docs/organizations/org-slugs-in-urls).

> \[!NOTE]
> Interested in a guide dedicated to switching organizations based on the slug? Let us know in [our feedback portal](https://feedback.clerk.com/roadmap?id=f95553cd-204d-43b8-b2b5-1f84ecf1bd59).

Update the `src/app/(protected)/components/sidebar.tsx` file to match the following:

```tsx {{ filename: 'src/app/(protected)/components/sidebar.tsx', ins: [6, 21, 33, 46, 67, [78, 86]], del: [5, 32, 45, 66], prettier: false }}
'use client'

import { Button } from "@/components/ui/button"
import { type Article } from '@/lib/models'
import { UserButton, useUser } from '@clerk/nextjs'
import { OrganizationSwitcher, useOrganization, UserButton, useUser } from '@clerk/nextjs'
import Link from "next/link"
import { useSupabase } from "@/lib/supabase-provider"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { useArticleStore } from "../store"

interface SidebarProps {
  showOrgSwitcher?: boolean
}

type NewArticle = Pick<Article, 'title' | 'content' | 'owner_id'>

export function Sidebar({ showOrgSwitcher = true }: SidebarProps) {
  const { user } = useUser()
  const { organization } = useOrganization()
  const { supabase } = useSupabase()
  const router = useRouter()
  const { articles, isLoadingaddArticle, addArticle } = useArticleStore()

  async function onNewArticleClicked() {
    if (!supabase || !user) return

    const newArticle: NewArticle = {
      title: 'New Article',
      content: '',
      owner_id: user.id
      owner_id: organization.id ?? user.id
    }

    const { error, data } = await supabase
      .from('articles')
      .insert(newArticle)
      .select()
      .single<Article>()

    if (error) {
      toast.error('Failed to create new article')
		} else {
      router.push(`/me/${data.id}`)
      router.push(`${organization ? `/orgs/${organization.slug}` : '/me'}/${data.id}`)
      addArticle(data)
    }
  }

  return (
    <div className="w-64 border-r p-4 flex flex-col">
      <div className="flex items-center gap-2 pb-2">
        <UserButton showName />
      </div>
      <Button onClick={onNewArticleClicked} className="mb-4">
        New Article
      </Button>
      <div className="overflow-y-auto flex-1 flex flex-col">
        {isLoading ? (
          <div className="text-center text-gray-500">Loading articles...</div>
        ) : (
          articles.map((article) => (
            <Link
              key={article.id}
              href={`/me/${article.id}`}
              href={`${organization ? `/orgs/${organization.slug}` : '/me'}/${article.id}`}
              className="px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100"
            >
              <h3 className="font-medium">{article.title || 'Untitled'}</h3>
              <div className="flex items-center gap-2 text-sm text-gray-500">
                <span>{new Date(article.updated_at).toLocaleDateString()}</span>
              </div>
            </Link>
          ))
        )}
      </div>
      {showOrgSwitcher && (
        <OrganizationSwitcher
          hideSlug={false}
          hidePersonal={false}
          afterCreateOrganizationUrl="/orgs/:slug"
          afterSelectOrganizationUrl="/orgs/:slug"
          afterSelectPersonalUrl="/me"
        />
      )}
    </div>
  )
}

```

Within the application, I now have this new dropdown where I can create an organization and invite others to it:

![Organization switcher open in Quillmate](./create-org.png)

### Update the RLS policies

Next, create a migration to add the `requesting_owner_id` function to the database and replace the existing RLS policies to use the function.

Run the following command in the terminal to create the migration file:

```bash
pnpm supabase migration new support_clerk_orgs
```

This will create a file in the `supabase/migrations` directory with the `support_clerk_orgs` prefix. Add the following SQL to that file:

```sql
-- Create the requesting_owner_id function
create or replace function requesting_owner_id()
returns text
language sql
stable
as $$
  select
    coalesce(
      (auth.jwt() -> 'o'::text) ->> 'id'::text,
      (auth.jwt() ->> 'sub'::text)
    );
$$;

-- Update the policies to use requesting_owner_id()
drop policy if exists "Users can view their own articles" on articles;
drop policy if exists "Users can insert their own articles" on articles;
drop policy if exists "Users can update their own articles" on articles;
drop policy if exists "Users can delete their own articles" on articles;

create policy "Users can view their own articles"
on articles for select
using (owner_id = requesting_owner_id());

create policy "Users can insert their own articles"
on articles for insert
with check (owner_id = requesting_owner_id());

create policy "Users can update their own articles"
on articles for update
using (owner_id = requesting_owner_id())
with check (owner_id = requesting_owner_id());

create policy "Users can delete their own articles"
on articles for delete
using (owner_id = requesting_owner_id());
```

Finally, apply the migration to the database by executing the following command in your terminal:

```bash
pnpm supabase db push
```

### Testing the changes

With the above changes in place, it's time to test by creating an “article” (which is the main entity of Quillmate) in your personal account and an organization.

Start the project by running the following command:

```bash
pnpm dev
```

Open the application in your browser using the URL displayed in your terminal. Note how the `<OrganizationSwitcher />` displays “Personal account” to indicate that you do not have an active organization selected.

Click the **New Article** button in the sidebar to create an article while in the personal account to populate the database with some data. The editor supports markdown syntax and setting an H1 will automatically set the name of the article in the sidebar.

In my environment, I have a single article named “Hello world” in this tenant:

![Hello world article in personal account](./personal-article.png)

Use the `<OrganizationSwitcher />` to create an organization and switch to it. Your list of articles should be blank - create another article here as well.

In my environment, I have a test organization named “D2 Frontiers”, where I have a completely different article. This is because Supabase is returning all records with an `owner_id` that matches the value of the `o`.`id` value in the claims:

![Article in an active organization](./org-article.png)

Next, access the organization settings by opening the `<OrganizationSwitcher />` and selecting **Manage** in the header. Navigate to **Members** and invite another user so they can access the application as well. I recommend using an alternate email address if you have one.

As an example, I’ve switched to a completely different user account that also has access to the “D2 Frontiers” organization and can access the same article!

![Article in an active organization as another user](./org-article-alt.png)

Back in the database, the values in the `owner_id` column are different depending on the owner type.

Note that the article created with my personal account starts has an `owner_id` starting with `user_` and the one created with the organization active starts with `org_`:

![Owner ID values in the database](./owner-id.png)

## Conclusion

Using Clerk with Supabase unlocks multi-tenancy and team-based functionality in your application in a matter of minutes with just a few small changes to the code. This empowers your users to create their own groups where individuals can be invited to collaborate within your application.

\* [https://www.rocket.chat/blog/collaboration-software](https://www.rocket.chat/blog/collaboration-software)

---

# How Clerk integrates with a Next.js application using Supabase
URL: https://clerk.com/blog/how-clerk-integrates-nextjs-supabase.md
Date: 2025-03-31
Category: Company
Description: Learn how Supabase works with Next.js to increase security and reduce development hours, and how Clerk integrates with this stack.

Supabase is one of the most popular backend platforms on the web, but it breaks from traditional architecture patterns when accessing data.

Full-stack applications typically have distinct frontend and backend codebases. When a user needs to access or modify data, the frontend proxy requests through the backend to the database. This prevents third parties from obtaining the connection string used for database operations.  In comparison, Supabase allows you to build applications *without* a backend, yet still keep your data safe.

In this article, you’ll learn how Supabase functions with Next.js applications, and how Clerk integrates with this configuration.

This article assumes you have [familiarity with Next.js and Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs), and have ideally built an application using them both.

## How Supabase works with Next.js applications

Supabase provides a way to access data from the client directly without going through your own backend.

As a full-stack JavaScript framework, Next.js supports accessing Supabase through server-side code, but most developers take advantage of the Supabase API to gain direct access to the underlying data. In this setup, the client will request data from the database through the API and include an authorization token so that the service can associate the request with a specific user.

To protect the data, Supabase uses a feature of Postgres (the underlying database engine) called Row Level Security (RLS). RLS is a way to secure database tables on a per-record by using policies to evaluate each request and determine if the user making a data access/modification request is authorized to do so on the specific rows.

> \[!NOTE]
> To learn a more about how the Supabase API works with RLS, check out [our article comparing Supabase Auth and Clerk](/blog/how-clerk-integrates-with-supabase-auth) where we dive deeper into both implementations.

## How Clerk integrates with Next.js and Supabase

When a user signs in with Clerk, they receive short-lived tokens that are used by backend services to validate requests. In a traditional configuration, this would be the backend code you write for your application. As stated in the previous section, applications using Supabase often bypass any custom backend code and send requests directly to Supabase, so there are a few considerations to be taken so that Clerk will work properly with Supabase.

### Verifying JWTs with the JWKS endpoint

In the traditional configuration, Clerk’s SDKs will automatically verify your request using the public key associated with Clerk application.

Supabase does not support this since you do not control the backend code that powers the Supabase APIs. However, Supabase does support integrating with Clerk as a third party authentication provider, where JWTs sent to the Supabase backend will automatically be verified with your Clerk application using the JWKS endpoint for that application.

A JSON Web Key Set (JWKS) is a JSON object that contains a set of keys used to verify the signatures of JWTs. This object is often publicly available through a standard URL so they can be used by external systems and integrations to perform this verification.

The following diagram shows what this flow looks like:

![JWT Verification Flow](./diagram.png)

1. The user signs in using Clerk
2. Clerk issues JWT to the user
3. User makes request to Supabase, including JWT
4. Supabase verifies with the Clerk application JWKS endpoint
5. Clerk verifies token
6. Supabase response to the user with the requested data

### Setting the correct role

Supabase supports a number of different roles which each provide different levels of access for managing database and storage operations. The role which the request assumes is set in the JWT itself as the `role` claim.

To adhere to Supabase standards, the `role` in the JWT should be set to `authenticated` so that the request has the correct level of security. The following snippet shows what the claims of the JWT are when properly configured:

```json
{
  "app_metadata": {},
  "aud": "authenticated",
  "azp": "http://localhost:3000",
  "email": "brian@clerk.dev",
  "exp": 1742938129,
  "iat": 1742938069,
  "iss": "https://modest-hog-24.clerk.accounts.dev",
  "jti": "01a722552fe233fc649b",
  "nbf": 1742938064,
  "role": "authenticated", // Note the 'role' claim
  "sub": "user_2s2XJgQ2iQDUAsTBpem9QTu8Zf7"
}
```

### Using the Clerk JWT for requests to Supabase

Clerk stores its token in cookies that automatically get sent with every request in the same domain. Without some infrastructure considerations, your Supabase application is likely not going to be accessible on the same domain as your application.

Fortunately, you can obtain the current session token using the `getToken` function of the SDK:

```tsx
import { useSession } from '@clerk/nextjs'

const { session } = useSession()
session.getToken()
```

When creating a Supabase client in your front end, you can provide the token created using the JWT template into the `accessToken` option of the Supabase client:

```tsx
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_KEY!,
  {
    // Use the session.getToken() method from Clerk
    accessToken: () => session?.getToken(),
  },
)
```

This will ensure that each request sent to Supabase will contain the custom token created with the JWT secret so that Supabase can validate it, extract the claims, and use it to access the requested data.

> \[!NOTE]
> Interested in more content featuring Supabase? [Let us know!](https://feedback.clerk.com/roadmap?id=f95553cd-204d-43b8-b2b5-1f84ecf1bd59)

### Using the Clerk user ID in RLS policies

Once the token has been verified, the claims within that token are accessible to Postgres using the built-in `auth.jwt()` function (used to access the JWT claims) and accessing the `sub` claim.

For example, say a `tasks` table has the following RLS policy defined:

```sql
create policy "select_by_user_id" on tasks
for select using (auth.jwt()->>'sub' = user_id);
```

Assuming the Clerk user ID is `user_2s2XJgQ2iQDUAsTBpem9QTu8Zf7`, running a `select` statement will effectively apply a filter to restrict what data is returned as shown below:

```sql
select * from tasks;
-- Turns into this:
select * from tasks where user_id = 'user_2s2XJgQ2iQDUAsTBpem9QTu8Zf7';
```

This can provide an additional layer of security for applications with a dedicated backend, or save the developer from having to build one in the first place, if implemented properly.

## Examining an implementation of Next.js using Clerk and Supabase

To see this implemented in a real-world application, we’ll explore the code for Quillmate. Quillmate is an open-source, web-based writing tool built with Next.js, Supabase, and Clerk.

> \[!NOTE]
> The source code for Quillmate is available [on GitHub](https://github.com/bmorrisondev/quillmate).

### Configuring the integration

To integrate Supabase with Clerk, you’d start in the Supabase Dashboard and navigate to **Authentication** > **Sign In/Up** > **Third Party Auth** and add Clerk as the provider. The modal that appears will prompt you for a **Clerk Domain**, but also contains a link to open the [Supabase Integration Setup page in Clerk](https://dashboard.clerk.com/setup/supabase) to select your application and enable the integration:

![Supabase Integration Setup](./supabase-dash.png)

Once completed, you’ll be provided with the Clerk Domain for your application and instance. You simply need to copy and paste this into the Supabase Dashboard and click **Create connection**.

![Supabase Integration Setup](./connect-supabase.png)

Supabase now knows that when a JWT is received that was issued by Clerk, it should use the JWKS endpoint available on the provided Clerk Domain for verification. On the Clerk side, the session claims have been updated to include `"role": "authenticated"` with all new JWTs created:

![Managed claim](./managed-claim.png)

### Accessing Supabase with a Clerk JWT

The following code demonstrates how the JWT template is accessed when creating the Supabase client. This code uses the React Context API to simplify creating and accessing the client with the Clerk `getToken` function, but you may create the client however you need as long as you can pass in the `getToken` function:

```tsx
'use client'

import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { useSession } from '@clerk/nextjs'
import { createContext, useContext, useEffect, useState } from 'react'

type SupabaseContext = {
  supabase: SupabaseClient | null
  isLoaded: boolean
}

const Context = createContext<SupabaseContext>({
  supabase: null,
  isLoaded: false,
})

type Props = {
  children: React.ReactNode
}

export default function SupabaseProvider({ children }: Props) {
  const { session } = useSession()
  const [supabase, setSupabase] = useState<SupabaseClient | null>(null)
  const [isLoaded, setIsLoaded] = useState(false)

  useEffect(() => {
    if (!session) return

    const client = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_KEY!,
      {
        accessToken: () => session?.getToken(),
      },
    )

    setSupabase(client)
    setIsLoaded(true)
  }, [session])

  return (
    <Context.Provider value={{ supabase, isLoaded }}>
      {!isLoaded ? <div>Loading...</div> : children}
    </Context.Provider>
  )
}

export const useSupabase = () => {
  const context = useContext(Context)
  if (context === undefined) {
    throw new Error('useSupabase must be used within a SupabaseProvider')
  }
  return {
    supabase: context.supabase,
    isLoaded: context.isLoaded,
  }
}
```

For the sake of this explanation, the only table of interest is the `articles` table. The `articles` table itself has a relatively simple schema:

```sql
create table if not exists public.articles (
  id bigint primary key generated always as identity,
  title text not null,
  content text not null,
  user_id text not null,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);
```

Quillmate will request a list of records from the `articles` table using the following code. Note that I am not leveraging the user ID within these queries. This is because the Clerk/Supabase integration will automatically parse the user ID from the request.

```tsx
const { data, error } = await supabase
  .from('articles')
  .select('*')
  .order('updated_at', { ascending: false })
```

And is protected from the following RLS policies. Note the use of `auth.jwt()->>'sub'` which extracts the `sub` claim (which is the user ID) from the JWT used on the request and compares it with the `user_id` column in the table.

```sql
CREATE POLICY "Users can view their own articles"
  ON public.articles
  FOR SELECT
  USING (auth.jwt()->>'sub' = user_id);

CREATE POLICY "Users can create their own articles"
  ON public.articles
  FOR INSERT
  WITH CHECK (auth.jwt()->>'sub' = user_id);

CREATE POLICY "Users can update their own articles"
  ON public.articles
  FOR UPDATE
  USING (auth.jwt()->>'sub' = user_id);

CREATE POLICY "Users can delete their own articles"
  ON public.articles
  FOR DELETE
  USING (auth.jwt()->>'sub' = user_id);
```

## Conclusion

Building an application with Supabase can challenge the architectural norms that developers have used for decades. However, once you properly understand how the data is accessed and secured by Supabase, building with this new approach can save development time and potentially increase table security by ensuring that each request is evaluated with RLS on a row-by-row basis.

This includes B2B applications, which is covered in our follow up article on [building multi-tenancy applications with Supabase and Clerk](/blog/multitenancy-clerk-supabase-b2b).

---

# How Clerk integrates with Supabase
URL: https://clerk.com/blog/how-clerk-integrates-with-supabase-auth.md
Date: 2025-03-31
Category: Guides
Description: Learn how Supabase Auth works and how Clerk can provide more capabilities in less time.

While Supabase Auth is the default choice for Supabase-powered apps, Clerk is a drop-in replacement offering the same simplicity with an enhanced feature set at your disposal and takes less time to implement. On top of reducing development time, Clerk offers much more than just authentication, from beautifully designed UI components to our suite of B2B tools for multi-tenant applications.

In this article, we’ll compare Supabase Auth with Clerk, looking at the differences in implementation and covering some of the additional capabilities offered by Clerk.

To follow along with this post, you should have a [basic understanding of Supabase](https://supabase.com/docs/guides/getting-started), ideally having built an application with Supabase as the backend.

## How Supabase Auth works

Supabase provides an easy approach to add authentication into your application. To properly compare it with Clerk, it’s important to understand how Supabase Auth is implemented.

A default Supabase project comes pre-configured with an `auth` schema in the underlying Postgres instance that stores a list of users and their credentials. This schema is used by the Supabase client SDK, specifically the helper functions which are used with your own forms to simplify the sign up and sign in processes.

The following snippet demonstrates the respective functions used with those forms:

```ts
// Sign-up
const { error } = await supabase.auth.signUp({
  email,
  password,
  options: {
    emailRedirectTo: `${location.origin}/auth/callback`,
  },
})

// Sign-in
const { error } = await supabase.auth.signInWithPassword({
  email,
  password,
})
```

Upon signing in, the service will mint a JWT and return it to the caller, which is automatically stored locally by the client SDK. Subsequent requests to Supabase will include the JWT so that the Supabase backend can authenticate the request. When Supabase receives an authenticated request, it will parse the claims from the JWT so it can be used with Row Level Security to prevent unauthorized access to data within the database.

The following diagram shows what this flow looks like:

![Auth diagram](./diagram.png)

1. The user signs in using Clerk
2. Clerk issues JWT to the user
3. User makes request to Supabase, including JWT
4. Supabase verifies with the Clerk application JWKS endpoint
5. Clerk verifies token
6. Supabase response to the user with the requested data

## How Row Level Security works with Supabase Auth

Row Level Security (RLS) is a feature of Postgres that allows you to control access to data by requiring certain criteria to match before the query will be processed.

RLS is enabled and configured on a per-table basis. When enabled, you can define RLS policies which will be evaluated by the database engine before any data in that table is read or modified.

The following snippet defines an RLS policy on the `articles` table that ensures only records with “published” in the `status`  column are returned:

```sql
create policy "Users can read published articles"
  on public.articles
  for select
  using (status = 'published');
```

Using the above policy as an example, Postgres will effectively transform the following query:

```sql
select * from articles;
```

Into:

```sql
select * from articles where status = 'published';
```

Supabase Auth uses RLS policies in a similar fashion but uses a helper function to identify who is making the request. When you’re using Supabase Auth, you have access to the `auth.uid()` function which returns the unique identifier for the user currently signed-in.

Building on the same example policy above, the following RLS policy verifies the user ID and only returns data belonging to that user:

```sql
create policy "Users can view their own articles"
  on public.articles
  for select
  using (auth.uid()::text = user_id);
```

### Why RLS?

Traditional applications have a dedicated backend system with custom logic to verify the requests being sent. This is normally where you parse the session or user ID from the request and adjust your queries (whether using SQL or an ORM) accordingly to prevent the user from accessing data they shouldn’t.

> \[!NOTE]
> If you want to further understand how authentication is implemented in a traditional configuration, check out our article on [Building a React login page template](/blog/building-a-react-login-page-template), which walks you through building session-based authentication from scratch.

While this same configuration can be set up with Supabase, most developers take advantage of the PostgREST API built into Supabase to save on the hours they’d otherwise spend building and maintaining the backend. PostgREST is a feature of Postgres that provides an API to access the database tables directly over HTTP.

The tradeoff of using PostgREST is that you don’t have access to modify the API logic itself. You instead have to rely on RLS to prevent users from accessing data they shouldn’t.

## Clerk as a drop-in replacement to Supabase Auth

Clerk’s integration with Supabase functions almost exactly like Supabase Auth with a few minor differences that are transparent when configured properly.

Supabase Auth automatically uses its own keys used to mint JWTs. Like Supabase Auth, Clerk employs application-specific JWT keys. Since they are different keys, they are incompatible with each other without additional configuration.

Fortunately, Supabase allows you to provide a JSON Web Key Set (JWKS) URL that Supabase can to verify incoming JWTs, and each Clerk application has a dedicated JWKS endpoint with these keys publicly available for this purpose.

A JSON Web Key Set (JWKS) is a JSON object that contains a set of keys used to verify the authenticity of JWTs. The JWKS is available by identity providers (like Clerk) through a public URL to be used with third party services for integrations.

### Connecting Clerk with Supabase

Clerk is a supported third-party authentication provider for Supabase which can be added in the Supabase dashboard, via **Authentication** > **Sign In/Up** > **Third Party Auth**. When adding Clerk as a provider, a modal will appear asking for the Clerk Domain, which is used to access the JWKS endpoint for your application.

From that modal, you can access Clerk's [Supabase Integration Setup page](https://dashboard.clerk.com/setup/supabase), where you can select your application and enable the integration. Once you enable the Supabase integration, all JWTs created by Clerk will include the `"role": "authenticated"` claim which Supabase uses to determine whether the user is authenticated.

The following image shows the setup page with the integration enabled:

![Supabase Integration Setup](./connect-supabase.png)

You can then use the domain provided in the **Clerk Domain** field (pictured above) when prompted. Supabase will automatically verify JWTs with Clerk instead of its own keys going forward.

![Add new Clerk connection in Supabase](./add-clerk.png)

Another difference is in the way that the user ID is referenced within RLS policies. Clerk uses string-based identifiers whereas Supabase uses UUIDs. Since the `auth.uid()` function returns the UUID for the current user, this function cannot be used when accessing Supabase data with Clerk.

You’d instead use the `auth.jwt()` function to access data within the JWT, specifically the `sub` claim which corresponds to the ID of the user:

```sql
create policy "Users can view their own articles"
  on public.articles
  for select
  using (auth.jwt()->>'sub' = user_id);
```

Finally, when creating the Supabase client within your application, you’d use the Clerk `session` object to request a JWT that is compatible with Supabase, which is the JWT template that uses your Supabase signing key.

When using `session.getToken()` for the `accessToken` parameter, requests to Supabase will use the correct JWT created by Clerk:

```tsx
import { createClient } from '@supabase/supabase-js'
import { useSession } from '@clerk/clerk-react'

const { session } = useSession()

const client = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_KEY!,
  {
    accessToken: () => session?.getToken(),
  },
)
```

> \[!NOTE]
> If you are using Next.js, you might be interested in our follow up article on [how Clerk integrates with Next.js and Supabase](/blog/how-clerk-integrates-nextjs-supabase).

## What other benefits does Clerk include?

Using Clerk provides a host of other benefits layered on top of [adding authentication](/nextjs-authentication) to your application.

As a developer, you can quickly configure various authentication strategies, including social sign-ons, passkeys, and email links, beyond the traditional username and password, accommodating your users based on their preferences.

Clerk also provides drop-in UI components that make it easy to extend user management in your application, often with just a single line of code. For example, adding our `<UserButton />` component to your navigation provides users a great experience for managing various aspects of their profile such as updating their sign-in providers, resetting their password, or even remotely signing out other devices.

![User Button](./user-button.jpeg)

If you are building a multi-tenant application, our suite of B2B tools makes it easy for you to quickly add teams and organizations to your application. The `<OrganizationSwitcher />` component enables users to create organizations, invite others, and set permissions, ensuring access is limited to what each user needs.

![Organization Switcher](./organization-switcher.jpeg)

## Conclusion

With just a bit more configuration, Clerk can not only act as a drop-in replacement for Supabase Auth but also provides more capabilities out of the box with access to a large number of commonly required user management features.

And because of our component-first approach to features built with Clerk, you can get up and running with authentication [in as little as 2 minutes](https://www.youtube.com/watch?v=QstMsE_HbgM).

---

# Next.js CVE-2025-29927
URL: https://clerk.com/blog/cve-2025-29927.md
Date: 2025-03-23
Category: Company
Description: On March 21, 2025, Next.js disclosed a critical security vulnerability, CVE-2025-29927, that may impact your application.

On March 21, 2025, Next.js disclosed a critical security vulnerability, [CVE-2025-29927](https://nextjs.org/blog/cve-2025-29927), that may impact your application.

This vulnerability allows attackers to bypass middleware-based authentication and authorization protections, potentially allowing unauthorized access to your application.

## Impacted applications

> \[!IMPORTANT]
> If your application is not using Next.js, or if it is hosted on Vercel or Netlify, it is not impacted.

Yesterday, we mistakenly [announced on X](https://x.com/clerk/status/1903497002828120426) that all applications using Clerk were not impacted. Since then, we have discovered two scenarios where your application may be impacted.

1. You use Clerk's middleware for protecting routes that do not directly read user data. This is most likely to impact static applications that solely rely on middleware for authentication checks. **If you call any of the following methods in routes protected by middleware, your page or endpoint is safe:**
   - `auth()`
   - `getAuth()`
   - `protect()`
   - `currentUser()`

2. Or, you have not upgraded to `@clerk/nextjs@5.2` or higher, which was released in June 2024.

## Patching your application

If your application is impacted, the remediation is to upgrade your `next` package as follows:

- For Next.js 15.x, this issue is fixed in `15.2.3` forward
- For Next.js 14.x, this issue is fixed in `14.2.25` forward
- For Next.js 13.x, this issue is fixed in `13.5.9` forward

If patching to a safe version is infeasible, it is recommended that you prevent external user requests which contain the `x-middleware-subrequest` header from reaching your Next.js application. If your application uses Cloudflare, this can safely be accomplished with a [Managed WAF](https://developers.cloudflare.com/changelog/2025-03-22-next-js-vulnerability-waf/) rule.

Even if you are not impacted, we strongly recommend that you upgrade to the latest versions of `next` and `@clerk/nextjs`.

## Additional support is available

We have also sent an email to the administrators of all Clerk applications that are potentially impacted by this vulnerability. If you have questions, need help determining whether your application is at risk, or need help with mitigation, reply to the email you received or reach out directly to [support@clerk.com](mailto:support@clerk.com)

## Security at Clerk

Our announcement on X that Clerk applications were not impacted was a significant error. We apologize, and will be reflecting on and improving our procedures for zero-day vulnerabilities to ensure it does not happen again.

Going forward, we are pleased that the Next.js team has committed to giving Clerk advance notice on vulnerabilities. We will be seeking similar relationships with other framework authors.

---

# Build a blog with tRPC, Prisma, Next.js and Clerk
URL: https://clerk.com/blog/build-a-blog-with-trpc-prisma-nextjs-clerk.md
Date: 2025-03-14
Category: Guides
Description: Learn how to work with tRPC, Prisma, Next.js, and Clerk by building a secure blog application

In this tutorial, you'll build a blog app from scratch using many modern and popular technologies such as Next.js, Clerk, tRPC, Prisma, and more. After reading this guide, you'll have a simple blog application that allows users to create and read posts.

Let's explore the various technologies used in the article:

- Next.js is the React framework used throughout this guide, specifically using the [App Router](/docs/quickstarts/nextjs).
- Clerk is used for [user management](/docs/user-authentication) and [authentication](/docs/nextjs-authentication).
- Prisma will be the ORM used to connect to the database.
- Vercel is used for hosting and automated deployments.
- Neon is a serverless Postgres database, and you'll actually use Vercel to automate deployment of the database as well.
- Tanstack Query simplifies the process of fetching and caching data.
- [tRPC](/docs/references/nextjs/trpc) provides a type-safe API endpoint wrapper around Tanstack Query.
- Zod is used for schema validation.
- Tailwind provides a simple and modern way to style your app with CSS.

You'll start by creating a Next.js app and integrating Clerk into it for authentication. Then you'll deploy the application to Vercel, where you will also create a Neon database that will be used by Prisma to access and manipulate data within that database. At this point the application will be fully functional, however the rest of the tutorial further enhances the type-safety of your app by adding Tanstack Query, tRPC, and zod. Finally, you'll learn how to create protected procedures using Clerk's authentication context.

To follow along, you should have Node.js installed on your computer and a Vercel account. Familarity with React and Next.js is recommended as well.

> Check out the finished product in Clerk's demo repository:
> [https://github.com/clerk/clerk-nextjs-trpc-prisma](https://github.com/clerk/clerk-nextjs-trpc-prisma)

## Setting up a Next.js application with Clerk

Start by opening your terminal and running the following command to create a new Next.js application. When prompted for the various configuration options, use the options specified below:

```bash
npx create-next-app@latest

# Use the following configuration
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … No
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
```

Once the application has been created, follow the [quickstart in the docs](https://clerk.com/docs/quickstarts/nextjs) to add Clerk to it.

Alternatively, you can clone the [Clerk Next.js quickstart repository](https://github.com/clerk/clerk-nextjs-app-quickstart). This repository contains a pre-configured Next.js app with Clerk already added in keyless mode, which allows you to test the authentication features in your app locally without having to create an account.

```bash
git clone https://github.com/clerk/clerk-nextjs-app-quickstart
```

### Create a Clerk application

Since keyless mode only works for local development, you will want to create a Clerk account and an application in the [dashboard](https://dashboard.clerk.com) to deploy your application to Vercel.

The Clerk Dashboard is where you, as the application owner, can manage your application's settings, users, and organizations. For example, if you want to enable phone number authentication, multi-factor authentication, social providers like Google, delete users, or create organizations, you can do all of this and more in the Clerk Dashboard.

### Set your Clerk API keys

You need to set your Clerk API keys in your app so that your app can use the configuration settings that you set in the Clerk Dashboard.

1. In the Clerk Dashboard, navigate to the [**API keys**](https://dashboard.clerk.com/last-active?path=api-keys) page.
2. In the **Quick Copy** section, copy your Clerk Publishable and Secret Keys.
3. In your `.env` file, set the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables to the values you copied from the Clerk Dashboard.

```env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY={{pub_key}}
CLERK_SECRET_KEY={{secret_key}}
```

## Install dependencies and test your app

While developing, it's best practice to keep your project running so that you can test your changes as you work. So, let's make sure the app is working as expected.

1. Run the following commands to install the dependencies and start the development server:
   ```bash
   npm install
   npm run dev
   ```
2. Open your browser and navigate to the URL displayed in your terminal. The default is `http://localhost:3000` and will be used through the remainder of the tutorial. It should render a new Next.js app, but with a "Sign in" and "Sign up" button in the top right corner.
   ![The development instance running.](./one.png)
3. Select the "Sign in" button. You should be redirected to your Clerk [Account Portal sign-in](https://clerk.com/docs/account-portal/overview#sign-in) page, which renders Clerk's [`<SignIn />`](https://clerk.com/docs/components/sign-in) component. The `<SignIn />` component will look different depending on the configuration of your Clerk instance.
   ![A Clerk Account Portal sign-in page.](./two.png)
4. Sign in to your Clerk application.
5. You should be redirected back to your app, where you should see Clerk's [`<UserButton />`](https://clerk.com/docs/components/user/user-button) component in the top right corner.

## Install Prisma ORM

Run the following command to install Prisma:

```bash
npm install prisma --save-dev
```

Then run `npx prisma init` to initialize Prisma in your project.

```bash
npx prisma init
```

This will create a new `prisma` directory in your project, with a `schema.prisma` file inside of it. The `schema.prisma` file is where you will define your database models.

The `prisma init` command will also update your `.env` file to include a `DATABASE_URL` environment variable, which is used to store your database connection string. If you have a database already, great! If not, let's spin one up using Vercel.

## Deploy to Vercel

Before you can create a database using Vercel, you first need to deploy your app to Vercel.

1. Create a repository on GitHub for your app. If you're not sure how to do this, follow the [GitHub docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository).
2. Go to [Vercel](https://vercel.com) and add a new project. While going through the process, select the **Environment Variables** dropdown, and add your Clerk Publishable and Secret Keys.
   ![Vercel dashboard showing where to input environment variables](./three.png)
3. Select **Deploy** to deploy your app to Vercel.
4. Select the **Settings** tab.
5. In the left sidenav, select **Functions**.
6. Under **Function Region**, there should be a tag next to one of the continents. Select the continent where the tag is, and the dropdown will reveal what regions on Vercel's network that your Vercel Functions will execute in. Take note of the region. Keep the Vercel dashboard open.
   ![Vercel dashboard with an arrow pointing to a tag that says "iad1", and an arrow pointing to a highlighted element that says "Washington, D.C., USA (EAST) - us-east-1 - iad1"](./four.png)

## Spin up a database

1. While still in Vercel's dashboard, select the **Storage** tab.
2. Select **Create Database**.
3. Select **Neon** as the database provider and select **Continue**.
4. Select the **Region** dropdown and select the region you noted earlier. You want your database's region to match your Vercel Functions region for optimal performance.
5. Select **Continue**.
6. Copy the environment variables and add them to your `.env` file. They should look something like this:

```env
# Recommended for most uses
DATABASE_URL=***

# For uses requiring a connection without pgbouncer
DATABASE_URL_UNPOOLED=***

# Parameters for constructing your own connection string
PGHOST=***
PGHOST_UNPOOLED=***
PGUSER=***
PGDATABASE=***
PGPASSWORD=***

# Parameters for Vercel Postgres Templates
POSTGRES_URL=***
POSTGRES_URL_NON_POOLING=***
POSTGRES_USER=***
POSTGRES_HOST=***
POSTGRES_PASSWORD=***
POSTGRES_DATABASE=***
POSTGRES_URL_NO_SSL=***
POSTGRES_PRISMA_URL=***
```

## Create a database model

Now that your database is created and connected to your app, it's time to create a database model. The main entity of the application is a `Post` that represents each entry in the blog app, so add the following `Post` model to your `schema.prisma` file:

```prisma
model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  authorId  String
}
```

## Update your database schema

Run the following command to apply your schema to your database:

```bash
npx prisma migrate dev --name init
```

This creates an initial migration creating the `Post` table and applies that migration to your database.

## Set up Prisma Client

Now it's time to set up the Prisma Client and connect it to your database. You'll want to create a single client and bind it to the `global` object so that only one instance of the client is created in your application. This helps resolve issues with hot reloading that can occur when using Prisma with Next.js in development mode.

Create the `lib/prisma.ts` file and add the following code to it:

```ts {{ filename: 'lib/prisma.ts' }}
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

const globalForPrisma = global as unknown as { prisma: typeof prisma }

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

export default prisma
```

### Query your database

Now that all of the set up is complete, it's time to start building out your app!

Let's start with your homepage. Replace the contents of `app/page.tsx` with the following code:

```tsx {{ filename: 'app/page.tsx' }}
import Link from 'next/link'
import prisma from '@/lib/prisma'

export default async function Page() {
  const posts = await prisma.post.findMany() // Query the `Post` model for all posts

  return (
    <div className="mx-auto mt-8 flex min-h-screen max-w-2xl flex-col">
      <h1 className="mb-8 text-4xl font-bold">Posts</h1>

      <div className="mb-8 flex max-w-2xl flex-col space-y-4">
        {posts.map((post) => (
          <Link
            key={post.id}
            href={`/posts/${post.id}`}
            className="flex flex-col rounded-lg px-2 py-4 transition-all hover:bg-neutral-100 hover:underline dark:hover:bg-neutral-800"
          >
            <span className="text-lg font-semibold">{post.title}</span>
            <span className="text-sm">by {post.authorId}</span>
          </Link>
        ))}
      </div>

      <Link
        href="/posts/create"
        className="inline-block rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
      >
        Create New Post
      </Link>
    </div>
  )
}
```

This code fetches all posts from your database and displays them on the homepage, showing the title and author ID for each post. It uses the [`prisma.post.findMany()`](https://www.prisma.io/docs/orm/reference/prisma-client-reference?utm_source=docs#findmany) method, which is a Prisma Client method that retrieves all records from the database.

That shows how to query for all records, but how do you query for a single record?

### Query a single record

Let's add a page that displays a single post. This page uses the URL parameters to get the post's ID, and then fetches it from your database and displays it on the page, showing the title, author ID, and content. It uses the [`prisma.post.findUnique()`](https://www.prisma.io/docs/orm/reference/prisma-client-reference?utm_source=docs#findunique) method, which is a Prisma Client method that retrieves a single record from the database.

Create the `app/posts/[id]/page.tsx` file and paste the following code in it:

```tsx {{ filename: 'app/posts/[id]/page.tsx' }}
import prisma from '@/lib/prisma'

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const post = await prisma.post.findUnique({
    where: { id: parseInt(id) },
  })

  if (!post) {
    return (
      <div className="mx-auto mt-8 flex min-h-screen max-w-2xl flex-col">
        <div>No post found.</div>
      </div>
    )
  }

  return (
    <div className="mx-auto mt-8 flex min-h-screen max-w-2xl flex-col">
      {post && (
        <article className="w-full max-w-2xl">
          <h1 className="mb-2 text-2xl font-bold sm:text-3xl md:text-4xl">{post.title}</h1>
          <p className="text-sm sm:text-base">by {post.authorId}</p>
          <div className="prose prose-gray prose-sm sm:prose-base lg:prose-lg mt-4 sm:mt-8">
            {post.content || 'No content available.'}
          </div>
        </article>
      )}
    </div>
  )
}
```

Test the page by navigating to a post's URL. For example, `http://localhost:3000/posts/1`. For now, it should show a "No post found" message because you haven't created any posts yet. Let's add a way to create posts.

### Create a new post

Next you'll create a page that allows users to create new posts. This page uses Clerk's [`auth()`](https://clerk.com/docs/references/nextjs/auth) helper to get the user's ID. It is a helper that is specific to Next.js App Router, and it provides authentication information on the server side.

- If there is no user ID, the user is not signed in, so a sign in button is displayed.
- If the user is signed in, the "Create New Post" form is displayed. When the form is submitted, the `createPost()` function is called. This function creates a new post in the database using the [`prisma.post.create()`](https://www.prisma.io/docs/orm/reference/prisma-client-reference?utm_source=docs#create) method, which is a Prisma Client method that creates a new record in the database.

Create the `app/posts/create/page.tsx` file and paste in the following code:

```tsx {{ filename: 'app/posts/create/page.tsx' }}
import Form from 'next/form'
import prisma from '@/lib/prisma'
import { redirect } from 'next/navigation'
import { SignInButton, useAuth } from '@clerk/nextjs'
import { revalidatePath } from 'next/cache'
import { auth } from '@clerk/nextjs/server'

export default async function NewPost() {
  const { userId } = await auth()

  // Protect this page from unauthenticated users
  if (!userId) {
    return (
      <div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center space-y-4">
        <p>You must be signed in to create a post.</p>
        <SignInButton>
          <button
            type="submit"
            className="inline-block cursor-pointer rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
          >
            Sign in
          </button>
        </SignInButton>
      </div>
    )
  }

  async function createPost(formData: FormData) {
    'use server'

    // Type check
    if (!userId) return

    const title = formData.get('title') as string
    const content = formData.get('content') as string

    await prisma.post.create({
      data: {
        title,
        content,
        authorId: userId,
      },
    })

    revalidatePath('/')
    redirect('/')
  }

  return (
    <div className="mx-auto max-w-2xl p-4">
      <h1 className="mb-6 text-2xl font-bold">Create New Post</h1>
      <Form action={createPost} className="space-y-6">
        <div>
          <label htmlFor="title" className="mb-2 block text-lg">
            Title
          </label>
          <input
            type="text"
            id="title"
            name="title"
            placeholder="Enter your post title"
            className="w-full rounded-lg border px-4 py-2"
          />
        </div>
        <div>
          <label htmlFor="content" className="mb-2 block text-lg">
            Content
          </label>
          <textarea
            id="content"
            name="content"
            placeholder="Write your post content here..."
            rows={6}
            className="w-full rounded-lg border px-4 py-2"
          />
        </div>
        <button
          type="submit"
          className="inline-block w-full rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
        >
          Create Post
        </button>
      </Form>
    </div>
  )
}
```

Test the page by navigating to the `/posts/create` page (ex: `http://localhost:3000/posts/create`) and create a new post. You should be redirected to the homepage, where you should see the new post.

# Configure tRPC, `@tanstack/react-query`, and `zod`

Now, you've got a Next.js, Clerk, and Prisma app that can create and display posts. You could stop here and have a perfectly functional app. But let's take it a step further and add tRPC to your app for type-safe API endpoints.

### Install the dependencies

Let's start by installing the following dependencies:

- `trpc` is a wrapper around your API endpoints to make them type-safe and easier to use.
- `zod` is a schema validation library, also used to enhance your app's type safety.
- `@tanstack/react-query` is a library for data fetching and caching.

Run the following command to install the packages:

```bash
npm i @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod --force
```

> At the time of writing, `clerk-next-app` includes React 19 as a peer dependency, but `@tanstack/react-query` does not. So, you'll need to use the `--force` flag when running the command above. You may not need the `--force` flag in the future.

## Create a tRPC server

Now, you'll configure tRPC for your app. You'll start by initializing a tRPC server that creates a `router` and `publicProcedure` that you can use to create your API endpoints.

Create the `app/server/trpc.ts` file and paste in the following code

```ts {{ filename: 'app/server/trpc.ts' }}
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

export const router = t.router
export const publicProcedure = t.procedure
```

### Create a tRPC endpoint

Now, you'll create a router that's going to have your procedures on it. The following code creates a router with a `getPosts` procedure that uses the tRPC `publicProcedure` you created in the previous step to make a query using [tRPC's `query()` method](https://trpc.io/docs/server/procedures). The query then uses Prisma to query the `Post` model in your database. That part should look familiar, because you've used `prisma.post.findMany()` in your app earlier!

Create the `app/server/routers/posts.ts` file and paste in the code below:

```ts {{ filename: 'app/server/routers/posts.ts' }}
import { publicProcedure, router } from '../trpc'
import prisma from '@/lib/prisma'

export const postRouter = router({
  getPosts: publicProcedure.query(async () => {
    return await prisma.post.findMany()
  }),
})

export type PostRouter = typeof postRouter
```

This is the file where you'll add all of your queries and mutations, so you'll probably update this file frequently as you build out your app.

### Connect the tRPC router to your App Router

Now you need to connect the tRPC router to your App Router. You'll use a Route Handler that uses [tRPC's `fetchRequestHandler()` method](https://trpc.io/docs/server/adapters/fetch#nextjs-edge-runtime) to pass requests from Next.js to the tRPC router.

Create the `app/api/trpc/[trpc]/route.ts` file and paste in the code below:

```ts {{ filename: 'app/api/trpc/[trpc]/route.ts' }}
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { postRouter } from '@/app/server/routers/posts'

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: postRouter,
    createContext: () => ({}),
  })

export { handler as GET, handler as POST }
```

At this point, your API endpoint should be working. You can test it by navigating to `http://localhost:3000/api/trpc/getPosts`. You should see a JSON response with the posts from your database.

## Create a tRPC client

So far, your app is entirely server-side and static. You need a way to mutate data, which is where `@tanstack/react-query` comes in. But to use tRPC with `@tanstack/react-query`, you need to create a tRPC client.

Create the `app/_trpc/client.ts` file and paste in the code below:

```ts {{ filename: 'app/_trpc/client.ts' }}
'use client'

import { createTRPCReact } from '@trpc/react-query'

import type { PostRouter } from '@/app/server/routers/posts'

export const trpc = createTRPCReact<PostRouter>({})
```

## Create a Tanstack Query + tRPC provider

To use Tanstack Query and tRPC together, you need to create a provider using the React Context API. This provider will make both the Tanstack Query client and the tRPC client available to your app using the `trpc.Provider` and `QueryClientProvider` components.

Create the `app/_trpc/Provider.tsx` file and paste in the code below:

```tsx {{ filename: 'app/_trpc/Provider.tsx' }}
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import React, { useState } from 'react'

import { trpc } from './client'

export default function Provider({ children }: { children: React.ReactNode }) {
  // Create a Tanstack Query client
  const [queryClient] = useState(() => new QueryClient({}))
  // Create a tRPC client
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    }),
  )
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  )
}
```

Now, wrap your app in the provider. Update the main layout to import the provider as `TRPCProvider` and wrap your app in it. It's very important that `<ClerkProvider>` is wrapped around `<TRPCProvider>`, and not the other way around, because the `<TRPCProvider>` needs to have access to the Clerk authentication context.

In `app/layout.tsx`, add the following code:

```tsx {{ filename: 'app/layout.tsx', ins: [12, 36, 51] }}
import type { Metadata } from 'next'
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import TRPCProvider from '@/app/_trpc/Provider'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

export const metadata: Metadata = {
  title: 'Clerk Next.js Quickstart',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <ClerkProvider>
      <TRPCProvider>
        <html lang="en">
          <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
            <header className="flex h-16 items-center justify-end gap-4 p-4">
              <SignedOut>
                <SignInButton />
                <SignUpButton />
              </SignedOut>
              <SignedIn>
                <UserButton />
              </SignedIn>
            </header>
            {children}
          </body>
        </html>
      </TRPCProvider>
    </ClerkProvider>
  )
}
```

Now, you can use the `trpc` client to fetch and mutate data in your app! Let's update the functionality of your app to use the `trpc` client.

## Use the tRPC client to fetch and mutate data

Let's start by updating the homepage where the list of posts is rendered. Since the page is still rendered server-side, you'll create a client component that uses the `trpc` client to fetch posts.

Create the `app/components/Posts.tsx` file and paste in the following code:

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

import Link from 'next/link'
import { trpc } from '../_trpc/client'

export default function Posts() {
  // Use the `getPosts` query from the TRPC client
  const getPosts = trpc.getPosts.useQuery()
  const { isLoading, data } = getPosts

  return (
    <div className="mb-8 flex max-w-2xl flex-col space-y-4">
      {isLoading && <div>Loading...</div>}
      {data?.map((post) => (
        <Link
          key={post.id}
          href={`/posts/${post.id}`}
          className="flex flex-col rounded-lg px-2 py-4 transition-all hover:bg-neutral-100 hover:underline dark:hover:bg-neutral-800"
        >
          <span className="text-lg font-semibold">{post.title}</span>
          <span className="text-sm">by {post.authorId}</span>
        </Link>
      ))}
    </div>
  )
}
```

Then, update the homepage to use the `<Posts />` component:

```tsx {{ filename: 'app/page.tsx', ins: [3, 19], del: [2, [10, 17]] }}
import Link from 'next/link'
import prisma from '@/lib/prisma'
import Posts from './components/Posts'

export default async function Page() {
  return (
    <div className="-mt-16 flex min-h-screen flex-col items-center justify-center">
      <h1 className="mb-8 text-4xl font-bold">Posts</h1>

      <div className="mb-8 flex max-w-2xl flex-col space-y-4">
        {posts.map((post) => (
          <Link
            key={post.id}
            href={`/posts/${post.id}`}
            className="flex flex-col rounded-lg px-2 py-4 transition-all hover:bg-neutral-100 hover:underline dark:hover:bg-neutral-800"
          >
            <span className="text-lg font-semibold">{post.title}</span>
            <span className="text-sm">by {post.authorId}</span>
          </Link>
        ))}
      </div>

      <Posts />

      <Link
        href="/posts/create"
        className="inline-block rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
      >
        Create New Post
      </Link>
    </div>
  )
}
```

Notice that the `prisma.post.findMany()` function is no longer used. Instead, your app is using `trpc.getPosts.useQuery()` in the `<Posts />` component to fetch the posts, because remember, you created a tRPC `postRouter` with a `getPosts` procedure that uses `prisma.post.findMany()`. So now, you don't need to use Prisma directly, you can use tRPC in order to have type safety and a better developer experience. Let's update the rest of your app to use tRPC.

Of course, let's test and make sure the new logic is working. Navigate to the homepage and make sure you can see the posts.

Once you've verified everything's working, let's go back to your `postRouter` and create more procedures to handle your other queries.

### Use tRPC to fetch a single post

In `app/server/routers/posts.ts`, update the code to match the following:

```ts {{ filename: 'app/server/routers/posts.ts', ins: [3, [6, 10]] }}
import { publicProcedure, router } from '../trpc'
import prisma from '@/lib/prisma'
import { z } from 'zod'

export const postRouter = router({
  getPost: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
    return await prisma.post.findUnique({
      where: { id: parseInt(input.id) },
    })
  }),
  getPosts: publicProcedure.query(async () => {
    return await prisma.post.findMany()
  }),
})

export type PostRouter = typeof postRouter
```

This adds a `getPost` procedure to fetch a single post by ID.

In `app/posts/[id]/page.tsx`, update the code to match the following:

```tsx {{ filename: 'app/posts/[id]/page.tsx', ins: [[32, 56]], del: [[1, 30]], prettier: false }}
import prisma from '@/lib/prisma'

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const post = await prisma.post.findUnique({
    where: { id: parseInt(id) },
  })

  if(!post) {
    return (
      <div className="flex min-h-screen flex-col max-w-2xl mx-auto mt-8">
        <div>No post found.</div>
      </div>
    )
  }

  return (
    <div className="flex min-h-screen flex-col max-w-2xl mx-auto mt-8">
      {post && (
        <article className="w-full max-w-2xl">
          <h1 className="mb-2 text-2xl font-bold sm:text-3xl md:text-4xl">{post.title}</h1>
          <p className="text-sm sm:text-base">by {post.authorId}</p>
          <div className="prose prose-gray prose-sm sm:prose-base lg:prose-lg mt-4 sm:mt-8">
            {post.content || 'No content available.'}
          </div>
        </article>
      )}
    </div>
  )
}

'use client'
import { trpc } from '@/app/_trpc/client'
import { use } from 'react'
export default function Post({ params }: { params: Promise<{ id: string }> }) {
  // Params are wrapped in a promise, so we need to unwrap them using React's `use()` hook
  const { id } = use(params)
  // Use the `getPost` query from the TRPC client
  const { data: post, isLoading } = trpc.getPost.useQuery({ id })

  return (
    <div className="flex min-h-screen flex-col max-w-2xl mx-auto mt-8">
      {isLoading && <p>Loading...</p>}
      {!isLoading && !post && <p>No post found.</p>}
      {!isLoading && post && (
        <article className="w-full max-w-2xl">
          <h1 className="mb-2 text-2xl font-bold sm:text-3xl md:text-4xl">{post.title}</h1>
          <p className="text-sm sm:text-base">by {post.authorId}</p>
          <div className="prose prose-gray prose-sm sm:prose-base lg:prose-lg mt-4 sm:mt-8">
            {post.content || 'No content available.'}
          </div>
        </article>
      )}
    </div>
  )
}
```

This replaces `prisma.post.findUnique()` with `trpc.getPost.useQuery()`. Because tRPC is using Tanstack Query to fetch the data, the query result includes the data and other states, such as loading and error. You can learn more about in the [Tanstack Query docs](https://tanstack.com/query/v4/docs/framework/react/guides/queries#:~:text=throughout%20your%20application.-,The%20query%20result,-returned%20by%20useQuery).

And before you go any further, test to make sure the new logic is working. Navigate to a post's URL, such as `http://localhost:3000/posts/1`, and make sure you can see the post.

If that's working, let's go back to your `postRouter` and add the last procedure you need to handle your create post functionality.

### Use tRPC to create a new post

In `app/server/routers/posts.ts`, add the following code:

```ts {{ filename: 'app/server/routers/posts.ts', ins: [[5, 9], [20, 29]] }}
import { publicProcedure, router } from '../trpc'
import prisma from '@/lib/prisma'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string(),
  content: z.string(),
  authorId: z.string(),
})

export const postRouter = router({
  getPost: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
    return await prisma.post.findUnique({
      where: { id: parseInt(input.id) },
    })
  }),
  getPosts: publicProcedure.query(async () => {
    return await prisma.post.findMany()
  }),
  // Protected procedure that requires a user to be signed in
  createPosts: publicProcedure.input(postSchema).mutation(async ({ input }) => {
    return await prisma.post.create({
      data: {
        title: input.title,
        content: input.content,
        authorId: input.authorId,
      },
    })
  }),
})

export type PostRouter = typeof postRouter
```

This adds a `createPosts` procedure that creates a new post.

In `app/posts/create/page.tsx`, replace the existing code with the following:

```tsx {{ filename: 'app/posts/create/page.tsx', ins: [1, 10, 11, 12, 16, [17, 31], [72, 82], 88, 97, 98, 110, 111, 123], del: [[3, 5], 7, 8, 14, 15, [50, 69], 87, 124], prettier: false }}
'use client'

import Form from 'next/form'
import prisma from '@/lib/prisma'
import { redirect } from 'next/navigation'
import { SignInButton, useAuth } from '@clerk/nextjs'
import { revalidatePath } from 'next/cache'
import { auth } from '@clerk/nextjs/server'

import { redirect } from 'next/navigation'
import { trpc } from '@/app/_trpc/client'
import { useState } from 'react'

export default async function NewPost() {
  const { userId } = await auth()
export default function NewPost() {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')
  // Use Clerk's `useAuth()` hook to get the user's ID
  const { userId, isLoaded } = useAuth()
  // Use the `createPosts` mutation from the TRPC client
  const createPostMutation = trpc.createPosts.useMutation()

  // Check if Clerk is loaded
  if (!isLoaded) {
    return (
      <div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center space-y-4">
        <div>Loading...</div>
      </div>
    )
  }

  // Protect this page from unauthenticated users
  if (!userId) {
    return (
      <div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center space-y-4">
        <p>You must be signed in to create a post.</p>
        <SignInButton>
          <button
            type="submit"
            className="inline-block cursor-pointer rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
          >
            Sign in
          </button>
        </SignInButton>
      </div>
    )
  }

  async function createPost(formData: FormData) {
    'use server'

    // Type check
    if (!userId) return

    const title = formData.get('title') as string
    const content = formData.get('content') as string

    await prisma.post.create({
      data: {
        title,
        content,
        authorId: userId,
      },
    })

    revalidatePath('/')
    redirect('/')
  }

  // Handle form submission
  async function createPost(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()

    createPostMutation.mutate({
      title,
      content,
      authorId: userId as string,
    })

    redirect('/')
  }

  return (
    <div className="mx-auto max-w-2xl p-4">
      <h1 className="mb-6 text-2xl font-bold">Create New Post</h1>
      <Form action={createPost} className="space-y-6">
      <form onSubmit={createPost} className="space-y-6">
        <div>
          <label htmlFor="title" className="mb-2 block text-lg">
            Title
          </label>
          <input
            type="text"
            id="title"
            name="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Enter your post title"
            className="w-full rounded-lg border px-4 py-2"
          />
        </div>
        <div>
          <label htmlFor="content" className="mb-2 block text-lg">
            Content
          </label>
          <textarea
            id="content"
            name="content"
            value={content}
            onChange={(e) => setContent(e.target.value)}
            placeholder="Write your post content here..."
            rows={6}
            className="w-full rounded-lg border px-4 py-2"
          />
        </div>
        <button
          type="submit"
          className="inline-block w-full rounded-lg border-2 border-current px-4 py-2 text-current transition-all hover:scale-[0.98]"
        >
          Create Post
        </button>
      </form>
      </Form>
    </div>
  )
}
```

This updates a few things. First, it turns this page into a client component, because Tanstack Query and the tRPC client are client-side. So now, the Server Action that you created before can no longer be used. Instead, the form data is handled using state. When the form is submitted, the `createPost()` function no longer uses `prisma.post.create()`, but instead uses `trpc.createPosts.useMutation()` from the tRPC client. Also, because the page is now a client component, Clerk's `auth()` helper no longer works, so it's replaced with Clerk's `useAuth()` hook. This introduces the benefit of having access to Clerk's loading state, so a loading UI is added.

And don't forget, test your changes. Navigate to the create post page, such as `http://localhost:3000/posts/create`, and make sure you can create a new post.

Once you've confirmed everything's working, you're almost done...

## Create protected procedures

In many applications, it's essential to restrict access to certain routes based on user authentication status. This ensures that sensitive data and functionality are only accessible to authorized users.

The benefit of using Clerk with tRPC is that you can create protected procedures using Clerk's authentication context. Clerk's [`Auth`](https://clerk.com/docs/references/backend/types/auth-object) object includes important authentication information like the current user's session ID, user ID, and organization ID. It also contains methods to check for the current user's permissions and to retrieve their session token. You can use the `Auth` object to access the user's authentication information in your tRPC queries.

### Create the tRPC context

In your `server` directory, create a `context.ts` file with the following code:

```ts {{ filename: 'app/server/context.ts' }}
import { auth } from '@clerk/nextjs/server'

export const createContext = async () => {
  return { auth: await auth() }
}

export type Context = Awaited<ReturnType<typeof createContext>>
```

This creates a context that will be used to create the context for every tRPC query sent to the server. The context will use the [`auth()`](/docs/references/nextjs/auth) helper from Clerk to access the user's `Auth` object.

### Pass the context to the tRPC server

Then, in your tRPC server (`app/api/trpc/[trpc]/route.ts`), pass the context:

```ts {{ filename: 'app/api/trpc/[trpc]/route.ts', ins: [3, 33], del: [10] }}
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/app/server/routers/posts'
import { createContext } from '@/app/server/context'

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
    createContext,
  })

export { handler as GET, handler as POST }
```

### Access the context data in your procedures

The tRPC context, or `ctx`, should now have access to the Clerk `Auth` object.

In your `server/trpc.ts` file, create a protected procedure:

```ts {{ filename: 'app/server/trpc.ts', ins: [2, 3, 6, [10, 19], 23], del: [1, 5] }}
import { initTRPC } from '@trpc/server'
import { initTRPC, TRPCError } from '@trpc/server'
import { Context } from './context'

const t = initTRPC.create()
const t = initTRPC.context<Context>().create()

// Check if the user is signed in
// Otherwise, throw an UNAUTHORIZED code
const isAuthed = t.middleware(({ next, ctx }) => {
  if (!ctx.auth.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }
  return next({
    ctx: {
      auth: ctx.auth,
    },
  })
})

export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(isAuthed)
```

### Use your protected procedure

Once you have created your procedure, you can use it in any router. In this case, you don't want unauthenticated users to be able to create posts, so let's update the `createPosts` mutation to be protected by swapping the `publicProcedure` with the `protectedProcedure`:

```ts {{ filename: 'app/server/routers/posts.ts', ins: [2, 22], del: [1, 21], prettier: false }}
import { publicProcedure, router } from '../trpc'
import { protectedProcedure, publicProcedure, router } from '../trpc'
import prisma from '@/lib/prisma'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string(),
  content: z.string(),
  authorId: z.string(),
})

export const postRouter = router({
  getPost: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
    return await prisma.post.findUnique({
      where: { id: parseInt(input.id) },
    })
  }),
  getPosts: publicProcedure.query(async () => {
    return await prisma.post.findMany()
  }),
  createPosts: publicProcedure.input(postSchema).mutation(async ({ input }) => {
  createPosts: protectedProcedure.input(postSchema).mutation(async ({ input }) => {
    return await prisma.post.create({
      data: {
        title: input.title,
        content: input.content,
        authorId: input.authorId,
      },
    })
  }),
})

export type PostRouter = typeof postRouter
```

## Finished!

At this point, you've got a fully functional app for creating and displaying posts. You can now add more features to your app, such as updating and deleting posts, adding comments, storing more author information from the Clerk [`User`](https://clerk.com/docs/references/javascript/user) object, and more.

---

# How to enrich PostHog events with Clerk user data
URL: https://clerk.com/blog/posthog-events-with-clerk.md
Date: 2025-02-28
Category: Guides
Description: Learn how to enrich PostHog events with Clerk user data to better understand your users and their actions on your website.

Data-driven decisions are critical for teams building SaaS products due to their ability to optimize processes, improve customer satisfaction, and drive growth. Attributing those data points to individual users can significantly enhance this process by providing more targeted insights about user groups and behaviors.

In this article, you’ll learn how events gathered by PostHog can be directly associated to individual users in applications using Clerk.

> \[!NOTE]
> This is the third article in the Kozi series, which walks you through building a project/knowledge management SaaS from the ground up using Clerk, Neon, Next.js, and more.
>
> [Learn more](https://github.com/bmorrisondev/kozi)

## What is PostHog?

PostHog is an open-source product analytics platform that allows developers to gain a deeper understanding of how their product is used with tools like event tracking, session replay, feature flags, and more. Using one of the PostHog SDKs, web applications can be configured to automatically collect data and transmit it to the platform. This data can fuel dashboards to help you make data-driven decisions on how to optimize their product.

When configured properly, the event data in PostHog can be attributed directly to your  users and identify which features they're utilizing.

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

## Enriching event data with user information

The PostHog SDK provides the `identify` function as a means to attribute a session to a specific user. This function also supports including arbitrary data about the current user to further enrich the data sent back to its platform. Furthermore, PostHog will proactively enrich past events once a session has been associated with a user so that you have the most accurate view of how your product is being used.

Clerk SDKs provide helper functions to easily gather information about the user currently using your product. The following snippet demonstrates how the current Clerk user data can be used with `identify` to enrich the event data sent to PostHog within a Next.js application:

```tsx
// The `useUser` hook returns information about the current user.
const { user } = useUser()

// That information can be used with the `posthog.identify` function to associate the data with the user.
posthog.identify(userId, {
  email: user.primaryEmailAddress?.emailAddress,
  username: user.username,
})
```

Read on to see how this code is implemented.

> \[!NOTE]
> If you want to learn more about integrating PostHog with Clerk, let us know in our [feedback portal](https://feedback.clerk.com/roadmap?id=c0964197-4879-4a3e-858f-521f783500c5).

## How to configure PostHog to use Clerk user data in Next.js

Let’s explore how to implement this in a real-world scenario by configuring this integration into Kozi. Kozi is an open-source project/knowledge management web application built with Next.js, Neon, and Clerk.

If you want to follow along on your computer, clone the [`article-2ph-start`](https://github.com/bmorrisondev/kozi/tree/article-2ph-start) branch of the Kozi repository and run the follow the instructions in the `README` to configure the project before proceeding.

### Configure the **`PostHogPageView.tsx`** component

The following client-side component has two `useEffects` that perform the following operations:

- The first will use the `posthog.capture` function with the `$pageview` event passing in the current URL.
- The second will run the `posthog.identify` function if the user is not already identified, passing in information from the `useAuth` and `useUser` Clerk hooks.
  - This function will also clear the user information from PostHog in the current session if user is no longer logged in using the `isSignedIn` boolean from the `useAuth` Clerk hook.

```tsx {{ filename: 'src/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,
      })
    }

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

  return null
}
```

This component is then added to the root layout file within the `<PostHogProvider>` tags:

```tsx {{ filename: 'src/app/layout.tsx', ins: [6, 34] }}
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import { ClerkProvider } from '@clerk/nextjs'
import { PostHogProvider } from './providers'
import PostHogPageView from './PostHogPageView'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
          <PostHogProvider>
            {children}
            <PostHogPageView />
          </PostHogProvider>
        </body>
      </html>
    </ClerkProvider>
  )
}
```

### Configure user interaction event tracking

While the above component will automatically capture pageviews using a default event name, PostHog can also capture custom events associated to your users:

```sql
posthog.capture('task_created');
```

> \[!NOTE]
> The `posthog.capture` function only works in the browser, so make sure to use it only in client components, otherwise the function will silently fail.

The `CreateTaskInput.tsx` is what renders the input at the bottom of a task list:

![The CreateTaskInput component](./create-task-input.png)

To instrument this component, you only need to add the `usePostHog` hook and insert a line in the function that hands the form submission:

```tsx {{ filename: 'src/app/app/components/CreateTaskInput.tsx', ins: [17, 24] }}
'use client'

import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { createTask } from '@/app/app/actions'
import { PlusIcon } from 'lucide-react'
import { usePostHog } from 'posthog-js/react'

interface CreateTaskInputProps {
  projectId?: string
}

export default function CreateTaskInput({ projectId }: CreateTaskInputProps) {
  const [title, setTitle] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)
  const posthog = usePostHog()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    if (!title.trim()) return

    posthog.capture('create_task')

    try {
      setIsSubmitting(true)
      const formData = new FormData()
      formData.append('title', title)
      if (projectId) {
        formData.append('project_id', projectId)
      }
      await createTask(formData)
      setTitle('')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <div className="w-full rounded-lg bg-white p-2 shadow dark:bg-gray-800">
      <form onSubmit={handleSubmit} className="flex w-full items-center gap-2">
        <Input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Add a task..."
          className="w-full border-0 bg-transparent text-gray-900 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-gray-100"
        />
        <Button
          type="submit"
          size="icon"
          disabled={isSubmitting || !title.trim()}
          className="rounded"
        >
          <PlusIcon className="h-4 w-4" />
        </Button>
      </form>
    </div>
  )
}
```

From that point forward, any time a user creates a task, PostHog will have an event logged that can be used for product analytics:

![The PostHog dashboard with a list of events showing the create\_task event](./create-task-event.png)

## Conclusion

Using PostHog with Clerk can unlock powerful user engagement insights that drive your product's growth. Tracking standard events like page views and custom events tailored to your application, you can identify usage trends that might otherwise go unnoticed, allowing you to confidently iterate on your product.

---

# How to build a secure project management platform with Next.js, Clerk, and Neon
URL: https://clerk.com/blog/build-secure-project-management-nextjs.md
Date: 2025-02-20
Category: Guides
Description: Learn a security-first approach to building web applications by building a secure project management platform with Next.js.

Around 30,000 websites and applications are hacked every day\*, and the developer is often to blame.

The vast majority of breaches occur due to misconfiguration rather than an actual vulnerability. This could be due to exposed database credentials, unprotected API routes, or data operations without the proper authorization checks just to name a few. It’s important to ensure that your application is configured in a way that prevents attackers from gaining unauthorized access to user data.

In this article, you’ll learn how to build a project management web application while considering security best practices throughout.

Although this article can be followed by itself, it is the second in a series covering the process of building **Kozi** - a collaborative project and knowledge management tool. Throughout the series, the following features will be implemented:

- Create organizations to invite others to manage projects as a team.
- A rich, collaborative text editor for project and task notes.
- A system to comment on projects, tasks, and notes.
- Automatic RAG functionality for all notes and uploaded files.
- Invite users from outside your organization to collaborate on individual tasks.
- Be notified when events occur on tasks you subscribe to, or you are mentioned in comments or notes.

## What makes this a “secure” project management system?

Data security is considered throughout this guide by using the following techniques:

### Clerk and the Next.js middleware

Clerk is a [user management platform for Next.js](/nextjs-authentication) designed to get authentication into your application as quick as possible by providing a complete suite of user management tools as well as drop-in UI components. Behind the scenes, Clerk creates fast expiring tokens upon user sign-in that are sent to your server with each request, where Clerk also verifies the identify of the user.

Clerk integrates with Next.js middleware to ensure every request to the application is evaluated before it reaches its destination. In the section where the middleware is configured, we instruct the middleware to protect any route starting with `/app` so that only authenticated users may access them. This means that before any functions are executed (on the client or server), the user will need to be authenticated.

### Server actions

In this project, server actions are the primary method of interacting with the data in the database. Direct access to the database should always happen on the server and NEVER on the client where tech-savvy users can gain access to the database credentials. Since all functions that access the database are built with server actions, they do not execute client-side.

It's important to note that calling these server actions should only ever be performed from protected routes. When a Next.js client component executes a server action, an HTTP POST request of form data is submitted to the current path with a unique identifier of the action for Next.js to route the data internally.

This means that calling a server function from an anonymous route might result in anonymous users getting access to the data. This potential vulnerability is addressed in the next section.

### Database requests

Protecting access to the functions is only one consideration. Each request will have an accompanying user identifier which can be used to determine the user making that request. This identifier is stored alongside the records the user creates, allowing each request for data to ONLY return the data associated with that user.

When making data modifications, the requesting user ID is cross-referenced with the records being modified or deleted so that one user cannot affect another user’s data.

The combination of protecting access to the routes, being mindful of calling server actions, and cross-referencing database queries with the user making the request ensures that the data within the application is secure and only accessible to those who have access to it.

## How to follow along

Kozi is an open-source project, with each article in the series having corresponding start and end branches. This makes it easy to jump in at any point to get hands-on experience with the concepts outlined in each piece, as well as a point of reference if you simply want to see the completed code. Here are links to the specific branches:

- [`article-2-start`](https://github.com/bmorrisondev/kozi/tree/article-2-start)
- [`article-2-end`](https://github.com/bmorrisondev/kozi/tree/article-2-end)

You should have a basic understanding of Next.js and React as well.

### Launching the project

Once the branch above is cloned, open the project in your editor or terminal and run the following command to start up the application:

```bash
npm install
npm run dev
```

Open your browser and navigate to the URL displayed in the terminal to access Kozi. At the bottom right of the screen, you should see Clerk is running in keyless mode. Click the button to claim your keys and associate this instance to your Clerk account. If you don’t have an account, you’ll be prompted to create one.

![Claim your Clerk keys](./claim-keys.png)

You are now ready to start building out the core functionality of Kozi!

## Setting up the database

To store structured data, you’ll be using a serverless instance of Postgress provided by Neon. Start by heading to [neon.tech](http://neon.tech) and creating an account if you don’t have one. Create a new database and copy the connection string as shown below.

![Copy the connection string](./neon-cs.png)

Create a new file in your local project named `.env.local` and paste the following snippet, replacing the placeholder for your specific Neon database connection string.

```
DATABASE_URL=<your_neon_connection_string>
```

### Configuring Prisma

Prisma is used as the ORM to access and manipulate data in the database, as well as apply schema changes to the database as the data needs are updated. Open the project in your IDE and start by creating the schema file at `prisma/schema.prisma`. Paste in the following code:

```prisma {{ filename: 'prisma/schema.prisma' }}
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Project {
  id          String   @id @default(cuid())
  name        String
  description String?
  owner_id    String
  created_at  DateTime @default(now())
  updated_at  DateTime @updatedAt
  is_archived Boolean @default(false)
}

model Task {
  id          String   @id @default(cuid())
  title       String
  description String?
  owner_id    String
  is_completed Boolean @default(false)
  created_at  DateTime @default(now())
  updated_at  DateTime @updatedAt
  project_id  String?
}
```

> \[!NOTE]
> We’re using the `owner_id` column instead of `user_id` since this application will be updated to support teams and organizations in a future entry.

Next, create the `src/lib/db.ts` file and paste in the following code which will be used throughout the application to create a connection to the database:

```ts {{ filename: 'src/lib/db.ts' }}
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```

To sync the schema changes to Neon, run the following command in the terminal:

```bash
npx prisma db push
```

If you open the database in the Neon console and navigate to the Tables menu item, you should see the `projects` and `tasks` tables shown.

![Neon tables](./neon-tables.png)

Finally, since it is not best practice to use the Prisma client in any client-side components, you’ll want a file to store interfaces so that TypeScript can recognize the structure of your objects when passing them between components.

Create the `src/app/app/models.ts` file and paste in the following:

```ts {{ filename: 'src/app/app/models.ts' }}
export interface Task {
  id: string
  title: string
  description?: string | null
  is_completed: boolean
  created_at: Date
  updated_at: Date
  project_id?: string | null
  owner_id: string
}

export interface Project {
  name: string
  id: string
  description: string | null
  owner_id: string
  created_at: Date
  updated_at: Date
  is_archived: boolean
}
```

## Configure `/app` as a protected route with Clerk

Clerk’s middleware uses a helper function called `createRouteMatcher` that lets you define a list of routes to protect. This includes any pages, server actions, or API handlers stored in the matching folders of the project.

All of the core functionality of the application will be stored in the `/app` route, so update `src/middleware.ts` to use the `createRouteMatcher` to protect everything in that folder:

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

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

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) await 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)(.*)',
  ],
}
```

The `/app` route will use a different layout from the landing page, which will contain a collapsible sidebar that contains the `<UserButton />` (a Clerk UI component that lets users manage their profile and sign out), an inbox for tasks, and a list of projects that tasks can be created in.

Start by creating the `src/app/app/components/Sidebar.tsx` file to render the elements of the sidebar:

```tsx {{ filename: 'src/app/app/components/Sidebar.tsx' }}
'use client'

import { cn } from '@/lib/utils'
import { ChevronRightIcon, ChevronLeftIcon, InboxIcon } from 'lucide-react'
import React from 'react'
import Link from 'next/link'
import { UserButton } from '@clerk/nextjs'

function Sidebar() {
  const [isCollapsed, setIsCollapsed] = React.useState(false)

  return (
    <div
      className={cn(
        'h-screen border-r border-gray-200 bg-gradient-to-b from-blue-50 via-purple-50/80 to-blue-50 p-4 dark:border-gray-800 dark:from-blue-950/20 dark:via-purple-950/20 dark:to-blue-950/20',
        'transition-all duration-300 ease-in-out',
        isCollapsed ? 'w-16' : 'w-64',
      )}
    >
      <nav className="space-y-2">
        <div className="flex items-center justify-between gap-2">
          <div
            className={cn(
              'transition-all duration-300',
              isCollapsed ? 'w-0 overflow-hidden' : 'w-auto',
            )}
          >
            <UserButton showName />
          </div>
          <button
            onClick={() => setIsCollapsed(!isCollapsed)}
            className="flex-shrink-0 rounded-lg p-1 transition-colors hover:bg-white/75 dark:hover:bg-gray-800/50"
          >
            {isCollapsed ? (
              <ChevronRightIcon className="h-4 w-4" />
            ) : (
              <ChevronLeftIcon className="h-4 w-4" />
            )}
          </button>
        </div>

        <div
          className={cn(
            'transition-all duration-300',
            isCollapsed ? 'w-0 overflow-hidden' : 'w-auto',
          )}
        >
          <Link
            href="/app"
            className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-white/75 dark:text-gray-200 dark:hover:bg-gray-800/50"
          >
            <InboxIcon className="h-4 w-4" />
            <span>Inbox</span>
          </Link>
        </div>
      </nav>
    </div>
  )
}

export default Sidebar
```

Now create `src/app/app/layout.tsx` to render the sidebar with the pages in the `/app` route:

```tsx {{ filename: 'src/app/app/layout.tsx' }}
import * as React from 'react'
import Sidebar from './components/Sidebar'

export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  )
}
```

Next, create `src/app/app/page.tsx` which is just a simple page that renders some text to make sure the `/app` route works as expected:

```tsx {{ filename: 'src/app/app/page.tsx' }}
import React from 'react'
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function AppHome() {
  const { userId } = await auth()

  if (!userId) {
    return redirect('/sign-in')
  }

  return <div className="flex h-screen">Inbox</div>
}
```

Open the application in your browser and test out the changes by navigating to the `/app` which should automatically redirect you to the `/sign-in` route where you can create an account and make sure `/app` only works when authenticated.

## Working with tasks

At the core of every project is a list of tasks, so now we’ll configure the ability to create and work with tasks in the default Inbox list. Several components will be used to provide the following application structure. The following image shows how these components will be used:

![Kozi UI diagram](./kozi-diagram.png)

These are all client components so they will need corresponding server actions so they can interact with the database securely. Create the `src/app/app/actions.ts` file and paste in the following code:

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

import { auth } from '@clerk/nextjs/server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createTask(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const title = formData.get('title') as string
  if (!title?.trim()) {
    throw new Error('Title is required')
  }

  await prisma.task.create({
    data: {
      title: title.trim(),
      owner_id: userId,
      project_id: null,
    },
  })

  revalidatePath('/app')
}

export async function toggleTask(taskId: string) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const task = await prisma.task.findUnique({
    where: { id: taskId },
  })

  if (!task || task.owner_id !== userId) {
    throw new Error('Task not found or unauthorized')
  }

  await prisma.task.update({
    where: { id: taskId },
    data: { is_completed: !task.is_completed },
  })

  revalidatePath('/app')
}
```

We’re going to start with the `<CreateTaskInput />` component which renders the field where users can create tasks. Create the `src/app/app/components/CreateTaskInput.tsx` file and paste in the following:

```tsx {{ filename: 'src/app/app/components/CreateTaskInput.tsx' }}
'use client'

import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { createTask } from '@/app/app/actions'
import { PlusIcon } from 'lucide-react'

export default function CreateTaskInput() {
  const [title, setTitle] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    // Don't create a task if the title is empty
    if (!title.trim()) return

    try {
      setIsSubmitting(true)
      const formData = new FormData()
      formData.append('title', title)
      await createTask(formData)
      setTitle('')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <div className="group relative w-full rounded-full bg-white p-2 transition-shadow duration-200 focus-within:shadow-[0_4px_20px_-2px_rgba(96,165,250,0.3),0_4px_20px_-2px_rgba(192,132,252,0.3)] dark:bg-gray-800">
      <div className="absolute inset-0 rounded-full bg-gradient-to-r from-blue-400/25 to-purple-400/25 transition-opacity duration-200 group-focus-within:from-blue-400 group-focus-within:to-purple-400"></div>
      <div className="absolute inset-[1px] rounded-full bg-white transition-all group-focus-within:inset-[2px] dark:bg-gray-800"></div>
      <div className="relative">
        <form onSubmit={handleSubmit} className="flex w-full items-center gap-2">
          <Input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Add a task..."
            className="flex-1 border-0 bg-transparent text-gray-900 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-gray-100"
          />
          <Button
            type="submit"
            size="icon"
            disabled={isSubmitting || !title.trim()}
            className="flex !h-[30px] !min-h-0 !w-[30px] items-center justify-center !rounded-full !p-0 !leading-none"
          >
            <PlusIcon className="h-4 w-4" />
          </Button>
        </form>
      </div>
    </div>
  )
}
```

Next, we’ll move on to `<TaskCard />`, which will display the name of the task and allow users to toggle it using a checkbox, as is standard in task-centric applications. Create the `src/app/app/components/TaskCard.tsx` file and paste in the following:

```tsx {{ filename: 'src/app/app/components/TaskCard.tsx' }}
'use client'

import React from 'react'
import { toggleTask } from '../actions'
import { cn } from '@/lib/utils'
import { Task } from '@prisma/client'

interface Props {
  task: Task
}

export default function TaskCard({ task }: Props) {
  return (
    <div
      className={cn(
        'cursor-pointer rounded-lg border border-transparent p-2 transition-colors duration-200 hover:border-gray-100 dark:border-gray-800 dark:hover:bg-gray-800/50',
        task.is_completed && 'opacity-50',
      )}
    >
      <div className="flex items-start justify-between">
        <div className="flex items-start gap-3">
          {/* Checkbox */}
          <button
            onClick={(e) => {
              e.stopPropagation()
              toggleTask(task.id)
            }}
            className="mt-1 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"
          >
            {task.is_completed && (
              <svg
                className="h-3 w-3 text-gray-500 dark:text-gray-400"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M5 13l4 4L19 7"
                />
              </svg>
            )}
          </button>
          {/* Task details */}
          <div>
            <h3
              className={cn(
                'font-medium',
                task.is_completed && 'text-gray-400 line-through dark:text-gray-500',
              )}
            >
              {task.title}
            </h3>

            {task.description && (
              <p
                className={cn(
                  'mt-1 text-sm text-gray-500 dark:text-gray-400',
                  task.is_completed && 'line-through opacity-75',
                )}
              >
                {task.description}
              </p>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}
```

Finally, create the `<TaskList />` component to render the list of tasks and the input to create new ones. Create the `src/app/app/components/TaskList.tsx` file and paste in the following:

```tsx {{ filename: 'src/app/app/components/TaskList.tsx' }}
'use client'

import React from 'react'
import TaskCard from './TaskCard'
import CreateTaskInput from './CreateTaskInput'
import { Task } from '@prisma/client'

interface Props {
  title: string
  tasks: Task[]
}

export default function TaskList({ title, tasks }: Props) {
  return (
    <div className="flex h-screen w-full max-w-2xl flex-col gap-4 p-8">
      <h1 className="text-lg font-semibold md:text-xl">{title}</h1>

      <div className="w-full flex-1 rounded-xl">
        <div className="space-y-2">
          {tasks.length === 0 ? (
            <p className="text-gray-500 dark:text-gray-400">No tasks</p>
          ) : (
            tasks.map((task) => <TaskCard key={task.id} task={task} />)
          )}
        </div>
      </div>
      <div className="w-full">
        <CreateTaskInput />
      </div>
    </div>
  )
}
```

With all of our components created, update the `src/app/app/page.tsx` to match the following code which uses the components created above, as well as queries the database for all tasks on load:

```tsx {{ filename: 'src/app/app/page.tsx', ins: [4, 5, [14, 29]], del: [30], prettier: false }}
import React from 'react'
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/db'
import TaskList from './components/TaskList'

export default async function AppHome() {
  const { userId } = await auth()

  if (!userId) {
    return redirect('/sign-in')
  }

  // Get the user's inbox tasks
  const tasks = await prisma.task.findMany({
    where: {
      owner_id: userId,
      project_id: null,
    },
    orderBy: {
      created_at: 'desc',
    },
  })

  return (
    <div className="flex h-screen">
      <TaskList title="Inbox" tasks={tasks} />
    </div>
  )
  return <div className="flex h-screen">Inbox</div>
}
```

If you access the application again, you can now create tasks in your inbox and complete them.

### Editing and deleting tasks

Now that you can create tasks, the next step is to set up a modal so clicking the task (outside of the checkbox) will display the modal and allow you to change the name of the task and set a description if needed.

As a design decision, this modal does not include a save button but rather debounces any edits for 1 second to create an experience where users can quickly save values and avoid another click. The modal will also create a menu in the header which allows you to delete the task.

Start by appending the following code to `src/app/app/actions.ts`:

```ts {{ filename: 'src/app/app/actions.ts' }}
export async function updateTask(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const id = formData.get('id') as string
  const title = formData.get('title') as string
  const description = formData.get('description') as string

  if (!id || !title?.trim()) {
    throw new Error('Invalid input')
  }

  const task = await prisma.task.findUnique({
    where: { id },
  })

  if (!task || task.owner_id !== userId) {
    throw new Error('Task not found or unauthorized')
  }

  await prisma.task.update({
    where: { id },
    data: {
      title: title.trim(),
      description: description?.trim() || null,
    },
  })

  revalidatePath('/app')
}

export async function deleteTask(taskId: string) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  // Delete the task
  await prisma.task.delete({
    where: {
      id: taskId,
      owner_id: userId, // Ensure the task belongs to the user
    },
  })

  revalidatePath('/app')
}
```

Next, create the `src/app/app/components/EditTaskModal.tsx` and paste in the following:

```tsx {{ filename: 'src/app/app/components/EditTaskModal.tsx' }}
'use client'

import { useEffect, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { useRouter } from 'next/navigation'
import { updateTask, toggleTask, deleteTask } from '../actions'
import { Folder, MoreVertical, Trash2, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Task } from '../models'

interface Props {
  task: Task
  open: boolean
  onOpenChange: (open: boolean) => void
  projectName?: string
}

export default function EditTaskModal({
  task: initialTask,
  open,
  onOpenChange,
  projectName,
}: Props) {
  const [task, setTask] = useState(initialTask)
  const [title, setTitle] = useState(task.title)
  const [description, setDescription] = useState(task.description || '')
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
  const router = useRouter()

  // Reset form when modal opens
  useEffect(() => {
    if (open) {
      setTask(initialTask)
      setTitle(initialTask.title)
      setDescription(initialTask.description || '')
    }
  }, [open, initialTask])

  const saveChanges = useDebouncedCallback(async () => {
    if (!title.trim()) return

    try {
      setIsSubmitting(true)
      const formData = new FormData()
      formData.append('id', task.id)
      formData.append('title', title.trim())
      formData.append('description', description.trim())
      await updateTask(formData)
      router.refresh()
    } finally {
      setIsSubmitting(false)
    }
  }, 1000)

  async function onToggleCompleted() {
    const newIsCompleted = !task.is_completed
    setTask((prev) => ({ ...prev, is_completed: newIsCompleted }))
    try {
      await toggleTask(task.id)
    } catch (error) {
      // Revert on error
      setTask((prev) => ({ ...prev, is_completed: !newIsCompleted }))
    }
  }

  function titleRef(el: HTMLTextAreaElement | null) {
    if (el) {
      el.style.height = '2.5rem' // Set initial height
      const scrollHeight = el.scrollHeight
      const minHeight = 40 // 2.5rem in pixels
      el.style.height = `${Math.max(scrollHeight, minHeight)}px`
    }
  }

  function onTitleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
    setTitle(e.target.value)
    saveChanges()

    // Auto-adjust height after value changes
    const el = e.target
    el.style.height = '2.5rem' // Reset to minimum height
    const scrollHeight = el.scrollHeight
    const minHeight = 40 // 2.5rem in pixels
    el.style.height = `${Math.max(scrollHeight, minHeight)}px`
  }

  function onDescriptionChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
    setDescription(e.target.value)
    saveChanges()
  }

  async function handleDelete() {
    try {
      await deleteTask(task.id)
      onOpenChange(false)
      router.refresh()
    } catch (error) {
      console.error('Failed to delete task:', error)
    }
  }

  return (
    <>
      {/*  The edit task modal */}
      <Dialog open={open} onOpenChange={onOpenChange}>
        <DialogContent className="flex h-[80vh] flex-col gap-0 p-0 [&>button]:hidden">
          <DialogHeader className="border-b border-gray-200 p-3">
            <div className="flex items-center justify-between">
              <DialogTitle className="flex items-center gap-2 text-sm">
                <Folder size={14} /> {projectName ?? 'Inbox'}
              </DialogTitle>
              <div className="flex items-center gap-1">
                <DropdownMenu>
                  <DropdownMenuTrigger asChild>
                    <Button variant="ghost" size="icon" className="h-8 w-8">
                      <MoreVertical className="h-4 w-4" />
                    </Button>
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="end">
                    <DropdownMenuItem
                      onClick={() => setShowDeleteConfirm(true)}
                      className="text-red-600 dark:text-red-400"
                    >
                      <Trash2 className="mr-2 h-4 w-4" />
                      Delete Task
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-8 w-8"
                  onClick={() => onOpenChange(false)}
                >
                  <X className="h-4 w-4" />
                </Button>
              </div>
            </div>
          </DialogHeader>
          <div className="flex flex-1 flex-col">
            <div className="flex items-start border-b border-gray-200 p-3">
              <div className="pt-[0.7rem]">
                <input
                  type="checkbox"
                  checked={task.is_completed}
                  onChange={onToggleCompleted}
                  className="text-primary h-4 w-4 rounded border-gray-300 hover:cursor-pointer"
                />
              </div>

              <Textarea
                ref={titleRef}
                value={title}
                onChange={onTitleChange}
                placeholder="Task title"
                disabled={isSubmitting}
                className="min-h-0 flex-1 resize-none overflow-hidden border-none bg-transparent leading-normal font-semibold shadow-none ring-0 transition-colors outline-none hover:bg-gray-50 focus:border focus:border-gray-200 focus:shadow-none focus:ring-0 md:text-base dark:hover:bg-gray-800/50 dark:focus:border-gray-800"
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault()
                  }
                }}
              />
            </div>

            <div className="flex-1">
              <Textarea
                value={description}
                onChange={onDescriptionChange}
                placeholder="Add a description..."
                disabled={isSubmitting}
                className="h-full resize-y rounded-none border-0 p-3 shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:outline-none"
                onKeyDown={(e) => {
                  if (e.key === 'Enter') {
                    // Allow line breaks
                    e.stopPropagation()
                  }
                }}
              />
            </div>
          </div>
          <div className="flex justify-between border-t border-gray-200 p-2 text-[10px] text-gray-400 dark:text-gray-500">
            <div>
              Created {new Date(task.created_at).toLocaleDateString()} at{' '}
              {new Date(task.created_at).toLocaleTimeString()}
            </div>
            <div>
              Updated {new Date(task.updated_at).toLocaleDateString()} at{' '}
              {new Date(task.updated_at).toLocaleTimeString()}
            </div>
          </div>
        </DialogContent>
      </Dialog>

      {/*  The alert dialog for deleting a task */}
      <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Are you sure?</AlertDialogTitle>
            <AlertDialogDescription>
              This action cannot be undone. This will permanently delete the task.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}
```

Finally, update `src/app/app/TaskCard.tsx` to include the `EditTaskModal` component and handle user click events:

```tsx {{ filename: 'src/app/app/TaskCard.tsx', ins: [7, [14, 22], 25, 27, [85, 90]], del: [] }}
'use client'

import React from 'react'
import { toggleTask } from '../actions'
import { cn } from '@/lib/utils'
import { Task } from '@prisma/client'
import EditTaskModal from './EditTaskModal'

interface Props {
  task: Task
}

export default function TaskCard({ task }: Props) {
  const [isModalOpen, setIsModalOpen] = React.useState(false)

  const handleClick = (e: React.MouseEvent) => {
    const target = e.target as HTMLElement
    // Don't open modal if clicking the checkbox
    if (!target.closest('button')) {
      setIsModalOpen(true)
    }
  }

  return (
    <>
      <div
        onClick={handleClick}
        className={cn(
          'cursor-pointer rounded-lg border border-transparent p-2 transition-colors duration-200 hover:border-gray-100 dark:border-gray-800 dark:hover:bg-gray-800/50',
          task.is_completed && 'opacity-50',
        )}
      >
        <div className="flex items-start justify-between">
          <div className="flex items-start gap-3">
            {/* Checkbox */}
            <button
              onClick={(e) => {
                e.stopPropagation()
                toggleTask(task.id)
              }}
              className="mt-1 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"
            >
              {task.is_completed && (
                <svg
                  className="h-3 w-3 text-gray-500 dark:text-gray-400"
                  fill="none"
                  viewBox="0 0 24 24"
                  stroke="currentColor"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M5 13l4 4L19 7"
                  />
                </svg>
              )}
            </button>
            {/* Task details */}
            <div>
              <h3
                className={cn(
                  'font-medium',
                  task.is_completed && 'text-gray-400 line-through dark:text-gray-500',
                )}
              >
                {task.title}
              </h3>

              {task.description && (
                <p
                  className={cn(
                    'mt-1 text-sm text-gray-500 dark:text-gray-400',
                    task.is_completed && 'line-through opacity-75',
                  )}
                >
                  {task.description}
                </p>
              )}
            </div>
          </div>
        </div>
      </div>

      <EditTaskModal task={task} open={isModalOpen} onOpenChange={setIsModalOpen} />
    </>
  )
}
```

Now you can click anywhere outside of the checkbox of a task to open the modal to edit the task name and description or delete the task from the database.

## Working with projects

Users of Kozi can create projects to organize their tasks into categorized lists. Projects will be listed in the sidebar in their own section from the Inbox. When selected, the user will navigate to the `/app/projects/[_id]` route to see the tasks for that project. To start implementing this, update `src/app/app/actions.ts` to match the following:

```ts {{ filename: 'src/app/app/actions.ts', ins: [18, 24, [103, 139]], del: [] }}
'use server'

import { auth } from '@clerk/nextjs/server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createTask(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const title = formData.get('title') as string
  if (!title?.trim()) {
    throw new Error('Title is required')
  }

  const project_id = formData.get('project_id') as string | null

  await prisma.task.create({
    data: {
      title: title.trim(),
      owner_id: userId,
      project_id: project_id || null,
    },
  })

  revalidatePath('/app')
}

export async function toggleTask(taskId: string) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const task = await prisma.task.findUnique({
    where: { id: taskId },
  })

  if (!task || task.owner_id !== userId) {
    throw new Error('Task not found or unauthorized')
  }

  await prisma.task.update({
    where: { id: taskId },
    data: { is_completed: !task.is_completed },
  })

  revalidatePath('/app')
}

export async function updateTask(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const id = formData.get('id') as string
  const title = formData.get('title') as string
  const description = formData.get('description') as string

  if (!id || !title?.trim()) {
    throw new Error('Invalid input')
  }

  const task = await prisma.task.findUnique({
    where: { id },
  })

  if (!task || task.owner_id !== userId) {
    throw new Error('Task not found or unauthorized')
  }

  await prisma.task.update({
    where: { id },
    data: {
      title: title.trim(),
      description: description?.trim() || null,
    },
  })

  revalidatePath('/app')
}

export async function deleteTask(taskId: string) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  // Delete the task
  await prisma.task.delete({
    where: {
      id: taskId,
      owner_id: userId, // Ensure the task belongs to the user
    },
  })

  revalidatePath('/app')
}

export async function getProjects() {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  return prisma.project.findMany({
    where: {
      owner_id: userId,
    },
    orderBy: {
      created_at: 'asc',
    },
  })
}

export async function createProject(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const name = formData.get('name') as string
  if (!name?.trim()) {
    throw new Error('Project name is required')
  }

  const project = await prisma.project.create({
    data: {
      name: name.trim(),
      owner_id: userId,
    },
  })

  revalidatePath('/app')
  return project
}
```

Next, you’ll need to create the page to render the tasks for a given project. Create `src/app/app/projects/[_id]/page.tsx` and paste in the following:

```tsx {{ filename: 'src/app/app/projects/[_id]/page.tsx' }}
import React from 'react'
import { auth } from '@clerk/nextjs/server'
import { prisma } from '@/lib/db'
import { notFound, redirect } from 'next/navigation'
import TaskList from '../../components/TaskList'

interface ProjectPageProps {
  params: Promise<{
    _id: string
  }>
}

export default async function Project({ params }: ProjectPageProps) {
  const { userId } = await auth()

  // If the user is not logged in, redirect to the sign-in page
  if (!userId) {
    return redirect('/sign-in')
  }

  const { _id } = await params
  const project = await prisma.project.findUnique({
    where: {
      id: _id,
    },
  })

  // Check if the project exists and belongs to the user
  if (!project || project.owner_id !== userId) {
    notFound()
  }

  // Get the project tasks
  const tasks = await prisma.task.findMany({
    where: {
      project_id: _id,
      owner_id: userId,
    },
    orderBy: {
      created_at: 'desc',
    },
  })

  return (
    <div className="flex h-screen">
      <TaskList title={project.name} tasks={tasks} projectId={project.id} />
    </div>
  )
}
```

Notice in the `TaskList` component that we’ve added `projectId` to the list of props. This is so that the currently active project ID can be passed to `CreateTaskInput` so that when a task is created, it knows what project to associate it with. Let’s update those two components now.

Modify `app/src/src/components/CreateTaskInput.tsx` to match the following:

```tsx {{ filename: 'app/src/src/components/CreateTaskInput.tsx', ins: [[9, 11], 14, [28, 30]], del: [13], prettier: false }}
'use client'

import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { createTask } from '@/app/app/actions'
import { PlusIcon } from 'lucide-react'

interface Props {
  projectId?: string
}

export default function CreateTaskInput() {
export default function CreateTaskInput({ projectId }: Props) {
  const [title, setTitle] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    // Don't create a task if the title is empty
    if (!title.trim()) return

    try {
      setIsSubmitting(true)
      const formData = new FormData()
      formData.append('title', title)
      if (projectId) {
        formData.append('project_id', projectId)
      }
      await createTask(formData)
      setTitle('')
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <div className="group relative w-full rounded-full bg-white p-2 transition-shadow duration-200 focus-within:shadow-[0_4px_20px_-2px_rgba(96,165,250,0.3),0_4px_20px_-2px_rgba(192,132,252,0.3)] dark:bg-gray-800">
      <div className="absolute inset-0 rounded-full bg-gradient-to-r from-blue-400/25 to-purple-400/25 transition-opacity duration-200 group-focus-within:from-blue-400 group-focus-within:to-purple-400"></div>
      <div className="absolute inset-[1px] rounded-full bg-white transition-all group-focus-within:inset-[2px] dark:bg-gray-800"></div>
      <div className="relative">
        <form onSubmit={handleSubmit} className="flex w-full items-center gap-2">
          <Input
            type="text"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Add a task..."
            className="flex-1 border-0 bg-transparent text-gray-900 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-gray-100"
          />
          <Button
            type="submit"
            size="icon"
            disabled={isSubmitting || !title.trim()}
            className="flex !h-[30px] !min-h-0 !w-[30px] items-center justify-center !rounded-full !p-0 !leading-none"
          >
            <PlusIcon className="h-4 w-4" />
          </Button>
        </form>
      </div>
    </div>
  )
}
```

Next, update the `TaskCard` component to pass the name of the selected project through to the `EditTaskModal` to provide a quick reference to what project the task is part of.

Edit `src/app/app/components/TaskCard.tsx` to match the following:

```tsx {{ filename: 'src/app/app/components/TaskCard.tsx', ins: [11, 15, 75], del: [14], prettier: false }}
'use client';

import React from 'react';
import { toggleTask } from '../actions';
import EditTaskModal from './EditTaskModal';
import { cn } from '@/lib/utils';
import { Task } from '@prisma/client';

interface Props {
  task: Task;
  projectName: string;
}

export default function TaskCard({ task }: Props) {
export default function TaskCard({ task, projectName }: Props) {
  const [isModalOpen, setIsModalOpen] = React.useState(false);

  const handleClick = (e: React.MouseEvent) => {
    const target = e.target as HTMLElement;
    // Don't open modal if clicking the checkbox
    if (!target.closest('button')) {
      setIsModalOpen(true);
    }
  };

  return (
    <>
      <div
        onClick={handleClick}
        className={cn(
          "p-2 rounded-lg border border-transparent hover:border-gray-100 dark:border-gray-800 dark:hover:bg-gray-800/50 cursor-pointer transition-colors duration-200",
          task.is_completed && "opacity-50"
        )}
      >
        <div className="flex items-start justify-between">
          <div className="flex items-start gap-3">
            {/* Checkbox */}
            <button
              onClick={(e) => {
                e.stopPropagation();
                toggleTask(task.id);
              }}
              className="mt-1 h-4 w-4 flex-shrink-0 rounded border border-gray-300 dark:border-gray-600 flex items-center justify-center hover:border-gray-400 dark:hover:border-gray-500"
            >
              {task.is_completed && (
                <svg className="h-3 w-3 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                </svg>
              )}
            </button>
            {/* Task details */}
            <div>
              <h3 className={cn(
                "font-medium",
                task.is_completed && "line-through text-gray-400 dark:text-gray-500"
              )}>{task.title}</h3>

              {task.description && (
                <p className={cn(
                  "text-sm text-gray-500 dark:text-gray-400 mt-1",
                  task.is_completed && "line-through opacity-75"
                )}>
                  {task.description}
                </p>
              )}
            </div>
          </div>
        </div>
      </div>

      <EditTaskModal
        task={task}
        open={isModalOpen}
        onOpenChange={setIsModalOpen}
        projectName={projectName}
      />
    </>
  );
}
```

Now update `src/app/app/components/TaskList.tsx` to include the `projectId` prop and pass it to `CreateTaskInput`:

```tsx {{ filename: 'src/app/app/components/TaskList.tsx', ins: [10, 14, 26, 33], del: [13, 25, 32], prettier: false }}
'use client';

import TaskCard from './TaskCard';
import CreateTaskInput from './CreateTaskInput';
import { Task } from '@prisma/client';

interface Props {
  title: string;
  tasks: Task[];
  projectId?: string;
}

export default function TaskList({ title, tasks }: Props) {
export default function TaskList({ title, tasks, projectId }: Props) {
  return (
    <div className="h-screen flex flex-col w-full max-w-2xl p-8 gap-4">
      <h1 className="text-lg md:text-xl font-semibold">{title}</h1>

      <div className="w-full flex-1 rounded-xl">
        <div className="space-y-2">
          {tasks.length === 0 ? (
            <p className="text-gray-500 dark:text-gray-400">No tasks</p>
          ) : (
            tasks.map((task) => (
              <TaskCard key={task.id} task={task} />
              <TaskCard key={task.id} task={task} projectName={title} />
            ))
          )}
        </div>
      </div>
      <div className='w-full'>
        <CreateTaskInput />
        <CreateTaskInput projectId={projectId} />
      </div>
    </div>
  );
}
```

In order to access project data in real time from multiple client-side components, we’re going to use a Zustand store to keep things synchronized throughout the application. Using a store will allow projects to be edited and deleted without having to refresh the page. This will become more evident in the subsequent sections.

Create `src/lib/store.ts` and paste in the following:

```ts {{ filename: 'src/lib/store.ts' }}
import { Project } from '@/app/app/models'
import { create } from 'zustand'

interface ProjectStore {
  projects: Project[]
  setProjects: (projects: Project[]) => void
  updateProject: (id: string, updates: Partial<Project>) => void
}

export const useProjectStore = create<ProjectStore>((set) => ({
  projects: [],
  setProjects: (projects) => set({ projects }),
  updateProject: (id, updates) =>
    set((state) => ({
      projects: state.projects.map((project) =>
        project.id === id ? { ...project, ...updates } : project,
      ),
    })),
}))
```

The projects will be listed in the sidebar, alongside a button to create new projects as needed. Each element in the list will be its own component. Create `src/app/app/components/ProjectLink.tsx` and paste in the following:

```tsx {{ filename: 'src/app/app/components/ProjectLink.tsx' }}
'use client'

import React from 'react'
import Link from 'next/link'
import { FolderIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Project } from '../models'

interface Props {
  project: Project
  isCollapsed?: boolean
}

export default function ProjectLink({ project, isCollapsed }: Props) {
  return (
    <div className="group relative">
      <div className="flex items-center rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors group-hover:bg-white/75 dark:text-gray-200 dark:group-hover:bg-gray-800/50">
        <Link href={`/app/projects/${project.id}`} className="flex flex-1 items-center gap-2">
          <FolderIcon className="h-4 w-4 flex-shrink-0" />
          <span className={cn('transition-opacity duration-200', isCollapsed && 'opacity-0')}>
            {project.name}
          </span>
        </Link>
      </div>
    </div>
  )
}
```

Let’s create a component that will live in the sidebar that opens a modal to create a new project. Create the `src/app/app/components/CreateProjectButton.tsx` file and paste in the following:

```tsx {{ filename: 'src/app/app/components/CreateProjectButton.tsx' }}
'use client'

import { useState, useRef } from 'react'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { createProject } from '@/app/app/actions'
import { useFormStatus } from 'react-dom'
import { PlusIcon } from 'lucide-react'
import { useProjectStore } from '@/lib/store'

function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <Button type="submit" disabled={pending}>
      Create Project
    </Button>
  )
}

export default function CreateProjectButton() {
  const [isOpen, setIsOpen] = useState(false)
  const formRef = useRef<HTMLFormElement>(null)
  const { projects, setProjects } = useProjectStore()

  async function onSubmit(formData: FormData) {
    try {
      const project = await createProject(formData)
      setProjects([...projects, project])
      setIsOpen(false)
    } catch (error) {
      console.error('Failed to create project:', error)
    }
  }

  return (
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
      <DialogTrigger asChild>
        <Button variant="ghost" size="icon" className="h-5 w-5 text-sm">
          <PlusIcon />
        </Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Create a new project</DialogTitle>
        </DialogHeader>
        <form ref={formRef} action={onSubmit} className="space-y-4">
          <Input type="text" name="name" placeholder="Project name" required />
          <div className="flex justify-end">
            <SubmitButton />
          </div>
        </form>
      </DialogContent>
    </Dialog>
  )
}
```

Finally, you’ll update the sidebar to query the list of projects and populate the store when the component renders. Update `src/app/app/components/Sidebar.tsx` to match the following:

```tsx {{ filename: 'src/app/app/components/Sidebar.tsx', ins: [[7, 11], [15, 22], [69, 82]], del: [] }}
'use client'

import { cn } from '@/lib/utils'
import { ChevronRightIcon, ChevronLeftIcon, InboxIcon } from 'lucide-react'
import Link from 'next/link'
import { UserButton } from '@clerk/nextjs'
import { useEffect, useState } from 'react'
import CreateProjectButton from './CreateProjectButton'
import ProjectLink from './ProjectLink'
import { useProjectStore } from '@/lib/store'
import { getProjects } from '../actions'

function Sidebar() {
  const [isCollapsed, setIsCollapsed] = useState(false)
  const { projects, setProjects } = useProjectStore()

  useEffect(() => {
    // Only fetch if we don't have projects yet
    if (projects.length === 0) {
      getProjects().then(setProjects)
    }
  }, [projects.length, setProjects])

  return (
    <div
      className={cn(
        'h-screen border-r border-gray-200 bg-gradient-to-b from-blue-50 via-purple-50/80 to-blue-50 p-4 dark:border-gray-800 dark:from-blue-950/20 dark:via-purple-950/20 dark:to-blue-950/20',
        'transition-all duration-300 ease-in-out',
        isCollapsed ? 'w-16' : 'w-64',
      )}
    >
      <nav className="space-y-2">
        <div className="flex items-center justify-between gap-2">
          <div
            className={cn(
              'transition-all duration-300',
              isCollapsed ? 'w-0 overflow-hidden' : 'w-auto',
            )}
          >
            <UserButton showName />
          </div>
          <button
            onClick={() => setIsCollapsed(!isCollapsed)}
            className="flex-shrink-0 rounded-lg p-1 transition-colors hover:bg-white/75 dark:hover:bg-gray-800/50"
          >
            {isCollapsed ? (
              <ChevronRightIcon className="h-4 w-4" />
            ) : (
              <ChevronLeftIcon className="h-4 w-4" />
            )}
          </button>
        </div>

        <div
          className={cn(
            'transition-all duration-300',
            isCollapsed ? 'w-0 overflow-hidden' : 'w-auto',
          )}
        >
          <Link
            href="/app"
            className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-white/75 dark:text-gray-200 dark:hover:bg-gray-800/50"
          >
            <InboxIcon className="h-4 w-4" />
            <span>Inbox</span>
          </Link>
        </div>

        <div
          className={cn(
            'pt-4 transition-all duration-300',
            isCollapsed ? 'w-0 overflow-hidden' : 'w-auto',
          )}
        >
          <div className="flex items-center justify-between px-3 pb-2 text-xs font-semibold text-gray-500 dark:text-gray-400">
            <span>Projects</span>
            <CreateProjectButton />
          </div>
          {projects.map((project) => (
            <ProjectLink key={project.id} project={project} isCollapsed={isCollapsed} />
          ))}
        </div>
      </nav>
    </div>
  )
}

export default Sidebar
```

You can now add projects from the sidebar and add tasks to those projects.

### Editing and deleting projects

Following the same design approach as earlier, we’ll now update the project page so that users can simply click the name of a project to edit it. We’ll also debounce the save so there is no need to manually click a save button. Because a Zustand store is being used, updating the name of the project in the store will automatically cause the new name to be displayed in the sidebar without having to refresh the page.

Start by appending the following server actions to `src/app/app/actions.ts`:

```ts {{ filename: 'src/app/app/actions.ts' }}
export async function updateProject(formData: FormData) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const id = formData.get('id') as string
  const name = formData.get('name') as string

  if (!id || !name?.trim()) {
    throw new Error('Invalid input')
  }

  const project = await prisma.project.findUnique({
    where: {
      id,
      owner_id: userId,
    },
  })

  if (!project) {
    throw new Error('Project not found')
  }

  await prisma.project.update({
    where: { id },
    data: {
      name: name.trim(),
    },
  })

  revalidatePath('/app')
}

export async function deleteProject(projectId: string) {
  const { userId } = await auth()
  if (!userId) {
    throw new Error('Unauthorized')
  }

  const project = await prisma.project.findUnique({
    where: {
      id: projectId,
      owner_id: userId,
    },
  })

  if (!project) {
    throw new Error('Project not found')
  }

  // Delete all tasks associated with the project first
  await prisma.task.deleteMany({
    where: {
      project_id: projectId,
    },
  })

  // Then delete the project
  await prisma.project.delete({
    where: {
      id: projectId,
    },
  })
}
```

Since the project name is rendered in the `<TaskList />` component, update `src/app/app/components/TaskList.tsx` to match the following:

```tsx {{ filename: 'src/app/app/components/TaskList.tsx', ins: [[6, 12], [21, 48], [52, 87]], del: [88] }}
'use client'

import TaskCard from './TaskCard'
import CreateTaskInput from './CreateTaskInput'
import { Task } from '@prisma/client'
import { useDebouncedCallback } from 'use-debounce'
import { Input } from '@/components/ui/input'
import { updateProject } from '../actions'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/utils'
import { useProjectStore } from '@/lib/store'
import { useEffect, useState } from 'react'

interface Props {
  title: string
  tasks: Task[]
  projectId?: string
}

export default function TaskList({ title, tasks, projectId }: Props) {
  const [editedTitle, setEditedTitle] = useState(title)
  const [isSubmitting, setIsSubmitting] = useState(false)
  const router = useRouter()
  const { updateProject: updateProjectInStore } = useProjectStore()

  useEffect(() => {
    setEditedTitle(title)
  }, [title])

  const debouncedUpdate = useDebouncedCallback(async (newTitle: string) => {
    if (!projectId || !newTitle.trim() || newTitle === title) return

    try {
      setIsSubmitting(true)
      const formData = new FormData()
      formData.append('id', projectId)
      formData.append('name', newTitle.trim())
      await updateProject(formData)
      // Update the store
      updateProjectInStore(projectId, { name: newTitle.trim() })
      router.refresh()
    } catch (error) {
      // If there's an error, reset to the original title
      setEditedTitle(title)
    } finally {
      setIsSubmitting(false)
    }
  }, 1000)

  return (
    <div className="flex h-screen w-full max-w-2xl flex-col gap-4 p-8">
      {projectId ? (
        <div className="group relative">
          <Input
            value={editedTitle}
            onChange={(e) => {
              setEditedTitle(e.target.value)
              debouncedUpdate(e.target.value)
            }}
            className={cn(
              'h-auto w-full p-1 text-lg font-semibold md:text-xl',
              'border-0 bg-transparent ring-0 focus-visible:ring-0 focus-visible:ring-offset-0',
              'placeholder:text-gray-500 dark:placeholder:text-gray-400',
              'hover:bg-gray-50 focus:bg-gray-50 dark:hover:bg-gray-800/50 dark:focus:bg-gray-800/50',
              '-ml-1 rounded px-1 shadow-none transition-colors',
            )}
            disabled={isSubmitting}
          />
          <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="14"
              height="14"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
            >
              <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
            </svg>
          </div>
        </div>
      ) : (
        <h1 className="text-lg font-semibold md:text-xl">{title}</h1>
      )}
      <h1 className="text-lg font-semibold md:text-xl">{title}</h1>

      <div className="w-full flex-1 rounded-xl">
        <div className="space-y-2">
          {tasks.length === 0 ? (
            <p className="text-gray-500 dark:text-gray-400">No tasks</p>
          ) : (
            tasks.map((task) => <TaskCard key={task.id} task={task} projectName={title} />)
          )}
        </div>
      </div>
      <div className="w-full">
        <CreateTaskInput projectId={projectId} />
      </div>
    </div>
  )
}
```

To delete projects, we’ll use the same approach as we did with tasks by rendering a dropdown menu with an option to delete the project. Instead of in a modal though, we’ll add it to the `<ProjectLink />` component so that when the user hovers over a project in the sidebar, the menu icon will be displayed as a clickable button.

Update `src/app/app/components/ProjectLink.tsx` to match the following code:

```tsx {{ filename: 'src/app/app/components/ProjectLink.tsx', ins: [[8, 26], [34, 47], [59, 98]], del: [] }}
'use client'

import React from 'react'
import Link from 'next/link'
import { FolderIcon, MoreVertical, Trash2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Project } from '../models'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { deleteProject } from '../actions'
import { useProjectStore } from '@/lib/store'
import { useRouter } from 'next/navigation'

interface Props {
  project: Project
  isCollapsed?: boolean
}

export default function ProjectLink({ project, isCollapsed }: Props) {
  const [showDeleteDialog, setShowDeleteDialog] = React.useState(false)
  const [showMenu, setShowMenu] = React.useState(false)
  const { projects, setProjects } = useProjectStore()
  const router = useRouter()

  const handleDelete = async () => {
    try {
      await deleteProject(project.id)
      setProjects(projects.filter((p) => p.id !== project.id))
      router.push('/app')
    } catch (error) {
      console.error('Failed to delete project:', error)
    }
  }

  return (
    <div className="group relative">
      <div className="flex items-center rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors group-hover:bg-white/75 dark:text-gray-200 dark:group-hover:bg-gray-800/50">
        <Link href={`/app/projects/${project.id}`} className="flex flex-1 items-center gap-2">
          <FolderIcon className="h-4 w-4 flex-shrink-0" />
          <span className={cn('transition-opacity duration-200', isCollapsed && 'opacity-0')}>
            {project.name}
          </span>
        </Link>

        {!isCollapsed && (
          <DropdownMenu open={showMenu} onOpenChange={setShowMenu}>
            <DropdownMenuTrigger
              className="ml-2 rounded p-1 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
              onClick={(e) => e.preventDefault()}
            >
              <MoreVertical className="h-4 w-4 text-gray-500" />
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem
                className="text-red-600 dark:text-red-400"
                onClick={() => setShowDeleteDialog(true)}
              >
                <Trash2 className="mr-2 h-4 w-4" />
                Delete Project
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        )}

        <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Delete Project</AlertDialogTitle>
              <AlertDialogDescription>
                Are you sure you want to delete "{project.name}"? This action cannot be undone and
                will delete all tasks associated with this project.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                onClick={handleDelete}
                className="bg-red-600 hover:bg-red-700 dark:bg-red-900 dark:hover:bg-red-800"
              >
                Delete
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </div>
    </div>
  )
}
```

You can now update the names of projects and delete them as needed. Deleting a project will also delete any associated tasks with that project.

## Conclusion

When building any application, security should always be something considered early on in the process. By considering the principles laid out in this article, you can build a secure system with ease using Clerk and properly structuring the code that accesses your database.

In the next article of the series, we’ll explore how you can securely access the data within your Neon database from the front end using Row Level Security using Clerk.

\* Source: [**How Many Cyber Attacks Per Day: The Latest Stats and Impacts in 2025**](https://www.getastra.com/blog/security-audit/how-many-cyber-attacks-per-day/)

---

# Validate your SaaS idea while building an audience
URL: https://clerk.com/blog/validate-saas.md
Date: 2025-02-14
Category: Guides
Description: Learn how to validate a new SaaS idea with a Clerk waitlist and Loops newsletter.

**The fastest way to validate market demand for a SaaS? A waitlist.**

When used correctly, a waitlist isn't just a sign-up form - it's a powerful market research tool. By allowing potential users to signal their interest by registering their email, you create a valuable mechanism to validate your concept before investing significant time and resources.

Integrating your list with a communication tool such as a newsletter platform opens a direct channel with future customers to share product updates, solicit feedback, and build up anticipation for your launch.

In this article, you’ll learn how to use the Clerk drop-in waitlist component and integrate it with Loops.

## What is Clerk?

Clerk is a user management platform designed to get authentication integrated into your application as quickly and easily as possible. This includes drop-in UI components for common user management requirements such as sign-in/sign-up pages, social sign on providers, and user profiles.

One such is the Waitlist component that renders a form where potential customers can enter in their email address:

![The Clerk Waitlist component](./clerk-waitlist.png)

Upon entering their email address, it will be available in the Clerk dashboard under the **Waitlist** tab where you can quickly invite interested users to join your platform with a single click.

![The waitlist in the Clerk dashboard](./clerk-dash.png)

Clerk offers webhooks as a way to integrate with external systems. When email addresses are captured, Clerk offers a webhook event that can be used to inform another system that someone has entered their email into the waitlist.

The other system simply needs an HTTP endpoint designed to parse the webhook event data.

Loops offers an integration with Clerk by providing a webhook receiver endpoint that will automatically parse the event data and add the email address to your audience. It can also receive events when a user updates their info so that your list always contains the latest information about your users.

This integration combines the simplicity of Clerk's drop-in waitlist component with the flexibility of Loops's audience management, allowing you to validate market demand for your SaaS.

## How to follow along

While the process described above is the same regardless of the chosen framework, the remainder of this article will use [Next.js](/nextjs-authentication). You may use your own Next.js application with Clerk, bootstrap one using our [quickstart guide](/docs/quickstarts/nextjs), or clone the [`article-1` branch from the repository for Kozi](https://github.com/bmorrisondev/kozi/tree/article-1), an open-source project & knowledge management tool.

You’ll also need a domain name and a production Clerk instance configured with the appropriate DNS records. If you do not already and would still like to follow along, refer to [our guide on deploying your application to production](/docs/deployments/overview) before reading on.

### Why do I need a production Clerk instance?

When you first set up a Clerk application, we automatically create a development instance for you to quickly start integrating Clerk with your application. These development instances have a more relaxed security posture to make local development easier, such as not requiring HTTPS connections and using shared OAuth credentials for single sign-on providers across the Clerk platform.

Production instances are more secure and do not have the same security exceptions, but require several DNS records so that Clerk can handle authentication on your domain. This approach avoids cross-domain authentication which could increase the risk of a cross-site scripting attack.

## Adding the waitlist component

When added to a page, the `<Waitlist />` component renders [a form](/blog/building-a-nextjs-login-page-template) that matches the style of our other drop-in components. The following code snippet shows what would be required to add it to the `/waitlist` path of a Next.js application, along with a few Tailwind classes to center the form on the page:

```tsx {{ filename: 'src/app/waitlist/[[...waitlist]]/page.tsx' }}
import { Waitlist } from '@clerk/nextjs'

function WaitlistPage() {
  return (
    <div className="flex h-screen items-center justify-center">
      <Waitlist />
    </div>
  )
}

export default WaitlistPage
```

## Integrating with Loops

Over in Loops, navigate to your team settings, expand the **Integrations** node, and select **Clerk**. You can then enable the integration which will then show an **Endpoint URL** which Clerk can communicate with.

Copy this URL as you’ll need it in the next step.

![The Clerk integration in the Loops dashboard](./loops-clerk-integration.png)

Head back to the Clerk dashboard for your application and navigate to **Configure**, then select **Webhooks** from the left sidebar. Click **Add Endpoint**.

![The Clerk dashboard showing the Webhooks tab](./clerk-dash-webhooks.png)

Paste in the URL from Loops into the **Endpoint URL** field. In the **Subscribe to events** section, type in “waitlist” and check both options. You can also check `user.created` and `user.updated` as they are supported in Loops, but aren’t necessary for this guide. Once done, scroll to the bottom and click **Create**.

![The Clerk dashboard showing the Webhook URL and events](./clerk-dash-webhooks-config.png)

After creating the endpoint, locate the **Signing Secret** in the lower right of the field. Click the eye icon to show the secret and copy the value.

![The Clerk dashboard showing the Signing Secret](./clerk-dash-webhooks-config-secret.png)

Back in Loops, paste the signing secret into the **Signing Secret** field, toggle on the events you selected in Clerk, and click **Save** at the bottom of the form.

![The Loops dashboard showing where to place the signing secret](./loops-clerk-integration-secret.png)

And thats it! Going forward, anyone who enters their email address into your waitlist form will automatically be added to your mailing list in Loops.

## Conclusion

Gauging interest in your application idea is important for determining whether an idea is worth investing your time into. By integrating the Clerk `<Waitlist />` component with Loops, you can create a powerful feedback loop with your potential customers as the development of your application progresses.

While this article used Next.js to demonstrate how the waitlist component renders on a page, this component is available to many of our SDKs, including [React](/docs/quickstarts/react), [Vue.js](/docs/quickstarts/vue), [Astro](/docs/quickstarts/astro), and others.

---

# Postmortem: February 6, 2025 service outage
URL: https://clerk.com/blog/postmortem-feb-6-2025-service-outage.md
Date: 2025-02-11
Category: Company
Description: Learn more about our service outage, including the timeline of events and our remediations.

On Thursday, February 6th, 2025, a database query was directly executed to deprecate a feature for 3,700 customers, and an error in the query resulted in immediate downtime for those customers. In addition, the downtime triggered automatic retries elsewhere in our service which nearly overloaded our infrastructure, and created significant delays for our other customers for 4 minutes, until the retry backoff took effect.

The incident lasted a total of 26 minutes, from the initial error to when the query was successfully reversed, and our systems returned to normal.

As a provider of mission-critical infrastructure, we recognize that this outage is unacceptable. After a detailed review of the incident, we have determined several actions that can be taken to mitigate its recurrence. Some have already been implemented, while others will require more significant engineering efforts.

In this postmortem, we discuss the timeline of events, and our complete set of remediations.

## Timeline of events

- **9:43 UTC** — Erroneous update query runs, setting `false` values to `"true"` within a jsonb field.
- **9:45 UTC** — Engineers receive error alerts and begin investigating.
- **9:47 UTC** — First customer outage reports arrive.
- **9:48 UTC** — Internal incident is declared.
- **9:50 UTC** — Status page updated (initially with an incorrect start time).
- **10:00-10:04 UTC** — Engineers begin manually restoring service for customers while a bulk resolution is prepared.
- **10:05 UTC** — Bulk update query is executed to correct the issue.
- **10:06 UTC** — Bulk update query completes, service health is restored.
- **10:10 UTC** — Status page updated to reflect restored status with accurate start/end times.

## Remediations

### Tuning automatic retry mechanisms

One of our retry mechanisms was misconfigured to retry too aggressively on 500-class errors, which increased the blast radius of this event. An adjustment to the mechanism has already been applied, and an audit of other retry mechanisms is being conducted.

### Further limiting direct database access

Direct database access at Clerk is already significantly limited, with only a small subset of our most senior team having this permission. However, our processes indicated they should use their own judgement for when it is safe and appropriate to leverage the capability.

Going forward, these team members will retain access, but our policies will dictate that it is only leveraged in true emergency situations, when downtime is actively impacting our customers. Other changes must be executed from within our change management tooling.

### Mandating staged rollouts for all changes to critical infrastructure

In 2024, Clerk’s platform team developed several new mechanisms for staged rollouts. As Clerk has grown, we have seen a healthy culture where our engineers *demand* that staged rollout infrastructure is in place. In many cases, we’ve delayed launches to build more mechanisms where they are missing.

In our review since the incident, we confirmed that the vast majority of changes to our critical systems leverage staged rollouts. However, when our team noted exceptions, it was always because the change was considered simple, including the one that led to this incident.

In addition, our review revealed that different projects have approached building cohorts for staged rollouts differently.

Going forward, we will be mandating that all changes to critical infrastructure require staged rollouts. We will also codify a process for building and ordering cohorts, which will incorporate the number of active users an application is supporting, and the subscription plan that applications are enrolled on.

### Improving SDK resilience for session management service outages

Clerk’s session management service is designed with a once-per-minute JWT refresh. We leverage this design in three critical areas of our service:

- **Session revocation:** When a session is revoked administratively – either by the user or by an application administrator – the revocation is achieved by blocking new JWTs from being generated. Using a short-lived JWT means we can guarantee revocation within one minute.
- **Abuse detection and prevention:** CAPTCHAs during sign up have become less effective recently as AI has gained the ability to solve them. At the same time, freemium and trial pricing have become commonplace. We’re engaged in a constant cat-and-mouse game with these attackers, and have found that our once-per-minute session refresh mechanism is a much more effective place to detect and prevent abuse than sign up.
- **XSS mitigation:** JavaScript-accessible JWTs are an expectation of many application architectures, despite seeming antithetical to web security best practices on its face. The concern is that script-accessible JWTs can be exfiltrated during XSS attacks, which would allow continued use of the JWT even after the XSS is patched. Clerk can safely allow script access because our JWTs expire every minute, which ensures that successful exfiltration would not meaningfully extend an XSS attack.

In normal operation, our once-per-minute refresh is an implementation detail that most of our customers are not aware of. However, in the event of an outage like Thursday’s, it means our customers have a strong uptime dependency on Clerk.

Going forward, we would like to eliminate as much of this strong uptime dependency as possible. We believe we can update our SDKs so that if our session management service goes down, existing sessions are maintained throughout the outage, while new session creation, session revocation, abuse prevention, and XSS mitigation are not operational. This would result in future outages having less impact on our customers.

In the interest of full disclosure, we want to highlight that this is not a simple adjustment and will take time to develop. As a simple example of a challenge, we will need to ensure the `/.well-known/jwks.json` endpoint is hardened to avoid the downtime, and/or we need provide a mechanism to self-host the JWT public key. Regardless of the effort it takes, we are placing high priority on this project.

### Completely decoupling session management from user management

At a high level, Clerk operates two services: user management, which covers sign up, sign in, and user profiles, and session management, which only handles sessions. These two systems started tightly coupled, but have naturally decoupled with time as they represent significantly different workloads:

- User management requires relatively low read and write, but it has many moving pieces. There are many different settings, and our customers use thousands of different permutations of those settings. In addition, we’re frequently introducing new settings and modifying existing settings as authentication evolves.
- Session management is the opposite. It’s extremely high read, low write, and has relatively few moving pieces.

In this incident, an error in our user management service brought down our session management service.

Going forward, we plan to decouple session management from user management as much as possible. They will still be tightly *integrated*, since sign up and sign in lead to the creation of a session, but downtime in user management should not lead downtime in session management.

### Eliminating the use of JSON column types for structured and typed data

Some application settings are stored in JSON column types. These columns have been used primarily for convenience, with types being enforced at our compute layer. In this incident, strict typing was not enforced for the query because it was executed directly against the database, which led to the outage.

Going forward, in addition to further limiting direct database access, we are ceasing additional use of JSON column types for structured and typed data. Instead, we will use strongly typed database columns, which would have prevented the erroneous query from being executed. Over time, we will also migrate and deprecate our existing usage of JSON column types.

## Looking Ahead

We regret the impact this incident had on our customers. At Clerk, reliability is a top priority, and this postmortem reflects our commitment to transparency and continuous improvement.

Some fixes are already in place, while others—like enhanced SDK resilience and service decoupling—are being prioritized to prevent future incidents and strengthen our platform.

For any questions, please [contact support](/contact)

---

# Implement Role-Based Access Control in Next.js 15
URL: https://clerk.com/blog/nextjs-role-based-access-control.md
Date: 2025-02-07
Category: Company
Description: Learn Role-Based Access Control (RBAC) by building a complete Q&A platform.

Assigning permissions to individual users is a complex task, especially when you have a large number of users.

[Role-Based Access Control (RBAC)](/glossary#role-based-access-control-rbac) is a popular approach to managing access permissions in software applications, allowing you to assign different roles and permissions to different users.

This article will guide you through building a Q\&A platform using Next.js and Neon, and show you how to implement authentication and RBAC with [Clerk](/nextjs-authentication).

## What is Role-Based Access Control?

Before we get started, let's understand RBAC and how it will be implemented in the Q\&A platform.

RBAC is a security method that allows users to interact with features of an application or system based on their roles and the permissions granted to those roles.

This approach simplifies access management by grouping permissions into roles instead of assigning them to individual users, making it easier to maintain and scale as your application grows.

By implementing RBAC, organizations can enforce the [principle of least privilege](https://www.cyberark.com/what-is/least-privilege/), ensuring users only have access to the resources necessary for their specific responsibilities.

Below is a breakdown of the permissions for each role in the Q\&A platform:

| Role        | Description                                                               | Permissions                                                                                                                            |
| ----------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Viewer      | Users not signed in or logged into the Q\&A platform.                     | • View questions and answers                                                                                                           |
| Contributor | Registered users who can ask or answer questions in the Q\&A platform.    | Everything from the Viewer role + • Post questions• Answer questions• Edit own questions and answers• Delete own questions and answers |
| Moderator   | Users with additional permissions to manage content in the Q\&A platform. | Everything from the Contributor role + • Admin dashboard access• Approve and disapprove questions• Approve and disapprove answers      |
| Admin       | Users with full control over the Q\&A platform.                           | Everything from the Moderator role + • Edit others' questions and answers• Delete others' questions and answers• Manage user roles     |

With that in mind, let us jump straight into building the Q\&A platform and implement RBAC using Clerk.

> \[!IMPORTANT]
> Clerk provides two approaches to RBAC.
>
> [Organizations](/docs/organizations/overview) is ideal for B2B applications like GitHub or Notion, where your customers are teams or companies who need to invite team members and manage their roles. It includes built-in role management and components for role-based rendering.
>
> [User metadata](/docs/references/nextjs/basic-rbac), demonstrated in this guide, is better suited for B2C applications with simple permission structures where individual users have different access levels but don't belong to teams.

## What you'll build

Before writing any code, let's take a look at how our platform works.

The landing page enables users to sign up or sign in using the **Start Exploring** button. It features a clean design with a navigation menu and welcome message.

Once signed in, users can ask questions and provide answers. Each question displays the author's name, timestamp, and interaction options. Users can edit or delete their own content, and view answers from other contributors.

![Homepage of a Q\&A Platform with a centered welcome message, navigation menu at the top showing Home, Q\&A, and Admin options, and a user profile icon. The main content features a large 'Welcome to Our Q\&A Platform' heading, followed by a subtitle encouraging community participation and a 'Start Exploring' button. The page has a clean, minimal design with a white background and footer copyright text.](./home-page.png)

![Q\&A Platform page showing a question input field at the top with an 'Ask' button. Below are two example questions: 'What is React used for?' and 'How many days are in a week?' Each question displays the author's name (Brian Morrison), timestamp, and has edit and delete icons. Questions include their answers with similar metadata and interaction options. The page maintains the same header with navigation menu and user profile icon as the homepage.](./qa-page.png)

Administrators can review and moderate all submitted content through the Admin Dashboard. The page displays questions and answers with approval status indicators, allowing admins to approve or reject content using simple checkmark and X icons.

![Admin Dashboard showing moderation controls for Q\&A content. The page displays questions and answers with approval status indicators - green 'Approved' tags for accepted content and red 'Disapproved' tags for rejected content. Each entry has approve/reject buttons (green checkmark and red X icons). The dashboard includes a 'Set Roles' button at the top right. Questions shown include 'What is React used for?' and 'How many days are in a week?' with their respective answers and moderation statuses. The page maintains the consistent header with navigation and user profile.](./admin-dashboard.png)

Finally, administrators can manage user permissions through the role management page. Using a search interface, admins can find users and assign appropriate roles (Admin, Moderator, Contributor, or Viewer) or remove existing roles.

![User role management page with a search bar and Submit button at the top. Below shows a user profile for Brian Morrison with their current role listed as admin. A row of role management buttons includes 'Make Admin', 'Make Moderator', 'Make Contributor', 'Make Viewer', and a red 'Remove Role' button. The page maintains the standard Q\&A Platform header with navigation menu and user profile icon.](./user-roles-page.png)

## Building the frontend

In this section, we will build the frontend of the Q\&A platform. In order to follow along, you should have a basic understanding of React or Next.js, as well as Node.js installed.

> If you're new to Next.js authentication, check out our [Ultimate Guide to Next.js Authentication](/blog/nextjs-authentication) for foundational concepts.

### Install dependencies

Start by running the following command in your terminal to bootstrap a Next.js application, accepting the default options as they are presented:

```bash
npx create-next-app@latest qa-app
cd qa-app
```

Next, run the following commands to initialize shadcn/ui, once again accepting the default configuration options as they are presented:

```bash
npx shadcn@latest init
```

Add the necessary components to build the UI components of the Q\&A platform:

```bash
npx shadcn@latest add button input card separator badge
```

Finally, install Lucide React, which will be used to render the icons in the UI components:

```bash
npm install lucide-react
```

With the dependencies in place, let's create the components that used with the layout, starting with the type definitions we'll use throughout the process.

### Creating the header and updating the homepage

The header component will allow our users to navigate to different parts of the application, as well as hold the Clerk `UserButton` component (to be done later in this guide).

Create the `src/components/Header.tsx` file and paste in the following code:

```tsx {{ filename: 'src/components/Header.tsx' }}
'use client'

import Link from 'next/link'
import { Button } from '@/components/ui/button'

const Header = () => {
  return (
    <header className="border-b bg-white">
      <div className="container mx-auto px-4">
        <div className="flex h-16 items-center justify-between">
          <Link href="/" className="text-xl font-bold">
            Q&A platform
          </Link>
          <nav>
            <ul className="flex space-x-4">
              <li>
                <Link href="/">
                  <Button variant="ghost">Home</Button>
                </Link>
              </li>
              <li>
                <Link href="/qa">
                  <Button variant="ghost">Q&A</Button>
                </Link>
              </li>

              <li>
                <Link href="/admin">
                  <Button variant="ghost">Admin</Button>
                </Link>
              </li>
            </ul>
          </nav>
        </div>
      </div>
    </header>
  )
}

export default Header
```

With the header component in place, replace the code in `app/page.tsx` with the following, which will update the homepage to use the header component and match the demo:

```tsx {{ filename: 'app/page.tsx' }}
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import Header from '@/components/Header'

export default function Home() {
  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="flex-grow">
        <section className="flex w-full items-center justify-center py-12 md:py-24 lg:py-32 xl:py-48">
          <div className="container px-4 md:px-6">
            <div className="flex flex-col items-center space-y-4 text-center">
              <div className="space-y-2">
                <h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl/none">
                  Welcome to Our Q&A platform
                </h1>
                <p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
                  Join our community to ask questions, share knowledge, and learn from others.
                </p>
              </div>
              <div className="space-x-4">
                <Link href="/qa">
                  <Button size="lg">Start Exploring</Button>
                </Link>
              </div>
            </div>
          </div>
        </section>
      </main>
      <footer className="flex w-full items-center justify-center bg-gray-100 py-6 dark:bg-gray-800">
        <div className="container px-4 md:px-6">
          <p className="text-center text-sm text-gray-500 dark:text-gray-400">
            (c) 2024 Q&A platform. All rights reserved.
          </p>
        </div>
      </footer>
    </div>
  )
}
```

### Create the Q\&A section

Next, you'll build out the Q\&A section, where signed-in users can ask and answer questions and anonymous users can view questions. We'll start by creating a few shared components that will be used throughout the application before creating the page that will display the Q\&A section.

Let's start by creating a file named `types/types.d.ts` to define the types we'll be using. Populate it with the following code:

```ts {{ filename: 'types/types.d.ts' }}
interface Answer {
  id: number | null
  ans: string
  approved?: boolean | null
  contributor: string
  contributorId: string
  questionId: number
  timestamp?: string // ISO 8601 string format
}

interface Question {
  id: number | null
  quiz: string
  approved: boolean | null
  answers: Answer[]
  contributor: string
  contributorId: string
  timestamp?: string // ISO 8601 string format
}

type Roles = 'admin' | 'moderator' | 'contributor' | 'viewer'
```

Now you'll create the component that renders the form where users can ask questions. Create the `src/components/QuestionForm.tsx` file and paste in the following:

```tsx {{ filename: 'src/components/QuestionForm.tsx' }}
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

interface QuestionFormProps {
  onSubmit: (question: string) => void
}

export default function QuestionForm({ onSubmit }: QuestionFormProps) {
  const [quiz, setQuiz] = useState('')
  const [showSubmitText, setShowSubmitText] = useState(false)

  useEffect(() => {
    if (showSubmitText) {
      setTimeout(() => {
        setShowSubmitText(false)
      }, 7000)
    }
  }, [showSubmitText])

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (quiz.trim()) {
      onSubmit(quiz)
      setQuiz('')
      setShowSubmitText(true)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="mb-6">
      <div className="flex gap-2">
        <div className="flex-grow">
          <Input
            type="text"
            name="quiz"
            id="quiz"
            value={quiz}
            onChange={(e) => setQuiz(e.target.value)}
            placeholder="Ask a question..."
            className="flex-grow"
          />
          <div className="h-4 text-sm text-green-500 transition-all">
            {showSubmitText ? 'Your question has been submitted for review.' : ''}
          </div>
        </div>
        <Button type="submit">Ask</Button>
      </div>
    </form>
  )
}
```

Next, modify the `src/lib/utils.ts` file to add a helper function used to format dates in a more friendly way, which will be used in the `QuestionItem` and `AnswerItem` components you'll create in a moment:

```ts {{ filename: 'src/lib/utils.ts', ins: [[8, 17]], del: [] }}
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function formatDate(dateString: string) {
  const date = new Date(dateString)
  return date.toLocaleString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  })
}
```

Now create a `QuestionItem` component that manages and displays a question and its answers. Moreover, the component allows adding, editing, and deleting of questions or answers.

Create the `src/components/QuestionItem.tsx` file and paste the following code into the file:

```tsx {{ filename: 'src/components/QuestionItem.tsx' }}
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'
import { Pencil, Trash2 } from 'lucide-react'
import { Separator } from '@/components/ui/separator'
import AnswerItem from './AnswerItem'
import { formatDate } from '@/lib/utils'

interface Props {
  question: Question
  onEditQuestion: (id: number, newText: string) => void
  onDeleteQuestion: (id: number) => void
  onAddAnswer: (questionId: number, answerText: string) => void
  onEditAnswer: (answerId: number, newText: string) => void
  onDeleteAnswer: (answerId: number) => void
}

export default function QuestionItem({
  question,
  onEditQuestion,
  onDeleteQuestion,
  onAddAnswer,
  onEditAnswer,
  onDeleteAnswer,
}: Props) {
  const [answer, setAnswer] = useState('')
  const [isEditing, setIsEditing] = useState(false)
  const [editedQuestion, setEditedQuestion] = useState(question.quiz)
  const [showSubmitText, setShowSubmitText] = useState(false)

  useEffect(() => {
    if (showSubmitText) {
      setTimeout(() => {
        setShowSubmitText(false)
      }, 7000)
    }
  }, [showSubmitText])

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    if (answer.trim()) {
      if (question.id !== null) {
        onAddAnswer(question.id, answer)
        setShowSubmitText(true)
      }
      setAnswer('')
    }
  }

  const handleQuestionEdit = () => {
    if (editedQuestion.trim() && editedQuestion !== question.quiz) {
      if (question.id !== null) {
        onEditQuestion(question.id, editedQuestion)
      }
      setIsEditing(false)
    }
  }

  const handleAnswerEdit = async (answerId: number | null, newText: string) => {
    if (answerId !== null && question.id !== null) {
      await onEditAnswer(answerId, newText)
    }
  }

  const handleAnswerDelete = async (answerId: number | null) => {
    if (answerId !== null && question.id !== null) {
      await onDeleteAnswer(answerId)
    }
  }

  return (
    <Card>
      <CardHeader>
        {isEditing ? (
          <div className="flex gap-2">
            <Input
              value={editedQuestion}
              onChange={(e) => setEditedQuestion(e.target.value)}
              className="flex-grow"
            />
            <Button onClick={handleQuestionEdit}>Save</Button>
            <Button variant="outline" onClick={() => setIsEditing(false)}>
              Cancel
            </Button>
          </div>
        ) : (
          <div>
            <div className="mb-2 flex items-center justify-between">
              <CardTitle>{question.quiz}</CardTitle>

              <div>
                <Button variant="ghost" size="icon" onClick={() => setIsEditing(true)}>
                  <Pencil className="h-4 w-4" />
                </Button>
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => question.id !== null && onDeleteQuestion(question.id)}
                >
                  <Trash2 className="h-4 w-4" />
                </Button>
              </div>
            </div>
            <div className="text-sm text-gray-500">
              <span>{question.contributor}</span>
              <span> • </span>
              <span>{question.timestamp && formatDate(question.timestamp)}</span>
            </div>
          </div>
        )}
      </CardHeader>
      <CardContent>
        <h3 className="mb-2 font-semibold">Answers:</h3>
        {question.answers && question.answers.filter((a) => a.approved !== false).length > 0 ? (
          <ul className="space-y-4">
            {question.answers
              .filter((a) => a.approved !== false)
              .map((answer, index, filteredAnswers) => (
                <li key={answer.id}>
                  <AnswerItem
                    answer={answer}
                    onEditAnswer={(newText) => handleAnswerEdit(answer.id, newText)}
                    onDeleteAnswer={() => handleAnswerDelete(answer.id)}
                  />
                  {index < filteredAnswers.length - 1 && <Separator className="my-2" />}
                </li>
              ))}
          </ul>
        ) : (
          <p className="text-gray-500">No answers yet.</p>
        )}
      </CardContent>

      <CardFooter>
        <form onSubmit={handleSubmit} className="w-full">
          <div className="flex gap-2">
            <div className="flex-grow">
              <Input
                type="text"
                value={answer}
                onChange={(e) => setAnswer(e.target.value)}
                placeholder="Add an answer..."
              />

              <div className="h-4 text-sm text-green-500 transition-all">
                {showSubmitText ? 'Your answer has been submitted for review.' : ''}
              </div>
            </div>
            <Button type="submit">Answer</Button>
          </div>
        </form>
      </CardFooter>
    </Card>
  )
}
```

Your editor may be displaying an error regarding the `AnswerItem` component, which does not yet exist. This component is used to render answers for each question.

Create the `src/components/AnswerItem.tsx` file and paste the following code into the file:

```tsx {{ title: 'src/components/AnswerItem.tsx' }}
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Pencil, Trash2 } from 'lucide-react'
import { formatDate } from '@/lib/utils'

interface Props {
  answer: Answer
  onEditAnswer: (newText: string) => void
  onDeleteAnswer: () => void
}

function AnswerItem({ answer, onEditAnswer, onDeleteAnswer }: Props) {
  const [isEditing, setIsEditing] = useState(false)
  const [editedAnswer, setEditedAnswer] = useState(answer.ans)

  const handleEdit = () => {
    if (editedAnswer.trim() && editedAnswer !== answer.ans) {
      onEditAnswer(editedAnswer)
      setIsEditing(false)
    }
  }

  return (
    <div>
      {isEditing ? (
        <div className="flex w-full gap-2">
          <Input
            value={editedAnswer}
            onChange={(e) => setEditedAnswer(e.target.value)}
            className="flex-grow"
          />
          <Button onClick={handleEdit}>Save</Button>
          <Button variant="outline" onClick={() => setIsEditing(false)}>
            Cancel
          </Button>
        </div>
      ) : (
        <div className="space-y-2">
          <div className="flex items-start justify-between">
            <p>{answer.ans}</p>

            <div>
              <Button variant="ghost" size="icon" onClick={() => setIsEditing(true)}>
                <Pencil className="h-4 w-4" />
              </Button>
              <Button variant="ghost" size="icon" onClick={onDeleteAnswer}>
                <Trash2 className="h-4 w-4" />
              </Button>
            </div>
          </div>
          <div className="text-sm text-gray-500">
            <span>{answer.contributor}</span>
            <span> • </span>
            <span>{answer.timestamp && formatDate(answer.timestamp)}</span>
          </div>
        </div>
      )}
    </div>
  )
}

export default AnswerItem
```

Now you'll create the page that manages and displays questions and answers, handles form submissions, and interacts with the user. Note that the code below contains function placeholders that will be implemented later in this article to interact with the database.

Create the `app/qa/page.tsx` file and paste the following code into the file:

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

import { useState, useEffect } from 'react'
import QuestionForm from '../../components/QuestionForm'
import QuestionItem from '@/components/QuestionItem'
import Header from '../../components/Header'

export default function QAPage() {
  const [questions, setQuestions] = useState<Question[]>([])

  useEffect(() => {
    fetchQuestions()
  }, [])

  // These placeholders will be populated later in this guide
  const fetchQuestions = async () => {}
  const addQuestion = async (question: string) => {}
  const editQuestion = async (id: number, newText: string) => {}
  const deleteQuestion = async (id: number) => {}
  const addAnswer = async (questionId: number, answer: string) => {}
  const editAnswer = async (answerId: number, newText: string) => {}
  const deleteAnswer = async (answerId: number) => {}

  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="container mx-auto flex-grow p-4">
        <QuestionForm onSubmit={addQuestion} />
        {Array.isArray(questions) && (
          <div className="space-y-4">
            {questions.map((question) => (
              <QuestionItem
                key={question.id}
                question={question}
                onEditQuestion={editQuestion}
                onDeleteQuestion={deleteQuestion}
                onAddAnswer={addAnswer}
                onEditAnswer={editAnswer}
                onDeleteAnswer={deleteAnswer}
              />
            ))}
          </div>
        )}
      </main>
    </div>
  )
}
```

### Creating the admin area

With the home page and Q\&A page created, it's time to create the admin area. The admin area will be used to manage questions and answers.

Start by creating the `src/components/QuestionCard.tsx` which is used by the admin page for approving and managing questions and answers. Paste the following into that file:

```tsx {{ filename: 'src/components/QuestionCard.tsx' }}
'use client'

import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { CheckCircle, XCircle } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { formatDate } from '@/lib/utils'

interface Props {
  question: Question
  onQuestionApproved: (id: number) => void
  onQuestionDisapproved: (id: number) => void
  onAnswerApproved: (answerId: number) => void
  onAnswerDisapproved: (answerId: number) => void
}

export default function QuestionCard({
  question,
  onQuestionApproved,
  onQuestionDisapproved,
  onAnswerApproved,
  onAnswerDisapproved,
}: Props) {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex flex-col">
          <div className="mb-2 flex items-start justify-between">
            <div className="flex items-center space-x-2">
              <span className="text-xl">{question.quiz}</span>
              <ApprovalBadge approved={question.approved} />
            </div>

            <div>
              <Button
                variant="ghost"
                size="icon"
                onClick={() => question.id !== null && onQuestionApproved(question.id)}
              >
                <CheckCircle className="h-4 w-4 text-green-500" />
              </Button>
              <Button
                variant="ghost"
                size="icon"
                onClick={() => question.id !== null && onQuestionDisapproved(question.id)}
              >
                <XCircle className="h-4 w-4 text-red-500" />
              </Button>
            </div>
          </div>
          <div className="text-sm text-gray-500">
            <span>{question.contributor}</span>
            <span> • </span>
            <span>{question.timestamp && formatDate(question.timestamp)}</span>
          </div>
        </CardTitle>
      </CardHeader>
      <CardContent>
        <h3 className="mb-2 font-semibold">Answers:</h3>
        {question.answers && question.answers.length > 0 ? (
          <ul className="space-y-4">
            {question.answers.map((answer) => (
              <li key={answer.id} className="flex flex-col">
                <div className="mb-2 flex items-start justify-between">
                  <div className="flex items-center space-x-2">
                    <span>{answer.ans}</span>
                    <ApprovalBadge approved={answer.approved ?? null} />
                  </div>
                  <div>
                    <Button
                      variant="ghost"
                      size="icon"
                      onClick={() => {
                        if (question.id !== null && answer.id !== null) {
                          onAnswerApproved(answer.id)
                        }
                      }}
                    >
                      <CheckCircle className="h-4 w-4 text-green-500" />
                    </Button>
                    <Button
                      variant="ghost"
                      size="icon"
                      onClick={() => {
                        if (question.id !== null && answer.id !== null) {
                          onAnswerDisapproved(answer.id)
                        }
                      }}
                    >
                      <XCircle className="h-4 w-4 text-red-500" />
                    </Button>
                  </div>
                </div>
                <div className="text-sm text-gray-500">
                  <span>{answer.contributor}</span>
                  <span> • </span>
                  <span>{answer.timestamp && formatDate(answer.timestamp)}</span>
                </div>
              </li>
            ))}
          </ul>
        ) : (
          <p className="text-gray-500">No answers yet.</p>
        )}
      </CardContent>
    </Card>
  )
}

function ApprovalBadge({ approved }: { approved: boolean | null }) {
  if (approved === true) {
    return (
      <Badge variant="outline" className="border-green-300 bg-green-100 text-green-800">
        Approved
      </Badge>
    )
  } else if (approved === false) {
    return (
      <Badge variant="outline" className="border-red-300 bg-red-100 text-red-800">
        Disapproved
      </Badge>
    )
  } else {
    return (
      <Badge variant="outline" className="border-yellow-300 bg-yellow-100 text-yellow-800">
        Pending
      </Badge>
    )
  }
}
```

Create the `app/admin/page.tsx` file, used to render the area for admins and moderators, and paste the following code into the file:

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

import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import Header from '@/components/Header'
import QuestionCard from '@/components/QuestionCard'

export default function AdminPage() {
  const [questions, setQuestions] = useState<Question[]>([])

  useEffect(() => {
    fetchQuestions()
  }, [])

  // These placeholders will be populated later in this guide
  const fetchQuestions = async () => {}
  const onQuestionApproved = async (id: number) => {}
  const onQuestionDisapproved = async (id: number) => {}
  const onAnswerApproved = async (answerId: number) => {}
  const onAnswerDisapproved = async (answerId: number) => {}

  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="container mx-auto flex-grow p-4">
        <h1 className="mb-6 text-3xl font-bold">Admin Dashboard</h1>
        <div className="mb-4 flex justify-end">
          <Button>
            <Link href="/admin/set-user-roles">Set Roles</Link>
          </Button>
        </div>
        <div className="space-y-4">
          {questions.map((question) => (
            <QuestionCard
              key={question.id}
              question={question}
              onQuestionApproved={onQuestionApproved}
              onQuestionDisapproved={onQuestionDisapproved}
              onAnswerApproved={onAnswerApproved}
              onAnswerDisapproved={onAnswerDisapproved}
            />
          ))}
        </div>
      </main>
    </div>
  )
}
```

### Testing the application

With all of our pages created, you can test the application by running the following command in the terminal, which will start the dev server and allow you to view the application in your browser:

```bash
npm run dev
```

By default it runs on `localhost:3000`, but may use a different port if `3000` is already in use. Use the provided URL to access the application.

## Adding Clerk for authentication and authorization

Now let's add authentication to the application using Clerk. In your browser, go to [the Clerk dashboard](https://dashboard.clerk.com/) to create an account if you don't already have one, which will automatically walk you through setting up your first application with Clerk. If you already have an account, sign in and create a new application, which will also guide you through setting up Clerk.

Follow steps 1-3 shown in the onboarding guide to install and configure Clerk in your Next.js application. Return to this page once you are finished to continue the tutorial.

### Setting up Clerk in the application

At this point, the Clerk SDK should be installed, and the middleware should be defined per the quickstart instructions. Next, you'll need to wrap the application with the `<ClerkProvider>` which will allow Clerk to protect pages that require authentication.

Update the `app/layout.tsx` file to wrap the application with the `<ClerkProvider>`:

```tsx {{ filename: 'app/layout.tsx', ins: [4, 27, 33], del: [] }}
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import { ClerkProvider } from '@clerk/nextjs'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}
```

Add the following environment variables to your `.env.local` file, which tell Clerk where to redirect users when they sign up or sign in:

```env {{ filename: '.env.local' }}
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
```

Next, update the `header` component to include a `UserButton` if the user is signed in, or a `SignInButton` if the user is not signed in:

```tsx {{ filename: 'src/app/components/header.tsx', ins: [3, [34, 43]], del: [] }}
'use client'

import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
import Link from 'next/link'
import { Button } from '@/components/ui/button'

const Header = () => {
  return (
    <header className="border-b bg-white">
      <div className="container mx-auto px-4">
        <div className="flex h-16 items-center justify-between">
          <Link href="/" className="text-xl font-bold">
            Q&A platform
          </Link>
          <nav>
            <ul className="flex space-x-4">
              <li>
                <Link href="/">
                  <Button variant="ghost">Home</Button>
                </Link>
              </li>
              <li>
                <Link href="/qa">
                  <Button variant="ghost">Q&A</Button>
                </Link>
              </li>

              <li>
                <Link href="/admin">
                  <Button variant="ghost">Admin</Button>
                </Link>
              </li>

              <SignedIn>
                <li className="flex items-center">
                  <UserButton />
                </li>
              </SignedIn>
              <SignedOut>
                <li className="flex items-center rounded bg-black px-2 font-bold text-white">
                  <SignInButton mode="modal" />
                </li>
              </SignedOut>
            </ul>
          </nav>
        </div>
      </div>
    </header>
  )
}

export default Header
```

### Test it out

If your application is no longer running, start it up again with `npm run dev` and use the updated header to log into the application. This will let you create a user account and redirect you back to the home page.

Once logged in, notice how the header includes the `UserButton` instead of the `SignInButton`.

## Configuring RBAC with Clerk

Now let's add RBAC to the application using Clerk metadata. The role for a specific user will be set in the Clerk metadata, which is arbitrary data that is stored alongside a user that can be accessed and modified through the Clerk API, as well as directly in the Dashboard.

### Set a role for the user

Log into the Clerk dashboard and navigate to the **Users** page and select your user account. Scroll down to the *User metadata* section and select **Edit** next to the *Public* option.

Add the following JSON and select Save to manually add the admin role to your own user account in order for it to have all the system permissions. Later in the tutorial, you will add a basic admin tool to change a user's role.

```json
{
  "role": "admin"
}
```

### Include the user role with the Clerk metadata

Next, you'll need to update the token created by Clerk to include the metadata when it's created. This will allow you to check the role of the user without having to make an additional API call.

In the Clerk Dashboard, navigate to the **Sessions** page. Under the *Customize session token* section, select **Edit**. In the modal that opens, enter the following JSON and select **Save**.

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

### Declare the role types for the Metadata

Go back to the project and create a global type file to add type definitions for the metadata. Create the `types/global.d.ts` file and paste the following code into the file:

```ts {{ filename: 'types/global.d.ts' }}
export {}

declare global {
  interface CustomJwtSessionClaims {
    metadata: {
      role?: Roles
    }
  }
}
```

### Updating your middleware

The middleware is used to check each request it comes in and apply authentication logic. Let's update the middleware to check the role of the user and redirect them to the appropriate page.

Update `src/middleware.ts` as follows:

```ts {{ filename: 'src/middleware.ts', ins: [4, 5, [8, 17]], del: [7] }}
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

// The route matcher defines routes that should be protected
const isAdminRoute = createRouteMatcher(['/admin(.*)'])

export default clerkMiddleware()
export default clerkMiddleware(async (auth, req) => {
  // Fetch the user's role from the session claims
  const userRole = (await auth()).sessionClaims?.metadata?.role

  // Protect all routes starting with `/admin`
  if (isAdminRoute(req) && !(userRole === 'admin' || userRole === 'moderator')) {
    const url = new URL('/', req.url)
    return NextResponse.redirect(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)(.*)',
  ],
}
```

### Setting user roles from the application

Now we can define a new page in the application that will let admins set user roles. We'll start by creating the server actions that will be used to set the user role by passing in the role name.

Create the `src/app/admin/set-user-roles/actions.ts` file and paste the following code into the file:

```ts {{ filename: 'src/app/admin/set-user-roles/actions.ts' }}
'use server'

import { clerkClient } from '@clerk/nextjs/server'
import { checkRole } from './utils'

export async function setRole(formData: FormData): Promise<void> {
  const client = await clerkClient()

  try {
    const res = await client.users.updateUser(formData.get('id') as string, {
      publicMetadata: { role: formData.get('role') },
    })
    console.log({ message: res.publicMetadata })
  } catch (err) {
    throw new Error(err instanceof Error ? err.message : String(err))
  }
}

export async function removeRole(formData: FormData): Promise<void> {
  const client = await clerkClient()

  try {
    const res = await client.users.updateUser(formData.get('id') as string, {
      publicMetadata: { role: null },
    })
    console.log({ message: res.publicMetadata })
  } catch (err) {
    throw new Error(err instanceof Error ? err.message : String(err))
  }
}
```

Finally, we'll create a page that allows admins to search through users using the Clerk Backend API and the above server actions to set their role.

Create a file called `src/app/admin/set-user-roles/page.tsx` and paste in the following code to populate the page:

```tsx {{ filename: 'src/app/admin/set-user-roles/page.tsx' }}
// import { SearchUsers } from "./SearchUsers";
import { clerkClient } from '@clerk/nextjs/server'
import { removeRole, setRole } from './actions'
import Header from '@/components/Header'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

export default async function AdminDashboard(params: {
  searchParams: Promise<{ search?: string }>
}) {
  const query = (await params.searchParams).search

  const client = await clerkClient()
  const users = query ? (await client.users.getUserList({ query })).data : []

  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="container mx-auto flex-grow p-4">
        <form className="mb-6">
          <div className="flex flex-col gap-2">
            <label htmlFor="search">Search for users</label>
            <div className="flex gap-2">
              <Input id="search" name="search" type="text" className="flex-grow" />
              <Button type="submit">Submit</Button>
            </div>
          </div>
        </form>
        {users.map((user) => (
          <div key={user.id} className="flex min-h-screen flex-col">
            <div className="space-y-4 rounded-md bg-white p-4 shadow-md">
              <div className="text-lg font-semibold text-gray-800">
                {user.firstName} {user.lastName}
              </div>

              <div className="text-sm text-gray-600">
                {
                  user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)
                    ?.emailAddress
                }
              </div>

              <div className="text-sm font-medium text-blue-600">
                Role: {user.publicMetadata.role as string}
              </div>
              <div className="mt-2 flex space-x-4">
                <form action={setRole} className="mt-2">
                  <input type="hidden" value={user.id} name="id" />
                  <input type="hidden" value="admin" name="role" />
                  <Button type="submit">Make Admin</Button>
                </form>

                <form action={setRole} className="mt-2">
                  <input type="hidden" value={user.id} name="id" />
                  <input type="hidden" value="moderator" name="role" />
                  <Button type="submit">Make Moderator</Button>
                </form>

                <form action={setRole} className="mt-2">
                  <input type="hidden" value={user.id} name="id" />
                  <input type="hidden" value="contributor" name="role" />
                  <Button type="submit">Make Contributor</Button>
                </form>

                <form action={setRole} className="mt-2">
                  <input type="hidden" value={user.id} name="id" />
                  <input type="hidden" value="viewer" name="role" />
                  <Button type="submit">Make Viewer</Button>
                </form>

                <form action={removeRole} className="mt-2">
                  <input type="hidden" value={user.id} name="id" />
                  <Button
                    type="submit"
                    className="rounded-md bg-red-600 px-4 py-2 text-white transition hover:bg-red-700"
                  >
                    Remove Role
                  </Button>
                </form>
              </div>
            </div>
          </div>
        ))}
      </main>
    </div>
  )
}
```

Add three more users to the Q\&A platform, then go to Admin page and click the **Set Roles** button. Search for the users you added and set their roles by clicking either the **Make Admin**, **Make Moderator**, **Make Contributor**, or **Make Viewer** button.

## Integrate with Postgres using Neon

In this section, you will learn how to integrate Neon Postgres with Clerk in the Q\&A platform, using `drizzle-orm` and `drizzle-kit` to interact with the database.

### Creating the database

Start by creating a new Neon database. Open your browser and go to neon.tech. Create an account if you don't already have one, then create a new database. Once the database is created, you'll be presented with a Quickstart screen. Select the **Copy snippet** button to copy it to your clipboard:

![The Neon Quickstart screen](./neon-connection-string.png)

Paste it into your `.env.local` file as `DATABASE_URL` like so:

```env {{ filename: '.env.local' }}
DATABASE_URL=postgresql://neondb_owner:***************@ep-black-boat-a8ryq543-pooler.eastus2.azure.neon.tech/neondb?sslmode=require
```

### Install the dependencies

Now you'll need to install the following dependencies:

- `drizzle-orm` - The ORM that the application will use to interact with the database.
- `drizzle-kit` - The tool that will generate migrations and interact with the database.
- `@neondatabase/serverless` - The driver that will be used to connect to the database.

Run the following command to install the dependencies:

```bash
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit 
```

### Setting up the database schema

Next you'll create the schema file which `drizzle-orm` will use to interact with the database, while `drizzle-kit` will be used to apply schema changes to the database.

Create a new file called `src/db/schema.ts` and paste in the following code:

```ts {{ filename: 'src/db/schema.ts' }}
import { pgTable, serial, text, boolean, timestamp, integer } from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm'

// Questions table
export const questions = pgTable('questions', {
  id: serial('id').primaryKey(),
  quiz: text('quiz').notNull(),
  approved: boolean('approved'),
  contributor: text('contributor').notNull(),
  contributorId: text('contributor_id').notNull(),
  timestamp: timestamp('timestamp', { withTimezone: true }).defaultNow(),
})

// Answers table
export const answers = pgTable('answers', {
  id: serial('id').primaryKey(),
  ans: text('ans').notNull(),
  approved: boolean('approved'),
  contributor: text('contributor').notNull(),
  contributorId: text('contributor_id').notNull(),
  questionId: integer('question_id')
    .notNull()
    .references(() => questions.id),
  timestamp: timestamp('timestamp', { withTimezone: true }).defaultNow(),
})

// Define relationships using Drizzle's relations function
export const questionsRelations = relations(questions, ({ many }) => ({
  answers: many(answers),
}))

export const answersRelations = relations(answers, ({ one }) => ({
  question: one(questions, {
    fields: [answers.questionId],
    references: [questions.id],
  }),
}))
```

Create the `src/db/index.ts` file and paste in the following code, which is used by the application to establish a connection to the database:

```ts {{ filename: 'src/db/index.ts' }}
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import { questions, answers, questionsRelations, answersRelations } from './schema'

if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL must be a Neon postgres connection string')
}
const sql = neon(process.env.DATABASE_URL!)

export const db = drizzle(sql, {
  schema: { questions, answers, questionsRelations, answersRelations },
})
```

In the root of the project, create the `drizzle.config.ts` used by `drizzle-kit` to manage the database schema:

```ts {{ filename: 'drizzle.config.ts' }}
import { defineConfig } from 'drizzle-kit'
import { loadEnvConfig } from '@next/env'

loadEnvConfig(process.cwd())

if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL must be a Neon postgres connection string')
}

export default defineConfig({
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
  schema: './src/db/schema.ts',
})
```

Finally, run the following command from your terminal to push the schema to the Neon database:

```bash
npx drizzle-kit push
```

If you go to the tables section in your Neon dashboard, you should see that two tables named `questions` and `answers` were created.

### Defining database interactions

Now that the database schema is set up, you can start defining database interactions. We're going to start with the database calls used by the main Q\&A section of the app.

Create `src/app/qa/actions.ts` and paste in the following:

```ts {{ filename: 'src/app/qa/actions.ts' }}
'use server'
import { db } from '@/db/index'
import { questions, answers } from '@/db/schema'
import { and, desc, eq } from 'drizzle-orm'
import { currentUser } from '@clerk/nextjs/server'

// Fetches all questions, available to authenticated and anonymous users
export async function getAllQuestions(): Promise<Question[]> {
  const data = await db
    .select()
    .from(questions)
    .where(eq(questions.approved, true))
    .orderBy(desc(questions.timestamp))
  const res: Question[] = data.map((question) => ({
    id: question.id,
    quiz: question.quiz,
    approved: question.approved,
    contributor: question.contributor,
    contributorId: question.contributorId,
    timestamp: question.timestamp?.toISOString(),
  }))
  for (const question of res) {
    const answerData = await db
      .select()
      .from(answers)
      .where(and(eq(answers.questionId, question.id as number), eq(answers.approved, true)))
      .orderBy(desc(answers.timestamp))
    question.answers = answerData.map((answer) => ({
      id: answer.id,
      ans: answer.ans,
      approved: answer.approved,
      contributor: answer.contributor,
      contributorId: answer.contributorId,
      questionId: answer.questionId,
      timestamp: answer.timestamp?.toISOString(),
    }))
  }
  return res
}

// Creates a new question, available only to authenticated users
export const createQuestion = async (quiz: string) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  await db.insert(questions).values({
    quiz: quiz,
    contributor: user.fullName as string,
    contributorId: user.id,
  })
}

// Creates a new answer, available only to authenticated users
export const createAnswer = async (answer: string, questionId: number) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  await db.insert(answers).values({
    ans: answer,
    contributor: user.fullName as string,
    contributorId: user.id,
    questionId: questionId,
  })
}

// Deletes a question, available only to the question's contributor
export const deleteQuestion = async (id: number) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  try {
    const result = await db
      .delete(questions)
      .where(and(eq(questions.id, id), eq(questions.contributorId, user.id)))
    return result
  } catch (error) {
    console.error('Error deleting question:', error)
    throw new Error('Failed to delete question')
  }
}

// Deletes an answer, available only to the answer's contributor
export const deleteAnswer = async (id: number) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  try {
    await db.delete(answers).where(and(eq(answers.id, id), eq(answers.contributorId, user.id)))
  } catch (error) {
    console.error('Error deleting answer:', error)
    throw new Error('Failed to delete answer')
  }
}

// Updates a question, available only to the question's contributor
export const updateQuestion = async (id: number, newText: string) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  try {
    await db
      .update(questions)
      .set({ quiz: newText })
      .where(and(eq(questions.contributorId, user.id), eq(questions.id, id)))
  } catch (error) {
    console.error('Error updating question:', error)
    throw new Error('Failed to update question')
  }
}

// Updates an answer, available only to the answer's contributor
export const updateAnswer = async (id: number, newText: string) => {
  const user = await currentUser()
  if (!user) {
    throw new Error('Unauthorized')
  }

  try {
    await db
      .update(answers)
      .set({ ans: newText })
      .where(and(eq(answers.contributorId, user.id), eq(answers.id, id)))
  } catch (error) {
    console.error('Error updating answer:', error)
    throw new Error('Failed to update answer')
  }
}
```

Now we can wire up the placeholder functions in `src/app/qa/actions.ts` with the new database interactions we just created.

Update the `src/app/qa/page.tsx` file like so:

```tsx {{ filename: 'src/app/qa/page.tsx', ins: [7, [25, 58]], del: [[16, 23]] }}
'use client'

import { useState, useEffect } from 'react'
import QuestionForm from '../../components/QuestionForm'
import QuestionItem from '@/components/QuestionItem'
import Header from '../../components/Header'
import * as actions from '../../app/qa/actions'

export default function QAPage() {
  const [questions, setQuestions] = useState<Question[]>([])

  useEffect(() => {
    fetchQuestions()
  }, [])

  // These placeholders will be populated later in this guide
  const fetchQuestions = async () => {}
  const addQuestion = async (question: string) => {}
  const editQuestion = async (id: number, newText: string) => {}
  const deleteQuestion = async (id: number) => {}
  const addAnswer = async (questionId: number, answer: string) => {}
  const editAnswer = async (questionId: number, answerId: number, newText: string) => {}
  const deleteAnswer = async (questionId: number, answerId: number) => {}

  const fetchQuestions = async () => {
    const questions = await actions.getAllQuestions()
    setQuestions(questions)
  }

  const addQuestion = async (quiz: string) => {
    await actions.createQuestion(quiz)
    fetchQuestions()
  }

  const editQuestion = async (id: number, newText: string) => {
    await actions.updateQuestion(id, newText)
    fetchQuestions()
  }

  const deleteQuestion = async (id: number) => {
    await actions.deleteQuestion(id)
    fetchQuestions()
  }

  const addAnswer = async (questionId: number, answer: string) => {
    await actions.createAnswer(answer, questionId)
    fetchQuestions()
  }

  const editAnswer = async (answerId: number, newText: string) => {
    await actions.updateAnswer(answerId, newText)
    fetchQuestions()
  }

  const deleteAnswer = async (answerId: number) => {
    await actions.deleteAnswer(answerId)
    fetchQuestions()
  }

  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="container mx-auto flex-grow p-4">
        <QuestionForm onSubmit={addQuestion} />
        {Array.isArray(questions) && (
          <div className="space-y-4">
            {questions.map((question) => (
              <QuestionItem
                key={question.id}
                question={question}
                onEditQuestion={editQuestion}
                onDeleteQuestion={deleteQuestion}
                onAddAnswer={addAnswer}
                onEditAnswer={editAnswer}
                onDeleteAnswer={deleteAnswer}
              />
            ))}
          </div>
        )}
      </main>
    </div>
  )
}
```

Now the server actions will prevent users from editing questions or answers that do not belong to them, but we can create a better user experience by making sure only the person who posted the question or answer can edit or delete it. The `useUser` hook from Clerk can be used to get the current user's information.

Update the `QuestionItem` component like so:

```tsx {{ filename: 'src/components/QuestionItem.tsx', ins: [9, 28, 94, 107], del: [] }}
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'
import { Pencil, Trash2 } from 'lucide-react'
import { Separator } from '@/components/ui/separator'
import AnswerItem from './AnswerItem'
import { formatDate } from '@/lib/utils'
import { useUser } from '@clerk/nextjs'

interface Props {
  question: Question
  onEditQuestion: (id: number, newText: string) => void
  onDeleteQuestion: (id: number) => void
  onAddAnswer: (questionId: number, answerText: string) => void
  onEditAnswer: (answerId: number, newText: string) => void
  onDeleteAnswer: (answerId: number) => void
}

export default function QuestionItem({
  question,
  onEditQuestion,
  onDeleteQuestion,
  onAddAnswer,
  onEditAnswer,
  onDeleteAnswer,
}: Props) {
  const { user } = useUser()
  const [answer, setAnswer] = useState('')
  const [isEditing, setIsEditing] = useState(false)
  const [editedQuestion, setEditedQuestion] = useState(question.quiz)
  const [showSubmitText, setShowSubmitText] = useState(false)

  useEffect(() => {
    if (showSubmitText) {
      setTimeout(() => {
        setShowSubmitText(false)
      }, 7000)
    }
  }, [showSubmitText])

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    if (answer.trim()) {
      if (question.id !== null) {
        onAddAnswer(question.id, answer)
        setShowSubmitText(true)
      }
      setAnswer('')
    }
  }

  const handleQuestionEdit = () => {
    if (editedQuestion.trim() && editedQuestion !== question.quiz) {
      if (question.id !== null) {
        onEditQuestion(question.id, editedQuestion)
      }
      setIsEditing(false)
    }
  }

  const handleAnswerEdit = async (answerId: number | null, newText: string) => {
    if (answerId !== null && question.id !== null) {
      await onEditAnswer(answerId, newText)
    }
  }

  const handleAnswerDelete = async (answerId: number | null) => {
    if (answerId !== null && question.id !== null) {
      await onDeleteAnswer(answerId)
    }
  }

  return (
    <Card>
      <CardHeader>
        {isEditing ? (
          <div className="flex gap-2">
            <Input
              value={editedQuestion}
              onChange={(e) => setEditedQuestion(e.target.value)}
              className="flex-grow"
            />
            <Button onClick={handleQuestionEdit}>Save</Button>
            <Button variant="outline" onClick={() => setIsEditing(false)}>
              Cancel
            </Button>
          </div>
        ) : (
          <div>
            <div className="mb-2 flex items-center justify-between">
              <CardTitle>{question.quiz}</CardTitle>

              {user?.id === question.contributorId && (
                <div>
                  <Button variant="ghost" size="icon" onClick={() => setIsEditing(true)}>
                    <Pencil className="h-4 w-4" />
                  </Button>
                  <Button
                    variant="ghost"
                    size="icon"
                    onClick={() => question.id !== null && onDeleteQuestion(question.id)}
                  >
                    <Trash2 className="h-4 w-4" />
                  </Button>
                </div>
              )}
            </div>
            <div className="text-sm text-gray-500">
              <span>{question.contributor}</span>
              <span> • </span>
              <span>{question.timestamp && formatDate(question.timestamp)}</span>
            </div>
          </div>
        )}
      </CardHeader>
      <CardContent>
        <h3 className="mb-2 font-semibold">Answers:</h3>
        {question.answers && question.answers.filter((a) => a.approved !== false).length > 0 ? (
          <ul className="space-y-4">
            {question.answers
              .filter((a) => a.approved !== false)
              .map((answer, index, filteredAnswers) => (
                <li key={answer.id}>
                  <AnswerItem
                    answer={answer}
                    onEditAnswer={(newText) => handleAnswerEdit(answer.id, newText)}
                    onDeleteAnswer={() => handleAnswerDelete(answer.id)}
                  />
                  {index < filteredAnswers.length - 1 && <Separator className="my-2" />}
                </li>
              ))}
          </ul>
        ) : (
          <p className="text-gray-500">No answers yet.</p>
        )}
      </CardContent>

      <CardFooter>
        <form onSubmit={handleSubmit} className="w-full">
          <div className="flex gap-2">
            <div className="flex-grow">
              <Input
                type="text"
                value={answer}
                onChange={(e) => setAnswer(e.target.value)}
                placeholder="Add an answer..."
              />

              <div className="h-4 text-sm text-green-500 transition-all">
                {showSubmitText ? 'Your answer has been submitted for review.' : ''}
              </div>
            </div>
            <Button type="submit">Answer</Button>
          </div>
        </form>
      </CardFooter>
    </Card>
  )
}
```

And the `AnswerItem` component:

```tsx {{ filename: 'src/components/AnswerItem.tsx', ins: [6, 15, 44, 53], del: [] }}
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Pencil, Trash2 } from 'lucide-react'
import { formatDate } from '@/lib/utils'
import { useUser } from '@clerk/nextjs'

type Props = {
  answer: Answer
  onEditAnswer: (newText: string) => void
  onDeleteAnswer: () => void
}

function AnswerItem({ answer, onEditAnswer, onDeleteAnswer }: Props) {
  const { user } = useUser()
  const [isEditing, setIsEditing] = useState(false)
  const [editedAnswer, setEditedAnswer] = useState(answer.ans)

  const handleEdit = () => {
    if (editedAnswer.trim() && editedAnswer !== answer.ans) {
      onEditAnswer(editedAnswer)
      setIsEditing(false)
    }
  }

  return (
    <div>
      {isEditing ? (
        <div className="flex w-full gap-2">
          <Input
            value={editedAnswer}
            onChange={(e) => setEditedAnswer(e.target.value)}
            className="flex-grow"
          />
          <Button onClick={handleEdit}>Save</Button>
          <Button variant="outline" onClick={() => setIsEditing(false)}>
            Cancel
          </Button>
        </div>
      ) : (
        <div className="space-y-2">
          <div className="flex items-start justify-between">
            <p>{answer.ans}</p>
            {user?.id === answer.contributorId && (
              <div>
                <Button variant="ghost" size="icon" onClick={() => setIsEditing(true)}>
                  <Pencil className="h-4 w-4" />
                </Button>
                <Button variant="ghost" size="icon" onClick={onDeleteAnswer}>
                  <Trash2 className="h-4 w-4" />
                </Button>
              </div>
            )}
          </div>
          <div className="text-sm text-gray-500">
            <span>{answer.contributor}</span>
            <span> • </span>
            <span>{answer.timestamp && formatDate(answer.timestamp)}</span>
          </div>
        </div>
      )}
    </div>
  )
}

export default AnswerItem
```

Next let's create a set of server actions used by the admin area to manage questions and answers. Notice in the following code we dont need to check the user's role, because we are using the Clerk middleware to protect this route.

Create the `src/app/admin/actions.ts` file and add the following content:

```tsx {{ filename: 'src/app/admin/actions.ts' }}
'use server'
import { db } from '@/db/index'
import { questions, answers } from '@/db/schema'
import { eq, desc } from 'drizzle-orm'

export const getAllQuestionsWithAnswers = async () => {
  const questionsData = await db.select().from(questions).orderBy(desc(questions.timestamp))
  const res: Question[] = questionsData.map((question) => ({
    id: question.id,
    quiz: question.quiz,
    approved: question.approved,
    contributor: question.contributor,
    contributorId: question.contributorId,
    timestamp: question.timestamp?.toISOString(),
  }))
  for (const question of res) {
    const answerData = await db
      .select()
      .from(answers)
      .where(eq(answers.questionId, question.id as number))
      .orderBy(desc(answers.timestamp))
    if (!answerData) continue
    question.answers = answerData.map((answer) => ({
      id: answer.id,
      ans: answer.ans,
      approved: answer.approved,
      contributor: answer.contributor,
      contributorId: answer.contributorId,
      questionId: answer.questionId,
      timestamp: answer.timestamp?.toISOString(),
    }))
  }
  return res
}

export const approveQuestion = async (id: number) => {
  try {
    await db.update(questions).set({ approved: true }).where(eq(questions.id, id))
  } catch (error) {
    console.error('Error approving question:', error)
    throw new Error('Failed to approve question')
  }
}

export const disapproveQuestion = async (id: number) => {
  try {
    await db.update(questions).set({ approved: false }).where(eq(questions.id, id))
  } catch (error) {
    console.error('Error disapproving question:', error)
    throw new Error('Failed to disapprove question')
  }
}

export const approveAnswer = async (id: number) => {
  try {
    await db.update(answers).set({ approved: true }).where(eq(answers.id, id))
  } catch (error) {
    console.error('Error approving answer:', error)
    throw new Error('Failed to approve answer')
  }
}

export const disapproveAnswer = async (id: number) => {
  try {
    await db.update(answers).set({ approved: false }).where(eq(answers.id, id))
  } catch (error) {
    console.error('Error disapproving answer:', error)
    throw new Error('Failed to disapprove answer')
  }
}
```

Now let's do the same thing to the `admin` page as the `qa` page. Update the `src/app/admin/page.tsx` file like so:

```tsx {{ filename: 'app/admin/page.tsx', ins: [[8, 14], [30, 53]], del: [[23, 28]] }}
'use client'

import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import Header from '@/components/Header'
import QuestionCard from '@/components/QuestionCard'
import {
  approveQuestion,
  disapproveQuestion,
  getAllQuestionsWithAnswers,
  approveAnswer,
  disapproveAnswer,
} from './actions'

export default function AdminPage() {
  const [questions, setQuestions] = useState<Question[]>([])

  useEffect(() => {
    fetchQuestions()
  }, [])

  // These placeholders will be populated later in this guide
  const fetchQuestions = async () => {}
  const onQuestionApproved = async (id: number) => {}
  const onQuestionDisapproved = async (id: number) => {}
  const onAnswerApproved = async (answerId: number) => {}
  const onAnswerDisapproved = async (answerId: number) => {}

  const fetchQuestions = async () => {
    const questions = await getAllQuestionsWithAnswers()
    setQuestions(questions)
  }

  const onQuestionApproved = async (id: number) => {
    await approveQuestion(id)
    fetchQuestions()
  }

  const onQuestionDisapproved = async (id: number) => {
    await disapproveQuestion(id)
    fetchQuestions()
  }

  const onAnswerApproved = async (answerId: number) => {
    await approveAnswer(answerId)
    fetchQuestions()
  }

  const onAnswerDisapproved = async (answerId: number) => {
    await disapproveAnswer(answerId)
    fetchQuestions()
  }

  return (
    <div className="flex min-h-screen flex-col">
      <Header />
      <main className="container mx-auto flex-grow p-4">
        <h1 className="mb-6 text-3xl font-bold">Admin Dashboard</h1>
        <div className="mb-4 flex justify-end">
          <Button>
            <Link href="/admin/set-user-roles">Set Roles</Link>
          </Button>
        </div>
        <div className="space-y-4">
          {questions.map((question) => (
            <QuestionCard
              key={question.id}
              question={question}
              onQuestionApproved={onQuestionApproved}
              onQuestionDisapproved={onQuestionDisapproved}
              onAnswerApproved={onAnswerApproved}
              onAnswerDisapproved={onAnswerDisapproved}
            />
          ))}
        </div>
      </main>
    </div>
  )
}
```

## Test it out!

After completing all the steps throughout this guide, you can now start up the application once more to test out all the features!

Here are a couple of things to try:

- Create a new question as a user of each role.
- Try approving and disapproving questions and answers.
- Try editing and deleting questions and answers.
- Explore the data in the Neon database.

## Conclusion

In this tutorial, you have learned how to integrate Clerk for authentication, configure RBAC using metadata, and enforce role-based restrictions to ensure users only access features appropriate to their roles. Also, you learned how to integrate Neon Postgres database with Drizzle ORM for seamless data management and how to conditionally render UI based on user roles.

By following this tutorial, developers can build secure applications by implementing Role-Based Access Control (RBAC) with Clerk.

Here is the [source code](https://github.com/bmorrisondev/qa-app) (remember to give it a star ⭐).

---

# Build a Next.js sign-up form with React Hook Form
URL: https://clerk.com/blog/nextjs-sign-up-form.md
Date: 2025-02-04
Category: Guides
Description: Learn how to capture user credentials and save them securely with Argon2 password hashing.

In this post, you will learn how to build a sign-up form using the Next.js [App Router](/glossary/app-router) and the following technologies:

> This guide shows how to build authentication from scratch. For a production-ready solution, see our [Next.js Authentication](/nextjs-authentication) offering or explore our [comprehensive Next.js authentication guide](/blog/nextjs-authentication).

- [**Argon2**](https://en.wikipedia.org/wiki/Argon2) - Secure password hashing algorithm that provides strong protection against attacks.
- [**Drizzle**](https://orm.drizzle.team/) - ORM (Object-Relational Mapping) tool used to define the database schema and perform database operations, such as inserting or querying users.
- [**Zod**](https://zod.dev/) - TypeScript-first schema declaration and validation library.
- [**shadcn/ui**](https://ui.shadcn.com) - An assortment of beautifully-designed components you can copy into your app.
- [**React Hook Form**](https://react-hook-form.com/) - Library to simplify React form management and validation.

By the end of this guide, you will have a fully functional and secure sign-up form with the following features:

1. **Dynamic form validation** - Users receive feedback on the validity of their input when they type.
2. **Password strength feedback** - Input validation ensures users follow password best practices to create strong passwords.
3. **Secure password storage** - Passwords are hashed using Argon2 before being stored.

We won't be building the sign-up form step-by-step. Instead, you'll find the complete [source code for the post on GitHub](https://github.com/bookercodes/nextjs-sign-up-form-example-code). I will guide you through the key parts of the code, explaining how each section functions and contributes to the final product.

## Database schema

Let's begin with the database schema, as it defines the structure of the sign-up form and serves as its foundation.

```ts {{ filename: '@/db/schema.ts' }}
import { sql } from 'drizzle-orm'
import { AnyPgColumn, integer, pgTable, timestamp, uniqueIndex, varchar } from 'drizzle-orm/pg-core'

export const usersTable = pgTable(
  'users',
  {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    createdAt: timestamp('created_at').notNull().defaultNow(),
    email: varchar({ length: 254 }).notNull().unique(),
    passwordHash: varchar('password_hash', { length: 255 }).notNull(),
  },
  (table) => [uniqueIndex('emailUniqueIndex').on(lower(table.email))],
)

export function lower(email: AnyPgColumn) {
  return sql`lower(${email})`
}
```

I'm using Drizzle with Postgres, but one of the advantages of using an ORM like Drizzle is its flexibility - you can adapt it to work with almost any database with minimal adjustments.

The table includes these columns: `id`, `createdAt`, `email`, and `passwordHash`.

An important aspect often overlooked is ensuring emails are stored as unique and case-insensitive. While PostgreSQL offers the `citext` module for this purpose, I've opted for an index using the `lower` function. This approach keeps everything within the application code and avoids the need to run additional PostgreSQL queries.

While basic constraints like length are useful at the database level, validations like email format are best handled in the application layer. In the next section, we'll explore using Zod to define and validate the email and other inputs before storing them in the database.

## Zod validation

```ts {{ filename: '@/definitions/sign-up.ts' }}
import { z } from 'zod'

export const signUpFormSchema = z.object({
  email: z.string().email({ message: 'Please enter a valid email.' }).toLowerCase().trim(),
  password: z
    .string()
    .min(8, { message: 'Be at least 8 characters long' })
    .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
    .regex(/[0-9]/, { message: 'Contain at least one number.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Contain at least one special character.',
    }),
})

export type SignUpFormData = z.infer<typeof signUpFormSchema>
type SignUpFieldErrors = z.inferFlattenedErrors<typeof signUpFormSchema>['fieldErrors']

export type SignUpActionState = {
  formData?: SignUpFormData
  fieldErrors?: SignUpFieldErrors
}
```

It's important to validate user input on both the frontend (in the client component) and the backend (in the server function):

- Client-side validation provides instant feedback to users and improves the user experience by catching errors before submitting the form. However, client-side validation can be bypassed or may fail if JavaScript doesn't load correctly.
- Server-side validation acts as a crucial security layer, ensuring that invalid and potentially malicious data is caught and handled properly before it reaches the database, even if the client-side validation is circumvented.

Instead of duplicating validation code on the server and client, we use Zod to define the "shape" of a valid form in one place. By exporting the schema, it can be referenced on both the server and client and we keep the code nice and [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

> \[!TIP]
> Zod is more than just a validation library — it can also normalize inputs. In the snippet above, I use `trim()` on `email` to remove whitespace that a user might accidentally include at the end of their email.
>
> When you use Zod's `safeParse` method, it not only validates the input but also returns the formatted value.

## Server action

```ts {{ title: '@/actions/sign-up.ts' }}
'use server'

export async function signUp(
  _initialState: SignUpActionState,
  formData: FormData,
): Promise<SignUpActionState> {
  const form = Object.fromEntries(formData) as SignUpFormData

  const parsedForm = signUpFormSchema.safeParse(form)
  if (!parsedForm.success) {
    // If validation fails, return the form data and field errors
    return {
      formData: form,
      fieldErrors: parsedForm.error.flatten().fieldErrors,
    }
  }

  const [user] = await db
    .select()
    .from(usersTable)
    .where(eq(lower(usersTable.email), parsedForm.data.email))
  if (user) {
    // If the email is already taken, return the form data and an error message
    return {
      formData: form,
      fieldErrors: {
        email: ['The email you entered has already been taken.'],
      },
    }
  }

  const passwordHash = await hash(parsedForm.data.password)
  await db.insert(usersTable).values({
    email: parsedForm.data.email,
    passwordHash,
  })

  // Here is where you would create an active session for the user before redirecting

  redirect('/')
}
```

A [server action](https://react.dev/reference/rsc/server-functions) is a server-side function that can be called directly from client components. This allows you to run backend code, such as database queries and mutations, without needing to create separate API endpoints.

A common security oversight with server functions is assuming client-side validation is sufficient. However, server functions are essentially HTTP endpoints and a malicious actor could send invalid data directly using a tool like [cURL](https://en.wikipedia.org/wiki/CURL). This may lead to inconsistencies in your database and could even pose a security risk. For this reason, we use Zod to validate all incoming data on the server, even though we already have client-side validation in place.

The server function checks for existing users by querying the database with the provided email. If a user is found, it returns a field-level error message stating: `"The email you entered has already been taken"`.

## Password hashing

In the server action above, we first hash the password before storing it in the database:

```tsx {{ filename: '@/actions/sign-up.ts' }}
const passwordHash = await hash(parsedForm.data.password)

await db.insert(usersTable).values({
  email: parsedForm.data.email,
  passwordHash,
})
```

What is hashing and why is it important?

Storing passwords in plain text creates a significant security vulnerability. If an attacker gains access to your database through a data breach, they immediately have access to every user's account. Worse yet, since many people reuse passwords across services, compromised credentials could lead to breaches of users' accounts on other platforms.

This is where [password hashing](/glossary#hash) becomes crucial. A hash function transforms a password into an irreversible string of characters. When a user attempts to sign in, the system hashes their input password and compares it with the stored hash. If they match, you know the credentials are valid. This all happens without ever storing or exposing the actual password.

The code uses [Argon2](https://en.wikipedia.org/wiki/Argon2) for password hashing, which is considered one of the most secure hashing algorithms available today. While older algorithms like MD5 were once common, they've proven vulnerable to reverse-engineering attacks. Other popular options like Bcrypt are still secure, but Argon2 offers additional benefits - it's memory-hard (making it resistant to specialized hardware attacks) and was specifically designed to be future-proof against advances in password cracking technology.

## Creating the form with React Hook Form

```tsx {{ title: '@/components/sign-up-form.tsx', collapsible: true }}
'use client'

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SignUpActionState, signUpFormSchema, SignUpFormData } from '@/definitions/sign-up'
import { useActionState, useTransition } from 'react'
import InputError from './ui/input-error'

interface SignUpFormProps {
  action: (initialState: SignUpActionState, formData: FormData) => Promise<SignUpActionState>
}

export default function SignUpForm({ action }: SignUpFormProps) {
  const [actionState, submitAction, isPending] = useActionState(action, {})
  const [, startTransition] = useTransition()

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignUpFormData>({
    resolver: zodResolver(signUpFormSchema),
    mode: 'onTouched',
    defaultValues: actionState.formData,
  })

  return (
    <Card className="mx-auto w-full max-w-sm">
      <CardHeader>
        <CardTitle>Create your account</CardTitle>
        <CardDescription>Welcome! Please fill in the details to get started.</CardDescription>
      </CardHeader>
      <CardContent>
        <form
          action={submitAction}
          onSubmit={handleSubmit((_, e) => {
            startTransition(() => {
              const formData = new FormData(e?.target)
              submitAction(formData)
            })
          })}
          className="space-y-4"
          noValidate
        >
          <div className="space-y-2">
            <Label htmlFor="email" className={errors.email ? 'text-destructive' : ''}>
              Email
            </Label>
            <Input
              {...register('email')}
              id="email"
              type="email"
              placeholder="Enter your email"
              defaultValue={actionState.formData?.email}
              className={errors.email ? 'border-destructive ring-destructive' : ''}
              aria-invalid={errors.email ? 'true' : 'false'}
            />
            <InputError error={errors.email?.message} />
            <InputError error={actionState.fieldErrors?.email} />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password" className={errors.password ? 'text-destructive' : ''}>
              Password
            </Label>
            <Input
              {...register('password')}
              id="password"
              type="password"
              placeholder="Enter your password"
              defaultValue={actionState.formData?.password}
              className={errors.password ? 'border-destructive ring-destructive' : ''}
              aria-invalid={errors.password ? 'true' : 'false'}
            />
            <InputError error={errors.password?.message} />
            <InputError error={actionState.fieldErrors?.password} />
          </div>
          <Button className="w-full" type="submit" disabled={isPending}>
            Sign Up
          </Button>
        </form>
        <p className="text-muted-foreground mt-4 text-center text-sm">
          By joining, you agree to our{' '}
          <a href="/terms" className="hover:text-primary underline">
            Terms of Service
          </a>{' '}
          and{' '}
          <a href="/privacy" className="hover:text-primary underline">
            Privacy Policy
          </a>
        </p>
      </CardContent>
    </Card>
  )
}
```

While server-side validation is essential for security, relying on it alone creates a suboptimal user experience. Without client-side validation, users would need to submit the form to see if their input was valid - an experience that feels clunky and outdated.

The `SignUpForm` component uses React Hook Form to provide immediate, dynamic feedback as users type.

By passing the same Zod schema we use on the server to React Hook Form's `zodResolver`, we get automatic validation of password strength requirements and email format.

This creates a layered validation approach - immediate client-side feedback for a smooth user experience, backed by robust server-side validation for security.

As an added benefit, if JavaScript is disabled, the form gracefully falls back to server-side validation, displaying errors returned from the server function via [`useActionState`](https://react.dev/reference/react/hooks).

## Advanced sign-up form features

This concludes our guide to building a secure sign-up form with Next.js, React Hook Form, and Argon2. You now have a solid foundation with robust form validation and proper password hashing. Additionally, the form is built using progressive enhancement, meaning it works even without JavaScript. This means you'll never miss a potential sign-up, even if JavaScript fails to load due to network issues, browser settings, or extensions.

While this is a good start, production-ready sign-up forms usually require more sophisticated features. Here are some advanced capabilities to consider for your implementation:

User experience improvements:

- **Social Sign-In Options** - Improve conversion rate by enabling your users to sign up quickly by [authenticating with Google](/blog/nextjs-google-authentication) and other SSO providers.
- **Biometric Authentication with Passkeys** - Enable users to sign-up using fingerprint or facial recognition.
- **Web3 Authentication Options** - Enables users to authenticate using blockchain-based methods.

Security measures:

- **Email Verification** - Ensure user authenticity and prevent spam accounts by confirming the user's email address.
- **Bot Detection** - Utilize CAPTCHA or similar technologies to prevent automated and spam sign-ups.
- **Rate Limiting** - Protect against abuse by limiting the number of sign-up attempts from a single source.
- **Blocklist** - Block specific account identifiers, such as accounts with your competitor's email domain, from signing up.
- **Block Email Subaddresses** - Prevent sign-ups using email addresses with characters like `+`, `=`, or `#`.
- **Block High-Risk Disposable Email Addresses** - Reject sign-ups using email addresses from disposable email domains.

## So why Clerk then?

[Clerk](/) is a user management and authentication platform, so it might surprise you that we're publishing an article that explains how to implement user registration in Next.js yourself.

While implementing the sophisticated features listed above from scratch is possible, it requires significant development effort and security expertise. If these advanced features are important for your application but you don't want to build them yourself, consider using a complete user management and authentication platform like Clerk that provides these capabilities out of the box.

In addition to sign-up, Clerk provides sign-in and manages the entire session, allowing you to authenticate access to pages and access information about the current user wherever you need it.

Learn how to add not only a sign-up form but complete sign-in and session management in minutes:

The best part? [*Clerk uses components as the API*](/blog/a-component-is-worth-a-thousand-apis).

Instead of building your own form component and manually building all the necessary logic, you can just drop a Next.js [`<SignUp />`](/docs/components/authentication/sign-up) component in your page like so:

```tsx {{ title: 'app/sign-up/[[...sign-up]]/page.tsx' }}
import { SignUp } from '@clerk/nextjs'

export default function MySignUpPage() {
  return <SignUp />
}
```

Clerk's component-driven approach makes setup incredibly easy. You can further customise your sign-up process and manage advanced features directly from the Clerk dashboard once you create a free application following the link below.

---

# Build a Next.js login page template
URL: https://clerk.com/blog/building-a-nextjs-login-page-template.md
Date: 2025-01-31
Category: Guides
Description: Learn how to implement session-based authentication into a Next.js application from scratch.

Session-based authentication, introduced in 1960 at MIT, is still one of the most commonly implemented authentication strategies.

With session-based authentication, every user sign-in creates a session on the server that is associated with the user record in the database. These sessions include details such as a creation timestamp, expiration timestamp, and session status. The session ID is set in a cookie and sent back to the client so that the server can determine the user making any future requests from that client.

In this article, you’ll learn how to build session-based authentication into a Next.js application, from implementing the proper database tables to updating the website with authentication forms.

## Implementation overview

There are a set of common requirements when it comes to implementing session-based authentication in any application.

### Database schema

At least two tables are required:

- **users** - When users sign up, the application needs to at least store a user ID, username, and password so they can return in the future.
- **sessions** - The sessions table tracks the information described in the previous section, allowing the application to look up a session by ID and determine the user it’s associated with.

Any tables with records associated with specific users will also need a `userId` or similar column added to make the association.

### Backend changes

Beyond the required functionality to interact with the new database tables, the backend also needs to be able to hash and salt the user passwords so they are not stored in plain text. It’s also best practice to implement sign-up and login [form](/blog/validate-create-style-react-bootstrap-forms) validation server-side so that bad data is not committed to the database.

Protections also need to be added so that the session ID is checked with the database on each request, and that the user's permissions are verified before performing the requested operation or returning data to the client.

### Frontend changes

Since the frontend is what the user interacts with, there are some expected elements required such as sign-in forms, sign-up forms, and a sign-out button. To provide the best user experience, it’s also recommended to implement validation on the forms so that users get immediate feedback if their input is not acceptable before they submit. It also has the added benefit of preventing bad data from being sent to the server.

You should also ensure that unauthenticated users cannot access routes that are reserved for users who are signed in.

### Cookies

Cookies are a way to store small bits of arbitrary data in your browser. While they can be set in the browser, they are more commonly sent to the client from a server for an HTTP request. Cookies set by a server are automatically sent back to that server with every request.

In the context of session-based [authentication](/nextjs-authentication), the server will create a cookie to store the session identifier and send it back to the client upon successful sign-in. When a request is made, the server checks the session ID associated with the request and looks up which user the session belongs to so it can properly identify who is making the request and apply the appropriate authorization rules.

If you want to learn how to implement the same strategy in a standard React application, check out [our blog post covering how to do this with React and Express](/blog/building-a-react-login-page-template).

## Clerk for user management

While you’ll learn how to build a typical authentication system in this article, user management is a much bigger topic than simply allowing users to create accounts and sign in to your application.

Clerk is a [user management platform](/) that's designed to get you up and running with authentication quickly by providing [drop-in UI components](/docs/components/overview). For example, the following snippet demonstrates the code required to build a sign-in page into a Next.js application using Clerk:

```tsx {{ filename: 'src/app/sign-in/[[...sign-in]]/page.tsx' }}
import { SignIn } from '@clerk/nextjs'

export default function Page() {
  return <SignIn />
}
```

When using Clerk, you can easily configure the traditional email & password strategy as well as others like [social sign-in](/docs/authentication/social-connections/oauth) providers, [passkeys](/blog/what-are-passkeys), and even email & [SMS code](/glossary/sms-passcodes) authentication.

You’ll also provide your users an elegant way to manage their own account data, reset passwords, and connect multiple authentication providers, giving them the flexibility to sign-in to your application the way they want.

Add user management to your Next.js application with Clerk in as little as 2 minutes. Check out [our docs](/docs/quickstarts/nextjs) to learn how to get started!

## Introducing the demo project, Quillmate

Quillmate is an AI-powered application for writers. Users can use Quillmate to help them develop ideas, draft pieces, and ask the AI assistant to help with various tasks.

Quillmate is built with the following tech:

- **Next.js** - The entire application is built with Next.js
- **Vercel** - Since it is built with Next.js, it is easily deployable to Vercel.
- **OpenAI** - The AI functionality utilizes OpenAI’s APIs.
- **Neon** - All data is stored in a Postgres database provided by Neon.
- **Prisma** - Prisma is the ORM used to talk to the database.

If you want to follow along, clone the `build-nextjs-login-page-start` branch from the [GitHub repository](https://github.com/bmorrisondev/quillmate/tree/build-nextjs-login-page-start). Follow the instructions provided in the project’s README before proceeding.

## Install new dependencies

There are two new dependencies that need to be installed before modifying the existing codebase:

- `bcryptjs` - bcryptjs is a very popular hashing library that will be used to hash and salt the passwords before saving them to the database.
- `zod` - zod will be used for both client and server-side form validation, ensuring our data is always clean and providing a better user experience.

Install those dependencies with the following command:

```bash
npm install zod bcryptjs
```

## Updating the database schema

Now let’s get the database schema updated. Navigate your code editor to `prisma/schema.prisma` and make the following changes to define the new database tables:

```{{"filename": "prisma/schema.prisma", "ins": [[31, 51]], "del": []}}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Article {
  id          String        @id @default(uuid())
  title       String
  content     String
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @default(now()) @updatedAt
  chatMessages ChatMessage[]

  @@map("articles")
}

model ChatMessage {
  id        String   @id @default(uuid())
  articleId String?
  role      String
  content   String
  createdAt DateTime @default(now())
  article   Article? @relation(fields: [articleId], references: [id], onDelete: Cascade)

  @@map("chat_messages")
}

model User {
  id            String        @id @default(uuid())
  email         String       @unique
  passwordHash  String
  createdAt     DateTime     @default(now())
  updatedAt     DateTime     @default(now()) @updatedAt
  sessions      Session[]

  @@map("users")
}

model Session {
  id        String   @id @default(uuid())
  userId    String
  expiresAt DateTime
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("sessions")
}
```

Run the following command in the terminal to update the Prisma client and push the changes to the Neon database:

```bash
npx prisma generate
npx prisma db push
```

## Create the middleware

As mentioned earlier in this guide, you’ll need to separate your public routes from those that require the user to be signed in. In an application with dedicated backends and frontends, you’d typically separate the views in the frontend to prevent unauthorized users from accessing those views, and protect the backend API routes so that tech savvy users can’t bypass protections in the frontend.

Since Next.js is a full-stack framework, you can actually do both using middleware, which provides a way for you to intercept requests and apply your own logic to the request before the user reaches their destination.

Create `src/middleware.ts` and paste in the following code:

```tsx {{ filename: 'src/middleware.ts' }}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Add paths that don't require authentication
const publicPaths = ['/signin', '/signup', '/']

export function middleware(request: NextRequest) {
  // Get the session ID from the cookies
  const sessionId = request.cookies.get('sessionId')
  const { pathname } = request.nextUrl

  // Allow access to public paths
  if (publicPaths.includes(pathname)) {
    // Redirect to articles if already authenticated
    if (sessionId) {
      return NextResponse.redirect(new URL('/articles', request.url))
    }
    return NextResponse.next()
  }

  // Require authentication for all other paths
  if (!sessionId) {
    const signInUrl = new URL('/signin', request.url)
    signInUrl.searchParams.set('from', pathname)
    return NextResponse.redirect(signInUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
```

## Creating the sign-up and login routes

With the database changes and middleware in place, you can now add the necessary forms and logic to allow users to create an account and sign into the application. Each of these routes contain three files as follows:

- `page.tsx` - The client-side sign-up or login page the user will interact with.
- `actions.ts` - Server actions that are used to interact with the database to create new users and create sessions.
- `validation.ts` - Validation models that are shared between the sign-up or login page and server action, enabling validation on both ends of the application.

There are also a few functions that will be shared between both the sign-up and sign-in routes, so let’s get that set up before building them.

Create a the `src/app/auth/actions.ts` file and populate it with the following:

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

import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { db } from '@/db'

// Gets the current user info, redirecting to /signin if there is none
export async function requireAuth() {
  const user = await getCurrentUser()
  if (!user) {
    redirect('/signin')
  }
  return user
}

// Removes the session from the database and removes the cookie
export async function signOut() {
  const c = await cookies()
  const sessionId = c.get('sessionId')?.value

  if (sessionId) {
    await db.session.delete({
      where: { id: sessionId },
    })
  }

  c.delete('sessionId')
  redirect('/signin')
}

// Gets the current user info based on the sessionId cookie
export async function getCurrentUser() {
  const c = await cookies()
  const sessionId = c.get('sessionId')?.value
  if (!sessionId) return null

  const session = await db.session.findUnique({
    where: { id: sessionId },
    include: { user: true },
  })

  if (!session || session.expiresAt < new Date()) {
    c.delete('sessionId')
    return null
  }

  return session.user
}

// Create a session and set the sessionId cookie
export async function createSessionAndCookie(userId: string) {
  const SESSION_DURATION_DAYS = 7
  const expiresAt = new Date()
  expiresAt.setDate(expiresAt.getDate() + SESSION_DURATION_DAYS)

  const session = await db.session.create({
    data: {
      userId,
      expiresAt,
    },
  })

  const c = await cookies()

  // Set session cookie
  c.set('sessionId', session.id, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    expires: new Date(session.expiresAt),
  })
}
```

### Handling sign-up

Since `validation.ts` is the simplest of the three files, start by creating `src/app/signup/validation.ts` and paste in the following:

```tsx {{ filename: 'src/app/signup/validation.ts' }}
import { z } from 'zod'

// Validation schemas
export const signUpSchema = z
  .object({
    email: z.string().toLowerCase().email('Invalid email address'),
    password: z
      .string()
      .min(8, 'Password must be at least 8 characters')
      .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
      .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
      .regex(/[0-9]/, 'Password must contain at least one number'),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ['confirmPassword'],
  })
```

Next, create the server actions file at `src/app/signup/actions.ts` and paste in the following:

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

import { redirect } from 'next/navigation'
import bcrypt from 'bcryptjs'
import { db } from '@/db'
import { signUpSchema } from './validation'
import { createSessionAndCookie } from '../auth/actions'

export async function signUp(formData: FormData) {
  // Perform server-side validation
  const validatedFields = signUpSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
    confirmPassword: formData.get('confirmPassword'),
  })

  // Return errors if there are any
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  const { email, password } = validatedFields.data

  // Check if user already exists
  const existingUser = await db.user.findUnique({
    where: { email },
  })

  if (existingUser) {
    return {
      errors: {
        email: ['User with this email already exists'],
      },
    }
  }

  // Hash password and create user
  const passwordHash = await bcrypt.hash(password, 10)
  const user = await db.user.create({
    data: {
      email,
      passwordHash,
    },
  })

  // Create session, allowing the user to be immediately signed in
  await createSessionAndCookie(user.id)

  // Redirect the user to the protected route
  redirect('/articles')
}
```

Finally, create `src/app/signup/page.tsx` and paste in the following:

```tsx {{ filename: 'src/app/signup/page.tsx' }}
'use client'

import { useState } from 'react'
import Link from 'next/link'
import { signUp } from './actions'
import { signUpSchema } from './validation'

export default function SignUp() {
  const [errors, setErrors] = useState<{ [key: string]: string[] }>({})
  const [clientErrors, setClientErrors] = useState<{ [key: string]: string[] }>({})

  async function handleSubmit(formData: FormData) {
    // Reset errors
    setClientErrors({})

    // Validate form data
    const result = signUpSchema.safeParse({
      email: formData.get('email'),
      password: formData.get('password'),
      confirmPassword: formData.get('confirmPassword'),
    })

    if (!result.success) {
      const formattedErrors: { [key: string]: string[] } = {}
      result.error.errors.forEach((error) => {
        const path = error.path[0].toString()
        if (!formattedErrors[path]) {
          formattedErrors[path] = []
        }
        formattedErrors[path].push(error.message)
      })
      setClientErrors(formattedErrors)
      return
    }

    const serverResult = await signUp(formData)
    if (serverResult?.errors) {
      setErrors(serverResult.errors)
    }
  }

  function handleInputChange(field: string, value: string, formElement: HTMLFormElement) {
    const formData = new FormData(formElement)
    formData.set(field, value)

    const result = signUpSchema.safeParse({
      email: formData.get('email'),
      password: formData.get('password'),
      confirmPassword: formData.get('confirmPassword'),
    })

    if (!result.success) {
      const fieldErrors = result.error.errors
        .filter((error) => error.path[0] === field)
        .map((error) => error.message)

      if (fieldErrors.length > 0) {
        setClientErrors((prev) => ({
          ...prev,
          [field]: fieldErrors,
        }))
      } else {
        setClientErrors((prev) => ({
          ...prev,
          [field]: [],
        }))
      }
    } else {
      setClientErrors((prev) => ({
        ...prev,
        [field]: [],
      }))
    }
  }

  return (
    <div className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
          Create a new account
        </h2>
        <p className="mt-2 text-center text-sm text-gray-600">
          Or{' '}
          <Link href="/signin" className="font-medium text-blue-600 hover:text-blue-500">
            sign in to your account
          </Link>
        </p>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
          <form
            onSubmit={(e) => {
              e.preventDefault()
              const formData = new FormData(e.target as HTMLFormElement)
              handleSubmit(formData)
            }}
            className="space-y-6"
          >
            <div>
              <label htmlFor="email" className="block text-sm font-medium text-gray-700">
                Email address
              </label>
              <div className="mt-1">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none sm:text-sm"
                  onChange={(e) => handleInputChange('email', e.target.value, e.target.form!)}
                />
              </div>
              {(clientErrors.email || errors.email)?.map((error) => (
                <p key={error} className="mt-1 text-sm text-red-600">
                  {error}
                </p>
              ))}
            </div>

            <div>
              <label htmlFor="password" className="block text-sm font-medium text-gray-700">
                Password
              </label>
              <div className="mt-1">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="new-password"
                  required
                  className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none sm:text-sm"
                  onChange={(e) => handleInputChange('password', e.target.value, e.target.form!)}
                />
              </div>
              {(clientErrors.password || errors.password)?.map((error) => (
                <p key={error} className="mt-1 text-sm text-red-600">
                  {error}
                </p>
              ))}
            </div>

            <div>
              <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
                Confirm password
              </label>
              <div className="mt-1">
                <input
                  id="confirmPassword"
                  name="confirmPassword"
                  type="password"
                  autoComplete="new-password"
                  required
                  className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none sm:text-sm"
                  onChange={(e) =>
                    handleInputChange('confirmPassword', e.target.value, e.target.form!)
                  }
                />
              </div>
              {(clientErrors.confirmPassword || errors.confirmPassword)?.map((error) => (
                <p key={error} className="mt-1 text-sm text-red-600">
                  {error}
                </p>
              ))}
            </div>

            <div>
              <button
                type="submit"
                className="flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
              >
                Sign up
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
```

### Handling sign-in

Now let’s create the sign-in logic and views starting with the validation file as we did in the previous section. Create `src/app/signin/validation.ts` and paste in the following code:

```tsx {{ filename: 'src/app/signin/validation.ts' }}
import { z } from 'zod'

export const signInSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(1, 'Password is required'),
})
```

Next, create the server actions at `src/app/signin/actions.ts` and populate the file with the following:

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

import { redirect } from 'next/navigation'
import bcrypt from 'bcryptjs'
import { db } from '@/db'
import { signInSchema } from './validation'
import { createSessionAndCookie } from '../auth/actions'

export async function signIn(formData: FormData) {
  const validatedFields = signInSchema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  })

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  const { email, password } = validatedFields.data

  const user = await db.user.findUnique({
    where: { email },
  })

  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return {
      errors: {
        email: ['Invalid email or password'],
      },
    }
  }

  // Create session
  await createSessionAndCookie(user.id)

  redirect('/articles')
}
```

Then create the login page at `src/app/signin/page.tsx` and paste in the following:

```tsx {{ filename: 'src/app/signin/page.tsx' }}
'use client'

import { useState } from 'react'
import Link from 'next/link'
import { signIn } from './actions'
import { signInSchema } from './validation'

export default function SignIn() {
  const [errors, setErrors] = useState<{ [key: string]: string[] }>({})
  const [clientErrors, setClientErrors] = useState<{ [key: string]: string[] }>({})

  async function handleSubmit(formData: FormData) {
    // Reset errors
    setClientErrors({})

    // Validate form data
    const result = signInSchema.safeParse({
      email: formData.get('email'),
      password: formData.get('password'),
    })

    if (!result.success) {
      const formattedErrors: { [key: string]: string[] } = {}
      result.error.errors.forEach((error) => {
        const path = error.path[0].toString()
        if (!formattedErrors[path]) {
          formattedErrors[path] = []
        }
        formattedErrors[path].push(error.message)
      })
      setClientErrors(formattedErrors)
      return
    }

    const serverResult = await signIn(formData)
    if (serverResult?.errors) {
      setErrors(serverResult.errors)
    }
  }

  // Validate the fields as the user types
  function handleInputChange(field: string, value: string) {
    const result = signInSchema.safeParse({
      email: field === 'email' ? value : '',
      password: field === 'password' ? value : '',
    })

    if (!result.success) {
      const fieldError = result.error.errors.find((error) => error.path[0] === field)
      if (fieldError) {
        setClientErrors((prev) => ({
          ...prev,
          [field]: [fieldError.message],
        }))
      }
    } else {
      setClientErrors((prev) => ({
        ...prev,
        [field]: [],
      }))
    }
  }

  return (
    <div className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
          Sign in to your account
        </h2>
        <p className="mt-2 text-center text-sm text-gray-600">
          Or{' '}
          <Link href="/signup" className="font-medium text-blue-600 hover:text-blue-500">
            create a new account
          </Link>
        </p>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
          <form
            onSubmit={(e) => {
              e.preventDefault()
              const formData = new FormData(e.target as HTMLFormElement)
              handleSubmit(formData)
            }}
            className="space-y-6"
          >
            <div>
              <label htmlFor="email" className="block text-sm font-medium text-gray-700">
                Email address
              </label>
              <div className="mt-1">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none sm:text-sm"
                  onChange={(e) => handleInputChange('email', e.target.value)}
                />
              </div>
              {(clientErrors.email || errors.email)?.map((error) => (
                <p key={error} className="mt-1 text-sm text-red-600">
                  {error}
                </p>
              ))}
            </div>

            <div>
              <label htmlFor="password" className="block text-sm font-medium text-gray-700">
                Password
              </label>
              <div className="mt-1">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none sm:text-sm"
                  onChange={(e) => handleInputChange('password', e.target.value)}
                />
              </div>
              {(clientErrors.password || errors.password)?.map((error) => (
                <p key={error} className="mt-1 text-sm text-red-600">
                  {error}
                </p>
              ))}
            </div>

            <div>
              <button
                type="submit"
                className="flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
              >
                Sign in
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
```

## Associating articles with users

At this point, the user can now create an account and sign in as needed, but the `/articles` route which contains the protected pages still needs to have several pieces updated to ensure users can only work with articles associated with their account and not ALL articles.

Update `prisma/schema.prisma` one more time to update the `Article` model so those records are associated with a user by creating the `userId` column and Prisma relation:

```{{"filename": "prisma/schema.prisma", "ins": [12, 17, 40], "del": []}}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Article {
  id          String        @id @default(uuid())
  userId      String
  title       String
  content     String
  createdAt   DateTime      @default(now())
  updatedAt   DateTime      @default(now()) @updatedAt
  user        User          @relation(fields: [userId], references: [id], onDelete: Cascade)
  chatMessages ChatMessage[]

  @@map("articles")
}

model ChatMessage {
  id        String   @id @default(uuid())
  articleId String?
  role      String
  content   String
  createdAt DateTime @default(now())
  article   Article? @relation(fields: [articleId], references: [id], onDelete: Cascade)

  @@map("chat_messages")
}

model User {
  id            String        @id @default(uuid())
  email         String       @unique
  passwordHash  String
  createdAt     DateTime     @default(now())
  updatedAt     DateTime     @default(now()) @updatedAt
  articles      Article[]
  sessions      Session[]

  @@map("users")
}

model Session {
  id        String   @id @default(uuid())
  userId    String
  expiresAt DateTime
  createdAt DateTime @default(now())
  user      User     @reauthaulation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("sessions")
}
```

Apply your changes with the same terminal commands as before:

```bash
npx prisma generate
npx prisma db push
```

Next, you’ll update the server actions used for the `/articles` route to store the user’s ID whenever an article record is created, and filter returned articles when the user requests them, ensuring that users can only access the articles they are supposed to.

Update `src/app/articles/actions.ts` as follows:

```tsx {{ filename: 'src/app/articles/actions.ts', ins: [7, [10, 12], 20, 26, 32, 37], del: [] }}
'use server'

import { db } from '@/db'
import { requireAuth } from '@/app/auth/actions'

export async function fetchArticles() {
  const user = await requireAuth()

  return await db.article.findMany({
    where: {
      userId: user.id,
    },
    orderBy: {
      updatedAt: 'desc',
    },
  })
}

export async function createNewArticle() {
  const user = await requireAuth()

  return await db.article.create({
    data: {
      title: 'New Article',
      content: '# New Article\n\nStart writing your content here...',
      userId: user.id,
    },
  })
}

export async function saveArticle(id: string, title: string, content: string) {
  const user = await requireAuth()

  return await db.article.update({
    where: {
      id,
      userId: user.id,
    },
    data: {
      title,
      content,
      updatedAt: new Date(),
    },
  })
}

export async function getChatMessages(userId: string, articleId: string, since?: Date) {
  return await db.chatMessage.findMany({
    where: {
      articleId,
      ...(since && {
        createdAt: {
          gte: since,
        },
      }),
    },
    orderBy: {
      createdAt: 'asc',
    },
  })
}

export async function createChatMessage(
  userId: string,
  articleId: string,
  role: 'user' | 'assistant',
  content: string,
) {
  return await db.chatMessage.create({
    data: {
      articleId,
      role,
      content,
    },
  })
}
```

Finally, you’ll update `src/app/articles/page.tsx` to add a sign-out button that leverages the `signOut` function in our shared [authentication](/nextjs-authentication) utility file:

```tsx {{ filename: 'src/app/articles/page.tsx', ins: [8, [147, 156]], del: [] }}
'use client'

import { useState, useEffect, useCallback, useRef } from 'react'
import { ChatSidebar } from './components/ChatSidebar'
import { createNewArticle, fetchArticles, saveArticle } from './actions'
import { useDebounce } from '@/hooks/useDebounce'
import { MarkdownEditor } from './components/MarkdownEditor'
import { signOut } from '@/app/auth/actions'

interface Article {
  id: string
  title: string
  content: string
}

export default function ArticlesPage() {
  const [articles, setArticles] = useState<Article[]>([])
  const [selectedArticle, setSelectedArticle] = useState<Article | null>(null)
  const [content, setContent] = useState<string>('')
  const [isLoading, setIsLoading] = useState(true)
  const [isSaving, setIsSaving] = useState(false)
  const [context, setContext] = useState<string>()

  useEffect(() => {
    loadArticles()
  }, [])

  async function loadArticles() {
    try {
      const fetchedArticles = await fetchArticles()
      setArticles(fetchedArticles)
      if (fetchedArticles.length > 0 && !selectedArticle) {
        setSelectedArticle(fetchedArticles[0])
        setContent(fetchedArticles[0].content)
      }
      setIsLoading(false)
    } catch (error) {
      console.error('Failed to load articles:', error)
      setIsLoading(false)
    }
  }

  const handleArticleSelect = (article: Article) => {
    setSelectedArticle(article)
    setContent(article.content)
  }

  const handleNewArticle = async () => {
    try {
      const newArticle = await createNewArticle()
      if (newArticle) {
        setArticles((prev) => [...prev, newArticle])
        handleArticleSelect(newArticle)
      }
    } catch (error) {
      console.error('Failed to create article:', error)
    }
  }

  const extractTitleFromContent = (content: string): string | null => {
    const h1Match = content.match(/^#\s+(.+)$/m)
    return h1Match ? h1Match[1].trim() : null
  }

  const saveContent = useCallback(
    async (articleId: string, currentTitle: string, newContent: string) => {
      setIsSaving(true)
      try {
        const newTitle = extractTitleFromContent(newContent) || currentTitle
        await saveArticle(articleId, newTitle, newContent)

        // Update the articles list with the new title if it changed
        if (newTitle !== currentTitle) {
          setArticles((prev) =>
            prev.map((article) =>
              article.id === articleId ? { ...article, title: newTitle } : article,
            ),
          )
          if (selectedArticle?.id === articleId) {
            setSelectedArticle((prev) => (prev ? { ...prev, title: newTitle } : prev))
          }
        }
      } catch (error) {
        console.error('Failed to save article:', error)
      } finally {
        setIsSaving(false)
      }
    },
    [selectedArticle?.id],
  )

  const debouncedSave = useDebounce(saveContent, 1000)

  const handleContentChange = (newContent: string | undefined) => {
    if (!selectedArticle || !newContent) return
    setContent(newContent)
    debouncedSave(selectedArticle.id, selectedArticle.title, newContent)
  }

  const handleAskAssistant = useCallback((selectedText: string) => {
    setContext(selectedText)
  }, [])

  const handleAppendToArticle = useCallback(
    (text: string) => {
      if (!selectedArticle) return
      const newContent = content + text
      setContent(newContent)
      debouncedSave(selectedArticle.id, selectedArticle.title, newContent)
    },
    [content, selectedArticle, debouncedSave],
  )

  if (isLoading) {
    return <div className="flex flex-1 items-center justify-center">Loading...</div>
  }

  return (
    <div className="flex flex-1 overflow-hidden">
      {/* Article List Sidebar */}
      <div className="w-64 overflow-y-auto border-r border-gray-200 bg-white">
        <div className="flex h-full flex-col p-4">
          <div className="mb-4 flex items-center justify-between">
            <h2 className="text-lg font-semibold text-gray-900">Articles</h2>
            <button
              onClick={handleNewArticle}
              className="rounded-lg bg-blue-500 px-2 py-1 text-sm text-white hover:bg-blue-600"
            >
              New
            </button>
          </div>
          <div className="flex-1 space-y-1">
            {articles.map((article) => (
              <button
                key={article.id}
                onClick={() => handleArticleSelect(article)}
                className={`w-full rounded-lg px-3 py-2 text-left text-sm ${
                  selectedArticle?.id === article.id
                    ? 'bg-blue-100 text-blue-700'
                    : 'text-gray-700 hover:bg-gray-100'
                }`}
              >
                {article.title}
              </button>
            ))}
          </div>
          <div className="mt-4">
            <form action={signOut}>
              <button
                type="submit"
                className="w-full rounded-lg bg-gray-50 px-3 py-2 text-left text-sm text-gray-900 transition-all hover:bg-red-600 hover:text-white"
              >
                Sign out
              </button>
            </form>
          </div>
        </div>
      </div>

      {/* Markdown Editor */}
      <div className="relative h-full flex-1">
        {selectedArticle ? (
          <>
            <MarkdownEditor
              value={content}
              onChange={handleContentChange}
              height="100%"
              onAskAssistant={handleAskAssistant}
            />
            {isSaving && (
              <div className="absolute top-2 right-2 rounded-md bg-gray-800 px-2 py-1 text-xs text-white opacity-75">
                Saving...
              </div>
            )}
          </>
        ) : (
          <div className="flex h-full items-center justify-center p-8 text-gray-500">
            Select an article or create a new one
          </div>
        )}
      </div>

      {/* Chat Sidebar */}
      {selectedArticle ? (
        <ChatSidebar
          content={selectedArticle.content}
          articleId={selectedArticle.id}
          context={context}
          onClearContext={() => setContext(undefined)}
          onAppendToArticle={handleAppendToArticle}
        />
      ) : null}
    </div>
  )
}
```

## Update the homepage link

The last thing to do is update the “Get started” button on the home page to go to `/signin` instead of `/articles`, which will let users sign into the application if they are not already:

```tsx {{ filename: 'src/app/page.tsx', ins: [23], del: [22], prettier: false }}
import Link from 'next/link'
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { ArrowRight, Sparkles, Zap, RefreshCw } from 'lucide-react'

export default function LandingPage() {

  return (
    <div className="min-h-screen bg-gradient-to-b from-purple-100 to-white">
      <main className="container mx-auto px-4 py-16 space-y-24">
        {/* Hero Section */}
        <section className="text-center space-y-6">
          <h1 className="text-5xl font-extrabold tracking-tight text-gray-900 sm:text-6xl">
            Elevate Your Writing with{' '}
            <span className="inline-block text-transparent bg-clip-text bg-gradient-to-r from-purple-600 to-pink-500">
              QuillMate
            </span>
          </h1>
          <p className="text-xl text-gray-700 max-w-2xl mx-auto">
            Unlock your creativity and boost your productivity with our AI-powered writing assistant.
          </p>
          <Link href="/articles">
          <Link href="/signin">
            <Button size="lg" className="mt-8 bg-purple-600 hover:bg-purple-700 text-white">
              Get Started with QuillMate <ArrowRight className="ml-2 h-4 w-4" />
            </Button>
          </Link>
        </section>

        {/* Feature Cards */}
        <section className="space-y-8 max-w-2xl mx-auto">
          <Card className="border-purple-200 bg-white/80 backdrop-blur-sm shadow-md hover:shadow-lg transition-all duration-300">
            <CardHeader>
              <CardTitle className="flex items-center text-gray-900">
                <Sparkles className="mr-2 h-5 w-5 text-purple-500" />
                AI-Powered Suggestions
              </CardTitle>
              <CardDescription className="text-gray-600">
                Get intelligent writing suggestions and improvements in real-time as you type with QuillMate.
              </CardDescription>
            </CardHeader>
          </Card>

          <Card className="border-purple-200 bg-white/80 backdrop-blur-sm shadow-md hover:shadow-lg transition-all duration-300">
            <CardHeader>
              <CardTitle className="flex items-center text-gray-900">
                <Zap className="mr-2 h-5 w-5 text-purple-500" />
                Instant Content Generation
              </CardTitle>
              <CardDescription className="text-gray-600">
                Generate high-quality content for various purposes with just a few clicks using QuillMate's AI.
              </CardDescription>
            </CardHeader>
          </Card>

          <Card className="border-purple-200 bg-white/80 backdrop-blur-sm shadow-md hover:shadow-lg transition-all duration-300">
            <CardHeader>
              <CardTitle className="flex items-center text-gray-900">
                <RefreshCw className="mr-2 h-5 w-5 text-purple-500" />
                Style Adaptation
              </CardTitle>
              <CardDescription className="text-gray-600">
                Easily adapt your writing style for different audiences and purposes with QuillMate's intelligent assistance.
              </CardDescription>
            </CardHeader>
          </Card>
        </section>
      </main>
    </div>
  )
}
```

## Test it out!

Now that all the changes are implemented, execute the following command in your terminal to start up the dev server:

```bash
npm run dev
```

Open your browser and navigate to the URL displayed in the terminal and you’ll be able to create a user account, sign into the application, and start creating articles!

![QuillMate Demo](./quillmate-create-article.png)

After creating an article and chatting with the AI assistant, you can head to the Neon console to explore how the data is structured in the database.

![Neon Console](./neon-tables.png)

## Conclusion

You are now well-equipped to implement session-based authentication within your own application. By following the general guidance introduced at the beginning of this post, you can provide your users the ability to create accounts and sign in to your Next.js site.

While this guide outlines the steps required to implement authentication, adding sign up and sign in to a web application is only one aspect of user management. Consider giving Clerk a try for a complete user management platform that can be configured in minutes, saving you and your team hours of development, testing, and debugging effort.

Feel free to use the provided repository as a resource when building Next.js web applications going forward!

---

# How to implement Google authentication in Next.js 15
URL: https://clerk.com/blog/nextjs-google-authentication.md
Date: 2025-01-24
Category: Guides
Description: Learn how to add Google authentication to your Next.js app, implement a user button for profile management, and enable Google One Tap using Clerk.

This guide walks you through adding Google authentication to your Next.js 15 application in record time.

> For a comprehensive overview of all authentication methods in Next.js, see our [Ultimate Guide to Next.js Authentication](/blog/nextjs-authentication).

By the end, you'll implement essential authentication features including:

- Google authentication for sign-up and sign-in
- Route protection from unauthenticated access
- Google One Tap integration (optional)
- Access to Google services like Calendar on behalf of the authenticated user using [OAuth](/glossary#oauth) (optional)

You'll also learn how to add a polished user button dropdown that gives your users control over their [session](/glossary#session) and profile.

![Google sign-in, user button dropdown, and One Tap UI](./1.png)

To implement Google authentication, you can choose between building it yourself with an open-source library - giving you complete control over the implementation - or using Clerk, which offers the quickest path to integration in Next.js. Both approaches have their place, but in this guide, we'll use Clerk.

## An introduction to Clerk

![Clerk homepage showing user management platform and components](./2.png)

[Clerk](/nextjs-authentication) is a user management and authentication platform that makes it quick to add secure authentication to your Next.js application. We provide pre-built components like ⁠`<SignIn/>` and `<SignUp />` that you can configure to support various authentication methods, including Google.

Using familiar Next.js patterns like components and [middleware](/blog/what-is-middleware-in-nextjs), Clerk handles all the complex backend logic — from session management to route protection.

## How to implement Google authentication using Clerk

Before you implement Google authentication, you'll need to [**create a Clerk account**](/sign-up) to manage your users and authentication settings. Creating an account is free for your first 10,000 monthly users, and no credit card is required.

Sign in to the Clerk dashboard and create your first application. Give it a name and enable Google authentication (you can enable additional authentication methods at any time), then click "Create application".

To proceed with this guide, follow the quickstart steps in the Clerk dashboard. Once you've completed the setup, return here to continue with the next steps.

---

Welcome back!

Start your development server and visit [http://localhost:3000](http://localhost:3000) to sign up and create your first Clerk user.

The quickstart code demonstrates a basic Clerk implementation in your root layout. It wraps your application in ⁠`<ClerkProvider />` and adds authentication UI components — showing a `<SignInButton />` button for unauthenticated users and a `<UserButton />` for those signed in.

The authentication components work as expected, but positioning them in the main navigation bar would create a more predictable user experience. Let's quickly explore how to do that next before diving into route protection.

> \[!NOTE]
> If you've set up [Single Sign-On (SSO)](/glossary/single-sign-on-sso) with Google before, you know it usually starts with configuring Google Cloud credentials. With Clerk in development mode, you can skip this setup and start building immediately using our shared development keys. For production, you'll still need [custom Google credentials](/docs/authentication/social-connections/google#configure-for-your-production-instance).

## Adding authentication to your navigation bar

While some authentication solutions focus solely on authentication, Clerk also handles user management. This means you get access to components like `⁠<UserButton />` - a dropdown that shows which account is signed in and lets users manage their session and profile data.

Since `⁠<UserButton />` and ⁠`<SignInButton />` are standard React components, you can style and position them anywhere in your application.

Here's a quick example using [Tailwind CSS](/glossary/tailwind-css) to illustrate how it’s done:

```tsx {{ filename: 'src/app/layout.tsx', ins: [19, 20, 21, 22, 30, 31, 32, 28], del: [27], prettier: false }}
import { 
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton 
} from '@clerk/nextjs'
import './globals.css'

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <nav className="bg-white p-4 shadow-md">
            <div className="container mx-auto flex items-center justify-between">
              <h1 className="text-xl">My App</h1>
              <div className="flex items-center">
                <SignedOut>
                  <SignInButton />
                </SignedOut>
                <SignedIn>
                  <UserButton />
                  <UserButton showName />
                </SignedIn>
              </div>
            </div>
          </nav>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}
```

## Protecting routes from unauthenticated access

Now that users can sign in, let's explore how to restrict page access to authenticated users only. While there are several approaches to [route protection in Next.js](/docs/reference/nextjs/app-router/route-handlers), we will focus on using middleware here.

During the quickstart, you added Clerk middleware to your application. By default, all routes are public. Let's update the middleware to protect specific routes or patterns of routes from unauthenticated access:

```tsx {{ filename: 'src/middlware.ts', ins: [2, 3, 4, 7, 8, 9], del: [1, 6] }}
import { clerkMiddleware } from '@clerk/nextjs/server'
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/forum(.*)'])

export default clerkMiddleware()
export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) await 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)(.*)',
  ],
}
```

Above, we create a route matcher for paths starting with "⁠/dashboard" or ⁠"/forum" and then check each request against these patterns. If the route matches, use `⁠auth.protect()` to automatically redirect users to sign in. In effect, only authenticated users can access these pages.

> \[!NOTE]
> **Authentication vs authorization**
>
> [Authentication](/glossary#authentication) only verifies that a user is signed in. While this guide shows you how to protect routes from unauthenticated access, you might also need [authorization](/glossary#authorization) — checking if an authenticated user has *permission* to access specific resources based on ownership or roles.

## Adding Google One Tap support

[Google One Tap](https://developers.google.com/identity/gsi/web/guides/features) proactively prompts users to sign in with their Google account in a single click when they visit your site. This convenient approach can help increase your sign-in conversion rate compared to traditional authentication flows.

To implement Google One Tap, first, [update your Clerk application to use custom Google credentials](/docs/authentication/social-connections/google#configure-for-your-production-instance) instead of shared credentials.

Then, add ⁠`<GoogleOneTap />` to your layout:

```tsx {{ filename: 'src/app/layout.tsx', ins: [7, 25], prettier: false }}
import {
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton,
  GoogleOneTap,
} from '@clerk/nextjs'
import './globals.css'

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <nav className="bg-white p-4 shadow-md">
            <div className="container mx-auto flex items-center justify-between">
              <h1 className="text-xl">My App</h1>
              <div className="flex items-center">
                <SignedOut>
                  <GoogleOneTap />
                  <SignInButton />
                </SignedOut>
                <SignedIn>
                  <UserButton showName />
                </SignedIn>
              </div>
            </div>
          </nav>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}
```

When new users visit your application, Google One Tap will conveniently prompt them to sign in directly from the corner of the screen.

## Accessing Google services on behalf of the authenticated user using OAuth

![Accessing Google services via OAuth after Google sign-in](./3.png)
When users sign in with Google using Clerk, you're not just authenticating them — you're establishing a secure connection through OAuth that can do much more. While the authentication token proves who the user is, OAuth also enables your application to access Google services on their behalf.

Clerk simplifies this process. Beyond providing SSO with various providers, Clerk makes it straightforward to access user data from connected services. For example, you can retrieve an authenticated user's Google Calendar availability with just a few lines of code. For a detailed walkthrough, check out our [guide to accessing Google Calendar data](/blog/using-clerk-sso-access-google-calendar) with a complete demo application.

## Conclusion

In this guide, you've learned how to add Google authentication to your Next.js application using Clerk. Rather than building complex authentication logic yourself, Clerk provided pre-built components and middleware that enabled you to implement secure Google authentication in minutes. With Clerk's foundation in place, adding additional features like Google One Tap and OAuth access to Google services might have been easier to implement than you expected!

While we used Clerk's [Account Portal](/docs/customization/account-portal/overview) (a hosted authentication page) for the fastest implementation, you can also build your own sign-in and sign-up pages then render `<SignIn />` and `<SignUp />` directly in your application without the need for an external redirect. Learn how in this [guide](/docs/references/nextjs/custom-sign-in-or-up-page) or by following along with this video:

For more information about how to add Google as a social connection and an important note on switching to production, please refer to the [Clerk documentation](/docs/authentication/social-connections/google).