Learn how Neon compares to Aurora Serverless v2 - TL;DR: faster cold starts, responsive autoscaling, 80% lower 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

Database roles

Neon Authorize works with two primary database roles:

  • Authenticated role: This role is intended for users who are logged in. Your application should send the authorization token when connecting using this role.
  • Anonymous role: This role is intended for users who are not logged in. It should allow limited access, such as reading public content (e.g., blog posts) without authentication.

note

Some authentication providers, like Firebase, support "anonymous authentication" where a unique user ID is automatically generated for visitors who haven't explicitly logged in. This is useful for features like shopping carts, where you want to track a user's actions before they create an account. These anonymous users will still have a valid JWT and can use the anonymous role, making it possible to track their actions while maintaining security.

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, getToken } = auth(); // Gets the user's ID and getToken from the JWT or session
  const authToken = await getToken(); // Await the getToken function

  if (!userId) throw new Error('No user logged in'); // No user authenticated
  if (newTodo.userId !== userId) throw new Error('Unauthorized'); // User mismatch

  const db = drizzle(process.env.DATABASE_AUTHENTICATED_URL!, { schema });

  return db.$withAuth(authToken).insert(schema.todos).values({
    task: newTodo.newTodo,
    isComplete: false,
    userId, // Explicitly ties todo to the user
  });
}

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 only need to pass the JWT to the database - authorization checks happen automatically through RLS policies:

pgPolicy('create todos', {
  for: 'insert',
  to: 'authenticated',
  withCheck: sql`(select 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 }) {
  const { getToken } = auth();
  const authToken = await getToken();

  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 like Drizzle 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:

pgPolicy('view todos', {
  for: 'select',
  to: 'authenticated',
  using: sql`(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 Auth / GCP Identity Platformhttps://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.comdocs
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 AuthNot supported until Supabase supports asymmetric keys.N/A
Amazon Cognitohttps://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.jsondocs
Azure ADhttps://login.microsoftonline.com/{tenantId}/discovery/v2.0/keysdocs
Google Identityhttps://www.googleapis.com/oauth2/v3/certsdocs
Descope Authhttps://api.descope.com/{YOUR_DESCOPE_PROJECT_ID}/.well-known/jwks.jsondocs

JWT Audience Checks

Neon Authorize can also verify the aud claim in the JWT. This is useful if you want to restrict access to a specific application or service.

For authentication providers such as Firebase Auth and GCP Cloud Identity, Neon Authorize mandates the definition of an expected audience. This is because these providers share the same JWKS URL for all of their projects.

The configuration of the expected audience can be done via the Neon Authorize UI or via the Neon Authorize API.

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?