# Clerk Blog — Guides — Page 2

# A practical guide to testing Clerk Next.js applications
URL: https://clerk.com/blog/testing-clerk-nextjs.md
Date: 2025-04-11
Category: Guides
Description: An example-packed guide to writing effective tests for Clerk applications, covering everything from integration testing with React Testing Library to end-to-end testing using Playwright.

We understand that writing tests isn't the most exciting part of development. That's why they are often shelved as "tech debt" or pushed to the bottom of the priority list. But it's not just about motivation - writing good tests is *hard*.

You might wonder:

- What do I test?
- How do I test it?
- Should I write integration tests or end-to-end tests?
- How do I mock Clerk?

This post addresses these challenges directly to help you develop a meaningful testing strategy for your Next.js application using Clerk.

By the end, you'll be equipped to test critical authentication flows by mocking Clerk's API in integration tests.

You'll also learn how to incorporate end-to-end tests and simulate real-world user interactions with your application to make sure that your application works (and continues to work) as it should in production.

## Meet Pup Party

This post demonstrates how to add comprehensive tests to a sample application called Pup Party.

Pup Party allows dog owners to rate and discover dog-friendly cafes and restaurants. Users can evaluate venues as "Pup-approved" or "Pup-fail" based on criteria such as dog-friendliness, ambiance, and the quality of dog treats.

To follow along and implement tests step-by-step, download the starter code and follow the set up instructions [here](https://github.com/bookercodes/testing-clerk-nextjs-apps-example).

Alternatively, you can study the complete source code, including tests, [here](https://github.com/bookercodes/testing-clerk-nextjs-apps-example/tree/finished).

## Choosing the right testing approach

Before we dive in, it's important to take a moment to think about what exactly you should be testing and the types of tests that will be most effective. This planning step is crucial, as it guides you in creating tests that are not only meaningful but also efficient to run and maintain.

I've seen teams fall into three common traps:

1. Over-relying on unit tests that focus too much on internals
2. Struggling to scale and manage integration tests effectively due to a reliance on brittle test data and overly complex test logic
3. Over-investing in E2E tests, leading to slow, flaky test suites that bog down development

So, what's the right approach?

In this post, we turn to [Kent C. Dodds](https://kentcdodds.com/) and his [testing trophy](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications):

- Write mostly integration tests
- Supplement with well-placed E2E tests
- Don't overdo it

Following Kent's testing philosophy, this post primarily focuses on integration tests and later addresses E2E tests for critical user paths.

## Integration tests

To write integration tests, you will be using two essential tools:

- **[Jest](https://jestjs.io)**: A test runner and assertion framework to structure and execute your integration tests.
- **[React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro/)**: A popular testing framework for React applications that encourages testing components from the user's perspective, while still supporting the use of [Jest mocks](https://jestjs.io/docs/mock-functions) where necessary.

### Set up Jest and RTL

Start by installing these dev dependencies:

```bash {{ filename: 'Terminal' }}
npm install --save-dev jest \
  @types/jest \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/dom \
  @testing-library/user-event \
  jest-environment-jsdom \
  next-router-mock
```

Add a `test:jest` script to `package.json`:

```json {{ filename: './package.json', ins: [6], prettier: false }}
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint",
  "test:jest": "jest",
},
```

Add a `jest.config.ts` file to the root of your project:

```typescript {{ filename: './jest.config.ts' }}
import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  dir: './',
})

const config: Config = {
  clearMocks: true,
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  // Map next-router-mock to the next/navigation module it mocks
  // Learn more - https://github.com/scottrippey/next-router-mock
  moduleNameMapper: {
    '^next/navigation$': 'next-router-mock',
  },
}

export default createJestConfig(config)
```

Create a `jest.setup.ts` file in the root of your project to globally import @testing-library/jest-dom, which enhances Jest with custom matchers for more intuitive and readable DOM assertions:

```typescript {{ filename: './jest.setup.ts' }}
import '@testing-library/jest-dom'
```

In the ` ./tsconfig.json` file, add `"jest"` to the `types` array:

```json {{ filename: './tsconfig.json', ins: [3] }}
{
  "compilerOptions": {
    "types": ["jest"]
  }
}
```

Finally, create a ` ./__tests__` directory in your root folder. Within this directory, add an empty `index.test.tsx` file - this is where you'll set up your mocks, configure helpers, and write your integration tests in the next section.

#### Set up your mocks and helpers

In your integration tests, you should avoid making actual network calls, including to Clerk. Network calls can introduce variability and slow down your test suite, making it harder to achieve consistent and fast test results. Instead, you should [mock](https://stackoverflow.com/a/2666006) these libraries to control their behavior in your tests and keep your integration test suite lean.

> \[!IMPORTANT]
>
> It's a best practice to avoid writing integration tests for the internals of third-party libraries like Clerk, as they already have their own tests.

Below is the code to mock @clerk/nextjs, which allows you to simulate its behavior and focus on testing your application's logic without relying on external dependencies. Additionally, a helper function called `renderWithProviders` is defined. This function takes an `isSignedIn` argument, allowing you to simulate authenticated and unauthenticated user states in your tests.

```tsx {{ filename: './__tests__/index.test.tsx' }}
import { render, screen, waitFor } from '@testing-library/react'
import { ReactNode } from 'react'
import SubmitReviewPage from '../app/submit-review/page'
import { ClerkProvider, useAuth } from '@clerk/nextjs'
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider/next-13.5'
import { userEvent } from '@testing-library/user-event'

jest.mock('@clerk/nextjs', () => {
  const originalModule = jest.requireActual('@clerk/nextjs')
  return {
    ...originalModule,
    useAuth: jest.fn(() => ({ userId: null })),
    SignIn: () => <div data-testid="clerk-sign-in">Sign In Component</div>,
    ClerkProvider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
  }
})

const TestProviders = ({
  isLoggedIn = false,
  children,
}: {
  isLoggedIn?: boolean
  children: ReactNode
}) => {
  ;(useAuth as jest.Mock).mockReturnValue({ userId: isLoggedIn ? 'user-id' : null })

  // Here, we wrap our component in the ClerkProvider and MemoryRouterProvider to provide the necessary context for our tests.
  // MemoryRouterProvider is used to mock the Next.js router,
  // which is necessary for testing components that use the router.
  return (
    <MemoryRouterProvider>
      <ClerkProvider>{children}</ClerkProvider>
    </MemoryRouterProvider>
  )
}

const renderWithProviders = (ui: ReactNode, isLoggedIn?: boolean) => {
  return render(<TestProviders isLoggedIn={isLoggedIn}>{ui}</TestProviders>)
}
```

> \[!NOTE]
>
> The details about next-router-mock are outside the scope of this post, but you can learn more about it in the library's [documentation](https://github.com/scottrippey/next-router-mock) for a deeper understanding.

Now that you have Jest and RTL configured, along with your mocks and test context provider, it's time to write some meaningful integration tests!

You will implement three integration tests for Pup Party. For each test scenario, you'll read a definition of the requirements in plain text before implementing code to programmatically test the expected behavior and protect against regressions.

### Test case 1: Unauthenticated users cannot submit a review

If an unauthenticated user tries to access the submission page, they should be redirected to the sign-in page.

Here's the test implementation, along with comments explaining each notable line.

Add it to the bottom of index.test.tsx:

```tsx {{ filename: './__tests__/index.test.tsx', ins: [[3, 31]] }}
// Add below the previous snippet at the bottom of the file

describe('Submit Review Page', () => {
  // Grouping tests related to unauthenticated user scenarios
  describe('When a user is unauthenticated', () => {
    const isLoggedIn = false

    it('redirects them to sign in when they try to access the review submission page', async () => {
      // Render the SubmitReviewPage component with the user not logged in
      renderWithProviders(<SubmitReviewPage />, isLoggedIn)

      // Wait for the sign-in element to appear, indicating a redirect to sign-in
      waitFor(
        () => {
          expect(screen.getByTestId('clerk-sign-in')).toBeInTheDocument()
        },
        { timeout: 5000 }, // Timeout after 5 seconds if the element doesn't appear
      )

      // Ensure the review submission prompt is not visible, confirming the redirect
      waitFor(
        () => {
          expect(
            screen.queryByText('Review how dog-friendly this restaurant is!'),
          ).not.toBeInTheDocument()
        },
        { timeout: 5000 }, // Timeout after 5 seconds if the element is still visible
      )
    })
  })
})
```

In this test, an unauthenticated user's experience on the `<SubmitReviewPage />` is simulated by setting ` isSignedIn` to ` false` and passing it as the second argument to our ` renderWithProviders` function.

The [`waitFor`](https://testing-library.com/docs/dom-testing-library/api-async/#waitfor) utility from React Testing Library is used to verify that the UI updates correctly after state changes, specifically ensuring the user is redirected to the sign-in page and cannot access the review submission content.

> \[!TIP]
> You can see the complete test file [here](https://github.com/bookercodes/testing-clerk-nextjs-apps-example/blob/finished/__tests__/index.test.tsx) and reference it if you're following along and need help figuring out where things go.

### Test case 2: Authenticated users can successfully submit a valid review

When a signed-in user submits valid details in the review form, the form should process successfully and then display a confirmation message.

Here's the test implementation:

```tsx {{ filename: './__tests__/index.test.tsx', ins: [[6, 37]] }}
describe('Submit Review Page', () => {
  describe('When a user is authenticated', () => {
    const isLoggedIn = true

    // Add beneath "it" function in previous snippet
    it('allows them to submit a review successfully', async () => {
      const user = userEvent.setup()

      renderWithProviders(<SubmitReviewPage />, isLoggedIn)

      expect(
        await screen.findByText('Welcome to the Dog Friendly Restaurant Reviews form!'),
      ).toBeInTheDocument()

      expect(screen.queryByTestId('clerk-sign-in')).not.toBeInTheDocument()

      const reviewInput = await screen.findByRole('textbox', {
        name: /review/i,
      })
      const ratingInput = await screen.findByRole('spinbutton', {
        name: /rating/i,
      })

      await user.click(reviewInput)
      await user.type(reviewInput, 'Great place!')
      await user.click(ratingInput)
      await user.type(ratingInput, '5')

      await user.click(screen.getByText('Submit'))

      // expect the form to be cleared
      expect(screen.getByLabelText('Review')).toHaveValue('')
      expect(screen.getByLabelText('Rating')).toHaveValue

      // expect a success toast
      expect(await screen.findByText('Form submitted successfully!')).toBeInTheDocument()
    })
  })
})
```

In the code above, `isSignedIn` is set to `true` to simulate the experience of an authenticated user accessing Pup Party.

The `userEvent` utilities are used to mimic user actions, such as clicking into a form input, typing, hitting "Submit", and viewing a success notification.

> \[!TIP]
> Use React Testing Library's selectors like [`findByRole`](https://testing-library.com/docs/queries/byrole/) and [`getByLabelText`](https://testing-library.com/docs/queries/bylabeltext/) to create more robust tests.
>
> This approach, compared to using selectors like `getElementById` or `querySelector`, avoids relying on rigid DOM structures in your queries and assertions. By focusing on the roles and labels, you reduce the likelihood of introducing test failures from UI layout changes. It also promotes the use of semantic HTML, thereby enhancing your application's accessibility.

### Test case 3: Authenticated users must submit valid reviews

Requirements for the submission form:

- When authenticated users add a rating that is less than zero, they should see a "Rating must be a number above 0" error appear next to the rating field
- When authenticated users add a written review that is less than 5 characters long, they should see a "Review must be at least 5 characters" error appear next to the review field
- If authenticated users submit the form without correcting the errors, the form fields should retain the original values along with their error messages, and an additional "Please fix the errors in the form!" will be displayed to the user

Here's the test code that checks these requirements:

```tsx {{ filename: './__tests__/index.test.tsx', ins: [[5, 41], [43, 80]] }}
describe('Submit Review Page', () => {
  describe('When a user is authenticated', () => {
    const isLoggedIn = true

    it('displays error messages when the user adds a rating that is less than 0', async () => {
      const user = userEvent.setup()

      renderWithProviders(<SubmitReviewPage />, isLoggedIn)

      expect(
        await screen.findByText('Welcome to the Dog Friendly Restaurant Reviews form!'),
      ).toBeInTheDocument()

      expect(screen.queryByTestId('clerk-sign-in')).not.toBeInTheDocument()

      // Note the use of findByRole, as well as async/await. Role lookup is based on the role attribute, which is used to describe the purpose of an element.
      // This is useful for accessibility.
      const reviewInput = await screen.findByRole('textbox', {
        name: /review/i,
      })
      const ratingInput = await screen.findByRole('spinbutton', {
        // Note: can also find by Label!
        name: /rating/i,
      })

      await user.click(reviewInput)
      await user.type(reviewInput, 'Great place!')
      await user.click(ratingInput)
      await user.type(ratingInput, '-1')

      expect(await screen.findByText('Rating must be a number above 0')).toBeInTheDocument()

      await user.click(screen.getByText('Submit'))

      // Note the use of getByLabelText to find the input field by its label text, which helps with accessibility.
      expect(screen.getByLabelText('Review')).toHaveValue('Great place!')
      expect(screen.getByLabelText('Rating')).toHaveValue(-1)

      // expect an error message
      expect(await screen.findByText('Please fix the errors in the form!')).toBeInTheDocument()
    })

    it('displays error messages when the user adds a review that is less than 5 characters', async () => {
      const user = userEvent.setup()

      renderWithProviders(<SubmitReviewPage />, isLoggedIn)

      // Note here the user of findByText based on the page's text content as viewed by the user.
      expect(
        await screen.findByText('Welcome to the Dog Friendly Restaurant Reviews form!'),
      ).toBeInTheDocument()

      // Note the use of queryByTestId to check for non-existence.
      expect(screen.queryByTestId('clerk-sign-in')).not.toBeInTheDocument()

      // Note the use of findByRole, as well as async/await.
      const reviewInput = await screen.findByRole('textbox', {
        name: /review/i,
      })
      const ratingInput = await screen.findByRole('spinbutton', {
        // Note: can also find by Label!
        name: /rating/i,
      })

      await user.click(reviewInput)
      await user.type(reviewInput, 'ok')
      await user.click(ratingInput)
      await user.type(ratingInput, '5')

      expect(await screen.findByText('Review must be at least 5 characters')).toBeInTheDocument()

      await user.click(screen.getByText('Submit'))

      // Expect the form to be cleared
      expect(screen.getByLabelText('Review')).toHaveValue('ok')
      expect(screen.getByLabelText('Rating')).toHaveValue(5)

      // Expect an error message
      expect(await screen.findByText('Please fix the errors in the form!')).toBeInTheDocument()
    })
  })
})
```

> \[!NOTE]
> While the previous examples used a single test, here you've created separate tests for each validation error case, making it easier to identify and debug specific failures.

This code simulates a user interaction where an invalid review is submitted. It sets up a user event, renders the `<SubmitReviewPage />` for an authenticated user, and then mimics user actions of submitting a valid review but an invalid rating.

The `async` and `await` syntax is used to ensure that each action and corresponding assertion happens in the correct order, as some operations are asynchronous and need to complete before the next action or assertion takes place.

### Running the integration tests

Execute your integration tests by running `test:jest`:

```bash {{ filename: 'Terminal' }}
npm run test:jest
```

This script, defined earlier, will run the tests and display the results in your terminal:

![./jest-results.png](./jest-results.png)

## End-to-end tests

Having implemented integration tests, let's turn our attention to end-to-end (E2E) tests.

> \[!NOTE]
> Integration tests focus on verifying that specific components interact correctly with each other, while E2E tests validate complete user workflows across an entire application in production-like environments.
>
> Both are necessary because integration tests provide faster, more targeted feedback about component interactions during development, while E2E tests ensure the complete system functions properly from a user's perspective before release.
>
> [Learn more](https://microsoft.github.io/code-with-engineering-playbook/automated-testing/e2e-testing/) about end-to-end tests.

Unlike integration tests that mock Clerk, E2E tests interact with Clerk directly to simulate production conditions.

To write E2E tests, you will be using two essential tools:

- **[Playwright](https://playwright.dev/)**: A powerful automation framework that allows you to perform end-to-end testing across multiple browsers. It offers comprehensive features for simulating user interactions and validating web applications.
- **[@clerk/testing](https://www.npmjs.com/package/@clerk/testing)**: This utility offers helpful tools specifically designed for testing Clerk applications.

### Set up Playwright and @clerk/testing

Start by create a new directory called `./e2e` - this is where you'll write your E2E in the upcoming section.

Install `@clerk/testing` as a dev dependency:

```bash {{ filename: 'Terminal' }}
npm install @clerk/testing --save-dev
```

Initialize and install Playwright:

```bash {{ filename: 'Terminal' }}
npm init playwright
```

Choose the following options when prompted:

```bash {{ filename: 'Terminal' }}
> Do you want to use TypeScript or JavaScript? → TypeScript
> Where to put your end-to-end tests? → e2e
> Add a GitHub Actions workflow? → N
> Install Playwright browsers? → Y
```

> \[!NOTE]
>
> `npm init playwright` automatically installs Playwright as a dev dependency and creates a default `playwright.config.ts` file

Replace the contents of `./playwright.config.ts` to configure Playwright for end-to-end testing:

```typescript {{ filename: './playwright.config.ts' }}
import { defineConfig } from '@playwright/test'

// Set the port for the server
const PORT = process.env.PORT || 3000

// Set webServer.url and use.baseURL with the location of the WebServer
// respecting the correct set port
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
  // Look for tests in the "e2e" directory
  testDir: './e2e',
  // Set the number of retries for each, in case of failure
  retries: 1,
  // Run your local dev server before starting the tests.
  webServer: {
    command: 'npm run dev',
    // Base URL to use in actions like `await page.goto('/')`
    url: baseURL,
    // Set the timeout for the server to start
    timeout: 120 * 1000,
    // Reuse the server between tests
    reuseExistingServer: !process.env.CI,
  },
  use: {
    // Base URL to use in actions like `await page.goto('/')`.
    baseURL,

    // Collect trace when retrying the failed test.
    // See https://playwright.dev/docs/trace-viewer
    trace: 'retry-with-trace',
  },
})
```

Define an additional script to run E2E tests:

```json {{ filename: './package.json', ins: [7], prettier: false }}
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint",
  "test:jest": "jest",
  "test:playwright": "playwright test --ui"
},
```

To ensure your E2E tests don't inadvertently run when invoking the `test:jest` script - which could cause errors due to Playwright tests being incompatible with Jest - update `jest.config.ts` as follows:

```typescript {{ filename: './jest.config.ts', ins: [17] }}
import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  dir: './',
})

const config: Config = {
  clearMocks: true,
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  // map next-router-mock to the next/navigation module it mocks
  // learn more - https://github.com/scottrippey/next-router-mock
  moduleNameMapper: {
    '^next/navigation$': 'next-router-mock',
  },
  testPathIgnorePatterns: ['<rootDir>/e2e/', '<rootDir>/.next/', '<rootDir>/node_modules/'],
}

export default createJestConfig(config)
```

#### Enable Clerk testing tokens

Clerk [testing tokens](/docs/testing/overview#testing-tokens) allow you to bypass Clerk's bot detection mechanisms, which can otherwise block automated test requests with "bot traffic detected" errors.

The @clerk/testing library makes accessing Clerk testing tokens easy by automatically obtaining one when your test suite starts. It then offers the `setupClerkTestingToken` function, which injects the token, enabling your tests to bypass Clerk's bot detection mechanisms without any hassle. The `clerk.signIn` method internally uses the `setupClerkTestingToken` helper, so there's no need to call it separately when using this method.

> \[!NOTE]
> While it's helpful to understand why this is necessary, @clerk/testing and the Playwright integration abstract away the details, so you don't have to manage tokens manually.

To configure Playwright with Clerk, create `./e2e/global.setup.ts` and call the `clerkSetup()` function:

```typescript {{ filename: './e2e/global.setup.ts' }}
import { clerkSetup } from '@clerk/testing/playwright'
import { test as setup } from '@playwright/test'

// Setup must be run serially, this is necessary if Playwright is configured to run fully parallel: https://playwright.dev/docs/test-parallel
setup.describe.configure({ mode: 'serial' })

setup('global setup', async ({}) => {
  await clerkSetup()
})
```

Next, make sure `global.setup.ts` is called from `./playwright.config.ts`:

```typescript {{ filename: './playwright.config.ts', del: [1], ins: [2, [34, 49]] }}
import { defineConfig } from '@playwright/test'
import { defineConfig, devices } from '@playwright/test'

// Set the port for the server
const PORT = process.env.PORT || 3000

// Set webServer.url and use.baseURL with the location of the WebServer
// respecting the correct set port
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
  // Look for tests in the "e2e" directory
  testDir: './e2e',
  // Set the number of retries for each, in case of failure
  retries: 1,
  // Run your local dev server before starting the tests.
  webServer: {
    command: 'npm run dev',
    // Base URL to use in actions like `await page.goto('/')`
    url: baseURL,
    // Set the timeout for the server to start
    timeout: 120 * 1000,
    // Reuse the server between tests
    reuseExistingServer: !process.env.CI,
  },
  use: {
    // Base URL to use in actions like `await page.goto('/')`
    baseURL,

    // Collect trace when retrying the failed test.
    // See https://playwright.dev/docs/trace-viewer
    trace: 'retry-with-trace',
  },

  // Configure projects for major browsers
  projects: [
    {
      name: 'global setup',
      testMatch: /global\.setup\.ts/,
    },
    {
      name: 'Main tests',
      testMatch: /user-submits-review\.spec\.ts/,
      use: {
        ...devices['Desktop Chrome'], // or your browser of choice
      },
      dependencies: ['global setup'],
    },
  ],
})
```

#### Create a test Clerk user

Since E2E tests interact with the Clerk API and require user authentication, you must create a real Clerk user and supply your test runner with the user's credentials. These credentials allow your tests to sign in, simulate an authenticated user state, and ensure everything functions as expected in a real-world scenario.

Create a test user through the Clerk dashboard:

![./create-user.png](./create-user.png)

Add the username and password to `.env.local`. Also add your Clerk application's `SECRET_KEY` and `PUBLISHABLE_KEY` if you haven't already:

```bash {{ filename: './env.local' }}
E2E_CLERK_USER_USERNAME=xxxxxxxxx
E2E_CLERK_USER_PASSWORD=xxxxxxxxx
CLERK_SECRET_KEY=xxxxxxxxx
CLERK_PUBLISHABLE_KEY=xxxxxxxxx
```

> \[!IMPORTANT]
> Replace `xxxxxxxxx` with your actual credentials.
>
> If you have enabled usernames for your Clerk application, set `E2E_CLERK_USER_USERNAME` to a username. If your Clerk application does not support usernames, set it to an email address instead.

Next, update your `playwright.config.ts` file to load environment variables such that they can be accessed from your tests:

```typescript {{ filename: './playwright.config.ts', ins: [3, 4, 6, 7] }}
// At the top of the file
import { defineConfig, devices } from '@playwright/test'
import dotenv from 'dotenv'
import path from 'path'

// Read the .env.local file and set the environment variables
dotenv.config({ path: path.resolve(__dirname, '.env.local') })
```

You now have everything in place to write an end-to-end test.

### Test case: Users can authenticate and successfully submit a review

This test verifies a critical user flow in a Clerk-authenticated application: signing in, submitting a review, and signing out.

Since this is an end-to-end test, it will cover multiple points in the system, ensuring that real user interactions work as expected.

Create `./e2e/user-submits-review.spec.ts` and paste the following:

```typescript {{ filename: './e2e/user-submits-review.spec.ts' }}
import { test, expect } from '@playwright/test'
import { clerk } from '@clerk/testing/playwright'

test('user can sign in, submit a review and sign out', async ({ page }) => {
  await page.goto('/sign-in')

  // Clerk's signIn utility uses setupClerkTestingToken() under the hood, so no reason to call it separately
  await clerk.signIn({
    page,
    signInParams: {
      strategy: 'password',
      identifier: process.env.E2E_CLERK_USER_USERNAME!,
      password: process.env.E2E_CLERK_USER_PASSWORD!,
    },
  })

  await page.goto('/submit-review')

  await expect(page).toHaveURL('/submit-review')

  // Fill in the review form
  await page.getByLabel(/review/i).fill('Had a great experience!')
  await page.getByLabel(/rating/i).fill('5')
  await page.getByRole('button', { name: /submit/i }).click()

  await expect(page.getByText(/form submitted successfully/i)).toBeVisible()

  // Clerk's signOut utility uses setupClerkTestingToken() under the hood, so no reason to call it separately
  await clerk.signOut({ page })

  expect(page).toHaveURL('/')
})
```

> \[!TIP]
>
> The `clerk.signIn()` function automatically uses the `setupClerkTestingToken()` helper, so there's no need to call it manually.

The test starts by navigating to the sign-in page and using Clerk's `signIn` utility to authenticate a test user.

Once signed in, the test redirects to the review submission page, where it completes and submits a form, then verifies a success message. Finally, it signs out and confirms redirection to the home page.

Running this test in the future will help catch any errors introduced by code changes. If a change breaks the functionality, the test will fail, alerting you to the issue. If the test passes, you can be confident that this flow in your application is working as it should.

### Running the end-to end tests

Run `test:playwright` to execute your end-to-end tests:

```bash {{ filename: 'Terminal' }}
npm run test:playwright
```

The result:

![./playwright-results.png](./playwright-results.png)

## Conclusion

In the introduction, key questions were posed about testing in Clerk applications:

- What should be tested?
- How should it be tested?
- Should integration or end-to-end tests be used?
- How can Clerk be effectively mocked?

This post has provided comprehensive answers to these questions, guiding you through the process of developing a robust testing strategy against a practical application.

Equipped with this knowledge, you now have the tools to confidently implement a balanced testing strategy for any appliation using Clerk for [Next.js authentication](/nextjs-authentication).

For more detailed guidance on testing Clerk applications, be sure to check out the [Clerk documentation on testing](/docs/testing/overview).

---

# 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 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).

---

# 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.

---

# 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).

---

# What is middleware in Next.js?
URL: https://clerk.com/blog/what-is-middleware-in-nextjs.md
Date: 2025-01-16
Category: Guides
Description: Learn all about middleware in Next.js and how it works, as well as some of its common use cases, in this comprehensive guide.

Next.js middleware provides you with an incredible opportunity to customize the way your [Next.js application](https://nextjs.org) handles requests.

Middleware enables developers to intercept requests and perform operations like session validation, logging, and caching. While it may be tempting to use middleware to process and apply logic to every request to the application, doing so improperly might lead to massive performance depredations for your application. Once you understand how middleware works, you'll be better equipped to use middleware and understand when it shouldn't be used.

In this comprehensive guide, you'll learn what middleware is as it pertains to Next.js, how it works, and some of it's common use cases.

## What is Next.js middleware?

[Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) in Next.js refers to functions that run automatically for every incoming request, allowing you to inspect or modify the request data before it reaches your application's routing system.

Middleware can be used for a variety of purposes, such as [authentication](/nextjs-authentication), logging, and error handling. For example, you could use [middleware to authenticate](/docs/references/nextjs/clerk-middleware) incoming requests by checking tokens or credentials before allowing the request to proceed to your application's routing system.

Another benefit of using middleware in Next.js is its flexibility and customizability.

You can write your own middleware functions to fit the specific needs of your application to set application-wide settings or policies. This prevents you from having to worry about the complexity of having multiple layers of routing configuration.

By leveraging middleware, you can create a more robust, scalable, and maintainable application that meets the demands of complex web applications.

## When does Next.js process the middleware?

Next.js performs a series of operations when a request is received, so it helps to understand where middleware is handled in the order of operations:

### 1. `headers`

The `headers` configuration from `next.config.js` is applied first, setting the initial headers for every incoming request. This stage can be used to set security-related headers, such as content security policy or cross-origin resource sharing (CORS) headers.

### 2. `redirects`

The `redirects` configuration from `next.config.js` follows, determining how requests are redirected to other URLs. This stage handles URL rewriting and redirects, allowing you to manage routing rules that affect multiple pages or entire applications.

### 3. Middleware evaluation

Once `headers` and `redirects` are processed from the Next.js config file, the middleware is evaluated, and any logic within is executed. As you might expect, we’re going to dive deeper into this step throughout the guide.

### 4. `beforeFiles`

Next, the `beforeFiles` (`rewrites`) from `next.config.js` is applied. This stage allows you to perform additional rewriting or file-specific logic before routing takes place.

### 5. File system routes

The application's file system routes come into play next, including directories like `public/` and `_next/static/`, as well as individual pages and apps. This stage is where your application's static files are served.

### 6. `afterFiles`

Next up the `afterFiles` (`rewrites`) from `next.config.js` apply, providing a final chance to modify request data before dynamic routing takes place.

### 7. Dynamic Routes

Dynamic routes, like `/blog/[slug]`, execute next in the sequence. These routes require specific handling and rewriting logic to accommodate variables or parameters.

### 8. `fallback`

Finally, the `fallback` from `next.config.js` is applied, determining what happens when a request can't be routed using other configurations. This stage provides an opportunity to implement error handlers or fallback routes.

## What are some common use cases for Next.js middleware?

### Authentication

[Authentication](/nextjs-authentication) can be used with a login system where a user's credentials are validated before accessing sensitive routes or data. For instance, you might use Next.js middleware to validate a user's session on every request, redirecting them to the login page if their token is invalid.

[Clerk](https://clerk.com) uses Next.js middleware to intercept the request and determine the user's authentication state, something we'll explore in more detail later in this article.

### Logging

Logging logic can be added to middleware to track important events in your application, such as user actions or errors. You might implement logging using Next.js middleware to log every request to a centralized server, allowing you to analyze and debug issues more efficiently.

### Data fetching

While there are certain limitations to what kind of fetching can be performed, middleware can technically be used to load data from an API or database on every request, providing the most up-to-date information to users.

We'll explore the limitations of Next.js middleware in a later section.

### Request routing

Middleware can be used to customize the routing behavior of your application, such as catching all requests to a certain path and redirecting them to another route. This might be useful for implementing a catch-all error handler or for rewriting URLs to use a different domain.

### Cacheing

Cacheing can be used to improve performance by storing frequently used resources in memory and controlling the number of requests from individual users. The following example would check a cache object for the content of the request. If found, it is returned, otherwise, the response is intercepted and added to the cache for the next request.

```tsx
import { NextResponse } from 'next/server'

const cache = new Map()

export function middleware(request) {
  const { pathname } = request.nextUrl

  // Check if the response is cached
  if (cache.has(pathname)) {
    return new NextResponse(cache.get(pathname), {
      headers: { 'X-Cache': 'HIT' },
    })
  }

  // If not cached, proceed with the request
  const response = NextResponse.next()

  // Cache the response for future requests
  response.then((res) => {
    const clonedRes = res.clone()
    clonedRes.text().then((body) => {
      cache.set(pathname, body)
    })
  })

  response.headers.set('X-Cache', 'MISS')
  return response
}

export const config = {
  matcher: '/api/:path*',
}
```

### Rate limiting

Similarly, you could use middleware to keep track of requests coming from a single user or IP address and block the request if that user is making too many requests too frequently. This can help prevent upstream resources (ie: database) from being impacted for other uses.

### Page transforms

HTML rewrites and data transforms can be used to customize the behavior of your application when serving HTML files or transforming data in real-time. For instance, you might use Next.js middleware to rewrite URLs for images and other static assets, allowing you to host them on a different domain or with a custom subdomain.

### Analytics/reporting

Analytics and reporting can be used to track user behavior and monitor application performance, providing insights for improving the overall experience. You could use Next.js middleware to modify cookies on the fly, allowing you to integrate tracking scripts from third-party analytics providers without affecting the application's functionality.

### Internationalization

Internationalization can be used to deliver content in multiple languages and adapt the UI based on the user's locale. For example, you might determine a user's location by their IP or an HTTP header using middleware, redirecting users to a different language version of your application when they access it with a specific query parameter or cookie.

## How can I use middleware in a Next.js project?

To use middleware in a Next.js project, you'd create a single file at the root of the project called `middleware.ts` and add the necessary components.

Creating middleware involves defining a `middleware` function and (optionally) a matcher.

### The `middleware` function

The `middleware` function is where the logic of the middleware is stored. It uses a request as the single parameter and returns a response like so:

```tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Your middleware logic here
  return NextResponse.next()
}
```

Using the `NextRequest` and `NextResponse` objects, you could write a basic middleware to redirect requests to `/dashboard`, while allowing requests to other routes to proceed:

```tsx
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next()
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/sign-in', request.url))
  }
}
```

It's important to note that the `middleware` function must return one of the following responses:

- `NextResponse.next()` - Allows the request to proceed to its destination.
- `NextResponse.redirect()` - Redirects the response to another route.
- `NextResponse.rewrite()` - Transparently renders an alternate route internally without forcing the browser to redirect.
- `NextResponse.json()` - Returns raw JSON to the caller.
- `Response`/`NextResponse` - You can craft a custom response to the caller.

### The matcher

The matcher is how Next.js decides if a request should be processed by middleware. The matcher is defined in and exported via the `config` object like so:

```tsx
export const config = {
  matcher: '/hello',
}
```

You can also define a matcher in a number of ways. The above example matches a single route, but you can also include an array of routes:

```tsx
export const config = {
  matcher: ['/hello', '/world'],
}
```

And for more complex scenarios, you can use regex:

```tsx
export const config = {
  matcher: ['/hello', '/world', '//[a-zA-Z]+/'],
}
```

If a matcher is not specified, Next.js will use the middleware for ALL routes. This can cause the middleware to run when it really doesn't need to, which can lead to degraded performance and potentially increased hosting costs.

## How to combine multiple Next.js middleware

Next.js only supports one middleware file and function per project. If you need to use multiple functions, you'd have to create separate functions that return the appropriate response and call them in sequence, conditionally returning a response if a middleware generates one.

For example, let's say you want to use both a logging middleware and an authentication middleware.

First, create two separate middleware functions:

```tsx
export function logRequest(req) {
  console.log(`Request made to: ${req.nextUrl.pathname}`)
}
```

```tsx
import { NextResponse } from 'next/server'

// Assuming an in-memory cache for sessions
const sessions = new Map()

export function checkAuth(req) {
  const token = req.cookies.get('auth-token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  // Check if the session exists and is not expired
  if (sessions.has(token)) {
    const session = sessions.get(token)
    if (session.expiration < Date.now()) {
      // Session has expired, clear it from cache and redirect to login
      sessions.delete(token)
      return NextResponse.redirect(new URL('/login', req.url))
    }
    // Valid session, proceed with the request
    return
  } else {
    // Session not found or invalid, redirect to login
    return NextResponse.redirect(new URL('/login', req.url))
  }
}
```

Then, in your `middleware.ts` file, use both of these functions in sequence, returning the response from `checkAuth` if the authentication checks fail:

```tsx
import { NextResponse } from 'next/server'
import { checkAuth } from './middleware/checkAuth'
import { logRequest } from './middleware/logRequest'

// Main Middleware File
export function middleware(req) {
  logRequest(req)

  const authResponse = checkAuth(req)
  if (authResponse) return authResponse

  return NextResponse.next()
}

export const config = {
  matcher: ['/hello', '/world', '//[a-zA-Z]+/'],
}
```

## How does Clerk use Next.js middleware?

Clerk uses middleware to protect routes as they come into your Next.js application. The `clerkMiddleware` function actually wraps the typical middleware logic and internally will parse the cookies coming into the request and verify them with your userbase in Clerk.

```tsx
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

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)(.*)',
  ],
}
```

Since we wrap the middleware logic, we can extend it and provide helper functions like `auth` which makes it easier to protect routes:

```tsx
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/forum(.*)'])

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 body of the callback in `clerkMiddlware` works just like a standard middleware so you can also apply custom routing rules. For example, the following snippet shows you how to reroute the incoming request to `/onboarding` only if the user is logging in for the first time:

```tsx
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isPublicRoute = createRouteMatcher(['/'])

export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionClaims, redirectToSignIn } = await auth()

  // If the user isn't signed in and the route is private, redirect to sign-in
  if (!userId && !isPublicRoute(req)) {
    return redirectToSignIn({ returnBackUrl: req.url })
  }

  // Catch users who do not have `onboardingComplete: true` in their publicMetadata
  // Redirect them to the /onboading route to complete onboarding
  if (
    userId &&
    !sessionClaims?.metadata?.onboardingComplete &&
    req.nextUrl.pathname !== '/onboarding'
  ) {
    const onboardingUrl = new URL('/onboarding', req.url)
    return NextResponse.redirect(onboardingUrl)
  }

  // All other routes are protected and the user is authenticated, let them view the requested page
})

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)(.*)',
  ],
}
```

> To learn more about how this onboarding flow works, check out [How to Add an Onboarding Flow for your Application with Clerk on our blog](/blog/add-onboarding-flow-for-your-application-with-clerk).

## Limitations to consider with Next.js middleware

Next.js middleware has several limitations that developers should be aware of:

### Edge Runtime Constraints

Middleware runs on the Edge Runtime, limiting the APIs and libraries that can be used. The Edge Runtime provides a subset of Node.js APIs, which means that middleware must rely on these limited resources. While this limitation may seem restrictive, it helps ensure that middleware functions are fast and efficient.

Because of the Edge Runtime, they also cannot use native Node.js APIs or perform operations like reading and writing to the file system.

### Size Restriction

Middleware functions are limited to 1MB in size, including all bundled code. This restriction is in place to ensure that middleware does not consume too much memory and can handle a large number of requests efficiently.

### ES Modules Only

Only ES Modules can be used in middleware. CommonJS modules are not supported. This is because ES Modules provide a more secure and efficient way of managing dependencies, which is essential for middleware functions.

### No String Evaluation

JavaScript's `eval` and `new Function(evalString)` are not allowed within the middleware runtime. This restriction helps prevent potential security vulnerabilities by blocking access to arbitrary code execution.

### Performance Considerations

Since middleware runs before every request with a matched route, complex or time-consuming operations in middleware can block users from receiving responses quickly. To mitigate this issue, developers should focus on writing lightweight and efficient middleware code that does not hinder the performance of their application.

It's also the reason that accessing a database within the middleware is generally not a good idea. It not only adds latency to the request but could also impact the performance of your database.

### Limited Access to Request/Response

Middleware does not have full access to complete request and response objects, which can limit certain dynamic operations. Specifically, the middleware cannot access the full URL path name, the request/response body, and some of the headers.

To work around this limitation, developers can use techniques like callbacks or promises to interact with the request and response objects.

## Conclusion

You are now better equipped to use Next.js middleware in the real world.

In this article, we have explored Next.js middleware from an introductory level but also discussed how it works, when it runs in relation to other operations Next performs with every request, and some of the best use cases for middleware.

---

# How to customize Next.js metadata
URL: https://clerk.com/blog/how-to-customize-nextjs-metadata.md
Date: 2025-01-09
Category: Guides
Description: Learn all about metadata and how to set it in your Next.js application

Improperly configured website metadata can cause drastic issues in user experience and website discoverability.

It’s important to not only understand what metadata is, but how it’s used by the greater internet, and how to configure it in your [Next.js](https://nextjs.org/) application. Next.js offers a number of different options when it comes to setting metadata, and the best option depends on the version of the framework you are using, and the way you are using it to generate pages for your visitors.

In this article, you'll learn the various ways you can customize and optimize your Next.js website's metadata to improve its SEO and user experience, as well as suggestions on when to use each approach.

## How is metadata used?

Wikipedia defines [metadata](https://en.wikipedia.org/wiki/Metadata) as "data that provides information about other data".

In the context of websites, metadata refers to the invisible data that describes the content of a website. This metadata is used by search engines, social media platforms, and other web services to understand the structure, content, and meaning of your website.

Depending on the configuration, missing or incorrect metadata can actively hurt the performance of your website. Here are some ways metadata is used:

- **Search engine optimization (SEO)**: Search engines use metadata to determine the relevance of your webpage for specific searches, as well as how those search engines rank a specific web page. The more accurate and defined the metadata is, the better a search engine will know to serve it when it matches a query.
- **Social media sharing**: Social media platforms use Open Graph metadata to display your website's content in their feeds, such as Facebook, Twitter, and LinkedIn. This is the information that's used to show stylized cards when a link is pasted in.
- **Content discovery**: Metadata helps users discover content that matches their interests and preferences.
- **Rich snippets**: Search engines can extract structured data from your website to display rich snippets in search results. Below is an example of rich snippets that are displayed with a movie to show ratings:

![An example of rich snippets that are displayed with a movie to show ratings](./schema-pic.png)

## What problems can occur with misconfigured metadata?

A number of issues can arise when metadata is not configured correctly.

For instance, poorly configured metadata can lead to lower search rankings, making it less likely users will find your website (e.g. ranking outside of the first search engine results page (SERP). This can be extremely detrimental to websites that thrive from organic traffic such as online shops. Furthermore, inaccurate or misleading metadata can damage a website's reputation and credibility, leading users to question the trustworthiness of its content.

Broken links and 404 errors can occur when metadata is incorrect or missing, creating a poor user experience. Other issues that create a poor user experience include incomplete page titles, descriptions, or images, duplicate content issues, inconsistent branding across different pages or sections, mobile usability issues, which increase the likelihood of users abandoning  if errors or inconsistencies are encountered.

By properly setting up your metadata, you can avoid these problems and create a better experience for your users.

## How does Next.js handle metadata by default?

By default, Next.js includes basic metadata such as the page title, character set, and viewport settings in the HTML head section of each page. The following is the HTML `head` of a newly generated Next.js project:

```html
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Create Next App</title>
  <meta name="description" content="Generated by create next app" />
  <meta name="next-size-adjust" content="" />
  <!-- script and link tags removed for brevity -->
</head>
```

However, this default behavior does not provide any specific metadata for SEO or social media optimization. You’ll want to add your own metadata for the application, and often on a per-page basis.

## How to customize metadata in Next.js

Next.js offers several solutions for customizing metadata depending on the application's needs.

### Using `next/head`

The first approach uses the built-in `next/head` component that can include Open Graph tags, Twitter cards, and other relevant metadata. This allows you to tailor your metadata to specific pages or routes, giving you more control over how your content is presented in search results and social media platforms.

```tsx
import Head from 'next/head'

export default function Home() {
  return (
    <>
      <Head>
        <title>My Custom Title</title>
        <meta name="description" content="This is my custom description for SEO." />
        <meta name="keywords" content="Next.js, metadata, SEO" />
        <meta property="og:title" content="My Custom Title" />
        <meta property="og:description" content="Open Graph description for sharing." />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <h1>Welcome to My Page</h1>
      </main>
    </>
  )
}
```

This approach is primarily used for client-side components where the `head` of the page needs to be changed dynamically based on the client interaction. For example, if you need to update the title or favicon of a tab in the browser.

### Exporting the `metadata` object

In versions of Next.js that use the App Router, you may also define the page metadata by exporting a variable aptly named `metadata`. This lets you define your metadata in a structure:

```tsx {{ filename: 'app/page.tsx' }}
export const metadata = {
  title: 'Clerk | Authentication and User Management',
  description:
    'The easiest way to add authentication and user management to your application. Purpose-built for React, Next.js, Remix, and “The Modern Web”.',
  keywords: ['Next.js', 'Authentication', 'User Management'],
  openGraph: {
    title: 'Clerk',
    description: 'The best user management and authentication platform.',
    url: 'https://clerk.com',
  },
}

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

If you want to apply the same values across multiple pages, you can also export `metadata` from a layout file as well. This would apply the metadata to any child route of that layout.

Using this approach is best suited for pages with metadata that does not change frequently.

### Generating metadata dynamically

Finally, if you are generating metadata values on page load, you can use the `generateMetadata()` function to dynamically set the values. This is used in situations where a page template renders differently depending on the data that's loaded into it.

Using a blog as an example, there is typically one page template that's used to render every post, with the post slug being passed in as a page parameter. The following snippet demonstrates how this approach works. The slug is used to fetch the data for the post from an API before generating the metadata for that page:

```tsx {{ filename: 'app/blog/[slug]/page.tsx' }}
export async function generateMetadata({ params }) {
  const res = await fetch(`/api/posts/${params.slug}`)
  const post = await res.json()

  return {
    title: post.title,
    description: post.summary,
    openGraph: {
      title: post.title,
      description: post.summary,
      url: `https://clerk.com/blog/${params.slug}`,
      images: [{ url: post.image }],
    },
  }
}

export default function BlogPost({ params }) {
  return <h1>Blog Post: {params.slug}</h1>
}
```

Any place where the page is generated dynamically based on a data source would use this method.

## Next.js metadata inheritance

When customizing Next.js metadata, you don't need to specify the same values in all routes.

Next.js will automatically apply inheritance rules from the parent route to any child route if they are not defined. Using the blog example, if the root of the website has the following metadata defined in the topmost layout file:

```tsx
export const metadata = {
  title: 'Clerk | Authentication and User Management',
  description:
    'The easiest way to add authentication and user management to your application. Purpose-built for React, Next.js, Remix, and “The Modern Web”.',
  keywords: ['Next.js', 'Authentication', 'User Management'],
  openGraph: {
    title: 'Clerk',
    description: 'The best user management and authentication platform.',
    url: 'https://clerk.com',
  },
}
```

And also has this function that generates metadata for blog posts:

```tsx
export async function generateMetadata({ params }) {
  const res = await fetch(`/api/posts/${params.slug}`)
  const post = await res.json()

  return {
    title: post.title,
    description: post.summary,
    openGraph: {
      title: post.title,
      description: post.summary,
      url: `https://clerk.com/blog/${params.slug}`,
    },
  }
}
```

The `keywords` value would still be set on the blog post even though it is not explicitly defined in the `generateMetadata` function. This is because child metadata overwrites parent metadata, but only if a value is present.

## Conclusion

As you've learned throughout this guide, customizing metadata in Next.js can have a significant impact on user experience and how search engines rank your website. By understanding how to define and generate metadata dynamically, you'll be able to create a seamless and informative experience for your visitors.

---

# Building a React Login Page Template
URL: https://clerk.com/blog/building-a-react-login-page-template.md
Date: 2024-12-20
Category: Guides
Description: Learn how session-based authentication works and how to implement it in a React app with Express.

[Authentication](/glossary#authentication) is a crucial aspect of any web application that requires users to sign in and manage their accounts.

In this article, we'll be exploring how to implement a basic [authentication system using Express](/docs/quickstarts/express) as well as a signup and login form in [React.js](/glossary#react). You'll learn the difference between the JWT- and session-based authentication and some associated best practices. You'll then learn how to implement session authentication step by step using a real-world demo that's before getting access to a ready-to-use React login page template based on the steps outlined in this guide.

> For a production-ready solution, [learn more about our React support](/react-authentication) which handles all the complexity shown in this guide.

By the end of this tutorial, you'll have a solid understanding of how to create a login page using React, handle user registration, and protect routes from unauthenticated users.

We also have a guide on how to [build this same functionality into a Next.js application](/blog/building-a-nextjs-login-page-template) on our blog.

## Types of authentication

There are different ways to authenticate users, but the primary methods today are JWT-based authentication and session-based authentication.

### Session-based authentication

Session-based authentication is a method of tracking a user's login state by creating a unique [session](/glossary#session) for each authenticated user. When a user logs in, the server generates a session ID and stores session information server-side, typically in memory or a database. This session ID is then sent to the client, usually as a cookie, which the client includes with subsequent requests to prove authentication.

Cookies are used since they are sent back to the server with each request. When the server receives the session ID, it can look up the session in the database to both confirm its validity, as well as reference the associated user.

This article covers how to implement session-based authentication into a React application.

### JWT-based authentication

[JSON Web Token (JWT)](/glossary#json-web-token) authentication is an authentication method where a signed, encoded token is generated upon user login and returned to the client.

The JWT contains information such as the user ID, issue date, expiration date, etc. Once verified by the server using a private key, the server can trust that the information within the JWT correctly identifies the party that made the request.

Unlike session-based authentication, JWTs are self-contained and eliminate the need for server-side session storage.

You can read more about JWT vs. sessions in our article [Combining then benefits of session tokens and JWTs](/blog/combining-the-benefits-of-session-tokens-and-jwts).

## What is required to implement session authentication?

Implementing session-based authentication includes several key requirements.

### Storage

A storage mechanism is required to hold information about the users and the sessions. Using a database would require two tables, one for each entity. The following table diagram outlines the minimum requirements to implement session-based authentication.

![A database table diagram showing the minimum requirements to implement session-based authentication](./dbdiagram.png)

The `users` table will store general information about the user such as their name, email address, and a hashed version of the password. The `sessions` table will store details about each session.

Notice how the sessions table contains a userId field which is a foreign key of the users table. This is used to associate that session with a specific user.

### Handling sign up

Signing up the user will require a [form](/blog/validate-create-style-react-bootstrap-forms) to be created on the front end that is accessible if the user is not already authenticated. This typically includes, at minimum, inputs for username and password.

The server needs a route handler that can accept the user’s credentials when they click “Sign up”. The simplest implementation of this will create a user record and store the username and a hashed version of the password.

NEVER store the user credentials in plain text. If anyone obtains unauthorized access to your database, all of your users’ credentials will be exposed.

It’s also a best practice to implement validation on both the form the user interacts with and the route handler on the server. This ensures that any requirements for the inputs (ex: password complexity) are met before any records are created.

![A sequence diagram showing the steps involved in signing up a user](./sequence-diagram.png)

1. Using the login page in React, the user submits their username and password to the server.
2. The server creates a user record in the database.
3. The database returns the ID of the new record.
4. The server creates a session with the user ID.
5. The database returns the ID of the new record.
6. The server sets the cookie and responds back to the front end.

### Handling sign in

A login form is also required to allow the user to sign in. Again, the simplest implementation is at least a username and password.

As with handling sign-up, a route handler also needs to be created on the server to handle the credentials when they are submitted. Instead of creating a new user record, the credentials are verified with the existing user record.

If the React login page contains the proper credentials and they match when checked with the database, a session is created in the database. The token is added to a cookie that is sent back to the client.

Cookies are used for several reasons:

- They are automatically sent with each request to the server.
- Cookies can be configured to prevent client-side scripts from accessing them, making them more secure than local storage.
- An expiration can be set directly on the cookie, preventing the browser from trying to access a protected route when the session is expired.

![A sequence diagram showing the steps involved in signing in a user](./sequence-diagram.png)

1. Using the login page in React, the user submits their credentials to the server.
2. The server queries the user record to check the credentials.
3. The database returns the user record if found.
4. The server compares the hashed passwords. If successful, create a new session in the database.
5. The database responds with the ID of the session
6. The server sets the session ID in the cookie and sends it back to the client.

While this article will guide you through implementing session authentication in your React app, [check out how Clerk can accomplish it with only a few lines of code!](/docs/quickstarts/react)

### Protecting authorized routes

Finally, routes on both the front end and back end need to be configured to be protected. In a typical React application using `react-router-dom`, Routes can be wrapped in a parent component which will check the authorization status with the server before rendering them. When the React login page is used to authenticate a user, the `<ProtectedRoute />` component will automatically permit access to those views.

```tsx
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from '@/hooks/use-auth'
import { ProtectedRoute } from '@/components/protected-route'
import Home from '@/views/home'
import AppView from '@/views/app'

function AppRoot() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          {/* The following route is protected */}
          <Route
            path="/app"
            element={
              <ProtectedRoute>
                <AppView />
              </ProtectedRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  )
}

export default AppRoot
```

On the server, the session ID that is sent to the server will be checked with the `sessions` table in the database to make sure the sessions is valid before processing the request. In [Express](https://expressjs.com), middleware can be created that wraps routes or route collections to make sure this is done automatically on the proper requests.

## Follow along with Quillmate

To demonstrate how session-based authentication can be implemented, we’ll use a realistic project called Qulllmate.

Quillmate is an open-source web application where writers can create articles with the help of an AI assistant. The core entity of Quillmate is an article, and each article has its own AI assistant that understands what has already been written and helps when asked questions about the material.

The following video demonstrates the finished product, where the user can sign up, sign in, create articles, and ask AI for assistance:

Quillmate is built with React and uses Express to both serve the application, as well as handle backend requests. The project uses Open AI to ask questions about articles, and Neon to store the data.

Quillmate is built with the following tech stack:

- **React** - The front end of the application is built with React.
- **Express** - The application is hosted with Express. Requests to paths starting with `/api` will be handled by various API routes, whereas any other requests will serve up the React app.
- **Neon** - Neon is a PostgreSQL database platform that is used for storing structured data.
- **Prisma** - To simplify requests to the database, Prisma is used as an ORM.
- **Open AI** - The AI Assistant functionality is backed by requests to the Open AI API.

You may optionally clone the [quillmate-react](https://github.com/bmorrisondev/quillmate-react) repository to follow along yourself with this article. Follow the directions in the readme to get the project running on your own system.

## Adding the database tables

Let’s start by adding the `users` table and `sessions` table. This can be done by modifying the Prisma schema and running a script to apply the changes to the Neon database.

Modify the `prisma/schema.prisma` file and add the `User` and `Session` models:

```{{ filename: 'prisma/schema.prisma', prettier: false }}
model User {
  id           Int       @id @default(autoincrement())
  email        String    @unique
  name         String?
  passwordHash String    @map("password_hash")
  sessions     Session[]
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @default(now()) @updatedAt @map("updated_at")
  articles     Article[]

  @@map("users")
}

model Session {
  id        Int      @id @default(autoincrement())
  token     String   @unique
  userId    Int      @map("user_id")
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  expiresAt DateTime @map("expires_at")
  createdAt DateTime @default(now()) @map("created_at")

  @@map("sessions")
}
```

Next, update the `Article` model to include a relationship with the user:

```{{ filename: 'prisma/schema.prisma', ins: [9,10], prettier: false }}
model Article {
  id        Int      @id @default(autoincrement())
  title     String   @db.VarChar(256)
  content   String   @db.Text
  summary   String?  @db.Text
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
  messages  Message[]
  user      User     @relation(fields: [userId], references: [id])
  userId    Int      @map("user_id")

  @@map("articles")
}
```

Finally, push the schema changes to Neon and update the local client by running the following commands in your terminal:

```bash
npx prisma db push
npx prisma generate
```

## Updating Express to support authentication

Next, you’ll need to update the backend to support creating accounts, creating sessions, and protecting the necessary routes.

### Install dependencies

The following dependencies are required:

- `bcryptjs` - Used for salting and hashing passwords before saving them to the database.
- `cookie-parser` - An Express middleware that makes working with cookies easier.
- [`zod`](https://zod.dev) - Used for type validation.

Run the following commands to install those packages and their types:

```bash
npm install bcryptjs cookie-parser zod
npm install -D @types/bcryptjs @types/cookie-parser
```

### Create the Express auth middleware

Next, create the Express middleware that will be used to protect routes. This will be used by protected routes to make sure that the request is authorized, returning a 401 status if it is not. It will also handle removing the session from the database if it’s expired.

Create a file at `server/middleware/auth.ts` and paste in the following:

```ts {{ filename: 'server/middleware/auth.ts' }}
import { Request, Response, NextFunction } from 'express'
import { prisma } from '../db/prisma'

export async function requireAuth(req: Request, res: Response, next: NextFunction) {
  try {
    const token = req.cookies.session

    if (!token) {
      res.status(401).json({ error: 'Authentication required' })
      return
    }

    const session = await prisma.session.findUnique({
      where: { token },
      include: { user: true },
    })

    if (!session) {
      res.clearCookie('session')
      res.status(401).json({ error: 'Invalid session' })
      return
    }

    if (session.expiresAt < new Date()) {
      await prisma.session.delete({ where: { id: session.id } })
      res.clearCookie('session')
      res.status(401).json({ error: 'Session expired' })
      return
    }

    req.user = {
      id: session.user.id,
      email: session.user.email,
      name: session.user.name,
    }
    req.session = {
      id: session.id,
      token: session.token,
    }

    next()
  } catch (error) {
    console.error('Auth middleware error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
}
```

If your editor is complaining about setting the `req.user` and `req.session` values, create a file at `server/types/express.d.ts` and paste in the following:

```ts {{ filename: 'server/types/express.d.ts' }}
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: number
        email: string
        name: string | null
      }
      session?: {
        id: number
        token: string
      }
    }
  }
}

export {}
```

### Add routes to handle auth functions

Now let’s add the routes required to enable sign-up and sign-in. The route file will also include validation from  There will be four routes in total:

- `/api/signin` - Used by the React login page to create a session and return it back to the user.
- `/api/signup` - Used to create an account, which we’ll use to also create a session right away.
- `/api/signout` - Used to sign the user out, which deletes the session. This route is protected by the middleware.
- `/api/me` - Used by the front end to verify that the session is valid before loading the articles. This route is also protected by the middleware.

Create a file at `server/routes/auth.ts` and paste in the following:

```ts {{ filename: 'server/routes/auth.ts' }}
import { Router } from 'express'
import { prisma } from '../db/prisma'
import bcrypt from 'bcryptjs'
import { randomBytes } from 'crypto'
import { ZodError } from 'zod'
import { requireAuth } from '../middleware/auth'
import { z } from 'zod'

const router = Router()

const signUpSchema = z.object({
  email: z.string().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'),
  name: z.string().optional(),
})

// Sign up
router.post('/signup', async (req, res) => {
  try {
    const result = signUpSchema.safeParse(req.body)
    if (!result.success) {
      res.status(400).json({
        error: 'Validation failed',
        details: result.error.errors.map((err) => ({
          path: err.path.join('.'),
          message: err.message,
        })),
      })
      return
    }

    const { email, password, name } = result.data

    // Validate input
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password are required' })
      return
    }

    // Check if user exists
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      res.status(400).json({ error: 'Email already registered' })
      return
    }

    // Hash password
    const salt = await bcrypt.genSalt(10)
    const passwordHash = await bcrypt.hash(password, salt)

    // Create user
    const user = await prisma.user.create({
      data: {
        email,
        passwordHash,
        name,
      },
    })

    // Create session
    const token = randomBytes(32).toString('hex')
    const expiresAt = new Date()
    expiresAt.setDate(expiresAt.getDate() + 30) // 30 days from now

    await prisma.session.create({
      data: {
        token,
        userId: user.id,
        expiresAt,
      },
    })

    // Set cookie
    res.cookie('session', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      expires: expiresAt,
    })

    res.json({
      id: user.id,
      email: user.email,
      name: user.name,
    })
  } catch (err) {
    console.error('Sign up error:', err)
    if (err instanceof ZodError) {
      res.status(400).json({
        error: 'Validation failed',
        details: err.errors.map((err) => ({
          path: err.path.join('.'),
          message: err.message,
        })),
      })
      return
    }
    res.status(500).json({ error: 'Internal server error' })
  }
})

const signInSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(1, 'Password is required'),
})
// Sign in
router.post('/signin', async (req, res) => {
  try {
    const result = signInSchema.safeParse(req.body)
    if (!result.success) {
      res.status(400).json({
        error: 'Validation failed',
        details: result.error.errors.map((err) => ({
          path: err.path.join('.'),
          message: err.message,
        })),
      })
      return
    }

    const { email, password } = result.data

    // Validate input
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password are required' })
      return
    }

    // Find user
    const user = await prisma.user.findUnique({
      where: { email },
    })

    if (!user) {
      res.status(401).json({ error: 'Invalid credentials' })
      return
    }

    // Verify password
    const isValid = await bcrypt.compare(password, user.passwordHash)
    if (!isValid) {
      res.status(401).json({ error: 'Invalid credentials' })
      return
    }

    // Create session
    const token = randomBytes(32).toString('hex')
    const expiresAt = new Date()
    expiresAt.setDate(expiresAt.getDate() + 30) // 30 days from now

    await prisma.session.create({
      data: {
        token,
        userId: user.id,
        expiresAt,
      },
    })

    // Set cookie
    res.cookie('session', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      expires: expiresAt,
    })

    res.json({
      id: user.id,
      email: user.email,
      name: user.name,
    })
  } catch (err) {
    console.error('Sign in error:', err)
    if (err instanceof ZodError) {
      res.status(400).json({
        error: 'Validation failed',
        details: err.errors.map((err) => ({
          path: err.path.join('.'),
          message: err.message,
        })),
      })
      return
    }
    res.status(500).json({ error: 'Internal server error' })
  }
})

// Sign out
router.post('/signout', requireAuth, async (req, res) => {
  try {
    const token = req.cookies.session
    if (token) {
      await prisma.session.delete({
        where: { token },
      })
      res.clearCookie('session')
    }
    res.json({ message: 'Signed out successfully' })
  } catch (error) {
    console.error('Signout error:', error)
    res.status(500).json({ error: 'Failed to sign out' })
  }
})

// Get current user
router.get('/me', requireAuth, async (req, res) => {
  try {
    const token = req.cookies.session
    if (!token) {
      res.json({ user: null })
      return
    }

    const session = await prisma.session.findUnique({
      where: { token },
      include: { user: true },
    })

    if (!session || session.expiresAt < new Date()) {
      res.clearCookie('session')
      res.json({ user: null })
      return
    }

    res.json({
      id: session.user.id,
      email: session.user.email,
      name: session.user.name,
    })
  } catch (error) {
    console.error('Get current user error:', error)
    res.status(500).json({ error: 'Failed to get current user' })
  }
})

export default router
```

### Update the `/api/articles` route to filter by user

Next, we’ll need to update the `/api/articles` routes to ensure that when database records are created, the user information is saved with the record as well. This will allow queries to return only the articles created by that user.

Update `server/routes/articles.ts` like so:

```ts {{ filename: 'server/routes/articles.ts', ins: [[10, 13], [16, 18], [22, 30], [42, 45], [48, 60], [78, 81], [90, 100], [112, 115], 124, 125, [134, 136], [143, 151], [164, 168], 175, 176] }}
import { Router } from 'express'
import { prisma } from '../db/prisma'
import { requireAuth } from '../middleware/auth'

const router = Router()

// Get all articles
router.get('/', async (req, res) => {
  try {
    if (!req.user) {
      res.status(401).json({ error: 'User not found' })
      return
    }

    const allArticles = await prisma.article.findMany({
      where: {
        userId: req.user.id,
      },
      orderBy: {
        updatedAt: 'desc',
      },
      include: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
          },
        },
      },
    })
    res.json(allArticles)
  } catch (error) {
    console.error('Error fetching articles:', error)
    res.status(500).json({ error: 'Failed to fetch articles' })
  }
})

// Get single article
router.get('/:id', async (req, res) => {
  try {
    if (!req.user) {
      res.status(401).json({ error: 'User not found' })
      return
    }

    const article = await prisma.article.findUnique({
      where: {
        id: parseInt(req.params.id),
        userId: req.user.id,
      },
      include: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
          },
        },
      },
    })

    if (!article) {
      res.status(404).json({ error: 'Article not found' })
      return
    }

    res.json(article)
  } catch (error) {
    console.error('Error fetching article:', error)
    res.status(500).json({ error: 'Failed to fetch article' })
  }
})

// Create article
router.post('/', async (req, res) => {
  try {
    if (!req.user) {
      res.status(401).json({ error: 'User not found' })
      return
    }

    const { title, content, summary } = req.body

    const newArticle = await prisma.article.create({
      data: {
        title,
        content,
        summary,
        userId: req.user.id,
      },
      include: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
          },
        },
      },
    })
    res.json(newArticle)
  } catch (error) {
    console.error('Error creating article:', error)
    res.status(500).json({ error: 'Failed to create article' })
  }
})

// Update article
router.put('/:id', async (req, res) => {
  try {
    if (!req.user) {
      res.status(401).json({ error: 'User not found' })
      return
    }

    const { title, content, summary } = req.body
    const articleId = parseInt(req.params.id)

    // First verify the article belongs to the user
    const article = await prisma.article.findUnique({
      where: {
        id: articleId,
        userId: req.user.id,
      },
    })

    if (!article) {
      res.status(404).json({ error: 'Article not found' })
      return
    }

    const updatedArticle = await prisma.article.update({
      where: {
        id: articleId,
      },
      data: {
        title,
        content,
        summary,
        updatedAt: new Date(),
      },
      include: {
        user: {
          select: {
            id: true,
            name: true,
            email: true,
          },
        },
      },
    })

    res.json(updatedArticle)
  } catch (error) {
    console.error('Error updating article:', error)
    res.status(500).json({ error: 'Failed to update article' })
  }
})

// Delete article
router.delete('/:id', async (req, res) => {
  try {
    if (!req.user) {
      res.status(401).json({ error: 'User not found' })
      return
    }

    const articleId = parseInt(req.params.id)

    // First verify the article belongs to the user
    const article = await prisma.article.findUnique({
      where: {
        id: articleId,
        userId: req.user.id,
      },
    })

    if (!article) {
      res.status(404).json({ error: 'Article not found' })
      return
    }

    const deletedArticle = await prisma.article.delete({
      where: {
        id: articleId,
      },
    })

    res.json(deletedArticle)
  } catch (error) {
    console.error('Error deleting article:', error)
    res.status(500).json({ error: 'Failed to delete article' })
  }
})

export default router
```

### Update the Express server entry point

The last thing to do on the server is update `server/index.ts` to set up `cookie-parser`, register the `/api/auth` routes, and protect the `/api/articles` and `/api/ai` routes.

Update `server/index.ts` as follows:

```ts {{ filename: 'server/index.ts', ins: [[10, 12], 27, 28, 37, 40, 41] }}
import express from 'express'
import path from 'path'
import cors from 'cors'
import dotenv from 'dotenv'
dotenv.config()

import { createProxyMiddleware } from 'http-proxy-middleware'
import articlesRouter from './routes/articles'
import aiRouter from './routes/ai'
import cookieParser from 'cookie-parser'
import authRoutes from './routes/auth'
import { requireAuth } from './middleware/auth'

console.log(`NODE_ENV: ${process.env.NODE_ENV}`)

const app = express()
const PORT = process.env.PORT || 3000
const VITE_PORT = process.env.VITE_PORT || 5173

// Middleware
app.use(
  cors({
    origin:
      process.env.NODE_ENV === 'production' ? process.env.FRONTEND_URL : 'http://localhost:5173',
    credentials: true,
  }),
)
app.use(cookieParser())
app.use(express.json())

// API routes
app.use('/api', (req, res, next) => {
  console.log(`API Request: ${req.method} ${req.url}`)
  next()
})

// Public routes
app.use('/api/auth', authRoutes)

// Protected routes
app.use('/api/articles', requireAuth, articlesRouter)
app.use('/api/ai', requireAuth, aiRouter)

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' })
})

// Development: Proxy all non-API requests to Vite dev server
if (process.env.NODE_ENV !== 'production') {
  app.use(
    '/',
    createProxyMiddleware({
      target: `http://localhost:${VITE_PORT}`,
      changeOrigin: true,
      ws: true,
      // Don't proxy /api requests
      filter: (pathname: string) => !pathname.startsWith('/api'),
    }),
  )
} else {
  // Production: Serve static files
  app.use(express.static(path.join(__dirname, '../dist')))

  // Handle React routing in production
  app.get('*', (req, res) => {
    if (!req.path.startsWith('/api')) {
      res.sendFile(path.join(__dirname, '../dist/index.html'))
    }
  })
}

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
  if (process.env.NODE_ENV !== 'production') {
    console.log(`Proxying non-API requests to http://localhost:${VITE_PORT}`)
  }
})
```

## Implementing the login page in React

With the backend updated to support authentication, let’s update the frontend to do the same.

### Create a context, provider, and hook for global auth

Since the authentication state can affect the way the entire application behaves, we’ll create a React Context and Provider so we can check if the user is logged in from anywhere. We’ll also add the logic to sign up, sign in, and sign out from the context, making it easy to call those functions from various points of the app.

Create the `src/hooks/use-auth.tsx` file and add the following code:

```tsx {{ filename: 'src/hooks/use-auth.tsx' }}
import { createContext, useContext, useState, useEffect } from 'react'
import { User } from '@/types'

interface AuthContextType {
  user: User | null
  signIn: (email: string, password: string) => Promise<void>
  signUp: (email: string, password: string, name?: string) => Promise<void>
  signOut: () => Promise<void>
  isLoading: boolean
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    checkAuth()
  }, [])

  // Used to check the session status with the server
  const checkAuth = async () => {
    try {
      const response = await fetch('/api/auth/me', {
        credentials: 'include',
      })
      const data = await response.json()
      setUser(data)
    } catch (error) {
      console.error('Failed to check auth status:', error)
      setUser(null)
    } finally {
      setIsLoading(false)
    }
  }

  // Used to create a session and store the user data in the context
  const signIn = async (email: string, password: string) => {
    const response = await fetch('/api/auth/signin', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include',
      body: JSON.stringify({ email, password }),
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.error || 'Failed to sign in')
    }

    const data = await response.json()
    setUser(data)
  }

  // Used to sign up a new user, create a session, and store the user data in the context
  const signUp = async (email: string, password: string, name?: string) => {
    const response = await fetch('/api/auth/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include',
      body: JSON.stringify({ email, password, name }),
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(error.error || 'Failed to sign up')
    }

    const data = await response.json()
    setUser(data)
  }

  // Used to sign out a user and clear the user data from the context
  const signOut = async () => {
    await fetch('/api/auth/signout', {
      method: 'POST',
      credentials: 'include',
    })
    setUser(null)
  }

  return (
    <AuthContext.Provider value={{ user, signIn, signUp, signOut, isLoading }}>
      {children}
    </AuthContext.Provider>
  )
}

// Custom hook to access the auth context from any component
export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}
```

Next, update the `src/App.tsx` file to wrap the entire application with the provider:

```tsx {{ filename: 'src/App.tsx', ins: [4, 8, 16] }}
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import Home from '@/views/home'
import AppView from '@/views/app'
import { AuthProvider } from '@/hooks/use-auth'

function AppRoot() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/app" element={<AppView />} />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Routes>
      </Router>
    </AuthProvider>
  )
}

export default AppRoot
```

### Add sign-up and sign-in views

Next, let’s add the sign-up and sign-in views. These views will both be very similar, each containing form elements where the user can enter their credentials. Both also use `zod` for client-side validation, which prevents an unnecessary network trip to the server if the user does not populate the fields properly. If validation fails, errors will be shown on the respective fields.

The key difference between them is what happens when the user submits the form, specifically, the method called from the `useAuth` hook created earlier.

Create the `src/views/sign-in.tsx` page and populate it with the following code:

```tsx {{ filename: 'src/views/sign-in.tsx' }}
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useAuth } from '@/hooks/use-auth'
import { z, ZodError } from 'zod'

const signInSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(1, 'Password is required'),
})

export default function SignIn() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  })
  const [errors, setErrors] = useState<{
    email?: string
    password?: string
    submit?: string
  }>({})
  const [isSubmitting, setIsSubmitting] = useState(false)
  const navigate = useNavigate()
  const { signIn } = useAuth()

  const handleSubmit = async (data: typeof formData) => {
    try {
      setIsSubmitting(true)
      setErrors({})

      // Validate the form data
      const validatedData = signInSchema.parse(data)

      // Attempt sign in
      await signIn(validatedData.email, validatedData.password)
      navigate('/app')
    } catch (error) {
      if (error instanceof ZodError) {
        const formattedErrors: Record<string, string> = {}
        error.errors.forEach((err) => {
          if (err.path) {
            formattedErrors[err.path[0]] = err.message
          }
        })
        setErrors(formattedErrors)
      } else {
        setErrors({ submit: 'Failed to sign in. Please try again.' })
      }
    } finally {
      setIsSubmitting(false)
    }
  }

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target
    setFormData((prev) => ({ ...prev, [name]: value }))
    // Clear error when user starts typing
    if (errors[name as keyof typeof errors]) {
      setErrors((prev) => ({ ...prev, [name]: undefined }))
    }
  }

  return (
    <div className="flex min-h-screen items-center justify-center bg-purple-50/30 p-4">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle className="text-center text-2xl">Sign In</CardTitle>
        </CardHeader>
        <CardContent>
          <form
            onSubmit={(e) => {
              e.preventDefault()
              handleSubmit(formData)
            }}
            className="space-y-4"
          >
            <div className="space-y-2">
              <label htmlFor="email" className="text-sm font-medium">
                Email
              </label>
              <Input
                id="email"
                name="email"
                type="email"
                value={formData.email}
                onChange={handleChange}
                required
                placeholder="Enter your email"
                className={errors.email ? 'border-red-500' : ''}
              />
              {errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
            </div>
            <div className="space-y-2">
              <label htmlFor="password" className="text-sm font-medium">
                Password
              </label>
              <Input
                id="password"
                name="password"
                type="password"
                value={formData.password}
                onChange={handleChange}
                required
                placeholder="Enter your password"
                className={errors.password ? 'border-red-500' : ''}
              />
              {errors.password && <p className="text-sm text-red-500">{errors.password}</p>}
            </div>
            {errors.submit && <p className="text-center text-sm text-red-500">{errors.submit}</p>}
            <Button type="submit" className="w-full" disabled={isSubmitting}>
              {isSubmitting ? 'Signing In...' : 'Sign In'}
            </Button>
            <div className="text-center text-sm">
              Don't have an account?{' '}
              <a href="/signup" className="text-purple-600 hover:text-purple-500">
                Sign up
              </a>
            </div>
          </form>
        </CardContent>
      </Card>
    </div>
  )
}
```

Do the same with `src/views/sign-up.tsx`:

```tsx {{ filename: 'src/views/sign-up.tsx' }}
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useAuth } from '@/hooks/use-auth'
import { z, ZodError } from 'zod'

const signUpSchema = z.object({
  email: z.string().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'),
  name: z.string().optional(),
})

export default function SignUp() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    name: '',
  })
  const [errors, setErrors] = useState<{
    email?: string
    password?: string
    name?: string
    submit?: string
  }>({})
  const [isSubmitting, setIsSubmitting] = useState(false)
  const navigate = useNavigate()
  const { signUp } = useAuth()

  const handleSubmit = async (data: typeof formData) => {
    try {
      setIsSubmitting(true)
      setErrors({})

      // Validate the form data
      const validatedData = signUpSchema.parse(data)

      // Attempt sign up
      await signUp(validatedData.email, validatedData.password, validatedData.name)
      navigate('/app')
    } catch (error) {
      if (error instanceof ZodError) {
        const formattedErrors: Record<string, string> = {}
        error.errors.forEach((err) => {
          if (err.path) {
            formattedErrors[err.path[0]] = err.message
          }
        })
        setErrors(formattedErrors)
      } else {
        setErrors({ submit: 'Failed to create account. Please try again.' })
      }
    } finally {
      setIsSubmitting(false)
    }
  }

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target
    setFormData((prev) => ({ ...prev, [name]: value }))
    // Clear error when user starts typing
    if (errors[name as keyof typeof errors]) {
      setErrors((prev) => ({ ...prev, [name]: undefined }))
    }
  }

  return (
    <div className="flex min-h-screen items-center justify-center bg-purple-50/30 p-4">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle className="text-center text-2xl">Sign Up</CardTitle>
        </CardHeader>
        <CardContent>
          <form
            onSubmit={(e) => {
              e.preventDefault()
              handleSubmit(formData)
            }}
            className="space-y-4"
          >
            <div className="space-y-2">
              <label htmlFor="email" className="text-sm font-medium">
                Email
              </label>
              <Input
                id="email"
                name="email"
                type="email"
                value={formData.email}
                onChange={handleChange}
                required
                placeholder="Enter your email"
                className={errors.email ? 'border-red-500' : ''}
              />
              {errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
            </div>
            <div className="space-y-2">
              <label htmlFor="password" className="text-sm font-medium">
                Password
              </label>
              <Input
                id="password"
                name="password"
                type="password"
                value={formData.password}
                onChange={handleChange}
                required
                placeholder="Create a password"
                className={errors.password ? 'border-red-500' : ''}
              />
              {errors.password && <p className="text-sm text-red-500">{errors.password}</p>}
            </div>
            <div className="space-y-2">
              <label htmlFor="name" className="text-sm font-medium">
                Name (optional)
              </label>
              <Input
                id="name"
                name="name"
                type="text"
                value={formData.name}
                onChange={handleChange}
                placeholder="Enter your name"
                className={errors.name ? 'border-red-500' : ''}
              />
              {errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
            </div>
            {errors.submit && <p className="text-center text-sm text-red-500">{errors.submit}</p>}
            <Button type="submit" className="w-full" disabled={isSubmitting}>
              {isSubmitting ? 'Creating Account...' : 'Create Account'}
            </Button>
          </form>
        </CardContent>
      </Card>
    </div>
  )
}
```

Now register the views in `src/App.tsx`:

```tsx {{ filename: 'src/App.tsx', ins: [5, 6, 14, 15] }}
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import Home from '@/views/home'
import AppView from '@/views/app'
import { AuthProvider } from '@/hooks/use-auth'
import SignIn from '@/views/auth/sign-in'
import SignUp from '@/views/auth/sign-up'

function AppRoot() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/signin" element={<SignIn />} />
          <Route path="/signup" element={<SignUp />} />
          <Route path="/app" element={<AppView />} />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Routes>
      </Router>
    </AuthProvider>
  )
}

export default AppRoot
```

### Protecting the App page

To protect routes, we’ll create a separate component that will use the context & provider and check the authentication state before allowing the user to proceed. If the authentication state is being checked by the provider, a “Loading…” message will be rendered for the user. If the user is not logged in, they will be redirected to the sign-in view.

Create the `src/components/protected-route.tsx` file and add the following:

```tsx {{ filename: 'src/components/protected-route.tsx' }}
import { Navigate } from 'react-router-dom'
import { useAuth } from '@/hooks/use-auth'

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, isLoading } = useAuth()

  if (isLoading) {
    return (
      <div className="flex min-h-screen items-center justify-center bg-purple-50/30">
        <div className="text-purple-600">Loading...</div>
      </div>
    )
  }

  if (!user) {
    return <Navigate to="/signin" replace />
  }

  return <>{children}</>
}
```

Update `src/App.tsx` and wrap the element for the `/app` route in the `<ProtectedRoute>` component:

```tsx {{ filename: 'src/App.tsx', ins: [7, 8, [18, 25]], del: [15] }}
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from '@/hooks/use-auth'
import SignIn from '@/views/auth/sign-in'
import SignUp from '@/views/auth/sign-up'
import Home from '@/views/home'
import AppView from '@/views/app'
import { ProtectedRoute } from '@/components/protected-route'

function AppRoot() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/app" element={<AppView />} />
          <Route path="/signin" element={<SignIn />} />
          <Route path="/signup" element={<SignUp />} />
          <Route
            path="/app"
            element={
              <ProtectedRoute>
                <AppView />
              </ProtectedRoute>
            }
          />
          <Route path="*" element={<Navigate to="/" replace />} />
        </Routes>
      </Router>
    </AuthProvider>
  )
}

export default AppRoot
```

### Add a sign-out button

The last thing to add is a sign-out button that lets users log out of the app once they are finished. This will use the `signOut` function of the `useAuth` hook to remove the session from the database and clear the cookie set in the browser.

Update `src/components/article-list.tsx` with the following:

```jsx {{ filename: 'src/components/article-list.tsx', ins: [4, 19, [49, 57]], prettier: false }}
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Article } from "@/types"
import { useAuth } from "@/hooks/use-auth"

interface ArticleListProps {
  articles: Article[]
  selectedArticle: Article | null
  onArticleSelect: (article: Article) => void
  onNewArticle: () => void
}

export function ArticleList({
  articles,
  selectedArticle,
  onArticleSelect,
  onNewArticle,
}: ArticleListProps) {
  const { signOut } = useAuth()

  return (
    <div className="flex h-full flex-col">
      <div className="border-b border-purple-100 p-4 bg-white">
        <div className="flex justify-between items-center">
          <h2 className="text-lg font-semibold">Articles</h2>
          <Button onClick={onNewArticle} size="sm">New</Button>
        </div>
      </div>
      <ScrollArea className="flex-1">
        <div className="space-y-4 p-4">
          {articles.map(article => (
            <div
              key={article.id}
              className={`p-4 rounded-lg cursor-pointer transition-colors ${
                selectedArticle?.id === article.id
                  ? 'bg-purple-100'
                  : 'hover:bg-purple-50'
              }`}
              onClick={() => onArticleSelect(article)}
            >
              <h3 className="font-medium mb-1">{article.title}</h3>
              <div className="text-sm text-gray-500">
                <p>Updated {new Date(article.updatedAt).toLocaleDateString()}</p>
              </div>
            </div>
          ))}
        </div>
      </ScrollArea>
      <div className="border-b border-t border-purple-100 p-4 bg-white">
        <Button
          onClick={() => signOut()}
          variant="outline"
          className="w-full"
        >
          Sign Out
        </Button>
      </div>
    </div>
  )
}
```

## Testing the app

With all the changes in place, you may test the application! Run the application with the following command in your terminal:

```bash
npm run dev
```

Now open `localhost:3000` in your browser and try creating a user to use the application, create an article, and experiment with the AI functionality! I encourage you to also log into Neon and check the contents of the database, specifically the `users` and `sessions` tables as they contain the records needed to support authentication.

## So why Clerk then?

Clerk is a user management and authentication platform, so it might surprise you that we’re publishing an article walking you through how to implement authentication yourself.

This article covers how to implement a single method of authentication, but in reality, user management is much more than just session-based authentication. For example, it’s commonplace in modern web applications to also support social login providers like [Google](/blog/add-google-login-next13-app) or Apple. A password reset flow is also a critical requirement for supporting your own authentication setup.

These are just a few of the many features Clerk supports out of the box, oftentimes with a single line of code.

Using Clerk, you can easily create a sign-in page by just importing and rendering the [`<SignIn />`](/docs/components/authentication/sign-in) [component](/docs/components/overview) like so:

```tsx
import { SignIn } from '@clerk/clerk-react'

export default function SignInView() {
  return <SignIn />
}
```

If you want to learn how easy it is to get Clerk working in a React application, check out the [quickstart on our docs](/docs/quickstarts/react).

If you want to download a template version of the React login page template to use in your own project, check out the [react-session-auth-template](https://github.com/bmorrisondev/react-session-auth-quickstart) repository on GitHub.

## Conclusion

In conclusion, this article has walked you through how to implement a basic authentication system using React and Express. By setting up a session-based authentication flow, we've covered how to create a sign-in page, handle user registration, log users in and out, as well as protect routes with a simple `ProtectedRoute` component.

While implementing a custom authentication system can be a great learning experience, it's also important to consider the larger picture of what user management really entails. In reality, you'll often need to support features like [social login](/blog/social-sso-in-next-js) providers, password reset flows, and more.

That's where Clerk comes in - a [user management and authentication platform](https://clerk.com) that simplifies the process of implementing these features with just a few lines of code. If you're interested in learning more about how easy it is to get Clerk working in a React application, check out the [quickstart guide](/docs/quickstarts/react) on our documentation site.