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.
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.
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:
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:
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();
const db = drizzle(process.env.DATABASE_AUTHENTICATED_URL!, { schema });
return db.$withAuth(authToken).insert(schema.todos).values({
task: newTodo,
isComplete: false,
});
}
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.
auth.user_id()
from the JWT
How Neon Authorize gets 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.
pg_session_jwt
extension works
How the 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 bypg_session_jwt
, retrieves the authenticated user's ID from the JWT (it looks for it in thesub
field).user_id
: This refers to theuser_id
column in thetodos
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.
Provider | Supported? | JWKS URL | Documentation |
---|---|---|---|
Clerk | ✅ | https://{yourClerkDomain}/.well-known/jwks.json | docs |
Stack Auth | ✅ | https://api.stack-auth.com/api/v1/projects/{project_id}/.well-known/jwks.json | docs |
Auth0 | ✅ | https://{yourDomain}/.well-known/jwks.json | docs |
Firebase Auth / GCP Identity Platform | ✅ | https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com | docs |
Stytch | ✅ | https://{live_or_test}.stytch.com/v1/sessions/jwks/{project-id} | docs |
Keycloak | ✅ | https://{your-keycloak-domain}/auth/realms/{realm-name}/protocol/openid-connect/certs | docs |
Supabase Auth | ❌ | Not supported until Supabase supports asymmetric keys. | N/A |
Amazon Cognito | ✅ | https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json | docs |
Azure AD | ✅ | https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys | docs |
Google Identity | ✅ | https://www.googleapis.com/oauth2/v3/certs | docs |
Descope Auth | ✅ | https://api.descope.com/{YOUR_DESCOPE_PROJECT_ID}/.well-known/jwks.json | docs |
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.
Clerk (Frontend) + Neon Authorize
A Todo List built with Clerk, Next.js, and Neon Authorize (SQL from the Frontend)
Stack Auth + Neon Authorize
A Todo List built with Stack Auth, Next.js, and Neon Authorize (SQL from the Backend)
Auth0 + Neon Authorize
A Todo List built with Auth0, Next.js, and Neon Authorize (SQL from the Backend)
Stytch + Neon Authorize
A Todo List built with Stytch, Next.js, and Neon Authorize (SQL from the Backend)
Azure AD B2C + Neon Authorize
A Todo List built with Azure AD B2C, Next.js, and Neon Authorize (SQL from the Backend)
Descope + Neon Authorize
A Todo list built with Descope, Next.js, and Neon Authorize (SQL from the frontend)
Neon Authorize with custom JWTs
A demo of Neon Authorize with custom generated JWTs
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.