Build multi-tenant RAG with Neon's database-per-user model — no nosy neighbors, max isolation, minimal costs
Docs/Neon Authorize

About Neon Authorize

Secure your application at the database level using Postgres's Row-Level Security

What you will learn:

  • JSON Web Tokens (JWT)

  • Row-level Security (RLS)

  • How Neon Authorize works

Neon Authorize integrates with third-party JWT-based authentication providers like Auth0 and Clerk, bringing authorization closer to your data by leveraging Row-Level Security (RLS) at the database level.

Authentication and authorization

When implementing user authentication in your application, third-party authentication providers like Clerk, Auth0, and others simplify the process of managing user identities, passwords, and security tokens. Once a user's identity is confirmed, the next step is authorization — controlling who can do what in your app based on their user type or role — for example, admins versus regular users. With Neon Authorize, you can manage authorization directly within Postgres, either alongside or as a complete replacement for security at other layers.

How Neon Authorize works

Most authentication providers issue JSON Web Tokens (JWTs) on user authentication to convey user identity and claims. The JWT is a secure way of proving that logged-in users are who they say they are — and passing that proof on to other entities.

With Neon Authorize, the JWT is passed on to Neon, where you can make use of the validated user identity directly in Postgres. To integrate with an authentication provider, you will add your provider's JWT discovery URL to your Neon project. This lets Neon retrieve the necessary keys to validate the JWTs.

import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_AUTHENTICATED_URL, { authToken: myAuthProvider.getJWT() });

await sql(`select * from todos`);

Behind the scenes, the Neon Proxy performs the validation, while Neon's open source pg_session_jwt extension makes the extracted user_id available to Postgres. You can then use Row-Level Security (RLS) policies in Postgres to enforce access control at the row level, ensuring that users can only access or modify data according to the defined rules. Since these rules are implemented directly in the database, they can offer a secure fallback — or even a primary authorization solution — in case security in other layers of your application fail. See when to rely on RLS for more information.

neon authorize architecture

Using Neon Authorize with custom JWTs

If you don’t want to use a third-party authentication provider, you can build your application to generate and sign its own JWTs. Here’s a sample application that demonstrates this approach: See demo

Before and after Neon Authorize

Let's take a before/after look at moving authorization from the application level to the database to demonstrate how Neon Authorize offers a different approach to securing your application.

Before Neon Authorize (application-level checks):

In a traditional setup, you might handle authorization for a function directly in your backend code:

export async function insertTodo(newTodo: { newTodo: string; userId: string }) {
  const { userId } = auth(); // Gets the user's ID from the JWT or session

  if (!userId) throw new Error('No user logged in'); // No user authenticated

  if (newTodo.userId !== userId) throw new Error('Unauthorized'); // User mismatch

  // Inserts the new todo, linking it to the authenticated user
  await fetchWithDrizzle(async (db) => {
    return db.insert(schema.todos).values({
      task: newTodo.newTodo,
      isComplete: false,
      userId, // Explicitly ties todo to the user
    });
  });

  revalidatePath('/');
}

In this case, you have to:

  • Check if the user is authenticated and their userId matches the data they are trying to modify.
  • Handle both task creation and authorization in the backend code.

After Neon Authorize (RLS in the database):

With Neon Authorize, you can let the database handle the authorization through Row-Level Security (RLS) policies. Here's an example of applying authorization for creating new todo items, where only authenticated users can insert data:

CREATE POLICY "create todos" ON "todos"
    AS PERMISSIVE FOR INSERT
    TO authenticated
    WITH CHECK (auth.user_id() = user_id);

Now, in your backend, you can simplify the logic, removing the user authentication checks and explicit authorization handling.

export async function insertTodo(newTodo: { newTodo: string }) {
  await fetchWithDrizzle(async (db) => {
    return db.insert(schema.todos).values({
      task: newTodo.newTodo,
      isComplete: false,
    });
  });

  revalidatePath('/');
}

This approach is flexible: you can manage RLS policies directly in SQL, or use an ORM to centralize them within your schema. Keeping both schema and authorization in one place can make it easier to maintain security. Some ORMs like Drizzle are adding support for declaritive RLS, which makes the logic easier to scan and scale.

How Neon Authorize gets auth.user_id() from the JWT

Let's break down the RLS policy controlling who can view todos to see what Neon Authorize is actually doing:

CREATE POLICY "view todos" ON "todos" AS PERMISSIVE
  FOR SELECT TO authenticated
  USING ((select auth.user_id() = user_id));

This policy enforces that an authenticated user can only view their own todos. Here's how each component works together.

What Neon does for you

When your application makes a request, Neon validates the JWT by checking its signature and expiration date against a public key. Once validated, Neon extracts the user_id from the JWT and uses it in the database session, making it accessible for RLS.

How the pg_session_jwt extension works

The pg_session_jwt extension enables RLS policies to verify user identity directly within SQL queries:

using: sql`(select auth.user_id() = user_id)`,
  • auth.user_id(): This function, provided by pg_session_jwt, retrieves the authenticated user's ID from the JWT (it looks for it in the sub field).
  • user_id: This refers to the user_id column in the todos table, representing the owner of each to-do item.

The RLS policy compares the user_id from the JWT with the user_id in the todos table. If they match, the user is allowed to view their own todos; if not, access is denied.

When to rely on RLS

For early-stage applications, RLS might offer all the security you need to scale your project. For more mature applications or architectures where multiple backends read from the same database, RLS centralizes authorization rules within the database itself. This way, every service that accesses your database can benefit from secure, consistent access controls without needing to reimplement them individually in each connecting application.

RLS can also act as a backstop or final guarantee to prevent data leaks. Even if other security layers fail — for example, a front-end component exposes access to a part of your app that it shouldn't, or your backend misapplies authorization — RLS ensures that unauthorized users will not be able to interact with your data. In these cases, the exposed action will fail, protecting your sensitive database-backed resources.

Supported providers

Here is a non-exhaustive list of authentication providers. The table shows which providers Neon Authorize supports, links out to provider documentation for details, and the discovery URL pattern each provider typically uses.

ProviderSupported?JWKS URLDocumentation
Clerkhttps://{yourClerkDomain}/.well-known/jwks.jsondocs
Stack Authhttps://api.stack-auth.com/api/v1/projects/{project_id}/.well-known/jwks.jsondocs
Auth0https://{yourDomain}/.well-known/jwks.jsondocs
Firebase AuthN/AN/A
Stytchhttps://{live_or_test}.stytch.com/v1/sessions/jwks/{project-id}docs
Keycloakhttps://{your-keycloak-domain}/auth/realms/{realm-name}/protocol/openid-connect/certsdocs
Supabase AuthN/AN/A
Amazon Cognitohttps://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.jsondocs
Azure ADhttps://login.microsoftonline.com/{tenantId}/discovery/v2.0/keysdocs
GCP Cloud IdentityN/AN/A
Descope Authhttps://api.descope.com/{YOUR_DESCOPE_PROJECT_ID}/.well-known/jwks.jsondocs

Sample applications

You can use these sample ToDo applications to get started using Neon Authorize with popular authentication providers.

Current limitations

While this feature is in its early-access phase, there are some limitations to be aware of:

  • Authentication provider requirements:
    • Your authentication provider must support Asymmetric Keys. For example, Supabase Auth will not be compatible until asymetric key support is added. You can track progress on this item here.
    • The provider must generate a unique set of public keys for each project and expose those keys via a unique URL for each project.
  • Connection type: Your application must use HTTP to connect to Neon. At this time, TCP and WebSockets connections are not supported. This means you need to use the Neon serverless driver over HTTP as your Postgres driver.
  • JWT expiration delay: After removing an authentication provider from your project, it may take a few minutes for JWTs signed by that provider to stop working.
  • Algorithm support: Only JWTs signed with the ES256 and RS256 algorithms are supported.

These limitations will evolve as we continue developing the feature. If you have any questions or run into issues, please let us know.

Last updated on

Was this page helpful?