# Clerk Blog — Page 2

# Clerk ranked #4 fastest-growing software vendor on Ramp’s December 2025 list
URL: https://clerk.com/blog/clerk-ranked-4-fastest-growing-software-vendor-by-ramp.md
Date: 2025-12-04
Category: Company
Description: Recognition highlights Clerk’s rapid customer adoption

We’re excited to share that **Clerk has been ranked the #4 fastest-growing software vendor by customer count** on Ramp’s **[Top SaaS Vendors on Ramp – December 2025](https://ramp.com/velocity/top-saas-vendors-on-ramp-december-2025)** list. The ranking is based entirely on real customer spend and adoption patterns across Ramp’s platform — making this recognition especially meaningful.

Ramp’s monthly analysis tracks which vendors are seeing the fastest acceleration in **new customer acquisition** and **new spend**. Clerk’s placement near the top of the list reflects the growth we’re seeing every day: more teams choosing to trust Clerk with authentication, user management, organizations, and billing.

This year, we’ve seen rapid adoption from AI-native builders, SaaS companies, and enterprises rethinking their identity foundations. As applications become more sophisticated — and as agent-based systems introduce new requirements around access, auditability, and permissions — Clerk has become a key part of the way modern companies manage users and devices.

As we continue investing in authentication, users, organizations, and billing, our focus remains unchanged: giving developers best-practice solutions with unmatched ease of use.

**A sincere thank you to every team that partnered with Clerk this year.** This recognition is a direct reflection of your trust, and a preview of what’s ahead.

---

# How do I handle JWT verification in Next.js?
URL: https://clerk.com/blog/how-do-i-handle-jwt-verification-in-nextjs.md
Date: 2025-10-16
Category: Guides
Description: Learn all about JWTs: what they are, how they are created, and how to verify them in a Next.js application

In this article, you’ll learn what JWTs are and how to verify them in a Next.js application. Code samples are also provided to demonstrate how to access the JWTs transmitted through a cookie or a request header.

**Summary:**

- JWTs are specially crafted, cryptographically signed tokens that are used to identify users or systems making requests.
- JWTs can be signed using symmetric or asymmetric cryptography, with the latter being more secure while supporting distributed systems.
- They are verified by extracting the value from the request and checking the signature using a shared secret or public key, which allows the receiving server to trust the encoded claims.

## What Is a JWT?

A JSON Web Token (JWT) is a compact, specially crafted string used to represent a user, session, or other principal. JWTs are cryptographically signed, and that signature is used to confidently identify the party making a request.

A JWT is structured into three distinct segments, each separated by a period to indicate where one part ends and another begins. Each segment is base64 encoded before being joined to create the final token. The following diagram shows what a JWT looks like, along with each segment being color-coded:

![Color-coded diagram showing the different segments of a JWT.](./jwt-diagram.png)

### Header

The header contains information to help the receiving system understand the token's purpose. This includes the type (typically set to `jwt`) and the algorithm used to generate the signature.

### Claims

The claims are the primary payload of the JWT and contain information about the party for which the JWT was created. The following list outlines claims that are standard across most JWTs:

- **Subject** (`sub`) - Identifies who the JWT was created for. Often this is the username or user ID.
- **Issuer** (`iss`) - Who created the JWT
- **Expiration** (`exp`) - The epoch timestamp for when the JWT expires
- **Issued at** (`iat`) - The timestamp for when the JWT was created
- **Not before** (`nbf`) - A timestamp to restrict use of the JWT before a specific moment
- **Audience** (`aud`) - The designated audience for the JWT

Not all of these claims are required. The following snippet shows the claims from the Clerk token shown in the image above:

```json
{
  "azp": "https://quillmate.ink",
  "exp": 1759338857,
  "fva": [0, -1],
  "iat": 1759338797,
  "iss": "https://clerk.quillmate.ink",
  "jti": "890f222a3f2a4ce5f782",
  "nbf": 1759338787,
  "sid": "sess_33TQq7sI3bxADiC3KgBVJlASViA",
  "sts": "active",
  "sub": "user_2urxDrHRFn0g7jbmVH88oTLuME6",
  "v": 2
}
```

You can also add any arbitrary JSON-compliant data to a JWT. Say, for example, you are using `sub` as the user ID but also want to encode the username, you can create a `username` claim and simply store it the JWT claims before signing.

### Signature

The signature is the result of combining the base64 encoded versions of the header and claims, and using the specified algorithm to generate a cryptographic signature. This signature is used to ensure that the JWT has not been tampered with, which is how the receiving system can be confident in the JWT's authenticity.

## How JWT verification uses cryptography

There are two cryptographic methods by which JWTs are created: symmetric and asymmetric.

When symmetric encryption is used, the server uses a shared secret for encrypting and decrypting the token. When a JWT is received by the server, it uses that secret to verify that the signature of the JWT is valid. If either the header or claims have been tampered with, then the signature verification will fail. When using symmetric encryption, any server that needs to verify JWTs would need a copy of the secret.

Asymmetric encryption uses public key cryptography which involves two keys: a public key that can be shared with anyone and a private key that should be secured as you would secure a password. The private key is capable of creating and verifying signatures, whereas the public key is only capable of verifying signatures against a pre-existing payload. With both keys, the signature is verified with the same logic as described in the previous section.

With asymmetric encryption, only the public key is used to verify the signature. All servers would need a copy of this key, but since the public key can only verify signatures and not create signatures, it is a much more secure method of verifying signatures.

> \[!NOTE]
> A deep dive into cryptography used for JWT signing is beyond the scope of this article, but you can check [the RFC](https://tools.ietf.org/html/rfc7515) if you are interested in learning more.

## How are JWTs created?

In a typical web application, users will start by submitting their credentials (e.g., username and password). When the server receives these credentials, it will verify them with the database to ensure the user record is found and the provided information matches what's stored. The server will then create a JWT and send it back to the client, often by setting it in a cookie but the server can also provide it in a response body and leave it up to the client to store for later use.

The following sequence diagram demonstrates this flow visually:

1. The client sends the user's credentials to the server
2. The server checks the credentials with the database
3. The database responds to the server to confirm the user record exists and credentials match
4. The server uses the private key to create a JWT and sends it back to the client

![JWT authorization sequence diagram.](./sequence.png)

> \[!NOTE]
> If you want to explore this in practice, we have [a guide that covers how to build JWT authentication into a Next.js application from scratch](/blog/how-to-add-authentication-to-a-nextjs-application).

## Verifying JWTs in a Next.js app

JWTs are verified on each request to a server. When the server receives a request and the accompanying JWT, it will use the public key to verify the signature of the JWT. The verification also checks various claims encoded within the JWT such as the `iat`, `nbf`, and `exp` values to ensure the token is not being used outside of its designated window. If the server is able to verify the signature, it can trust the data encoded within the JWT.

Since the public key can be cached on the receiving server, communicating with additional auth servers or databases is not required.

The following snippets demonstrate how to verify a JWT using the `jose` library in a Next.js application. In these scenarios, the public key is stored in the `JWT_PUBLIC_KEY` environment variable. Here is an example of that key:

```text
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGJjKFz2TB66CY3O3J8E
Q1HbMhg0IE/zK6rxOWx4wuMLXsWhFzEOgqQhEPXAC2IzoIQ5JiV8MKe/xI5NQPr6
sBsPNvf50VBEu/4LAo2fJLFFaqfLpV6p0kLU93EgmklrYaGhU+qhYWdqJQGlafTZ
bNG07VM3mO4qX6iGGn2uhF+KCwGRA06wIJHkprWlSjxM0HjpHCBJ5Vd44D2D5nRT
jQ6W+SEQBiU8CerIPLlEHlCMxVarwq9Pa385vi1PPoDcCB9j/qY6FupDB1e3q27x
+bubo+ZtAmF4mOIPHH/xdg3AhoDYYJcCarCUzfFAEbXM5b6vQx4kCTxTvlN6bRJ2
NwIDAQAB
-----END PUBLIC KEY-----
```

### Cookies

Cookies are small bits of data stored in a web browser that are automatically transmitted to servers that match the domain for which they were created when a request is sent. This snippet shows how to parse and verify the JWT from the `token` cookie:

```tsx
import React from 'react'
// Read cookies on the server in the App Router
import { cookies } from 'next/headers'
// Jose is used for standards-compliant JWT crypto/validation
import * as jose from 'jose'

// RS256 public key (PEM/SPKI). Keep this in env, not in code.
const publicKeyPem = process.env.JWT_PUBLIC_KEY as string

async function getPublicKey() {
  // Convert PEM string to a CryptoKey usable by jose.jwtVerify
  return jose.importSPKI(publicKeyPem, 'RS256')
}

async function Page() {
  // Access the cookie store on the server
  const cookieStore = await cookies()
  // JWT expected to be set as a 'token' cookie
  const tokenCookie = await cookieStore.get('token')
  // Use a placeholder for the token claims
  let claims: jose.JWTPayload | null = null

  if (tokenCookie) {
    try {
      const publicKey = await getPublicKey()
      // Verify signature, exp/nbf, and decode claims (throws on failure)
      const { payload } = await jose.jwtVerify(tokenCookie?.value, publicKey)
      claims = payload
    } catch (err) {
      // Treat any verification/parsing error as unauthorized
      console.error('Error parsing token', err)
    }
  }

  if (claims) {
    // Authorized view when token is valid
    return <div>Authorized content</div>
  } else {
    // Fallback when missing/invalid token
    return <div>Unauthorized content</div>
  }
}

export default Page
```

> \[!NOTE]
> You can learn more about how `jose.jwtVerify` works in [the `jose` docs on GitHub](https://github.com/panva/jose/blob/main/docs/jwt/verify/functions/jwtVerify.md).

Cookies are often considered the most secure method since you can restrict JavaScript from accessing the value of cookies, limiting what can potentially exfiltrate a user's token for impersonation.

### Headers

Request headers are a popular way to transmit JWTs and can be used across domains; however, since the client needs to manually add the header to each request, JavaScript needs to be able to access the value of the JWT. The most common header to transmit a JWT with is the `Authorization` header with a value formatted as `Bearer {TOKEN}`.

Using `fetch` to make a request to an API route would be similar to the following:

```ts
await fetch('/api/data', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
  },
})
```

The JWT in the header would be verified in the route handler like so:

```ts
import * as jose from 'jose'

const publicKeyPem = process.env.JWT_PUBLIC_KEY as string

async function getPublicKey() {
  return jose.importSPKI(publicKeyPem, 'RS256')
}

export async function POST(req: Request) {
  // Placeholder for verified JWT claims
  let claims: jose.JWTPayload | null = null

  // Read the Authorization header in a case-insensitive way since header casing can vary by clients/proxies.
  const auth = req.headers.get('authorization') || req.headers.get('Authorization')
  // Ensure the header exists and uses the expected "Bearer <token>" scheme.
  if (!auth || !auth.startsWith('Bearer ')) {
    return new Response(null, { status: 401 })
  }
  // Extract the raw JWT by removing the leading "Bearer " prefix and trimming whitespace.
  const token = auth.slice(7).trim()

  try {
    const publicKey = await getPublicKey()
    const { payload } = await jose.jwtVerify(token, publicKey)
    claims = payload
  } catch (err) {
    console.error('Error parsing token', err)
  }

  if (claims) {
    return new Response(null, { status: 200 })
  } else {
    return new Response(null, { status: 401 })
  }
}
```

Using the `Authorization` header is common practice but you can transmit a JWT in any header that makes sense for your application.

### Other methods

The methods above are the most common and secure methods of transmitting a JWT, but ultimately, any way that the token can be transmitted from a client to the server will work, provided the server expects it. Alternate methods you may encounter are as a path or query parameter in a URL, however, these approaches are typically not recommended as they pose potential security risks by exposing tokens via logging platforms or in the browser history.

## Conclusion

This article covered what JWTs are, how they are created, and how to verify them in a Next.js application. Clerk handles this entire process for you in only a few lines of code, while going beyond simple JWT verification by providing a full user management suite. This includes social sign-in with popular providers, multi-tenancy with RBAC, and even a billing solution for SaaS platforms that need to provide subscriptions.

---

# Committing to Agent Identity: Clerk raises $50m Series C from Menlo and Anthropic’s Anthology Fund
URL: https://clerk.com/blog/series-c.md
Date: 2025-10-15
Category: Company
Description: Funding will be used to advance Agent Identity, expand products, and elevate developer experience.

We’re excited to share that Clerk has raised a **$50 million Series C**, led by Menlo and Anthropic’s Anthology Fund, alongside Georgian and previous investors.

Clerk started with a simple vision: offer best-practice authentication with spectacularly easy developer experience. Since then, we’ve become the user management solution of choice, and scaled to manage **over 200 million users for over 15,000 applications**.

A key reason developers choose Clerk is because authentication is always evolving, and Clerk helps them stay on the cutting edge. In our short history, we’ve already helped our customers navigate the introduction of both passkeys and crypto wallets on sign-in pages.

Today, everyone’s attention is on AI. It’s clear that “agents” need a new authentication solution: one that lets them act on behalf of humans with fine-grained permissions, and is easily auditable by both their operating human and application administrators.

The path to secure agent identity won’t come from Clerk alone, but we’re eager to help bring it to life. As the IETF works to extend OAuth with agent identity specifications, we’re laying the groundwork to adopt those standards as soon as they’re ready.

Once formalized, Clerk will bring support to our signature `<SignUp />` and `<SignIn />` components, and make agent identity available to our customers with just a few clicks.

Beyond agent identity, this capital will also be used for:

- **Doubling down on developer experience**: maintaining our emphasis on spectacular DX with deeper integrations, improved documentation, and expanded support for vibe coding and agentic workflows
- **Expanding our product surface**: improving our existing products for multi-tenancy and billing, and adding new products to support other business primitives
- **Investing in reliability**: making sure that the infrastructure powering your users is always there when you need it

Of course, this Series C would not have been possible without our customers, partners, and the incredible team at Clerk. We’re endlessly grateful for your support, and look forward to collaborating on this next chapter of both Clerk and authentication itself.

---

# What is the best way to handle authentication in Next.js App Router?
URL: https://clerk.com/blog/best-auth-nextjs-app-router.md
Date: 2025-09-26
Category: Guides
Description: Learn about the various authentication options available when using Next.js app router, and how to properly secure access to protected areas of your application.

Next.js has dramatically simplified the process of building full stack applications with React by providing the flexibility of server- and client-side rendering and introducing patterns like middleware and server actions, but these bring additional complexities when it comes to securing your application. With multiple points of protection, it can be challenging to understand how to properly implement Next.js App Router authentication and check that your users are authenticated before allowing them to access the data they are requesting.

In this article, you'll learn about the available Next.js App Router authentication options, and how to check the authentication status in middleware, layouts, server actions, and more.

## What are the main authentication options for Next.js App Router?

### Fully custom

One option is to create a fully customized authentication solution for your application.

This gives you the most control over your authentication stack, but it also puts all of the responsibility on you to build a fast and secure authentication system. You’ll have to consider:

- Where will you store your user data?
- How do you verify user email addresses?
- What will your sign-in/up experience be like?
- How do you implement single sign-on?
- How will the server identify the user?

On top of implementation details, you’ll also be responsible for monitoring the ever-evolving landscape of cybersecurity to understand newly discovered vulnerabilities, as well as taking time to patch your code to protect against them.

> \[!NOTE]
> Learn more about how to implement [JWT authentication in a Next.js app](/blog/how-to-add-authentication-to-a-nextjs-application) in our dedicated guide.

### Package-based solution

There are many pre-built packages available for developers to implement Next.js app router authentication. These packages often include the base logic for implementing authentication (sign in, sign up, etc), but you are still responsible for storing the user data, which is a benefit and a drawback. While you can easily access user data in your own database, you are still responsible for securing that data and making sure that attackers cannot gain access to it.

On top of that, you’d need to make sure that the package is continually kept up to date to ensure that any vulnerabilities are mitigated. Some packages are maintained by well-known and highly reputable security teams, while others might be independently maintained and worked on only when the package owner is able.

### Third-party providers

Services like Clerk offer hosted solutions for authentication and user management. These platforms typically have opinionated approaches to authentication which can provide a clear implementation direction. Good hosted solutions also have dedicated security teams to ensure that products using the platform are protected against emerging threats. And while these platforms often store user data on your behalf, you’d have access to the user data through data exports, automatic synchronization, and API access.

Not all third-party authentication solutions are alike though. You’ll have to do your research to compare and contrast the features each platform enables. Furthermore, it’s a good idea to reference unaffiliated sources to understand potential security flaws that real-world products have encountered while using a solution you might be interested in.

## How to implement authentication in Next.js App Router?

While the above section details the available Next.js App Router authentication options to consider when securing your application, understanding the different ways you can secure your application once you make that decision is another factor.

Ultimately, the way you protect areas of your application depends on how you identify the user making the request. The following examples assume the application uses a JWT stored within a cookie that's sent back to the server with each request.

You’ll notice in many of the samples below, the same approach is being used throughout to identify the user making the request by:

- Parsing the `token` cookie
- Validating and parsing the JWT claims into a `user` object
- Checking that the user details are parsed successfully

```tsx
// Parse the cookie
const token = request.cookies.get('token')?.value

// Validate and parse the claims
if (token) {
  // Use the `parseToken` helper function to extract the claims into `user`
  user = await parseToken(token)
}

// Check the user
if (!user) {
  // The user is not authorized
}
```

For reference, here is the full implementation of `parseToken`:

```ts {{ filename: 'src/lib/jwt.ts' }}
import * as jose from 'jose'

const publicKeyPem = process.env.JWT_PUBLIC_KEY as string

async function getPublicKey() {
  return jose.importSPKI(publicKeyPem, 'RS256')
}

export async function parseToken(token: string): Promise<jose.JWTPayload | null> {
  if (!token) return null

  try {
    const publicKey = await getPublicKey()
    const { payload } = await jose.jwtVerify(token, publicKey)
    return payload
  } catch (err) {
    console.error('Error parsing token', err)
    return null
  }
}
```

> \[!NOTE]
> This snippet is included for quick reference as it's used throughout the article. You can see the full implementation of `src/lib/jwt.ts`, including code to create JWTs, [on GitHub](https://github.com/clerk/nextjs-jwt-auth-demo/blob/main/src/middleware.ts).

### Middleware

Next.js middleware allows developers to execute functions on every incoming request and decide what to do with the request. In the context of Next.js App Router authentication, middleware can decide if the user should proceed to their requested page or be redirected to a sign in page if they are not authenticated.

Below is a sample middleware file that shows how to redirect the user to `/sign-in` if they try to access `/dashboard` and are signed out:

```tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { parseToken } from './lib/jwt'

export async function middleware(request: NextRequest) {
  // Create a variable to hold user details
  let user
  const token = request.cookies.get('token')?.value
  if (token) {
    // Use the `parseToken` helper function to extract the claims into `user`
    user = await parseToken(token)
  }

  // Get the pathname of the requested URL
  const { pathname } = request.nextUrl

  // If there is no user info and the request is to /dashboard, redirect to /sign-in
  if (pathname.includes('/dashboard') && !user) {
    const url = new URL('/sign-in', req.nextUrl)
    return NextResponse.redirect(url)
  }
}

export const config = {
  matcher: '/(.*)',
}
```

> \[!NOTE]
> A full demo of this implementation including sign-up and sign-in flows can be found [on GitHub](https://github.com/clerk/nextjs-jwt-auth-demo).

### Protecting individual pages and routes

You can opt to protect individual pages and routes by using the same pattern as above and deciding how to address unauthenticated users. The following snippet will parse the user and redirect users to `/sign-in` if they are not already authenticated:

```jsx
import { cookies } from 'next/headers'
import { parseToken } from '@/lib/jwt'
import { redirect } from 'next/navigation'

async function DashboardPage() {
  let user
  const token = (await cookies()).get('token')?.value
  if (token) {
    user = await parseToken(token)
  }
  if (!user) {
    redirect('/sign-in')
  }

  return <div>{/* Dashboard content */}</div>
}

export default DashboardPage
```

And this snippet will respond with a `401 Unauthorized` status when a user tries to access an API endpoint without being authenticated:

```tsx
import { parseToken } from '@/lib/jwt'
import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  let user
  const token = request.cookies.get('token')?.value
  if (token) {
    user = await parseToken(token)
  }
  if (!user) {
    return new Response('', { status: 401 })
  }

  return new Response(JSON.stringify({ message: 'Ok!' }), { status: 200 })
}
```

### Securing server actions

Server Actions are functions that execute on the server but can be called in server and client components to handle a variety of tasks such as form submissions. When it comes to authentication, you should treat them with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform the action.

When a server action is called, Next.js will make an HTTP request to the current route with a special header to tell the server what function to execute. This means that even if you use middleware to protect the `/dashboard` route, if you call a server function that is stored in the `/dashboard` route from `/` it will not consider any rules defined in the middleware.

To ensure that server actions are always secure, make sure to check the user details in each individual function:

```tsx
'use server'

import { cookies } from 'next/headers'
import { parseToken } from '@/lib/jwt'

export async function getChartData() {
  const cookieStore = await cookies()
  const tokenCookie = await cookieStore.get('token')
  const user = await parseToken(tokenCookie?.value as string)

  if (!user) {
    throw new Error('User is unauthorized')
  }

  return {
    message: 'OK!',
  }
}
```

### Client-side pages and components

It’s not recommended to add authorization code or security checks in client-side components since the code is downloaded to the user’s device regardless of authentication state. Any security checks performed locally are purely for cosmetic reasons or to inform the user they need to be authenticated to view the contents of that component.

If you are building an application where secure client-side components are mounted on public pages, the best approach is to use `fetch` to request data from an API that performs security checks in the route handler before responding with the data, or an unauthorized status code. Calling a server action that has security rules built into the function is also acceptable.

The following `Chart.tsx` component demonstrates how this should work when fetching data from an API:

```tsx
'use client'
import { useEffect, useState } from 'react'

function Chart() {
  // Set up component state
  const [isLoading, setIsLoading] = useState(true)
  const [requiresAuth, setRequiresAuth] = useState<boolean>()
  const [data, setData] = useState<any>()

  // On component render, fetch data from /api/data (implementation shown above)
  useEffect(() => {
    async function loadData() {
      const res = await fetch('/api/data')
      // If the response is a 401 status, indicate the viewer is unauthorized
      if (res.status === 401) {
        setRequiresAuth(true)
      } else {
        const json = await res.json()
        setData(json)
      }
      setIsLoading(false)
    }
    loadData()
  }, [])

  if (isLoading) {
    return <div>Loading...</div>
  }

  // If the init logic determines the viewer is unauthorized, render that message
  if (requiresAuth) {
    return <div>You need to sign in to view this component</div>
  }

  return <div>{/* Chart data */}</div>
}

export default Chart
```

### A note on securing layouts

Layouts in Next.js are special React components that are applied to any pages in the same directory as well as nested directories. This means that as the user accesses routes deeper within the application, each layout would be wrapped by its parent layout before rendering the final page content as children of the layout.

Take the following application structure as an example:

```txt
src/
│
├── app/
│   ├── layout.tsx       # RootLayout (wraps entire application)
│   ├── page.tsx         # Root page
│   │
│   └── dashboard/
│       ├── layout.tsx   # DashboardLayout (specific to /dashboard routes)
│       └── page.tsx     # Dashboard main page
```

If the user were to access the `/` route, the resulting page would load with its content wrapped in the `RootLayout`. Accessing the `/dashboard` route would first apply the `RootLayout`, then the `DashboardLayout`, and finally the page content.

While it may be tempting to add security and authorization code to layouts, Next.js does not run the code in a layout on every single navigation event due to Partial Rendering. This means that there is no guarantee the code will run consistently and it could lead to vulnerabilities in your application. Instead, you should do the security checks close to your data source or the component that'll be conditionally rendered.

> \[!NOTE]
> You can learn more about [Partial Rendering in the Next.js docs](https://nextjs.org/docs/app/getting-started/linking-and-navigating#client-side-transitions).

## Tips on choosing the best solution for your application

### Choosing the implementation

When it comes to choosing between a custom authentication system, packages, or a hosted platform, it ultimately comes down to the level of effort or engineering hours you are willing to give to your implementation. Custom solutions require the most time and responsibility and are well suited for dedicated teams of security-minded engineers.

Packages are a middle-ground. Some of the hard work has already been done, but you are still completely responsible for the way it is implemented, and for keeping it up to date. Finally, great hosted solutions often provide a turn-key offering with dedicated personnel to prioritize security for their customers (you) and offer support for implementation and any ongoing issues you might encounter.

### Securing your application

As stated earlier, selecting the best method from the list above highly depends on how your application is structured.

Middleware is easily the most secure and flexible to work with. It lets you apply security checks before the request ever reaches the code within pages, layouts, server actions, etc. One thing to consider is that Next.js defaults to the edge runtime for middleware, which does not support all Node.js APIs and may cause some confusion when building your middleware. This however can be changed in the middleware configuration:

```ts {{ filename: 'src/middleware.ts' }}
export const config = {
  runtime: 'nodejs',
}
```

Securing individual pages and routes would be used sparingly or only in special circumstances since it's very narrowly scoped. It would however be useful for API routes where only some methods need to be protected, such as a `GET` request that's public but a `POST` request that requires authentication.

Securing client-side code should only be done for cosmetic purposes as it doesn’t actually apply any security, and adding authorization code directly in a layout should be avoided. Finally, any server actions should always be protected individually since they could be called from any area of the application by mistake.

## Try Clerk for Next.js App Router authentication

If you want to quickly add authentication and user management to your application, Clerk can be implemented in as little as two minutes and provides comprehensive Next.js App Router authentication. Our solution uses middleware to apply security rules to all requests and wraps your entire application at the root layout to ensure that all child routes, server or client rendered, have access to the user context, allowing you to easily secure your application in any way you see fit.

---

# Postmortem: Database Incident (September 14–18, 2025)
URL: https://clerk.com/blog/2025-09-18-database-incident-postmortem.md
Date: 2025-09-18
Category: Company
Description: A detailed post-mortem of the database incident that occurred between September 14-18, 2025, including root cause analysis, timeline, and remediation steps.


Between September 14th and September 18th, 2025, we experienced a database incident that intermittently impacted customer traffic with significant request failures and latency spikes. The issue originated from an automatic database upgrade by our cloud provider, which exposed an interaction with our connection pooling configuration. This document explains the timeline, root cause, contributing factors, and our mitigation of the issue.

## Timeline

- **Sep 14, 05:30 UTC** – Cloud provider auto-upgrades our database (minor version).
- **Sep 14, shortly after** – Internal monitoring detects an increased but still acceptable database load. No impact on latency is observed.
- **Sep 15, 13:08 UTC** – Internal monitoring alerts about significant request failures and increased latency. The incident begins.
- **Sep 15–18** – Engineering teams continually investigate, optimize queries, and tune database parameters.
- **Sep 15, 14:09 UTC** – Request latency normalizes but our database remains unhealthy.
- **Sep 15, 14:58-15:16 UTC** – Significant request failures and latency spikes.
- **Sept 17, 00:22 UTC** – A deploy including a new query optimization appears to eliminate spikes in database load.
- **Sept 17, 01:00 UTC** – Spikes in database load reappear. However, engineers note that the remaining spikes now appear periodic, occurring roughly every 15 minutes. During these periodic spikes, session management was functional due to the session endpoints higher tolerance for latency, as well as the retry mechanism in our SDKs.
- **Sep 17, 07:15-08:10 UTC** - Engineering teams perform a manual minor version upgrade and increase database capacity. User sessions remain active throughout. For a period of 3-6 minutes, new sign ins, sign ups, and other account management activities face failures. The database health improves notably and latency spikes again appear to be eliminated.
- **Sep 17, 15:07-15:14 UTC** - Significant request failures and latency spikes. Engineers again observe periodic spikes in database load every 15 minutes.
- **Sep 18, 03:19 UTC** – Root cause identified, and a fix applied to database connection pooling configuration. After release, database load immediately dropped below pre-incident levels. Latency spikes resolved and stability restored.

## Root Cause

The incident was triggered by an automatic minor version upgrade of our database (Postgres) performed by our cloud provider on September 14th. While this was a minor upgrade, it contained a significant internal change to how Postgres handled connection locks.

### 1. Postgres change to connection lock handling

- In versions prior to the upgrade, Postgres' lock manager granted new connections with O(n²) time complexity, meaning each connection request slowed as more connections were processed.
- This inefficiency inadvertently acted as a natural rate limiter, spacing out how quickly new client connections were created.
- After the upgrade, this bottleneck was removed and [connection granting was optimized](https://www.postgresql.org/docs/release/13.14/) [(commit)](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=dc9d424cf), meaning large batches of new connections could be granted nearly instantaneously.

### 2. Our Cloud Run and connection pooling configuration

- Our Cloud Run configuration leverages many containers. During a deploy, these containers spin up sequentially, within about a minute.
- Our connection pooler in each Cloud Run container was configured with a static maximum connection lifetime of 15 minutes, which causes each connection to be closed and replaced every 15 minutes.
- Under the old Postgres behavior, new connections were created slowly enough that these expirations were naturally spread out over time.
- Under the new Postgres behavior, all expired connections were recreated simultaneously, leading to synchronized connection recycling.
- Unknowingly, our database was in a new state: Synchronized connection recycling of a single Cloud Run container could be managed by our database, but synchronized connection recycling across all Cloud Run containers creates unmanageable load.

### 3. Back pressure causes synchronization of connection recycling across all Cloud Run containers

- At deploy time, each Cloud Run container start is sufficiently spread out such that the load from each connection recycling is unnoticeable.
- Over time, any back pressure in Clerk's database causes the recycling events of each container to synchronize through a "[bus bunching](https://en.wikipedia.org/wiki/Bus_bunching)" effect. Once the busses are bunched, there is a "[thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem)" effect every 15 minutes as each containers refreshes its connections all at once.
- As this effect continues to compound every 15 minutes, more connections begin cycling simultaneously, eventually exhausting the pool of active connections and leading to customer-facing latency spikes.

## Mitigations During the Incident

- **Query optimization**: We optimized several expensive queries, permanently reducing baseline database load. Additionally, we refined several critical indexes and carried out a comprehensive re-indexing, resulting in reduced database overhead.
- **Traffic shaping**: To protect stability, we temporarily applied more aggressive blocking of abusive traffic. While effective, this may have inadvertently resulted in a small number of legitimate requests being rejected (HTTP 429).
- **Cloud provider engagement**: We explored rolling back the auto-upgrade, but our provider was unwilling to revert the database version.

## Why Diagnosis Was Difficult

Although the root cause may seem straightforward in hindsight, several factors combined to make identification challenging:

### 1. Metrics resolution hid the cycling pattern

- Our database connection metrics were sampled at 60-second intervals, but the connection recycling occurred within seconds every 15 minutes.
- This meant our dashboards showed only *occasional spikes* in active connections, rather than the synchronized, repeating pattern.
- Because the spikes appeared sporadic rather than rhythmic, we initially treated them as a symptom of underlying load rather than the initiating cause.

### 2. Overlapping events confused the timeline

- During the same window, we were mitigating a large volume of fraudulent sign ups targeting some of our customers.
- This attack created its own periodic load surges every \~10 minutes, which overlapped with the database's 15-minute connection cycling.
- The similar periods led us to initially suspect the attack was the primary driver of latency spikes, delaying deeper focus on the database layer.

### 3. Confounding jobs and queries

- Our database also runs several recurring background jobs and scheduled queries.
- Some of these happen on 10, 15, and 30-minute intervals, which coincidentally aligned with the timing of observed spikes.
- These jobs, while legitimate contributors to load, became false positives during our investigation, diverting attention and masking the real synchronization issue.

### 4. Mixed symptoms across APIs

- Latency spikes were seen in our Frontend API but not consistently in our Backend API, which has the same connection pooling logic.
- This discrepancy made the issue harder to triangulate, as it suggested selective load / query inefficiency rather than a systemic connection pooling effect.
- Looking back, we believe that we did not observe this with our Backend API, due to the fact that it simply just manages a lot less connections (and the Database could handle the cycling load, even if they all became synchronized).

### 5. Release freeze effect not obvious

- Ironically, we imposed a release freeze (intended to minimize risk during the incident) unintentionally made the issue worse, since normal deployments naturally stagger the connection creation across machines as the fleet of machines rolled out.
- Because this wasn't an intuitive connection, it obscured one of the clearest signals that would have otherwise pointed to connection synchronization.

## Resolution and current status

- We adjusted our connection pooling strategy to include a jitter to prevent synchronized cycling.
- Since the fix, the database has remained stable and is performing better than before the upgrade, due to the performance improvements that were implemented during the incident.

## Additional remediations and closing

Of course, we are incredibly sorry for the downtime this incident caused, but we also know that apologies are not enough. While we are excited this incident is behind us, it's important to stress that we're not moving on from infrastructure improvements. Incidents like this are unacceptable, and we recognize Clerk has faced too many recently.

Since our [June 26 incident](https://clerk.com/blog/postmortem-jun-26-2025-service-outage), our team has been urgently focused on improving our resilience. We've shipped improvements every single week, and internally, we have more confidence in our reliability, and our reliability roadmap, than ever before.

Beyond the narrow resolution to this issue, we have additional remediation work planned. Most notably:

- **Evaluating other database providers:** Over the past month, we have already been evaluating database solutions that offer more control over upgrades and downgrades, and improved performance compared to our current provider.
- **Additional service isolation:** Our ability to keep sessions operational during much of this incident was a direct result of our remediations following the June 26 incident. Now, we want to extend that concept further. For example, isolating Sign Up and Sign In infrastructure from other parts of the system.
- **Continued database optimizations:** Throughout this incident, we implemented a handful of new-to-Clerk optimization techniques to our most costly queries. We'll continue marching down the list to further improve our database performance.
- **Investigating solutions for tenant isolation:** Throughout this incident, we received many requests to isolate one application from others, and we recognize the potential benefits of that approach. We are encouraged by many modern approaches to sharding and multi-tenant infrastructure, and will continue evaluating solutions. In full disclosure, this is likely on a longer time horizon than the other improvements.

We know every incident carries a cost for you and your users, and we take that responsibility seriously. Our focus is on earning back your trust through reliability, not words, and to ensure that Clerk is an infrastructure partner you can always count on.

---

# How do I add authentication to a Next.js app?
URL: https://clerk.com/blog/how-to-add-authentication-to-a-nextjs-application.md
Date: 2025-09-15
Category: Guides
Description: Learn how Next.js authentication works by implementing JWTs from scratch, including user registration, sign-in/sign-out functionality, and middleware protection.

Authentication is core to building any multi-user product, and it's important to get it right from the start.

There are a number of methods you can use when adding authentication into a product, and Next.js has its own paradigms to consider. Understanding the [Next.js authentication](/nextjs-authentication) strategies available is key to knowing which is best for your application, and properly implementing it is the next challenge.

In this article you'll learn about the most common authentication strategies, as well as how to add JWT authentication to a Next.js application.

## Next.js authentication strategies: Choosing the right approach

There are many strategies to select from when planning your approach to authentication. The most common approaches are session token authentication, JWT-based authentication, and OAuth. Let's touch on how each of these compare.

### Session tokens

Session token authentication is the oldest on this list but is still widely used today. When a user signs into an application using session token authentication, the backend service will verify the user's credentials against the database and, assuming they are valid, create an entity called a "session". Each session has some commonly tracked attributes stored such as the user it's associated with, when it was created, when it expires, and its status (valid, expired, etc). The session identifier is sent back to the user's device to be used with subsequent network requests.

The most common method of storing the session ID client-side is in a browser cookie so the ID is sent with future network requests automatically. When received by the server, the session is cross-referenced with the user for which it was created so that the server knows who is making the request and can apply security appropriately.

> \[!NOTE]
> To learn more about session management in detail, check out our comprehensive guide on [what is session management](/blog/what-is-session-management) and how it works in modern applications.

### JWT

JSON Web Tokens (JWTs) are specially formatted strings that contain embedded information about a particular user or session and are cryptographically signed by the server. When a user signs in, the server will validate the user's credentials just like with session token authentication but instead of creating a session record (commonly in a database table), the details are encoded into a JWT and signed before being sent back to the client. The JWT is also sent with each request but since it contains the user and/or session details, the server does not have to look up those in the database. The server can simply verify the JWT signature is valid and can trust the encoded details if it is.

This has some benefits and drawbacks. One of the primary benefits is the speed by which requests are validated as no datastore lookups are required. Since verification is mostly performed on the receiving server, this also makes JWT authentication more scalable than session token authentication. As long as a server has a cached version of the signing secret (or public key in asymmetric signing configurations), that server can verify the JWTs authenticity.

The primary drawback is the lack of control if a JWT is leaked to an unauthorized party. Since all of the session information is embedded with the token and the verification process does not require any additional checks with a central datastore, there is no standard way to invalidate tokens once they've been signed and sent out.

> \[!NOTE]
> Learn more about how Clerk overcomes this drawback with [a hybrid authentication strategy](/blog/combining-the-benefits-of-session-tokens-and-jwts).

### OAuth

OAuth is a standard that allows a user to authenticate with one system and access multiple services using a single account. If you've ever signed into a web application with a Google or Apple account, you've used OAuth. In a typical configuration, the service provider (SP) will redirect users attempting to sign in to an identity provider (IdP) to supply their credentials and create a session. Once authenticated, the user's device will receive a code that can be provided to the SP, which will communicate with the IdP to verify the code, create the JWT, and send it back to the user.

This flow (known as the "Authorization Code Grant") describes how the SP and IdP work together to create the session and is only one of many flows that are part of the OAuth spec.

> \[!NOTE]
> A full explanation of OAuth is beyond the scope of this article, but we have [another on our blog that covers OAuth](/blog/how-oauth-works) and its implementation in detail.

## How to implement Next.js authentication with JWT tokens

Now that you have a solid understanding of some common authentication strategies, let's learn how to manually implement Next.js authentication using JWT tokens. To do this, you'll step through the following:

- Configure a SQLite database to store user records
- Set up public/private keys to sign and validate JWTs in a helper
- Create sign-up and sign-in pages
- Configure a **Sign out** button
- Show claims from the JWT within a server-rendered page

> \[!NOTE]
> This guide is meant to act as an introduction to JWT authentication by demonstrating a minimal implementation, however a robust and scalable solution involves significantly more than what's covered. Considerations for a complete user management solution are discussed after the tutorial.

To follow along, you'll need the following:

- A general understanding of React, and ideally experience with Next.js
- Node.js installed on your workstation

You'll use the supplied starter repository that is a Next.js application preconfigured with SQLite, a few shadcn/ui components, and a dashboard page with some dummy data. Through this guide, you'll create the sign-up page, sign-in page, sign-out button, and you'll configure the middleware to enforce authentication on the `/dashboard` route.

Upon signing in, the middleware will parse the JWT (stored as a cookie) to determine the user's authentication status. Server actions will be used throughout the various authentication functions.

The following dependencies are also pre-installed:

- `bcrypt` to salt and hash user passwords before storing them in the database.
- `jose` to create and validate JWTs

> \[!NOTE]
> [Hashing](https://www.geeksforgeeks.org/dsa/what-is-hashing/) is a process that lets you obfuscate data with an irreversible cryptographic method so it is not stored in plain text, which should never be done with passwords in a database.

Before moving on, clone the `start` branch of this repository: [clerk/nextjs-jwt-auth-demo](https://github.com/clerk/nextjs-jwt-auth-demo/tree/start). Once cloned, open the project in your code editor of choice and run `pnpm install` to install the dependencies.

### Creating the SQLite and JWT helpers

You'll start by creating a helper file that lets the application interact with the SQLite database. The helper will create the connection, create the `users` table if it does not yet exist, and return a connection object to the caller. The table needed to support user authentication contains only three columns:

- `id` is the unique identifier for the user
- `username` is their username
- `password_hash` is the salted and hashed representation of their password

Create the `src/lib/db.ts` and populate it with the following:

```ts {{ filename: 'src/lib/db.ts' }}
import sqlite3 from 'sqlite3'
import path from 'path'

sqlite3.verbose()

const db = new sqlite3.Database(path.join(process.cwd(), 'sqlite.db'), (err) => {
  if (err) {
    console.error(err.message)
  } else {
    console.log('Connected to the SQLite database.')
  }
})

db.serialize(() => {
  db.run(
    'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password_hash TEXT)',
  )
})

export { db }
```

Next, you'll create the JWT helper file that contains the configuration for `jose` as well as the `createToken` function to generate a new JWT for the user and `parseToken` which verifies the token's validity and returns the claims (the data encoded within JWTs) to the caller if it is.

Create the `src/lib/jwt.ts` file and add the following:

```ts {{ filename: 'src/lib/jwt.ts' }}
import * as jose from 'jose'

const privateKeyPem = process.env.JWT_PRIVATE_KEY as string
const publicKeyPem = process.env.JWT_PUBLIC_KEY as string

const jwtConfig = {
  protectedHeader: { alg: 'RS256', typ: 'JWT' },
}

interface CustomJWTPayload extends jose.JWTPayload {
  username?: string
}

export async function parseToken(token: string): Promise<CustomJWTPayload | null> {
  if (!token) return null

  try {
    // Import the public key
    const publicKey = await jose.importSPKI(publicKeyPem, 'RS256')
    const { payload } = await jose.jwtVerify(token, publicKey)
    return payload
  } catch (err) {
    console.error('Error parsing token', err)
    return null
  }
}

export async function createToken(sub: string, username: string): Promise<string> {
  try {
    // Import the private key
    const privateKey = await jose.importPKCS8(privateKeyPem, 'RS256')
    return await new jose.SignJWT({ sub, username })
      .setProtectedHeader(jwtConfig.protectedHeader)
      .setIssuedAt()
      .setExpirationTime('1h')
      .sign(privateKey)
  } catch (err) {
    console.error('Error creating token', err)
    throw err
  }
}
```

Notice in the above code that the `JWT_PRIVATE_KEY` and `JWT_PUBLIC_KEY` are being referenced from the environment variables. To set this up, run the following command in your terminal to generate a key pair and set them in the `.env.local` file:

```bash
npm run generate-keys
```

Inspecting the `.env.local` file will look similar to the following (albeit with a larger value for each variable):

```{{ filename: '.env.local' }}
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkq..."
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvAIBAD..."
```

> \[!NOTE]
> Make sure to protect the `JWT_PRIVATE_KEY` as anyone with it can generate tokens on behalf of any user of your application!

### Building the sign-up flow

Before users can sign-in and use the application, they'll need a way to sign-up first. Create the `src/app/actions.ts` file to store the server actions required to sign-up. This configuration will check if a record with that username exists (responding with an error if found), creates the JWT and stores it as a cookie, and redirects the user to the `/dashboard` route.

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

import bcrypt from 'bcrypt'
import { db } from '@/lib/db'
import { RunResult } from 'sqlite3'
import { cookies } from 'next/headers'
import { createToken } from '@/lib/jwt'

const SALT_ROUNDS = 10

// Hashes the password for storing it
async function hashPassword(password: string) {
  const salt = await bcrypt.genSalt(SALT_ROUNDS)
  const hash = await bcrypt.hash(password, salt)
  return { hash, salt }
}

// Creates the user record in the database
async function createUserRecord(username: string, hash: string): Promise<number> {
  return new Promise((resolve, reject) => {
    db.run(
      'INSERT INTO users (username, password_hash) VALUES (?, ?)',
      [username, hash],
      function (err: Error, results: RunResult) {
        if (err) {
          reject(err)
        }
        resolve(results?.lastID)
      },
    )
  })
}

interface CheckCountResult extends RunResult {
  count: number
}

async function checkDoesUserExist(username: string): Promise<boolean> {
  return new Promise((resolve, reject) => {
    db.get(
      'select count(*) as count from users where username=?',
      [username],
      (err: Error, results: CheckCountResult) => {
        if (err) {
          reject(err)
        }
        resolve(results.count !== 0)
      },
    )
  })
}

// The action used in the sign-up route
export async function registerUser(username: string, password: string) {
  try {
    const userExists = await checkDoesUserExist(username)
    if (userExists) {
      throw new Error('User with this name already exists')
    }

    // Hash the password
    const { hash } = await hashPassword(password)

    // Create the user record
    const userId = await createUserRecord(username, hash)

    // Create the token and set it in a cookie
    const token = await createToken(userId?.toString(), username as string)
    const cookieStore = await cookies()
    cookieStore.set('token', token, {
      path: '/',
      httpOnly: true,
      maxAge: 3600, // 1 hour in seconds
    })
  } catch (err) {
    throw err
  }
}
```

Now create the `src/app/sign-up/page.tsx` file to store the sign-up form used to create an account:

```tsx {{ filename: 'src/app/sign-up/page.tsx' }}
'use client'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useState } from 'react'
import { registerUser } from '../actions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { useRouter } from 'next/navigation'

function SignUpPage() {
  const router = useRouter()
  const [username, setUsername] = useState<string>('')
  const [pass, setPass] = useState<string>('')
  const [confPass, setConfPass] = useState<string>('')
  const [err, setErr] = useState<string>()

  async function register() {
    if (!username) {
      setErr('Must specify a username')
      return
    }
    if (pass !== confPass) {
      setErr('Passwords do not match')
      return
    }
    try {
      await registerUser(username as string, pass as string)
      router.push('/dashboard')
    } catch (err) {
      setErr((err as Error).message)
    }
  }

  return (
    <div className="align-center flex flex-col items-center justify-center p-8">
      <Card className="flex w-[400px] flex-col gap-2 p-4">
        <h1>Sign up</h1>
        <Label>Username</Label>
        <Input value={username} onChange={(e) => setUsername(e.target.value)} />
        <Label>Password</Label>
        <Input type="password" value={pass} onChange={(e) => setPass(e.target.value)} />
        <Label>Confirm password</Label>
        <Input type="password" value={confPass} onChange={(e) => setConfPass(e.target.value)} />
        <Button onClick={register}>Sign up</Button>
        {err && (
          <Alert variant="destructive">
            <AlertTitle>Error</AlertTitle>
            <AlertDescription>{err}</AlertDescription>
          </Alert>
        )}
      </Card>
    </div>
  )
}

export default SignUpPage
```

You can now start the application with `npm run dev`, access it using the provided URL, and navigate to the `/sign-up` route to create a user. After creating a user, you'll be redirected to the `/dashboard` route.

### Configure sign-out

Since the JWT is stored in a cookie with the `httpOnly` flag, client-side JavaScript will not be able to access it, so you'll need to configure a server action to clear the cookie. Update the `actions.ts` file and append the `signOut` function as shown below:

```ts {{ filename: 'src/app/actions.ts', fold: [[1, 79]] }}
'use server'

import bcrypt from 'bcrypt'
import { db } from '@/lib/db'
import { RunResult } from 'sqlite3'
import { cookies } from 'next/headers'
import { createToken } from '@/lib/jwt'

const SALT_ROUNDS = 10

// Hashes the password for storing it
async function hashPassword(password: string) {
  const salt = await bcrypt.genSalt(SALT_ROUNDS)
  const hash = await bcrypt.hash(password, salt)
  return { hash, salt }
}

// Creates the user record in the database
async function createUserRecord(username: string, hash: string): Promise<number> {
  return new Promise((resolve, reject) => {
    db.run(
      'INSERT INTO users (username, password_hash) VALUES (?, ?)',
      [username, hash],
      function (err: Error, results: RunResult) {
        if (err) {
          reject(err)
        }
        resolve(results?.lastID)
      },
    )
  })
}

interface CheckCountResult extends RunResult {
  count: number
}

async function checkDoesUserExist(username: string): Promise<boolean> {
  return new Promise((resolve, reject) => {
    db.get(
      'select count(*) as count from users where username=?',
      [username],
      (err: Error, results: CheckCountResult) => {
        if (err) {
          reject(err)
        }
        resolve(results.count !== 0)
      },
    )
  })
}

// The action used in the sign-up route
export async function registerUser(username: string, password: string) {
  try {
    const userExists = await checkDoesUserExist(username)
    if (userExists) {
      throw new Error('User with this name already exists')
    }

    // Hash the password
    const { hash } = await hashPassword(password)

    // Create the user record
    const userId = await createUserRecord(username, hash)

    // Create the token and set it in a cookie
    const token = await createToken(userId?.toString(), username as string)
    const cookieStore = await cookies()
    cookieStore.set('token', token, {
      path: '/',
      httpOnly: true,
      maxAge: 3600, // 1 hour in seconds
    })
  } catch (err) {
    throw err
  }
}

export async function signOut() {
  const cookieStore = await cookies()
  cookieStore.delete('token')
}
```

Next, create a Sign Out button component at `src/components/SignOutButton.tsx` and paste in the following:

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

import { Button } from '@/components/ui/button'
import React from 'react'
import { signOut } from '../app/actions'

function SignOutButton() {
  async function onClick() {
    await signOut()
    window.location.pathname = '/'
  }

  return <Button onClick={onClick}>Sign out</Button>
}

export default SignOutButton
```

Then you'll need to update the Navigation component to check if the user is logged in and render the button if they are. Since it is a server-rendered component, you can use the `next/headers` package to access the request cookies and the `parseToken` function to verify the user is signed in.

Update the `src/components/Navigation.tsx` file as follows:

```tsx {{ filename: 'src/components/Navigation.tsx', ins: [[1, 3], [8, 10], [19, 26]], del: [[27, 30]] }}
import { cookies } from 'next/headers'
import { parseToken } from '@/lib/jwt'
import SignOutButton from './SignOutButton'
import Link from 'next/link'
import Logo from './Logo'

async function Navigation() {
  const cookieStore = await cookies()
  const tokenCookie = await cookieStore.get('token')
  const user = await parseToken(tokenCookie?.value as string)

  return (
    <nav className="flex flex-row items-center justify-between border-b-1 border-b-neutral-200 p-6">
      <Link href="/" className="flex flex-row items-center gap-2">
        <Logo />
        Next.js JWT Auth Demo
      </Link>

      {user ? (
        <SignOutButton />
      ) : (
        <div className="flex flex-row gap-2">
          <Link href="/sign-in">Sign in</Link>
          <Link href="/sign-up">Sign up</Link>
        </div>
      )}
      <div className="flex flex-row gap-2">
        <Link href="/sign-in">Sign in</Link>
        <Link href="/sign-up">Sign up</Link>
      </div>
    </nav>
  )
}

export default Navigation
```

Now access the application in your browser once again and click the **Sign out** button in the navigation. You'll be redirected if you are on the `/dashboard` page and the navigation bar will update to show the **Sign in** and **Sign up** links.

### Configure the sign-in page and actions

Now that sign-up and sign-out are working, you'll need a way for existing users to sign-in. Update the `actions.ts` file once again and append the following actions:

```ts {{ filename: 'src/app/actions.ts', fold: [[1, 84]] }}
'use server'

import bcrypt from 'bcrypt'
import { db } from '@/lib/db'
import { RunResult } from 'sqlite3'
import { cookies } from 'next/headers'
import { createToken } from '@/lib/jwt'

const SALT_ROUNDS = 10

// Hashes the password for storing it
async function hashPassword(password: string) {
  const salt = await bcrypt.genSalt(SALT_ROUNDS)
  const hash = await bcrypt.hash(password, salt)
  return { hash, salt }
}

// Creates the user record in the database
async function createUserRecord(username: string, hash: string): Promise<number> {
  return new Promise((resolve, reject) => {
    db.run(
      'INSERT INTO users (username, password_hash) VALUES (?, ?)',
      [username, hash],
      function (err: Error, results: RunResult) {
        if (err) {
          reject(err)
        }
        resolve(results?.lastID)
      },
    )
  })
}

interface CheckCountResult extends RunResult {
  count: number
}

async function checkDoesUserExist(username: string): Promise<boolean> {
  return new Promise((resolve, reject) => {
    db.get(
      'select count(*) as count from users where username=?',
      [username],
      (err: Error, results: CheckCountResult) => {
        if (err) {
          reject(err)
        }
        resolve(results.count !== 0)
      },
    )
  })
}

// The action used in the sign-up route
export async function registerUser(username: string, password: string) {
  try {
    const userExists = await checkDoesUserExist(username)
    if (userExists) {
      throw new Error('User with this name already exists')
    }

    // Hash the password
    const { hash } = await hashPassword(password)

    // Create the user record
    const userId = await createUserRecord(username, hash)

    // Create the token and set it in a cookie
    const token = await createToken(userId?.toString(), username as string)
    const cookieStore = await cookies()
    cookieStore.set('token', token, {
      path: '/',
      httpOnly: true,
      maxAge: 3600, // 1 hour in seconds
    })
  } catch (err) {
    throw err
  }
}

export async function signOut() {
  const cookieStore = await cookies()
  cookieStore.delete('token')
}

export async function signinUser(username: string, password: string) {
  try {
    // Get the user record and verify the provided password matches what's stored
    const user = await fetchUserFromDb(username)

    // Compare the provided password with what's stored in the database
    // and make sure they match.
    const isPasswordValid = await bcrypt.compare(password, user.password_hash)
    if (!isPasswordValid) {
      throw new Error('Username and/or password is incorrect')
    }

    if (!user.id || !user.username) {
      console.error('Error parsing user details', user)
      throw new Error('Unknown error')
    }

    // Create the token and set it in a cookie
    const token = await createToken(user.id?.toString(), user.username as string)
    const cookieStore = await cookies()
    cookieStore.set('token', token, {
      path: '/',
      httpOnly: true,
      maxAge: 3600, // 1 hour in seconds
    })
  } catch (err) {
    throw err
  }
}

interface FetchUserResult extends RunResult {
  id?: number
  username?: string
  password_hash?: string
}

// Fetches the user record from the database using the provided username
async function fetchUserFromDb(username: string): Promise<FetchUserResult> {
  return new Promise((resolve, reject) => {
    db.get('select * from users where username=?', [username], (err, results: FetchUserResult) => {
      if (err) {
        reject(err)
      }
      if (!results.id) {
        reject('User not found')
      }
      resolve(results)
    })
  })
}
```

Then create the `src/app/sign-in/page.tsx` file to display the sign-in form:

```tsx {{ filename: 'src/app/sign-in/page.tsx' }}
'use client'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useState } from 'react'
import { signinUser } from '../actions'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { useRouter } from 'next/navigation'

function SignInPage() {
  const router = useRouter()
  const [username, setUsername] = useState<string>('')
  const [pass, setPass] = useState<string>('')
  const [err, setErr] = useState<string>()

  async function onSignInClicked() {
    if (!username || !pass) {
      setErr('Must specify username and password')
      return
    }
    try {
      await signinUser(username as string, pass as string)
      router.push('/dashboard')
    } catch (err) {
      setErr((err as Error).message)
    }
  }

  return (
    <div className="align-center flex flex-col items-center justify-center p-8">
      <Card className="flex w-[400px] flex-col gap-2 p-4">
        <h1>Sign in</h1>
        <Label>Username</Label>
        <Input value={username} onChange={(e) => setUsername(e.target.value)} />
        <Label>Password</Label>
        <Input type="password" value={pass} onChange={(e) => setPass(e.target.value)} />
        <Button onClick={onSignInClicked}>Sign in</Button>
        {err && (
          <Alert variant="destructive">
            <AlertTitle>Error</AlertTitle>
            <AlertDescription>{err}</AlertDescription>
          </Alert>
        )}
      </Card>
    </div>
  )
}

export default SignInPage
```

In the application, navigate to the `/sign-in` route and use the credentials you created earlier to sign-in and access the `/dashboard` route once again.

### Protecting routes with Next.js authentication middleware

As of now, even unauthenticated users can access `/dashboard` if they enter the path in their browser. Next.js authentication middleware can be configured to intercept inbound requests and check the cookies to ensure a user is signed-in before allowing them to access protected routes. Furthermore, the middleware can also be configured to redirect unauthenticated users to `/sign-in` if they attempt to access `/dashboard`.

Create the `src/middleware.ts` file and populate it like so to achieve this protection:

```ts {{ filename: 'src/middleware.ts' }}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { parseToken } from './lib/jwt'

export async function middleware(request: NextRequest) {
  // Create a variable to hold user details
  let user
  const token = request.cookies.get('token')?.value
  if (token) {
    // Use the `parseToken` helper function to extract the claims into `user`
    user = await parseToken(token)
  }

  // Get the pathname of the requested URL
  const { pathname } = request.nextUrl

  // If there is no user info and the request is to /dashboard, redirect to /sign-in
  if (pathname.includes('/dashboard') && !user) {
    const url = request.nextUrl.clone()
    url.pathname = '/sign-in'
    return NextResponse.redirect(url)
  }

  // If the user is signed in and trying to access the auth routes, redirect them to the home page
  if ((pathname.includes('/sign-in') || pathname.includes('/sign-up')) && user) {
    const url = request.nextUrl.clone()
    url.pathname = '/'
    return NextResponse.redirect(url)
  }
}

export const config = {
  matcher: '/(.*)',
}
```

> \[!WARNING]
> While this approach does protect accessing routes and pages with `/dashboard` in the pathname, it may not necessarily protect server actions stored within the `/dashboard` folder since Next.js uses the browser's current URL to call server actions.

> \[!NOTE]
> If you want to learn more about Next.js middleware, we have [an in-depth guide in our blog](/blog/what-is-middleware-in-nextjs) that dives deep into the topic.

### Display user information on the Dashboard page (optional)

Since the `/dashboard` route is protected by our Next.js authentication system, any users that can access it will have already signed in, meaning the information in the JWT can be trusted. The `next/headers` package can be used in page content just like in the `Navigation.tsx` component to parse details about the authenticated user and render the details on the page.

To do this, update `src/app/dashboard/page.tsx` as follows to display the user's username in the page header:

```tsx {{ filename: 'src/app/dashboard/page.tsx', ins: [3, 4, 7, 8, 9, 15], del: [14] }}
import { Card } from '@/components/ui/card'
import React from 'react'
import { cookies } from 'next/headers'
import { parseToken } from '@/lib/jwt'

async function DashboardPage() {
  const cookieStore = await cookies()
  const tokenCookie = await cookieStore.get('token')
  const user = await parseToken(tokenCookie?.value as string)

  return (
    <div className="max-w-800px flex flex-col gap-4 p-8">
      <div className="flex flex-row justify-between">
        <h1>Welcome!</h1>
        <h1>Welcome {user?.username}!</h1>
      </div>
      <div className="grid grid-cols-3 gap-2">
        <Card>
          <div className="p-4">
            <div className="text-lg font-semibold">Users</div>
            <div className="text-2xl font-bold">1,234</div>
            <div className="text-sm text-gray-500">Active this month</div>
          </div>
        </Card>
        <Card>
          <div className="p-4">
            <div className="text-lg font-semibold">Revenue</div>
            <div className="text-2xl font-bold">$12,345</div>
            <div className="text-sm text-gray-500">This month</div>
          </div>
        </Card>
        <Card>
          <div className="p-4">
            <div className="text-lg font-semibold">New Signups</div>
            <div className="text-2xl font-bold">321</div>
            <div className="text-sm text-gray-500">Past 7 days</div>
          </div>
        </Card>
      </div>
    </div>
  )
}

export default DashboardPage
```

Accessing the `/dashboard` page will now show the username in the welcome message!

## Now what?

You now have a functional system that allows users to sign up and sign in to this demo app, however there are a number of missing, critical user management features:

- Email address verification
- Password reset functionality
- Advanced attack protection and rate limiting
- Session management and refresh tokens
- Multi-factor authentication
- Social login providers

These are just to name a few of the gaps. Production-ready authentication in Next.js goes well beyond basic JWT implementation, and this is where [Clerk's Next.js authentication solution](/docs/quickstarts/nextjs) comes in.

### Why choose Clerk for Next.js authentication?

Clerk is a complete user management platform that allows developers to add enterprise-grade Next.js authentication into their applications as quickly as possible. With Next.js applications, this can be done in just a few lines of code.

Once implemented, you'll automatically gain all of the features listed above along with many more such as:

- One-click social authentication (Google, GitHub, Apple, etc.)
- Simple multi-tenancy for B2B applications (including custom RBAC)
- Subscription management with [Clerk Billing](/billing)
- Advanced security features that protect against bots, brute force attacks, and abuse
- Pre-built UI components that can be configured to match your application's design

### Get started with production-ready Next.js authentication

If you're ready to implement a robust Next.js authentication solution for your Next.js application, check out our [Next.js quickstart guide](/docs/quickstarts/nextjs) to learn how to get authentication added to your application in as little as 2 minutes. You'll have a complete, secure, and scalable authentication system without the complexity of building and maintaining it yourself.

---

# Introducing Free Trials in Clerk Billing
URL: https://clerk.com/blog/introducing-free-trials-in-clerk-billing.md
Date: 2025-09-02
Category: Company
Description: Clerk Billing now supports free trials to help developers boost conversion rates and reduce buyer friction.

Let’s face it: a frictionless billing system is great, but giving users a risk‑free peek at premium features can turn hesitation into conversion.

Clerk Billing was designed to take the same great developer experience we built for implementing [authentication](/user-authentication) and apply that philosophy to subscription-based billing. It allows developers to drop in a single [`<PricingTable />`](/docs/components/pricing-table) component to display a beautiful pricing table that is typical across hosted SaaS applications. Selecting a tier prompts the user to enter their payment information and once processed, any entitlements configured with their selected plan are flagged on their account for you to easily allow access to gated content.

With the addition of free trials, your users can now more easily experience premium features of your app to decide if it's the right fit for them before automatically converting to paid customers. In this article, we'll explore why you should use free trials for your app and how to implement it with [Clerk Billing](/blog/add-subscriptions-to-your-saas-with-clerk-billing).

## Free trials lead to higher conversions

Free trials reduce buyer friction and drive higher conversion rates by removing the immediate financial commitment. The data is compelling: [SaaS trial-to-paid conversion rates average around **25%**](https://userpilot.com/blog/saas-average-conversion-rate/). In B2B SaaS, [opt-out trials (requiring credit card upfront) can reach nearly **49%** conversion](https://userpilot.com/blog/saas-average-conversion-rate/), while opt-in trials typically convert around 18%.

Beyond metrics, free trials create **product-qualified leads (PQLs)** who've actively engaged with your product. This makes [conversion rates a reliable gauge of real value](https://www.zigpoll.com/content/what-are-the-most-effective-strategies-to-increase-free-trial-to-paid-subscription-conversion-rates-on-a-saas-website) while reducing churn from sign-up to paid status.

### Best practices with free trials

Allowing users to try your product for free is enough to increase conversion rates; however, there are a few considerations to maximize the impact. One practice is to test different trial lengths and models to optimize your conversion rates. By trying a few different trial periods and measuring the conversion rate, you can fine-tune what period leads to the highest revenue increase.

Another practice is to set realistic trial periods. For most SaaS applications, [7–14 days is a great starting point](https://www.zigpoll.com/content/what-are-the-most-effective-strategies-to-increase-free-trial-to-paid-subscription-conversion-rates-on-a-saas-website). If you have a more complex app, extending the period beyond that range could result in higher conversions due to the steps involved in utilizing the SaaS most effectively.

Trial periods present a perfect opportunity to engage users through product education. This can be through a very polished onboarding experience within the app, or something more traditional like using a newsletter platform for a drip campaign during the trial period to provide ways in which your app can be used.

As with most optimizations, success depends on rigorous measurement and iteration. Track key metrics like trial-to-paid conversion rates and analyze performance across different trial lengths and content strategies.

## How to configure free trials using Clerk Billing

If you've not yet used [Clerk Billing](/billing), configuring subscriptions is as simple as defining your subscription plans, associating the proper features with each plan, and adding the [`<PricingTable />`](/docs/components/pricing-table) component to your application.

The following screenshot demonstrates what a three-tier pricing table would look like for a task management app:

![PricingTable in a Next.js app](./1.png)

And the code for this page (built with Next.js) looks like this:

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

export default function Home() {
  return (
    <div className="min-h-screen items-center justify-items-center gap-16 p-8 pb-20 font-sans sm:p-20">
      <PricingTable />
    </div>
  )
}
```

Free trials can now be enabled for individual plans within the Clerk dashboard:

![Free Trials toggle in the Clerk dashboard](./2.png)

Once configured, the `<PricingTable />` component in your app will automatically update to reflect the plans where a free trial is available. You don't even have to make any changes to the code in your application.

![Updated PricingTable](./3.png)

When a user wants to trial a plan, they'll still be prompted for their payment information so that they can transition to a paying customer once the trial period ends. Your customers will automatically be notified when their trial period is coming to an end.

![Credit card capture modal open](./4.png)

If you handle your own messaging, you can also use the [`subscriptionItem.freeTrialEnding`](/docs/billing/events-webhooks#subscription-items) webhook to be notified when a customer's trial is ending so you can notify them or handle it accordingly. Below is a sample of the payload sent when a trial period is ending.

```json
{
  "data": {
    "created_at": 1716883200000,
    "id": "csub_item_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
    "interval": "month",
    "is_free_trial": true,
    "object": "subscription_item",
    "payer": {
      "created_at": 1716883200000,
      "email": "user@example.com",
      "first_name": "John",
      "id": "cpayer_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
      "image_url": "https://img.clerk.com/xxxxxx",
      "instance_id": "ins_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
      "last_name": "Doe",
      "object": "commerce_payer",
      "updated_at": 1716883200000,
      "user_id": "user_2g7np7Hrk0SN6kj5EDMLDaKNL0S"
    },
    "period_end": 1719561600000,
    "period_start": 1716883200000,
    "plan": {
      "amount": 2999,
      "currency": "USD",
      "free_trial_days": 14,
      "free_trial_enabled": true,
      "id": "cplan_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
      "is_default": false,
      "is_recurring": true,
      "name": "Premium Plan with Trial",
      "slug": "premium-trial"
    },
    "plan_id": "cplan_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
    "status": "active",
    "subscription_id": "csub_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
    "updated_at": 1716883200000
  },
  "event_attributes": {
    "http_request": {
      "client_ip": "192.168.1.100",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
    }
  },
  "instance_id": "ins_2g7np7Hrk0SN6kj5EDMLDaKNL0S",
  "object": "event",
  "timestamp": 1716883200,
  "type": "subscriptionItem.freeTrialEnding"
}
```

## Conclusion

[Free trials](/docs/billing/free-trials) represent more than just a conversion strategy. They can be the key deciding factor in whether a user tries your product or moves on to your competitor.

Clerk's free trial implementation stays true to our core philosophy by allowing developers the easiest possible path to providing trials for your customers. Your `<PricingTable />` component automatically adapts and payment collection flows seamlessly handle trial-to-paid transitions. All while respecting your customers by informing them of their trial status.

---

# Postmortem: August 28, 2025 - elevated API latency and errors
URL: https://clerk.com/blog/postmortem-aug-28-2025-elevated-latency-errors.md
Date: 2025-08-28
Category: Company
Description: On August 28, 2025, a credential stuffing attack caused elevated API latency and errors. This postmortem details the impact, root cause, and remediations.

On August 28, two short periods of a distributed credential stuffing attack to our authentication endpoints of a specific tenant, led to elevated latency across the Frontend and Backend APIs and elevated errors in the Backend API.

Services remained partially available while we mitigated load and stabilized the underlying infrastructure. Importantly, our mitigation controls kept session token issuance operating normally throughout the incident.

- **Impact window #1:** 14:53–15:15 UTC (≈22 minutes)
- **Impact window #2:** 17:04–17:16 UTC (≈12 minutes)

### Timeline (UTC)

- **14:53** — Alert triggered for high CPU utilization in the storage layer; elevated API latency observed.
- **15:00** — Incident declared; mitigation initiated.
- **15:15** — Metrics returned to baseline.
- **17:04** — Second spike in CPU and API latency detected.
- **17:16** — Metrics returned to baseline.

### Root Cause Analysis

Investigation points to several compounding contributors in the authentication and data-write path:

1. **Automated traffic targeting authentication flows** generated an unusually high volume of sign-in and sign-up attempts.
2. **Write-intensive activity** from those attempts increased contention on hot authentication-related tables.
3. **A recently introduced CDC consumer** (used for near real-time consumption of auth events) lagged under burst conditions, amplifying contention within a segment of the storage tier.

**Observed error rates during the incident windows:** 2.52% of Backend API requests and 0.14% of Frontend API requests returned errors.

There was **no data loss or corruption**. The impact was limited to increased latency and errors.

### Remediations

- We disabled the lagging change-stream processor pending adjustments.
- We are strengthening adaptive protections at the edge and auth layer (rate limiting, anomaly detection, and upstream filtering).
- We are performing schema and query-path improvements on authentication workloads to reduce contention under spikes.
- We will be further strengthening per-customer isolation to contain issues to the originating application and minimize blast radius.

---

# Introducing Mosaic: Bring Your Brand to Every Authentication Flow
URL: https://clerk.com/blog/introducing-mosaic-bring-your-brand-to-every-authentication-flow.md
Date: 2025-08-20
Category: Company
Description: Introducing Mosaic, our new Figma design system that mirrors every Clerk UI component. Design and prototype auth flows that look and feel like your product before writing a single line of code.

At Clerk, we believe authentication should be as elegant and intuitive as the rest of your product. That's why our platform provides developers with drop-in authentication components that are secure, scalable, and easy to implement—just like Stripe, but for auth.

But we also know that authentication is part of your **brand experience**. From sign-up to user profile, every touchpoint needs to reflect your visual identity. And while our UI components are easily customizable for developers, we are now bringing that to designers as well.

Today, we're excited to introduce **Mosaic**, our new Figma design system. Mosaic gives product teams a powerful, visual way to explore and brand Clerk's UI components before writing a single line of code.

## What Is Mosaic?

**Mosaic** is a Figma design system that mirrors every Clerk UI component—exactly as they appear in production. You can use it to:

- Visually prototype authentication flows that look and feel like your product
- Apply your brand's colors, typography, and imagery in minutes
- Seamlessly transition from Figma to production with pixel-perfect consistency

Whether you're a designer creating your first Clerk experience, or a developer collaborating with a design team, Mosaic provides a shared foundation to ensure your auth UI feels like *your* product—not ours.

## Why We Built Mosaic

Clerk has always been about giving you control without the complexity. We provide fully-featured authentication out of the box, and you can customize every aspect of the UI through [themes](/docs/customization/themes), [CSS variables](/docs/customization/variables), or the API.

But we noticed something important: many design teams wanted to *preview and prototype* these customizations visually—before implementation. Some teams were rebuilding Clerk's components manually in Figma. Others were stuck with screenshots or guesswork.

Mosaic solves that problem. It's a ready-to-use, fully branded design kit that helps you explore, customize, and validate Clerk components right in Figma. You no longer have to imagine what your sign-up form will look like—you can see it, test it, and iterate.

## Getting Started with Mosaic

We've designed Mosaic to be as intuitive as possible. Here's how to get started and make Clerk's components your own in just a few steps.

You can access the [Clerk Mosaic UI Components in the Figma Community](https://www.figma.com/community/file/1521965427913384177).

### 1. Watch the Intro Video

If you prefer learning by watching rather than reading, we've put together a concise, 5-minute [**introductory video**](https://youtu.be/zgqyEZc1SvY). It walks you through everything from setup to customization, so you can get up and running quickly.

### 2. Set Your Brand Colors

Before diving into components, start by defining your **core brand colors**.

Open Figma's **Variables panel** and update the following key color variables:

- `Primary`
- `Background`
- `Input`
- `Danger`
- `Success`
- `Warning`
- `Foreground`
- `MutedForeground`
- `PrimaryForeground`
- `InputForeground`
- `Neutral`
- `Border`
- `Shadow`
- `Ring`
- `Muted`

All Mosaic components are linked to these variables. That means once you set your colors, every component will update automatically—keeping your brand consistent throughout.

For developers implementing these designs, you can achieve the same dynamic theming using [Clerk's CSS variables support](/changelog/2025-07-15-clerk-css-variables-support).

Want to tweak the shape of UI elements? You can also adjust the **border radius variable** to match your design language, whether that's soft and rounded or sharp and clean.

### 3. Run the Clerk Mosaic Color Generator Plugin

Once your core colors are defined, run the [**Clerk Mosaic Color Generator plugin**](https://www.figma.com/community/plugin/1459093748129448022/clerk-ui-components-color-palette-generator) in Figma. This tool automatically generates the full color palette needed for Clerk's UI components—shades, tints, contrast-safe variants, and more.

This step ensures visual cohesion and accessibility across your entire auth experience, without manual color tweaking.

### 4. Set Your Avatar

Personalization matters—especially for components that display real user data.

To make your designs feel realistic and aligned with your brand, update the Figma company logo to your company logo. This will be used across the components to showcase your brand. Also, assign your logo's fill color to the `PrimaryForeground` variable to ensure proper contrast and consistency.

### 5. Set Your Typography

Next, bring in your **brand typography**.

In Figma Variables, update the following:

- `fontFamily`
- `fontFamilyButtons`

Mosaic will automatically apply these fonts across all headings, labels, buttons, and body text—giving your auth UI the same voice as the rest of your app.

### 6. Set Your Appearance Mode

With colors, typography, and imagery set, you're now ready to use Mosaic with your own **visual identity**.

Simply pull any UI component into your canvas and set the **appearance mode** you've configured. You'll instantly see each component styled with your brand's look and feel—ready for design reviews, prototyping, or handoff.

![Mosaic components in action](./Slide_16_9_-_1.png)

### 7. Explore Clerk's Component Library

Mosaic includes every core Clerk component, organized and documented within the Figma file. That includes:

- [`<SignIn />`](/docs/components/authentication/sign-in)
- [`<SignUp />`](/docs/components/authentication/sign-up)
- [`<UserProfile />`](/docs/components/user/user-profile)
- [`<UserButton />`](/docs/components/user/user-button)
- [`<OrganizationProfile />`](/docs/components/organization/organization-profile)
- [`<OrganizationSwitcher />`](/docs/components/organization/organization-switcher)
- [`<CreateOrganization />`](/docs/components/organization/create-organization)
- [`<VerifyEmail />`](/docs/components/authentication/verify-email)
- [`<VerifyCode />`](/docs/components/authentication/verify-code)
- [`<ResetPassword />`](/docs/components/authentication/reset-password)

Each component is interactive and pre-wired to respond to your Figma variables, so you can test different states and variations instantly. There's no need to recreate anything from scratch—just customize and go.

When you're ready to implement these components in code, our comprehensive [component documentation](/docs/components/overview) provides everything you need to get started.

(Foundational internal components used to build these aren't exposed—they're there to keep things structured, but you won't need to touch them.)

### 8. Staying Updated

One important note: Mosaic is a **Figma file**, so when you duplicate it to your own workspace, it becomes disconnected from future updates.

If we release a new component or update the design system, you'll need to **pull the latest version manually** and reapply your brand settings (colors, avatar, typography). We'll announce all updates clearly, so you never miss a new feature or visual improvement.

## Final Thoughts

Authentication is the front door to your product—and Mosaic ensures that door looks exactly the way you want it to.

With a simple, visual setup process, deeply customizable styles, and a full suite of production-matching components, Mosaic gives you the tools to design authentication flows that *belong* to your brand from the first pixel to the last.

We're excited to see what you build with it!

---

# Resilience in Practice: Regional Failover at Clerk
URL: https://clerk.com/blog/resilience-in-practice-regional-failover.md
Date: 2025-08-18
Category: Engineering
Description: See how Clerk's new regional failover kept services running during a cloud provider outage.

On Monday, August 4th, we shared that Clerk had implemented [automatic regional failover](/changelog/2025-08-04-regional-failover)
for critical parts of our infrastructure, a major upgrade to protect against large-scale, regional-level outages.

A few days later, that system was put to the test.

## The August 6th incident

On August 6th, between 02:30 UTC and 04:11 UTC, our primary cloud region experienced intermittent issues.
Outages came in short intervals of 5-10 minutes. During each disruption, our health checks detected failures and
automatically rerouted traffic to our failover region.

From a customer perspective, there was no noticeable disruption. Aside from a few early errors, which were
automatically retried by our SDKs, the only potential impact was a brief increase in API latency during some
failover periods.

### The timeline

*2:55 UTC*: We experienced a sudden spike of `429` responses.

![Graph of 429 response spike](./responses-429.png)

*2:58 UTC*: Our team was alerted about downtime on our services.

![Screenshot of internal alert](./alert.png)

*2:59 UTC*: Investigation began. We noticed that our failover region had already picked up traffic and scaled up its
available containers, explaining why no customers had reported issues.

![Screenshot of request throughput and container count in failover region](./regional-failover-kicks-in.png)

*3:35 UTC*: Google confirmed their internal incident.

![Screenshot of Google confirming incident](./google-incident.png)

*3:50 UTC*: Another switchover to our failover region occurred.

*4:11 UTC*: Google's network stabilized and traffic returned to our primary region.

## Why resilience matters so much to Clerk

As an authentication provider, Clerk sits in front of every application that uses our platform.
This means that if our services experience an outage, the impact is immediate and visible within our customers'
applications.
Even brief interruptions can affect sign-ins, sign-ups, and session management, critical flows for end users.

High resilience isn't just a nice-to-have for us. It's fundamental to ensuring our customers' apps remain
reliable and trusted.

## How our regional failover works

We've always run our services across multiple availability zones to handle localized failures.
But the [June 26th service outage](/blog/postmortem-jun-26-2025-service-outage) highlighted a gap: a single-region
architecture, even with AZ redundancy, is still vulnerable to full regional outages.

Our new setup adds a continuously running failover region:

- **Always-on failover region:** The failover region continuously handles live production traffic to ensure it stays warm, healthy, and ready at all times.
- **Fast detection & switchover:** Health checks trigger an immediate reroute when issues are detected in the primary region.
- **Bidirectional failover:** If the failover region experiences issues, traffic switches back to the primary.
- **Local storage in failover:** Data is replicated to a dedicated storage layer in the failover region, minimizing latency during switchover.

![Regional failover high-level architecture](./architecture.png)

## What's next

This failover system is an important milestone but not the end of our reliability journey.

We're actively working on:

- Increasing the resilience of our stateful systems
- Exploring multi-cloud redundancy to remove single-provider dependencies
- Further automating recovery playbooks to reduce operational response times even more

Last week's event validated our regional failover strategy, showing early positive ROI as we continue expanding our
resilience capabilities.

---

# Build a Cross-Platform B2B App with Clerk, Expo, and Supabase
URL: https://clerk.com/blog/build-a-cross-platform-b2b-app-expo-supabase.md
Date: 2025-08-07
Category: Guides
Description: Learn how to add multi-tenancy to your React Native & Expo app using Clerk and Supabase.

[B2B applications](/b2b-saas) with multi-tenancy capabilities often outperform their single-tenant counterparts, driving higher revenue and accelerated growth. The ability to serve multiple organizations from a single codebase is both a technical feature and a business advantage.

Popular mobile apps like Notion, Slack, and Asana have solved this problem by implementing robust multi-tenancy features, allowing users to effortlessly manage multiple organizations and switch between them. What was once a complex feature reserved for enterprise applications is now expected in even small-scale business apps.

In this article, you'll learn how to add multi-tenancy to your [React Native & Expo app](/expo-authentication) using Clerk and Supabase. We'll walk through the process from setup to implementation, showing you how to give your users a seamless experience switching between their organizations.

## What is multi-tenancy?

[Multi-tenancy](/glossary/multi-tenancy) is when a single instance of an application is used by multiple tenants or organizations. Each tenant has their own data and can't see or access the data of other tenants. Modern applications often allow users to belong to multiple tenants and switch between them as needed.

Building an application with multiple tenants is complex. Allowing users to switch between tenants means you have to carefully consider and plan your security model to ensure the application is performant and that tenant data cannot be accessed by users who don't belong to that tenant.

## How does Clerk help?

Clerk makes it easy to add multi-tenancy to your app by providing the necessary [APIs to allow users not only to switch between tenants, but also to create and manage their own organizations](/docs/organizations/overview). This eliminates the need to build complex organization management systems from scratch.

When a user switches between tenants, the token issued to that user automatically includes the organization ID of the active tenant, making it easy to check which tenant's data should be presented. By handling these authentication complexities, Clerk allows developers to focus on building the core functionality of their application.

> \[!TIP]
> If you're working with web applications, you can also explore similar patterns with [React authentication](/react-authentication).

## Demo: Adding multi-tenancy to a time tracking app

To demonstrate how to add multi-tenancy to an app with Clerk, we'll be adding multi-tenancy to a time tracking app called Aika. Here is an overview of the tasks we'll be doing:

1. Enable organizations for the Clerk application
2. Implement an organization switcher so users can switch between their organizations
3. Add a method for users to create organizations and invite others
4. Update the Supabase RLS policies to use the organization ID to filter data

If you want to follow along, you can clone the [orgs-start branch](https://github.com/bmorrisondev/aika/tree/orgs-start) of the [Aika repository](https://github.com/bmorrisondev/aika). Follow the setup instructions in the README to get the app running.

> \[!NOTE]
> In the provided code snippets, be sure to check the comments for additional context on what each line or block does.

### Step 1: Enable organizations

Access the application in the Clerk dashboard and navigate to the **Configure** tab. Then in the left navigation under **Organization management** select the **Settings** option. Finally, toggle **Enable organizations**.

![Enable organizations](./image1.png)

> \[!NOTE]
> For more detailed guidance, see the [organizations setup documentation](/docs/organizations/overview#enable-organizations).

### Step 2: Implement organizations on the settings screen

Most of the changes in the app will take place in the settings screen. A new button will be added to the list that lets the user see which organization they currently have active, and lets them switch between their organizations, create new ones, or accept invitations using modals and Clerk's helper functions.

Below is an annotated picture of what these changes will look like:

1. The settings screen with a button to open the organization selection modal (shown)
2. A secondary modal that appears when selecting an invitation
3. A new screen to create an organization
4. Another new screen to invite members to a new organization

![New screens](./image2.png)

Before starting, install the `@clerk/types` package since you'll use it for type safety. If you need help with the initial [Expo setup](/expo-authentication), refer to the [quickstart guide](/docs/quickstarts/expo).

```bash
pnpm install @clerk/types
```

Now you'll create the button to open the organization selection modal. This will use helper functions from the Clerk Expo SDK to display the active organization. Since no organization is active by default, the button will display "Personal account".

Open the `app/(tabs)/protected/settings.tsx` file and make the following changes:

```tsx {{ filename: 'app/(tabs)/protected/settings.tsx', ins: [1, 8, 9, [15, 26], [47, 64], [131, 156]], del: [7], collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import React, { useState } from 'react'
import { Image, StyleSheet, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { useAuth, useUser } from '@clerk/clerk-expo'
import { useAuth, useOrganization, useOrganizationList, useUser } from '@clerk/clerk-expo'
import { UserOrganizationInvitationResource } from '@clerk/types'

export default function HomeScreen() {
  const { user } = useUser()
  const { signOut } = useAuth()

  // Get the list of organizations the user is a member of and any invitations they have
  // Note: The `useOrganizationList` function requires that you specify what's included in the response
  const { userMemberships, userInvitations, setActive } = useOrganizationList({
    userMemberships: true,
    userInvitations: true,
  })

  // Get the active organization
  const { organization } = useOrganization()

  // A state object to control the visibility of the organization selection modal (will be added next)
  const [modalVisible, setModalVisible] = useState(false)

  return (
    <ThemedView style={styles.container}>
      <ThemedView style={styles.titleContainer}>
        <ThemedText type="title">Settings</ThemedText>
      </ThemedView>
      <ThemedView style={styles.contentContainer}>
        <ThemedText type="subtitle">User Information</ThemedText>
        <ThemedView style={styles.userContainer}>
          <ThemedView style={styles.userImageContainer}>
            <Image source={{ uri: user?.imageUrl || '' }} style={styles.userImage} />
          </ThemedView>
          <ThemedView style={styles.userInfoContainer}>
            <ThemedText type="defaultSemiBold">
              {user?.firstName} {user?.lastName}
            </ThemedText>
            <ThemedText>{user?.emailAddresses[0].emailAddress}</ThemedText>
          </ThemedView>
        </ThemedView>

        <ThemedText type="subtitle">Organization</ThemedText>
        <TouchableOpacity onPress={() => setModalVisible(true)}>
          <ThemedView style={styles.organizationContainer}>
            <ThemedView style={styles.organizationImageContainer}>
              <Image
                source={{ uri: organization?.imageUrl || user?.imageUrl || '' }}
                style={styles.organizationImage}
              />
            </ThemedView>
            <ThemedView style={styles.organizationInfoContainer}>
              <ThemedText type="defaultSemiBold">
                {organization?.name || 'Personal account'}
              </ThemedText>
              <ThemedText>{organization?.slug || 'No organization'}</ThemedText>
            </ThemedView>
            <Ionicons name="chevron-forward" size={24} color="#888" />
          </ThemedView>
        </TouchableOpacity>

        <ThemedView style={styles.signOutButtonContainer}>
          <TouchableOpacity onPress={() => signOut()}>
            <ThemedText style={{ color: 'red' }}>Sign Out</ThemedText>
          </TouchableOpacity>
        </ThemedView>
      </ThemedView>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 16,
  },
  titleContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
    marginTop: 16,
    paddingHorizontal: 16,
  },
  contentContainer: {
    gap: 12,
    marginBottom: 24,
    borderRadius: 8,
    padding: 16,
    width: '100%',
    height: 150,
    paddingHorizontal: 16,
  },
  userContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f7f7f7',
    padding: 8,
    borderRadius: 8,
  },
  userInfoContainer: {
    marginLeft: 12,
    flex: 1,
    backgroundColor: 'transparent',
  },
  userImageContainer: {
    width: 50,
    height: 50,
    borderRadius: 100,
    backgroundColor: 'transparent',
  },
  userImage: {
    width: 50,
    height: 50,
    borderRadius: 100,
    backgroundColor: 'transparent',
  },
  signOutButtonContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    borderRadius: 8,
    padding: 8,
    backgroundColor: '#ffebee',
    justifyContent: 'center',
  },
  organizationContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f7f7f7',
    padding: 8,
    borderRadius: 8,
    justifyContent: 'space-between',
  },
  organizationInfoContainer: {
    marginLeft: 12,
    flex: 1,
    backgroundColor: 'transparent',
  },
  organizationImageContainer: {
    width: 50,
    height: 50,
    borderRadius: 10,
    backgroundColor: 'transparent',
  },
  organizationImage: {
    width: 50,
    height: 50,
    borderRadius: 10,
    backgroundColor: 'transparent',
  },
})
```

Now create `components/OrganizationSwitcherModal.tsx` file that will store the code to display the organization switcher modal.

```tsx {{ filename: 'components/OrganizationSwitcherModal.tsx', collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import React from 'react'
import { FlatList, Image, Modal, SafeAreaView, StyleSheet, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'

interface Props {
  modalVisible: boolean
  setModalVisible: (visible: boolean) => void
  userInvitations: { data?: any[] } | null | undefined
  userMemberships: { data?: any[] } | null | undefined
  organization: any | null | undefined
  user: any | null | undefined
  handleOpenInvitation: (invitation: any) => void
  setActive?: (params: { organization: string | null }) => Promise<void>
}

export function OrganizationSwitcherModal({
  modalVisible,
  setModalVisible,
  userInvitations,
  userMemberships,
  organization,
  user,
  handleOpenInvitation,
  setActive,
}: Props) {
  // When the user selects an organization, use the parent component to set the active organization then redirect to the home screen
  const handleSelectOrganization = async (orgId: string) => {
    try {
      if (setActive) {
        if (orgId === 'personal-account') {
          await setActive({ organization: null })
        } else {
          await setActive({ organization: orgId })
        }
        setModalVisible(false)
        router.replace('/protected')
      }
    } catch (error) {
      console.error('Error setting active organization:', error)
    }
  }

  // When the user clicks the "Create New Organization" button, close the modal and navigate to the create organization screen
  const handleCreateOrganization = () => {
    // Close the modal first
    setModalVisible(false)
    // Navigate to the create organization screen
    router.push('/screens/create-organization')
  }
  return (
    <Modal
      animationType="slide"
      transparent={true}
      visible={modalVisible}
      onRequestClose={() => setModalVisible(false)}
    >
      <SafeAreaView style={styles.modalContainer}>
        <ThemedView style={styles.modalContent}>
          <ThemedView style={styles.modalHeader}>
            <ThemedText type="subtitle">Select Organization</ThemedText>
            <TouchableOpacity onPress={() => setModalVisible(false)}>
              <Ionicons name="close" size={24} color="#888" />
            </TouchableOpacity>
          </ThemedView>

          {/* Pending Invitations */}
          {(userInvitations?.data?.length || 0) > 0 && (
            <ThemedView style={styles.sectionContainer}>
              <ThemedView style={styles.sectionHeader}>
                <ThemedText type="defaultSemiBold" style={styles.sectionTitle}>
                  Pending Invitations
                </ThemedText>
                <ThemedView style={styles.badge}>
                  <ThemedText style={styles.badgeText}>{userInvitations?.data?.length}</ThemedText>
                </ThemedView>
              </ThemedView>
              <FlatList
                data={userInvitations?.data || []}
                keyExtractor={(item) => item.id}
                renderItem={({ item }) => (
                  <TouchableOpacity
                    style={[styles.invitationItem]}
                    onPress={() => handleOpenInvitation(item)}
                  >
                    <ThemedView style={styles.orgItemImageContainer}>
                      <Image
                        source={{
                          uri: item.publicOrganizationData.imageUrl || user?.imageUrl || '',
                        }}
                        style={styles.orgItemImage}
                      />
                    </ThemedView>
                    <ThemedView style={styles.orgItemInfoContainer}>
                      <ThemedText type="defaultSemiBold">
                        {item.publicOrganizationData.name}
                      </ThemedText>
                      <ThemedText>{item.publicOrganizationData.slug}</ThemedText>
                    </ThemedView>
                    <Ionicons name="mail-outline" size={24} color="#2196F3" />
                  </TouchableOpacity>
                )}
              />
              <ThemedView style={styles.divider} />
            </ThemedView>
          )}

          {/* Your Organizations */}
          <ThemedView style={styles.sectionContainer}>
            <ThemedView style={styles.sectionHeader}>
              <ThemedText type="defaultSemiBold" style={styles.sectionTitle}>
                Your Organizations
              </ThemedText>
            </ThemedView>

            <FlatList
              data={[
                // Personal account always at the top
                { isPersonal: true, id: 'personal-account', organization: null },
                ...(userMemberships?.data || []).map((membership) => ({
                  isPersonal: false,
                  id: membership.organization.id,
                  organization: membership.organization,
                })),
              ]}
              keyExtractor={(item) => item.id}
              renderItem={({ item }) => {
                // Handle personal account special case
                if (item.isPersonal) {
                  return (
                    <TouchableOpacity
                      style={[
                        styles.organizationItem,
                        organization?.id === undefined && styles.activeOrganization,
                      ]}
                      onPress={() => handleSelectOrganization(item.id)}
                    >
                      <ThemedView style={styles.orgItemImageContainer}>
                        <Image source={{ uri: user?.imageUrl || '' }} style={styles.orgItemImage} />
                      </ThemedView>
                      <ThemedView style={styles.orgItemInfoContainer}>
                        <ThemedText type="defaultSemiBold">Personal account</ThemedText>
                        <ThemedText>
                          {user?.username || user?.emailAddresses[0].emailAddress}
                        </ThemedText>
                      </ThemedView>
                      {organization?.id === undefined && (
                        <Ionicons name="checkmark" size={24} color="#4CAF50" />
                      )}
                    </TouchableOpacity>
                  )
                }

                // Regular organization item
                return (
                  <TouchableOpacity
                    style={[
                      styles.organizationItem,
                      item.id === organization?.id && styles.activeOrganization,
                    ]}
                    onPress={() => handleSelectOrganization(item.id)}
                  >
                    <ThemedView style={styles.orgItemImageContainer}>
                      <Image
                        source={{ uri: item.organization?.imageUrl || user?.imageUrl || '' }}
                        style={styles.orgItemImage}
                      />
                    </ThemedView>
                    <ThemedView style={styles.orgItemInfoContainer}>
                      <ThemedText type="defaultSemiBold">{item.organization?.name}</ThemedText>
                      <ThemedText>{item.organization?.slug}</ThemedText>
                    </ThemedView>
                    {item.id === organization?.id && (
                      <Ionicons name="checkmark" size={24} color="#4CAF50" />
                    )}
                  </TouchableOpacity>
                )
              }}
            />
          </ThemedView>

          {/* Add a button to create a new organization */}
          {/* This will direct them to app/screens/create-organization */}
          <TouchableOpacity style={styles.createOrgButton} onPress={handleCreateOrganization}>
            <Ionicons name="add-circle-outline" size={24} color="#fff" />
            <ThemedText style={styles.createOrgButtonText}>Create New Organization</ThemedText>
          </TouchableOpacity>
        </ThemedView>
      </SafeAreaView>
    </Modal>
  )
}

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
    justifyContent: 'flex-end',
    backgroundColor: 'rgba(0, 0, 0, 0)',
  },
  modalContent: {
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    padding: 16,
    maxHeight: '80%',
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: -2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
    paddingBottom: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  sectionContainer: {
    marginBottom: 16,
  },
  sectionHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  sectionTitle: {
    marginRight: 8,
  },
  badge: {
    backgroundColor: '#2196F3',
    borderRadius: 100,
    paddingHorizontal: 8,
  },
  badgeText: {
    color: 'white',
    fontSize: 12,
  },
  divider: {
    height: 1,
    backgroundColor: '#f0f0f0',
    marginVertical: 8,
  },
  invitationItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: '#f7f7f7',
  },
  organizationItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: '#f7f7f7',
  },
  activeOrganization: {
    backgroundColor: '#e3f2fd',
  },
  orgItemImageContainer: {
    width: 40,
    height: 40,
    borderRadius: 8,
    overflow: 'hidden',
  },
  orgItemImage: {
    width: 40,
    height: 40,
  },
  orgItemInfoContainer: {
    marginLeft: 12,
    flex: 1,
    backgroundColor: 'transparent',
  },
  createOrgButton: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#2196F3',
    padding: 12,
    borderRadius: 8,
  },
  createOrgButtonText: {
    color: 'white',
    marginLeft: 8,
  },
})
```

The `components/InvitationModal.tsx` is also needed so the user can accept invitations if they are selected from the organization switcher modal.

Create the `components/InvitationModal.tsx` and add the following code to it:

```tsx {{ filename: 'components/InvitationModal.tsx', collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import React from 'react'
import { Image, Modal, SafeAreaView, StyleSheet, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'

interface Props {
  isVisible: boolean
  onClose: () => void
  selectedInvitation: any | null
  user: any | null | undefined
  setActive?: (params: { organization: string | null }) => Promise<void>
  onComplete?: () => void
}

export function InvitationModal({
  isVisible,
  onClose,
  selectedInvitation,
  user,
  setActive,
  onComplete,
}: Props) {
  // Handle accepting an invitation and set the active organization
  const handleAcceptInvitation = async () => {
    try {
      if (!selectedInvitation) return
      await selectedInvitation?.accept()
      onClose()
      if (setActive) {
        await setActive({ organization: selectedInvitation?.publicOrganizationData.id })
      }
      if (onComplete) {
        onComplete()
      }
      router.replace('/protected')
    } catch (error) {
      console.error('Error accepting invitation:', error)
    }
  }

  // Handle skipping an invitation
  const handleSkipInvitation = async () => {
    try {
      onClose()
      if (onComplete) {
        onComplete()
      }
    } catch (error) {
      console.error('Error skipping invitation:', error)
    }
  }
  return (
    <Modal visible={isVisible} animationType="slide" transparent={true} onRequestClose={onClose}>
      <SafeAreaView style={styles.modalContainer}>
        <ThemedView style={styles.modalContent}>
          <ThemedView style={styles.modalHeader}>
            <ThemedText type="subtitle">Accept Invitation</ThemedText>
            <TouchableOpacity onPress={onClose}>
              <Ionicons name="close" size={24} color="#888" />
            </TouchableOpacity>
          </ThemedView>
          <ThemedView style={styles.modalBody}>
            <ThemedView style={styles.invitationDetails}>
              <ThemedView style={styles.orgImageContainer}>
                <Image
                  source={{
                    uri:
                      selectedInvitation?.publicOrganizationData.imageUrl || user?.imageUrl || '',
                  }}
                  style={styles.orgImage}
                />
              </ThemedView>
              <ThemedView style={styles.invitationTextContainer}>
                <ThemedText type="defaultSemiBold" style={styles.orgName}>
                  {selectedInvitation?.publicOrganizationData.name}
                </ThemedText>
                <ThemedText style={styles.orgSlug}>
                  {selectedInvitation?.publicOrganizationData.slug}
                </ThemedText>
              </ThemedView>
            </ThemedView>
            <ThemedText style={styles.invitationMessage}>
              You&apos;ve been invited to join this organization. Would you like to accept?
            </ThemedText>
          </ThemedView>
          <ThemedView style={styles.modalFooter}>
            <TouchableOpacity style={styles.rejectButton} onPress={handleSkipInvitation}>
              <ThemedText style={{ color: '#fff' }}>Not now</ThemedText>
            </TouchableOpacity>
            <TouchableOpacity style={styles.acceptButton} onPress={handleAcceptInvitation}>
              <ThemedText style={{ color: '#fff' }}>Accept</ThemedText>
            </TouchableOpacity>
          </ThemedView>
        </ThemedView>
      </SafeAreaView>
    </Modal>
  )
}

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
    justifyContent: 'flex-end',
    backgroundColor: 'rgba(0, 0, 0, 0)',
  },
  modalContent: {
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    padding: 16,
    maxHeight: '80%',
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: -2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
    paddingBottom: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  modalBody: {
    padding: 16,
  },
  invitationDetails: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 16,
  },
  orgImageContainer: {
    width: 60,
    height: 60,
    borderRadius: 10,
    overflow: 'hidden',
    marginRight: 16,
  },
  orgImage: {
    width: 60,
    height: 60,
  },
  invitationTextContainer: {
    flex: 1,
  },
  orgName: {
    fontSize: 18,
    marginBottom: 4,
  },
  orgSlug: {
    opacity: 0.7,
  },
  invitationMessage: {
    marginTop: 8,
    lineHeight: 22,
  },
  modalFooter: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 24,
  },
  rejectButton: {
    backgroundColor: '#f44336',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    flex: 1,
    marginRight: 8,
    alignItems: 'center',
  },
  acceptButton: {
    backgroundColor: '#4CAF50',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    flex: 1,
    marginLeft: 8,
    alignItems: 'center',
  },
})
```

Now back in the `app/(tabs)/protected/settings.tsx` file, add the two modal components to the screen. We're also adding two state objects for when a user selects an invitation and the required functions to handle those interactions:

```tsx {{ filename: 'app/(tabs)/protected/settings.tsx', ins: [11, 12, [30, 46], [86, 104]], collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import React, { useState } from 'react'
import { Image, StyleSheet, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { useAuth, useUser } from '@clerk/clerk-expo'
import { useAuth, useOrganization, useOrganizationList, useUser } from '@clerk/clerk-expo'
import { UserOrganizationInvitationResource } from '@clerk/types'

import { OrganizationSwitcherModal } from '@/components/OrganizationSwitcherModal'
import { InvitationModal } from '@/components/InvitationModal'

export default function HomeScreen() {
  const { user } = useUser()
  const { signOut } = useAuth()

  // Get the list of organizations the user is a member of and any invitations they have
  // Note: The `useOrganizationList` function requires that you specify what's included in the response
  const { userMemberships, userInvitations, setActive } = useOrganizationList({
    userMemberships: true,
    userInvitations: true,
  })

  // Get the active organization
  const { organization } = useOrganization()

  // A state object to control the visibility of the organization selection modal (will be added next)
  const [modalVisible, setModalVisible] = useState(false)
  const [selectedInvitation, setSelectedInvitation] =
    useState<UserOrganizationInvitationResource | null>(null)
  const [isInvitationModalVisible, setIsInvitationModalVisible] = useState(false)

  const handleInvitationComplete = () => {
    setSelectedInvitation(null)
  }

  const handleOpenInvitation = async (invitation: UserOrganizationInvitationResource) => {
    try {
      setSelectedInvitation(invitation)
      setModalVisible(false)
      setIsInvitationModalVisible(true)
    } catch (error) {
      console.error('Error setting active organization:', error)
    }
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedView style={styles.titleContainer}>
        <ThemedText type="title">Settings</ThemedText>
      </ThemedView>
      <ThemedView style={styles.contentContainer}>
        <ThemedText type="subtitle">User Information</ThemedText>
        <ThemedView style={styles.userContainer}>
          <ThemedView style={styles.userImageContainer}>
            <Image source={{ uri: user?.imageUrl || '' }} style={styles.userImage} />
          </ThemedView>
          <ThemedView style={styles.userInfoContainer}>
            <ThemedText type="defaultSemiBold">
              {user?.firstName} {user?.lastName}
            </ThemedText>
            <ThemedText>{user?.emailAddresses[0].emailAddress}</ThemedText>
          </ThemedView>
        </ThemedView>

        <ThemedText type="subtitle">Organization</ThemedText>
        <TouchableOpacity onPress={() => setModalVisible(true)}>
          <ThemedView style={styles.organizationContainer}>
            <ThemedView style={styles.organizationImageContainer}>
              <Image
                source={{ uri: organization?.imageUrl || user?.imageUrl || '' }}
                style={styles.organizationImage}
              />
            </ThemedView>
            <ThemedView style={styles.organizationInfoContainer}>
              <ThemedText type="defaultSemiBold">
                {organization?.name || 'Personal account'}
              </ThemedText>
              <ThemedText>{organization?.slug || 'No organization'}</ThemedText>
            </ThemedView>
            <Ionicons name="chevron-forward" size={24} color="#888" />
          </ThemedView>
        </TouchableOpacity>

        <OrganizationSwitcherModal
          modalVisible={modalVisible}
          setModalVisible={setModalVisible}
          userInvitations={userInvitations}
          userMemberships={userMemberships}
          organization={organization}
          user={user}
          handleOpenInvitation={handleOpenInvitation}
          setActive={setActive}
        />

        <InvitationModal
          isVisible={isInvitationModalVisible}
          onClose={() => setIsInvitationModalVisible(false)}
          selectedInvitation={selectedInvitation}
          user={user}
          setActive={setActive}
          onComplete={handleInvitationComplete}
        />

        <ThemedView style={styles.signOutButtonContainer}>
          <TouchableOpacity onPress={() => signOut()}>
            <ThemedText style={{ color: 'red' }}>Sign Out</ThemedText>
          </TouchableOpacity>
        </ThemedView>
      </ThemedView>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 16,
  },
  titleContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
    marginTop: 16,
    paddingHorizontal: 16,
  },
  contentContainer: {
    gap: 12,
    marginBottom: 24,
    borderRadius: 8,
    padding: 16,
    width: '100%',
    height: 150,
    paddingHorizontal: 16,
  },
  userContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f7f7f7',
    padding: 8,
    borderRadius: 8,
  },
  userInfoContainer: {
    marginLeft: 12,
    flex: 1,
    backgroundColor: 'transparent',
  },
  userImageContainer: {
    width: 50,
    height: 50,
    borderRadius: 100,
    backgroundColor: 'transparent',
  },
  userImage: {
    width: 50,
    height: 50,
    borderRadius: 100,
    backgroundColor: 'transparent',
  },
  signOutButtonContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    borderRadius: 8,
    padding: 8,
    backgroundColor: '#ffebee',
    justifyContent: 'center',
  },
  organizationContainer: {
    gap: 4,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f7f7f7',
    padding: 8,
    borderRadius: 8,
    justifyContent: 'space-between',
  },
  organizationInfoContainer: {
    marginLeft: 12,
    flex: 1,
    backgroundColor: 'transparent',
  },
  organizationImageContainer: {
    width: 50,
    height: 50,
    borderRadius: 10,
    backgroundColor: 'transparent',
  },
  organizationImage: {
    width: 50,
    height: 50,
    borderRadius: 10,
    backgroundColor: 'transparent',
  },
})
```

Next, you'll add the two screens to allow users to create organizations. When the user selects "Create organization" they will be directed to the `create-organization` screen first. When they specify the name and slug of the organization, they will then go to the `add-organization-members` screen where they can invite members to the organization by email.

Create the `app/screens/create-organization.tsx` file and add the following code:

```tsx {{ filename: 'app/screens/create-organization.tsx', collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import React, { useState } from 'react'
import { StyleSheet, TextInput, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { useOrganizationList } from '@clerk/clerk-expo'

export default function CreateOrganizationScreen() {
  const [name, setName] = useState('')
  const [slug, setSlug] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')

  // Function to generate slug from name
  const generateSlug = (name: string): string => {
    return name
      .toLowerCase()
      .trim()
      .replace(/[^\w\s-]/g, '') // Remove special characters except whitespace and hyphens
      .replace(/\s+/g, '-') // Replace spaces with hyphens
      .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
  }

  // Update slug when name changes
  const handleNameChange = (text: string) => {
    setName(text)
    setSlug(generateSlug(text))
  }

  // Using the Clerk Expo SDK helper function to create an organization and set it as active
  const { createOrganization, setActive } = useOrganizationList({ userMemberships: true })

  // Function to reset the form state
  const resetForm = () => {
    setName('')
    setSlug('')
    setError('')
    setIsLoading(false)
  }

  const handleCreateOrganization = async () => {
    if (!name.trim()) {
      setError('Organization name is required')
      return
    }

    setIsLoading(true)
    setError('')

    try {
      if (createOrganization) {
        const organization = await createOrganization({
          name: name.trim(),
          slug: slug.trim() || undefined, // Use the provided slug or let Clerk generate one
        })

        // Reset the form state
        resetForm()

        // Set the active organization
        setActive({ organization: organization.id })

        // Navigate to the add members screen with the new organization ID
        router.push('/screens/add-organization-members')
      }
    } catch (error) {
      console.error('Error creating organization:', error)
      setError('Failed to create organization. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedView style={styles.header}>
        <TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
          <Ionicons name="arrow-back" size={24} color="#000" />
        </TouchableOpacity>
        <ThemedText type="title">Create Organization</ThemedText>
      </ThemedView>

      <ThemedView style={styles.content}>
        <ThemedView style={styles.inputContainer}>
          <ThemedText type="subtitle">Organization Name *</ThemedText>
          <TextInput
            value={name}
            onChangeText={handleNameChange}
            placeholder="Enter organization name"
            style={styles.input}
          />
        </ThemedView>

        <ThemedView style={styles.inputContainer}>
          <ThemedText type="subtitle">Organization Slug (optional)</ThemedText>
          <ThemedText style={styles.helpText}>
            The slug will be used in URLs and must be unique. If not provided, one will be
            generated.
          </ThemedText>
          <TextInput
            value={slug}
            onChangeText={(text: string) => setSlug(text.toLowerCase().replace(/\s+/g, '-'))}
            placeholder="your-organization-slug"
            style={styles.input}
          />
        </ThemedView>

        {error ? <ThemedText style={styles.errorText}>{error}</ThemedText> : null}

        <TouchableOpacity
          style={[styles.nextButton, (!name.trim() || isLoading) && styles.disabledButton]}
          onPress={handleCreateOrganization}
          disabled={!name.trim() || isLoading}
        >
          <ThemedText style={styles.nextButtonText}>
            {isLoading ? 'Creating...' : 'Next'}
          </ThemedText>
        </TouchableOpacity>
      </ThemedView>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 16,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 16,
    paddingBottom: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  backButton: {
    marginRight: 16,
  },
  content: {
    padding: 16,
    flex: 1,
  },
  inputContainer: {
    marginBottom: 24,
  },
  input: {
    marginTop: 8,
    borderWidth: 1,
    borderColor: '#e0e0e0',
    borderRadius: 8,
    padding: 12,
  },
  helpText: {
    fontSize: 12,
    color: '#666',
    marginTop: 4,
  },
  nextButton: {
    backgroundColor: '#2196F3',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 16,
  },
  nextButtonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 16,
  },
  disabledButton: {
    backgroundColor: '#BDBDBD',
  },
  errorText: {
    color: 'red',
    marginBottom: 16,
  },
})
```

Then create the `app/screens/add-organization-members.tsx` file and add the following code:

```tsx {{ filename: 'app/screens/add-organization-members.tsx', collapsible: true }}
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import React, { useState } from 'react'
import { FlatList, StyleSheet, TextInput, TouchableOpacity } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { useOrganization } from '@clerk/clerk-expo'

interface MemberEmail {
  id: string
  email: string
}

export default function AddOrganizationMembersScreen() {
  const [emails, setEmails] = useState<MemberEmail[]>([])
  const [currentEmail, setCurrentEmail] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')

  const { organization } = useOrganization()

  const isValidEmail = (email: string) => {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }

  const handleAddEmail = () => {
    if (!currentEmail.trim()) return

    if (!isValidEmail(currentEmail)) {
      setError('Please enter a valid email address')
      return
    }

    // Check if email already exists in the list
    if (emails.some((item) => item.email.toLowerCase() === currentEmail.toLowerCase())) {
      setError('This email has already been added')
      return
    }

    setEmails([...emails, { id: Date.now().toString(), email: currentEmail.trim() }])
    setCurrentEmail('')
    setError('')
  }

  const handleRemoveEmail = (id: string) => {
    setEmails(emails.filter((email) => email.id !== id))
  }

  const handleInviteMembers = async () => {
    if (emails.length === 0) {
      setError('Please add at least one email address')
      return
    }

    setIsLoading(true)
    setError('')

    try {
      if (organization) {
        // Invite members using the Clerk Expo SDK
        await organization.inviteMembers({
          emailAddresses: emails.map((e) => e.email),
          role: 'org:member',
        })

        // Navigate back to the home screen
        router.replace('/protected')
      } else {
        throw new Error('Organization not found')
      }
    } catch (error) {
      console.error('Error inviting members:', error)
      setError('Failed to invite members. Please try again.')
    } finally {
      setIsLoading(false)
    }
  }

  const handleSkip = () => {
    // Navigate back to home screen without inviting anyone
    router.replace('/protected')
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedView style={styles.header}>
        <TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
          <Ionicons name="arrow-back" size={24} color="#000" />
        </TouchableOpacity>
        <ThemedText type="title">Add Members</ThemedText>
      </ThemedView>

      <ThemedView style={styles.content}>
        <ThemedText type="subtitle">Invite members to your organization</ThemedText>
        <ThemedText style={styles.helpText}>
          Enter email addresses of people you&apos;d like to invite to your organization.
        </ThemedText>

        <ThemedView style={styles.inputContainer}>
          <ThemedView style={styles.emailInputRow}>
            <TextInput
              value={currentEmail}
              onChangeText={(text: string) => {
                setCurrentEmail(text)
                if (error) setError('')
              }}
              placeholder="Enter email address"
              style={styles.input}
              keyboardType="email-address"
              autoCapitalize="none"
              onSubmitEditing={handleAddEmail}
            />
            <TouchableOpacity
              style={styles.addButton}
              onPress={handleAddEmail}
              disabled={!currentEmail.trim()}
            >
              <Ionicons name="add" size={24} color="white" />
            </TouchableOpacity>
          </ThemedView>

          {error ? <ThemedText style={styles.errorText}>{error}</ThemedText> : null}
        </ThemedView>

        <ThemedView style={styles.emailListContainer}>
          <FlatList
            data={emails}
            keyExtractor={(item) => item.id}
            renderItem={({ item }) => (
              <ThemedView style={styles.emailItem}>
                <ThemedText>{item.email}</ThemedText>
                <TouchableOpacity onPress={() => handleRemoveEmail(item.id)}>
                  <Ionicons name="close-circle" size={20} color="#888" />
                </TouchableOpacity>
              </ThemedView>
            )}
            ListEmptyComponent={
              <ThemedView style={styles.emptyList}>
                <ThemedText style={styles.emptyListText}>No members added yet</ThemedText>
              </ThemedView>
            }
          />
        </ThemedView>

        <ThemedView style={styles.buttonContainer}>
          <TouchableOpacity style={styles.skipButton} onPress={handleSkip} disabled={isLoading}>
            <ThemedText style={styles.skipButtonText}>Skip</ThemedText>
          </TouchableOpacity>

          <TouchableOpacity
            style={[styles.inviteButton, isLoading && styles.disabledButton]}
            onPress={handleInviteMembers}
            disabled={emails.length === 0 || isLoading}
          >
            <ThemedText style={styles.inviteButtonText}>
              {isLoading ? 'Inviting...' : 'Invite Members'}
            </ThemedText>
          </TouchableOpacity>
        </ThemedView>
      </ThemedView>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 16,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 16,
    paddingBottom: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  backButton: {
    marginRight: 16,
  },
  content: {
    padding: 16,
    flex: 1,
  },
  helpText: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
    marginBottom: 16,
  },
  inputContainer: {
    marginBottom: 16,
  },
  emailInputRow: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  input: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#e0e0e0',
    borderRadius: 8,
    padding: 12,
  },
  addButton: {
    backgroundColor: '#2196F3',
    width: 48,
    height: 48,
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 8,
  },
  emailListContainer: {
    flex: 1,
    marginTop: 8,
    borderWidth: 1,
    borderColor: '#e0e0e0',
    borderRadius: 8,
    padding: 8,
  },
  emailItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  emptyList: {
    padding: 24,
    alignItems: 'center',
  },
  emptyListText: {
    color: '#888',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 16,
  },
  skipButton: {
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    borderWidth: 1,
    borderColor: '#e0e0e0',
    flex: 1,
    marginRight: 8,
  },
  skipButtonText: {
    fontWeight: '600',
  },
  inviteButton: {
    backgroundColor: '#2196F3',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    flex: 2,
  },
  inviteButtonText: {
    color: 'white',
    fontWeight: '600',
  },
  disabledButton: {
    backgroundColor: '#BDBDBD',
  },
  errorText: {
    color: 'red',
    marginTop: 8,
  },
})
```

The last detail is to add the new screens into the routing configuration in `app/(tabs)/_layout.tsx`. These are being added to the protected stack so only authorized users may access them.

Open that file and make the following changes:

```tsx {{ filename: 'app/(tabs)/_layout.tsx', ins: [16, 17] }}
import { useAuth } from '@clerk/clerk-expo'
import { Stack } from 'expo-router'

export default function AppLayout() {
  const { isSignedIn } = useAuth()

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Protected guard={!isSignedIn}>
        <Stack.Screen name="index" />
        <Stack.Screen name="sign-up" />
      </Stack.Protected>

      <Stack.Protected guard={isSignedIn!}>
        <Stack.Screen name="protected" />
        <Stack.Screen name="screens/create-organization" />
        <Stack.Screen name="screens/add-organization-members" />
      </Stack.Protected>
    </Stack>
  )
}
```

Now you may access the application using `pnpm start` and then `w` to open the application in your web browser. Try creating a new organization and adding a member to it. It's best if you have an alternate email address you can use for this. If you do, invite that email address then sign into the app as that account and check the organization switcher modal as that user to test accepting an invitation.

### Step 3: Update RLS policies to use organization ID

One thing you may notice is that the data on the home screen stays the same regardless of the active organization. This is because the existing [RLS policies](https://supabase.com/docs/guides/database/postgres/row-level-security) still use the `sub` claim (which is the user ID) to identify the user making requests. We need to update the RLS policies to use the `o`.`id` claim instead to identify the active organization of the user making the request. The policies will be configured to fall back on the `sub` claim to identify the user if the `o`.`id` claim is not present (as in, they are in their personal account).

Instead of accessing the claims from the [JWT](/glossary#json-web-token) directly, a custom SQL function can be used to extract the correct claim and return it as a string to the RLS policy. This approach minimizes potential mistakes and centralizes the logic for accessing the claims in one place.

Start by running the following command in your terminal to create a new migration file.

```bash
supabase migration new setup-orgs
```

A new file with the name `setup-orgs` will be created in the `migrations` directory. Open that file and add the following SQL to add the function, update the existing policies, and add a `created_by` column to the `time_entries` table (which will be used in the next step to identify timers on the home screen of the app):

```sql {{ filename: 'supabase/migrations/{TIMESTAMP}_setup-orgs.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;

-- Update RLS policies to use the new requesting_owner_id function
-- Update select policy
DROP POLICY IF EXISTS select_own_time_entries ON time_entries;
CREATE POLICY select_own_time_entries ON time_entries
  FOR SELECT
  USING (owner_id = requesting_owner_id());

-- Update insert policy
DROP POLICY IF EXISTS insert_own_time_entries ON time_entries;
CREATE POLICY insert_own_time_entries ON time_entries
  FOR INSERT
  WITH CHECK (owner_id = requesting_owner_id());

-- Update update policy
DROP POLICY IF EXISTS update_own_time_entries ON time_entries;
CREATE POLICY update_own_time_entries ON time_entries
  FOR UPDATE
  USING (owner_id = requesting_owner_id());

-- Update delete policy
DROP POLICY IF EXISTS delete_own_time_entries ON time_entries;
CREATE POLICY delete_own_time_entries ON time_entries
  FOR DELETE
  USING (owner_id = requesting_owner_id());

-- Add created_by column to time_entries table
alter table time_entries add created_by text default (auth.jwt() ->> 'sub'::text);
```

Now run the following command to apply the migration:

```bash
supabase db push
```

Next you'll need to update some of the database access code in `app/(tabs)/protected/index.tsx` to use the organization ID instead of the user ID when creating records. These changes also add a `setInterval` to refresh the entries every 30 seconds, acting as a polling mechanism to detect changes from other users.

Open the `app/(tabs)/protected/index.tsx` file and make the following changes:

```tsx {{ filename: 'app/(tabs)/protected/index.tsx', ins: [11, 19, 20, 25, 26, 43, 44, [107, 109], 174, [241, 261]], del: [10, 106, 173, [228, 239]], collapsible: true }}
import React, { useEffect, useRef, useState } from 'react'
import { FlatList, StyleSheet } from 'react-native'

import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { TimeEntryItem } from '@/components/TimeEntryItem'
import { TimerCounter } from '@/components/TimerCounter'
import { TimerInputForm } from '@/components/TimerInputForm'
import { createSupabaseClerkClient } from '@/utils/supabase'
import { useAuth, useUser } from '@clerk/clerk-expo'
import { useAuth, useOrganization, useUser } from '@clerk/clerk-expo'

interface TimeEntry {
  id: string
  description: string
  start_time: string
  end_time: string | null
  created_at: string
  // This is the user ID of the user who created the time entry
  created_by: string
}

export default function HomeScreen() {
  const { user } = useUser()
  // This is the active organization of the user
  const { organization } = useOrganization()
  const { getToken } = useAuth()
  const [description, setDescription] = useState('')
  const [isTimerRunning, setIsTimerRunning] = useState(false)
  const [currentEntryId, setCurrentEntryId] = useState<string | null>(null)
  const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([])
  const [elapsedTime, setElapsedTime] = useState(0)
  // We track the start time to calculate elapsed time
  const startTimeRef = useRef<Date | null>(null)
  // Track the last time we updated the elapsed time
  const lastUpdateRef = useRef<number>(0)

  // Timer interval reference
  const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  const supabase = createSupabaseClerkClient(getToken())

  // This is the owner ID of the time entry, defaults to the user ID if no organization is active
  const ownerId = organization?.id || user?.id

  // Stop the timer counter
  const stopTimerCounter = () => {
    if (timerIntervalRef.current) {
      clearInterval(timerIntervalRef.current)
      timerIntervalRef.current = null
    }
    setElapsedTime(0)
    startTimeRef.current = null
  }

  // Start the timer counter
  const startTimerCounter = (initialStartTime?: Date) => {
    const start = initialStartTime || new Date()
    startTimeRef.current = start
    lastUpdateRef.current = Date.now()

    // Calculate initial elapsed time
    const now = new Date()
    const initialDiffInSeconds = Math.floor((now.getTime() - start.getTime()) / 1000)
    setElapsedTime(initialDiffInSeconds)

    // Clear any existing interval
    if (timerIntervalRef.current) {
      clearInterval(timerIntervalRef.current)
      timerIntervalRef.current = null
    }

    // Set up the interval to update elapsed time every second
    const intervalId = setInterval(() => {
      if (startTimeRef.current) {
        // Use the current time for accurate timing
        const now = new Date()
        const diffInSeconds = Math.floor((now.getTime() - startTimeRef.current.getTime()) / 1000)

        // Only update if the time has actually changed
        if (diffInSeconds !== elapsedTime) {
          setElapsedTime(diffInSeconds)
        }
      }
    }, 500) // Update more frequently for better accuracy

    timerIntervalRef.current = intervalId
  }

  // Function to fetch time entries from Supabase
  const fetchTimeEntries = async () => {
    try {
      const { data, error } = await supabase
        .from('time_entries')
        .select('*')
        .order('start_time', { ascending: false })

      if (error) {
        console.error('Error fetching time entries:', error)
        return
      }

      setTimeEntries(data || [])

      // Check if there's an active timer (entry without end_time)
      const activeEntry = data?.find((entry) => !entry.end_time)
      const activeEntry = data?.find(
        (entry: TimeEntry) => !entry.end_time && entry.created_by === user?.id,
      )

      if (activeEntry) {
        setIsTimerRunning(true)
        setCurrentEntryId(activeEntry.id)
        setDescription(activeEntry.description)

        // Start the timer counter with the saved start time
        const startDate = new Date(activeEntry.start_time)
        startTimerCounter(startDate)
      }
    } catch (error) {
      console.error('Error fetching time entries:', error)
    }
  }

  // Function to update a time entry
  const updateTimeEntry = async (id: string, updates: Partial<TimeEntry>) => {
    try {
      const { error } = await supabase.from('time_entries').update(updates).eq('id', id)

      if (error) {
        console.error('Error updating time entry:', error)
        return
      }

      // Refresh the time entries list
      fetchTimeEntries()
    } catch (error) {
      console.error('Error updating time entry:', error)
    }
  }

  // Function to delete a time entry
  const deleteTimeEntry = async (id: string) => {
    try {
      const { error } = await supabase.from('time_entries').delete().eq('id', id)

      if (error) {
        console.error('Error deleting time entry:', error)
        return
      }

      // Refresh the time entries list
      fetchTimeEntries()
    } catch (error) {
      console.error('Error deleting time entry:', error)
    }
  }

  // Start a new timer
  const startTimer = async () => {
    if (!description.trim()) {
      alert('Please enter what you are working on')
      return
    }

    try {
      const startDate = new Date()
      const { data, error } = await supabase
        .from('time_entries')
        .insert({
          description: description.trim(),
          start_time: startDate.toISOString(),
          owner_id: user?.id,
          owner_id: ownerId,
        })
        .select()

      if (error) {
        console.error('Error starting timer:', error)
        return
      }

      if (data && data[0]) {
        setIsTimerRunning(true)
        setCurrentEntryId(data[0].id)
        startTimerCounter(startDate)
        fetchTimeEntries()
      }
    } catch (error) {
      console.error('Error in startTimer:', error)
    }
  }

  // Stop the current timer
  const stopTimer = async () => {
    if (!currentEntryId) return

    try {
      const { error } = await supabase
        .from('time_entries')
        .update({ end_time: new Date().toISOString() })
        .eq('id', currentEntryId)

      if (error) {
        console.error('Error stopping timer:', error)
        return
      }

      setIsTimerRunning(false)
      setCurrentEntryId(null)
      setDescription('')
      stopTimerCounter()
      fetchTimeEntries()
    } catch (error) {
      console.error('Error in stopTimer:', error)
    }
  }

  // Update the elapsed time even when the app is in background
  useEffect(() => {
    if (isTimerRunning && startTimeRef.current) {
      const now = new Date()
      const diffInSeconds = Math.floor((now.getTime() - startTimeRef.current.getTime()) / 1000)
      setElapsedTime(diffInSeconds)
    }
  }, [isTimerRunning])

  // Fetch time entries when component mounts
  useEffect(() => {
    fetchTimeEntries()

    // Clean up timer interval when component unmounts
    return () => {
      if (timerIntervalRef.current) {
        clearInterval(timerIntervalRef.current)
        timerIntervalRef.current = null
      }
    }
  }, [])

  // Fetch time entries when component mounts or user changes
  useEffect(() => {
    fetchTimeEntries()

    // Set up periodic refresh every 30 seconds to detect changes from other users
    const refreshInterval = setInterval(() => {
      fetchTimeEntries()
    }, 30000)

    // Clean up interval on component unmount
    return () => {
      if (timerIntervalRef.current) {
        clearInterval(timerIntervalRef.current)
        timerIntervalRef.current = null
      }

      if (refreshInterval) {
        clearInterval(refreshInterval)
      }
    }
  }, [ownerId])

  return (
    <ThemedView style={styles.container}>
      <ThemedView style={styles.titleContainer}>
        <ThemedText type="title">⏳ Aika Timer</ThemedText>
      </ThemedView>

      {/* Timer Form */}
      <ThemedView style={styles.formContainer}>
        {!isTimerRunning ? (
          <TimerInputForm
            description={description}
            onDescriptionChange={setDescription}
            onStartTimer={startTimer}
          />
        ) : (
          <TimerCounter
            isRunning={isTimerRunning}
            description={description}
            elapsedTime={elapsedTime}
            onStart={startTimer}
            onStop={stopTimer}
          />
        )}
      </ThemedView>

      {/* Time Entries List */}
      <ThemedView style={styles.entriesContainer}>
        <ThemedText type="subtitle" style={styles.entriesTitle}>
          Previous Work Logs
        </ThemedText>
        {timeEntries.length === 0 ? (
          <ThemedText style={styles.emptyText}>
            No work logs yet. Start tracking your time!
          </ThemedText>
        ) : (
          <FlatList
            data={timeEntries}
            keyExtractor={(item) => item.id}
            renderItem={({ item }) => (
              <TimeEntryItem item={item} onUpdate={updateTimeEntry} onDelete={deleteTimeEntry} />
            )}
            style={styles.list}
            scrollEnabled={true}
            showsVerticalScrollIndicator={true}
            contentContainerStyle={styles.listContentContainer}
          />
        )}
      </ThemedView>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  titleContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
    marginTop: 16,
    paddingHorizontal: 16,
  },
  formContainer: {
    gap: 12,
    marginBottom: 24,
    borderRadius: 8,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    width: '100%',
    height: 150,
    paddingHorizontal: 16,
  },
  disabledInput: {
    backgroundColor: '#f0f0f0',
    color: '#666',
  },
  entriesContainer: {
    gap: 12,
    flex: 1,
  },
  list: {
    paddingHorizontal: 16,
  },
  listContentContainer: {
    flexGrow: 1,
    paddingBottom: 16,
  },
  entriesTitle: {
    paddingHorizontal: 16,
  },
  emptyText: {
    fontStyle: 'italic',
    color: '#888',
    marginTop: 8,
    paddingHorizontal: 16,
  },
  signOutButton: {
    backgroundColor: '#64748B',
    padding: 12,
    borderRadius: 6,
    alignItems: 'center',
    marginTop: 8,
  },
  container: {
    flex: 1,
    paddingTop: 16,
  },
  contentContainer: {
    padding: 16,
  },
})
```

The last thing to do is update the `TimeEntryItem` component to render the name of the user who created the entry. This will use the `created_by` field in the database, which is set to the user ID when the entry is created.

This is used along with the [`useOrganization` hook](/docs/references/react/use-organization) from the Clerk Expo SDK to load the list of members in the organization and find the member with the matching user ID.

Open the `components/TimeEntryItem.tsx` and make the following changes:

```tsx {{ filename: 'components/TimeEntryItem.tsx', ins: [1, 24, 83, 84, 90, [139, 157], 270, [274, 277], [585, 591]], collapsible: true }}
import { useOrganization, useUser } from '@clerk/clerk-expo'
import { Ionicons } from '@expo/vector-icons'
import React, { useEffect, useRef, useState } from 'react'
import {
  Animated,
  Easing,
  Modal,
  StyleSheet,
  TextInput,
  TouchableOpacity,
  View,
} from 'react-native'

// Import ThemedText and ThemedView components
import { ThemedText } from './ThemedText'
import { ThemedView } from './ThemedView'

interface TimeEntry {
  id: string
  description: string
  start_time: string
  end_time: string | null
  created_at: string
  created_by?: string
}

interface TimeEntryItemProps {
  item: TimeEntry
  onUpdate?: (id: string, updates: Partial<TimeEntry>) => Promise<void>
  onDelete?: (id: string) => Promise<void>
}

// Custom slow spinner component
function SlowSpinner() {
  const spinValue = useRef(new Animated.Value(0)).current

  useEffect(() => {
    // Create a continuous rotation animation
    const startRotation = () => {
      // Reset the value to 0 when starting
      spinValue.setValue(0)

      // Create the animation
      Animated.timing(spinValue, {
        toValue: 1,
        duration: 3000, // Slower animation (3 seconds per rotation)
        easing: Easing.linear,
        useNativeDriver: true,
      }).start(() => startRotation()) // When complete, run again
    }

    // Start the animation loop
    startRotation()

    // Cleanup function
    return () => {
      // This will stop any pending animations when component unmounts
      spinValue.stopAnimation()
    }
  }, [spinValue])

  const spin = spinValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  })

  return (
    <Animated.View
      style={{
        transform: [{ rotate: spin }],
        width: 16,
        height: 16,
        borderWidth: 2,
        borderColor: '#2563EB',
        borderTopColor: 'transparent',
        borderRadius: 8,
      }}
    />
  )
}

export function TimeEntryItem({ item, onUpdate, onDelete }: TimeEntryItemProps) {
  const { user } = useUser()
  const { organization } = useOrganization()
  const [isModalVisible, setIsModalVisible] = useState(false)
  const [editedDescription, setEditedDescription] = useState(item.description)
  const [startDate, setStartDate] = useState(new Date(item.start_time))
  const [endDate, setEndDate] = useState(item.end_time ? new Date(item.end_time) : null)
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [creatorName, setCreatorName] = useState<string | null>(null)

  // Track the text inputs separately from the actual date objects
  const [startDateText, setStartDateText] = useState('')
  const [endDateText, setEndDateText] = useState('')

  // Reset form state when modal is opened
  useEffect(() => {
    if (isModalVisible) {
      setEditedDescription(item.description)
      setStartDate(new Date(item.start_time))
      setEndDate(item.end_time ? new Date(item.end_time) : null)

      // Initialize text fields with formatted dates
      const formattedStartDate = formatDate(new Date(item.start_time).toISOString())
      const formattedStartTime = formatTime(new Date(item.start_time))
      setStartDateText(`${formattedStartDate} ${formattedStartTime}`)

      if (item.end_time) {
        const formattedEndDate = formatDate(new Date(item.end_time).toISOString())
        const formattedEndTime = formatTime(new Date(item.end_time))
        setEndDateText(`${formattedEndDate} ${formattedEndTime}`)
      } else {
        setEndDateText('')
      }
    }
  }, [isModalVisible, item.description, item.start_time, item.end_time])

  // Format date to display in a readable format
  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    return date.toLocaleDateString()
  }

  // Format time to display in a readable format
  const formatTime = (date: Date) => {
    return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
  }

  // Initialize date text fields on component mount
  useEffect(() => {
    if (startDate) {
      setStartDateText(`${formatDate(startDate.toISOString())} ${formatTime(startDate)}`)
    }
    if (endDate) {
      setEndDateText(`${formatDate(endDate.toISOString())} ${formatTime(endDate)}`)
    }
  }, [startDate, endDate])

  // Get creator name if created_by is available
  useEffect(() => {
    if (item.created_by) {
      // If the current user is the creator
      if (user && user.id === item.created_by) {
        setCreatorName('You')
      } else {
        organization?.getMemberships().then((memberships) => {
          const member = memberships.data.find((m) => m.publicUserData?.userId === item.created_by)
          setCreatorName(
            `${member?.publicUserData?.firstName} ${member?.publicUserData?.lastName} (${member?.publicUserData?.identifier})` ||
              'Another team member',
          )
        })
      }
    } else {
      setCreatorName(null)
    }
  }, [item.created_by, user])

  // Calculate duration between start and end time
  const calculateDuration = (start: string, end: string | null) => {
    if (!end) return 'In progress'

    const startDate = new Date(start)
    const endDate = new Date(end)
    const diffMs = endDate.getTime() - startDate.getTime()

    const diffMins = Math.floor(diffMs / 60000)
    const hours = Math.floor(diffMins / 60)
    const mins = diffMins % 60

    return `${hours}h${mins}m`
  }

  // Handle saving changes
  const handleSave = async () => {
    if (!onUpdate) return

    // Try to parse dates from text inputs before saving
    let validStartDate = startDate
    let validEndDate = endDate

    // Parse start date text
    try {
      const [datePart, timePart] = startDateText.split(' ')
      if (datePart && timePart) {
        const [month, day, year] = datePart.split('/')
        const [hours, minutes] = timePart.replace('AM', '').replace('PM', '').trim().split(':')

        if (month && day && year && hours && minutes) {
          const newDate = new Date()
          newDate.setFullYear(parseInt(year), parseInt(month) - 1, parseInt(day))

          let hrs = parseInt(hours)
          if (timePart.includes('PM') && hrs < 12) hrs += 12
          if (timePart.includes('AM') && hrs === 12) hrs = 0

          newDate.setHours(hrs, parseInt(minutes))
          validStartDate = newDate
        }
      }
    } catch {
      // Use the existing startDate if parsing fails
    }

    // Parse end date text if it exists
    if (endDateText && endDate) {
      try {
        const [datePart, timePart] = endDateText.split(' ')
        if (datePart && timePart) {
          const [month, day, year] = datePart.split('/')
          const [hours, minutes] = timePart.replace('AM', '').replace('PM', '').trim().split(':')

          if (month && day && year && hours && minutes) {
            const newDate = new Date()
            newDate.setFullYear(parseInt(year), parseInt(month) - 1, parseInt(day))

            let hrs = parseInt(hours)
            if (timePart.includes('PM') && hrs < 12) hrs += 12
            if (timePart.includes('AM') && hrs === 12) hrs = 0

            newDate.setHours(hrs, parseInt(minutes))
            validEndDate = newDate
          }
        }
      } catch {
        // Use the existing endDate if parsing fails
        console.error('Failed to parse end date')
      }
    }

    setIsSubmitting(true)
    try {
      await onUpdate(item.id, {
        description: editedDescription,
        start_time: validStartDate.toISOString(),
        end_time: validEndDate ? validEndDate.toISOString() : null,
      })
      setIsModalVisible(false)
    } catch (error) {
      console.error('Failed to update time entry:', error)
    } finally {
      setIsSubmitting(false)
    }
  }

  // Handle deleting the entry
  const handleDelete = async () => {
    if (!onDelete) return

    setIsSubmitting(true)
    try {
      await onDelete(item.id)
      setIsModalVisible(false)
    } catch (error) {
      console.error('Error deleting time entry:', error)
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <ThemedView style={styles.container}>
      {/* View mode - always visible */}
      <TouchableOpacity
        style={styles.entryItem}
        onPress={() => setIsModalVisible(true)}
        activeOpacity={0.7}
      >
        <ThemedView style={styles.entryContent}>
          <View style={styles.descriptionContainer}>
            <ThemedText type="defaultSemiBold" numberOfLines={1}>
              {item.description}
            </ThemedText>
            {organization && creatorName && (
              <ThemedText style={styles.creatorText}>{creatorName}</ThemedText>
            )}
          </View>
          {item.end_time ? (
            <ThemedText style={styles.durationText}>
              {calculateDuration(item.start_time, item.end_time)}
            </ThemedText>
          ) : (
            <View style={styles.inProgressContainer}>
              <SlowSpinner />
              <ThemedText style={styles.inProgressText}>In progress</ThemedText>
            </View>
          )}
        </ThemedView>
        <View style={styles.rightSection}>
          <ThemedText style={styles.dateText}>{formatDate(item.start_time)}</ThemedText>
          <Ionicons name="chevron-forward" size={16} color="#888" />
        </View>
      </TouchableOpacity>

      {/* Edit mode - in modal */}
      <Modal
        visible={isModalVisible}
        onRequestClose={() => setIsModalVisible(false)}
        animationType="fade"
        transparent
      >
        <View style={styles.modalBackdrop}>
          <View style={styles.modalContainer}>
            <ThemedView style={styles.modalContent}>
              <ThemedText style={styles.modalTitle}>Edit Time Entry</ThemedText>

              <View style={styles.formGroup}>
                <ThemedText style={styles.label}>Description</ThemedText>
                <TextInput
                  style={styles.input}
                  value={editedDescription}
                  onChangeText={setEditedDescription}
                  placeholder="What were you working on?"
                  placeholderTextColor="#aaa"
                />
              </View>

              <View style={styles.formGroup}>
                <ThemedText style={styles.label}>Start Time</ThemedText>
                <TextInput
                  style={styles.input}
                  value={startDateText}
                  onChangeText={(text) => {
                    // Just update the text field without validation
                    setStartDateText(text)

                    // Try to parse the date but don't throw errors
                    try {
                      const [datePart, timePart] = text.split(' ')
                      if (datePart && timePart) {
                        const [month, day, year] = datePart.split('/')
                        const [hours, minutes] = timePart
                          .replace('AM', '')
                          .replace('PM', '')
                          .trim()
                          .split(':')

                        if (month && day && year && hours && minutes) {
                          const newDate = new Date(startDate)
                          newDate.setFullYear(parseInt(year), parseInt(month) - 1, parseInt(day))

                          let hrs = parseInt(hours)
                          if (timePart.includes('PM') && hrs < 12) hrs += 12
                          if (timePart.includes('AM') && hrs === 12) hrs = 0

                          newDate.setHours(hrs, parseInt(minutes))
                          setStartDate(newDate)
                        }
                      }
                    } catch {
                      // Silently fail - we'll use the previous valid date if parsing fails
                    }
                  }}
                  placeholder="MM/DD/YYYY HH:MM AM/PM"
                  placeholderTextColor="#aaa"
                />
              </View>

              <View style={styles.formGroup}>
                <ThemedText style={styles.label}>End Time</ThemedText>
                {endDate ? (
                  <TextInput
                    style={styles.input}
                    value={endDateText}
                    onChangeText={(text) => {
                      // Just update the text field without validation
                      setEndDateText(text)

                      // Try to parse the date but don't throw errors
                      try {
                        const [datePart, timePart] = text.split(' ')
                        if (datePart && timePart) {
                          const [month, day, year] = datePart.split('/')
                          const [hours, minutes] = timePart
                            .replace('AM', '')
                            .replace('PM', '')
                            .trim()
                            .split(':')

                          if (month && day && year && hours && minutes) {
                            const newDate = new Date(endDate)
                            newDate.setFullYear(parseInt(year), parseInt(month) - 1, parseInt(day))

                            let hrs = parseInt(hours)
                            if (timePart.includes('PM') && hrs < 12) hrs += 12
                            if (timePart.includes('AM') && hrs === 12) hrs = 0

                            newDate.setHours(hrs, parseInt(minutes))
                            setEndDate(newDate)
                          }
                        }
                      } catch {
                        // Silently fail - we'll use the previous valid date if parsing fails
                      }
                    }}
                    placeholder="MM/DD/YYYY HH:MM AM/PM"
                    placeholderTextColor="#aaa"
                  />
                ) : (
                  <View style={styles.inProgressContainer}>
                    <SlowSpinner />
                    <ThemedText style={styles.inProgressText}>In progress</ThemedText>
                  </View>
                )}
              </View>

              <View style={styles.buttonContainer}>
                <TouchableOpacity
                  style={styles.cancelButton}
                  onPress={() => setIsModalVisible(false)}
                  disabled={isSubmitting}
                >
                  <ThemedText style={styles.buttonText}>Cancel</ThemedText>
                </TouchableOpacity>
                <TouchableOpacity
                  style={[styles.button, styles.deleteButton]}
                  onPress={handleDelete}
                  disabled={isSubmitting}
                >
                  <ThemedText style={styles.buttonText}>Delete</ThemedText>
                </TouchableOpacity>
                <TouchableOpacity
                  style={[styles.button, styles.saveButton]}
                  onPress={handleSave}
                  disabled={isSubmitting}
                >
                  <ThemedText style={styles.buttonText}>Save</ThemedText>
                </TouchableOpacity>
              </View>
            </ThemedView>
          </View>
        </View>
      </Modal>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    marginVertical: 4,
  },
  modalContent: {
    padding: 16,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  entryItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    borderBottomWidth: 1,
    borderBottomColor: 'transparent',
    backgroundColor: '#f7f7f7',
    padding: 8,
    marginVertical: 4,
    borderRadius: 6,
    gap: 3,
    alignItems: 'center',
  },
  entryContent: {
    gap: 4,
    alignItems: 'flex-start',
    justifyContent: 'center',
    backgroundColor: 'transparent',
    flex: 1,
  },
  dateText: {
    fontSize: 12,
    color: '#888',
  },
  rightSection: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 4,
  },
  durationText: {
    fontWeight: 'bold',
    fontFamily: 'monospace',
  },
  inProgressContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 6,
    backgroundColor: '#EBF4FF', // Light blue background
    paddingVertical: 1,
    paddingHorizontal: 6,
    borderRadius: 100,
  },
  inProgressText: {
    color: '#2563EB',
    fontWeight: '600',
    fontSize: 12,
  },
  formGroup: {
    marginBottom: 16,
  },
  label: {
    fontSize: 14,
    fontWeight: '600',
    marginBottom: 6,
  },
  input: {
    borderWidth: 1,
    borderRadius: 6,
    padding: 10,
    borderColor: '#ccc',
  },
  dateTimeButton: {
    borderWidth: 1,
    borderRadius: 6,
    padding: 10,
    backgroundColor: '#333',
    justifyContent: 'center',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    gap: 10,
    marginTop: 20,
  },
  cancelButton: {
    backgroundColor: '#555',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
  },
  webDatePickerContainer: {
    backgroundColor: '#333',
    padding: 10,
    borderRadius: 6,
    marginTop: 8,
    gap: 10,
  },
  webDateInput: {
    borderWidth: 1,
    borderColor: '#444',
    borderRadius: 4,
    padding: 8,
    backgroundColor: '#222',
    color: 'white',
    marginBottom: 10,
  },
  webDatePickerButton: {
    backgroundColor: '#2563EB',
    padding: 8,
    borderRadius: 4,
    alignItems: 'center',
    marginTop: 8,
  },
  webDatePickerButtonText: {
    color: 'white',
    fontWeight: '600',
  },
  button: {
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderRadius: 6,
    alignItems: 'center',
    justifyContent: 'center',
  },
  saveButton: {
    backgroundColor: '#2563EB',
  },
  deleteButton: {
    backgroundColor: '#DC2626',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
  },
  modalBackdrop: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContainer: {
    width: '90%',
    maxWidth: 500,
    borderRadius: 8,
    overflow: 'hidden',
  },
  descriptionContainer: {
    gap: 2,
  },
  creatorText: {
    color: '#777',
    fontSize: 12,
  },
})
```

Now when you use the app within an organization, you'll see the time entries of every user within that organization, including their active ones!

## Conclusion

Multi-tenancy is a powerful feature that allows you to create a single instance of an application that can be used by multiple tenants or organizations. Many platforms use a multi-tenancy model to increase the number of users and revenue. Clerk makes it easy to add multi-tenancy to your app by providing robust [APIs for managing organizations](/docs/organizations/overview) and memberships.

These APIs can be used with Expo to implement multi-tenancy into your [cross-platform React Native app](/docs/quickstarts/expo).

---

# Highlights from the MiduDev/Clerk Hackathon
URL: https://clerk.com/blog/highlights-midudev-clerk-hackathon.md
Date: 2025-08-07
Category: Company
Description: Explore the top 5 projects from the MiduDev/Clerk Hackathon, showcasing creativity, technical skills, and community engagement.

We recently teamed up with [MiduDev](https://www.youtube.com/@midudev), one of the most popular Spanish-speaking tech YouTubers, to host a community hackathon. Over 150 projects were submitted, each showing off how developers are building with Clerk in creative and meaningful ways.

After careful review, five projects stood out and took home the top prizes. This article is a breakdown of how each app was judged, but more importantly an examination of each app that was built based on the choices of each developer which lead to their success.

## How projects were judged

While all submissions were evaluated across a variety of dimensions, the scoring emphasized three main criteria:

- **Creativity** – Was the idea unique, unexpected, or imaginative?
- **Clerk Integration** – Was Clerk used in a meaningful and intentional way?
- **User Experience (UX)** – Was the final product polished, intuitive, and a joy to use?

The top 5 finalists nailed all three. Not only was the idea thought out and executed well, they felt cohesive and genuinely useful. Let’s take a look at what made them stand out.

## 1st Place: Key Leap

> \[Key Leap] represents the deepest and most imaginative **Clerk integration** we saw.
>
> — MiduDev

![Key Leap](./image1.png)

[Key Leap](https://keyleap.vercel.app/) was a clear standout because it reimagined what authentication can be. It used Clerk beyond just the drop-in sign-up and sign-in components but used it as the *core mechanic* of an interactive game.

Players progress by meeting access-level challenges, essentially playing with Clerk’s permission model. The experience is deceptively simple but technically deep, making it a brilliant blend of creativity, Clerk integration, and UX.

## 2nd Place: Finanzz

[Finanzz](https://finanzz.vercel.app/) is a mobile-first finance management web application. It uses AI to simplify various processes such as logging expenses using voice and image recognition, automatic transaction categorization, and even learns over time to help the user make better financial decisions.

The developer used Clerk to quickly create a great login experience for users of Finanzz so they could focus on the core logic of the application.

## 3rd Place: SnippetLab

[SnippetLab](https://snippetlab.app/) impressed with its developer-first mindset. It’s a code snippet manager with both a web UI and a powerful CLI for fetching saved snippets. Various social media features have also been integrated such as liking, sharing, and commenting on snippets.

The developer took advantage of Clerk Elements to design authentication forms that matched the theme of the rest of the app while still leveraging our secure user management platform.

## 4th Place: Atomox

![Atomox](./image2.png)

[Atomox](https://atomox.vercel.app/) is a community-driven platform for creating and sharing visual web UI components. Developers can browse components, interact with them directly in the browser, and view the source code that powers each one. Its visual polish and thoughtful UX are only a few elements that made it stand out.

Behind the scenes, Clerk handles user authentication and profile management, giving the app a solid foundation for growing a creative community.

## 5th Place: SoulsPixel

![SoulsPixel](./image3.png)

[SoulsPixel](https://soulpixel.klasinky.com/soul) is a collaborative pixel board enhanced with gamification. Users place pixels on the board to earn achievements, unlock special features, and earn their place on the leaderboard. On top of that, the history of the board can be replayed to see where each pixel was placed over time.

Clerk powers user identity, allowing users to sign-in using Google or GitHub while requiring unique usernames to power the activity feed within the app.

## Conclusion

This hackathon was definitely a showcase of technical skills, but also a celebration of creativity, craft, and community. Every winning project demonstrates that no matter how technical or creative an application is, Clerk is a great choice to seamlessly integrate user management into any application.

We’re grateful to MiduDev for helping bring this event to life, and even more thankful to everyone who submitted a project!

---

# Add multi-tenancy to an app built with Clerk, Lovable, and Supabase
URL: https://clerk.com/blog/multi-tenancy-clerk-lovable.md
Date: 2025-07-11
Category: Guides
Description: Learn how to transform your single-user app into a team-ready B2B platform using Clerk Organizations with Lovable and Supabase.

Adding multi-tenancy to a B2B is foundational if you're building for teams or organizations. You want separate data contexts, scoped permissions, and a seamless experience switching between personal and org-owned content.

In this article, you'll learn how to use **Lovable**, **Clerk**, and **Supabase** to implement multi-tenancy in a React app without manually handling JWT parsing or user scopes. This guide builds on a previous project and layers in organization support with just a few key updates.

> \[!NOTE]
> Since coding with AI is non-deterministic, your results of the final app may differ from whats shown in this article, although the functionality will be similar.

## Project Recap: Lovable Vibes

In [a recent guide](/blog/build-app-with-lovable-supabase-clerk), we walked through building a full-stack app with Lovable that stores AI coding rules in Supabase. We scaffolded the application without actually touching the code and instead leveraged Lovable's AI chat. Upon logging into the application, users can create new entries to store their vibe coding rules, giving them a name, optionally specifying a project, and tagging them using a number of preconfigured frameworks and languages.

![Lovable Vibes](./image1.png)

> \[!TIP]
> If you want to follow along with this guide, complete the steps in [the previous article](/blog/build-app-with-lovable-supabase-clerk).

## Add Multi-tenancy with Clerk Organizations

Now, we’re going to extend that same project to support multi-tenancy using Clerk organizations. You’ll add the drop-in `<OrganizationSwitcher />` component (shown below) which will allow your users to self-manage the teams they create within the application, including inviting others and managing their permissions.

![The OrganizationSwitcher component](./image2.png)

### Enable Clerk Organizations

Start in the [Clerk Dashboard](https://dashboard.clerk.com). Access your Clerk application, head to **Configure** > **Settings** (under **Organization management**), and toggle on **Organizations**. A new list of settings will appear, allowing you to fine-tune the way that organizations work with your app. The defaults can be left as-is for this guide.

> \[!NOTE]
> Learn about the full capabilities of Clerk Organizations in the [Clerk documentation](/docs/organizations/overview).

### Adding the `<OrganizationSwitcher />` to the project

Open your project in Lovable and run the following prompt to add the `<OrganizationSwitcher />` to the app:

```
Add the `<OrganizationSwitcher />` from Clerk to the nav bar.
```

Once Lovable is finished, your app should update to include a new element in the header that says “Personal account”. Clicking this will open a menu allowing you to create an organization. Go ahead and create an organization, providing it a name and inviting another user (which could be a secondary email address you might have).

![The OrganizationSwitcher component in the header](./image3.png)

### Updating the RLS policies

After the organization is created, the switcher will update to show the name of the active organization, but you might notice that the list of rules in the app hasn’t changed. That's because when the project was initially created, the RLS policies (which ensures that users can only access data they are authorized to) were configured to use the user’s ID from the token.

The policies will need to be changed to use the organization ID if present, and fall back to the user ID if not. This will configure the database such that if a user has an active organization, the records belonging to that organization will be returned.

Run the following prompt in Lovable to create a database function to parse the correct ID from the request and update the policies accordingly:

```
Run the following script in Supabase to create the requesting_owner_id function:

-- 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;

Update all of the RLS policies and change the using statement to check requesting_user_id() = owner_id
```

You’ll also need to make sure that the app uses the correct ID when creating records. Run the following prompt to make those changes:

```
When creating rule sets, if the user has an active organization, use that as the `owner_id`, otherwise use the user ID
```

### Test it out

With an organization active, try adding a rule to ensure it properly saves. Now use the organization switcher to access your “Personal account” (which means the current user has no active organization) and you’ll see any entries you added before implementing multi-tenancy.

If you invited an alternate email address, try signing into the app with that account and the organization switcher will have a bubble on it indicating that you’ve been invited to an organization. Accepting the invitation and switching to that organization will show you the same rule set that was added to your first user account!

## Conclusion

With just a few changes to your Supabase policies and some UI wiring via Clerk, you’ve now turned a single-user app into a multi-tenant experience. Users can switch between personal and team workspaces, and all rule data is securely scoped to the appropriate context.

This setup scales well for small teams and early-stage [B2B products](/b2b-saas) using robust developer platforms designed for this purpose. Clerk handles the complexity of authentication and team management, Supabase enforces data isolation with RLS, and Lovable helps you stitch everything together quickly.