This project demonstrates how to leverage Neon RLS using Custom JWT's. Instead of relying on JWTs generated by third-party authentication providers (like Clerk or Auth0), this demo showcases how to create and use JWTs signed directly by your server.
Neon RLS lets you move authorization logic closer to your data in PostgreSQL. By using custom JWTs, you maintain complete control over the token generation process while still benefiting from integration with RLS. This approach is valuable when you have specific requirements for your JWT structure or want to avoid dependencies on external auth providers for certain internal workflows.
This demo is built as a HONC API and exposes the following endpoints to illustrate these concepts:
/token— Generates and returns a custom JWT signed by the server. This token contains claims that will be used by PostgreSQL to enforce RLS. The token includes atenant_idclaim that determines the data access rights in this example./.well-known/jwks.json— Serves the JSON Web Key Set (JWKS) containing the public key. Neon RLS uses this to verify the signature of the custom JWTs you provide./api/users— Retrieves a list of users. Access to individual user records is controlled by RLS based on thesubclaim in the JWT./api/tenants— Retrieves a list of tenants. Access to tenant records is also controlled by RLS based on thetenant_idclaim in the JWT.
The database schema is defined using Drizzle ORM and can be found in src/db/schema.ts. The generated SQL migrations are located in the drizzle/ directory. A seed.sql file is included to populate the database with initial demo data.
Before you begin, ensure you have the following installed:
- Bun (or Node.js with npm/yarn/pnpm)
- Wrangler (for deploying to Cloudflare Workers)
- A Neon account and project
Follow these steps to run the demo:
-
Install the dependencies:
bun install
-
Generate Key Pair: Generate the public and private keys used for signing the JWTs. Run the following command:
bun generate-keys.ts
This will create
publicKey.jwk.jsonandprivateKey.jwk.jsonfiles containing the respective keys in JWK format. -
Set Up Neon Project:
-
Configure
wrangler.toml: Create yourwrangler.tomlfile with the following configuration. Replace the placeholder values with your actual Neon connection strings and the content of your generated key files:name = "my-honc-service" compatibility_date = "2024-07-25" compatibility_flags = [ "nodejs_compat" ] [vars] # neondb_owner role - used for migrations and seeding, which you copied from Neon Console DATABASE_URL = "postgres://<user>:<password>@<host>:<port>/<database>?sslmode=require" # authenticated, passwordless role (you can keep this empty for now) DATABASE_AUTHENTICATED_URL = "" # Contents of publicKey.jwk.json PUBLIC_KEY='{"kty":"RSA", ... }' # Contents of privateKey.jwk.json PRIVATE_KEY='{"kty":"RSA", ... }'
Important: Make sure to copy the content of the JWK files into the
PUBLIC_KEYandPRIVATE_KEYvariables as strings. -
Deploy to Cloudflare Workers: Deploy the demo application:
bun run deploy
-
Verify Public Key Endpoint: After deployment, verify that the public key is being served correctly by visiting the JWKS endpoint in your browser. Replace
<your-name>with your Cloudflare Workers subdomain:https://my-honc-service.<your-name>.workers.dev/.well-known/jwks.jsonYou should see the contents of your
publicKey.jwk.jsonfile. -
Configure Neon RLS:
-
Go to the RLS page in your Neon console (project > settings).
-
Add a new authentication provider.
-
Set the JWKS URL to the URL you verified in the previous step (e.g.,
https://my-honc-service.<your-name>.workers.dev/.well-known/jwks.json). -
Follow the steps in the UI to setup the roles for Neon RLS. You should ignore the schema related steps if you're following this guide
-
Note down the connection strings for both the
neondb_ownerrole and theauthenticated, passwordlessrole. You'll need both. Theneondb_ownerrole has full privileges and is used for migrations, while theauthenticatedrole will be used by the application and will have its access restricted by RLS.
-
-
Apply Database Migrations:
-
Create a
.dev.varsfile in the project root and populate it with your Neon database URLs:cp .dev.vars.example .dev.vars
Update the
.dev.varsfile with the connection strings you noted down from Neon RLS:DATABASE_AUTHENTICATED_URL="postgres://<user>.<role>:<password>@<host>:<port>/<database>?sslmode=require" # authenticated role DATABASE_URL="postgres://<user>:<password>@<host>:<port>/<database>?sslmode=require" # neondb_owner role
-
Apply the database migrations using the
neondb_ownerrole:bun run db:migrate
-
-
Seed the Database: Seed the database with initial data using the
neondb_ownerrole:bun run db:seed
-
Update
wrangler.tomlEnsure that theDATABASE_AUTHENTICATED_URLin yourwrangler.tomlfile is set to the connection string for theauthenticatedrole. -
Redeploy: Deploy the application again to ensure the correct
DATABASE_AUTHENTICATED_URLfor the authenticated role is used:bun run deploy
-
Verify API Endpoints: Access the API endpoints in your browser to verify that the RLS is working as expected.
-
Users Endpoint: Visiting this endpoint will use a custom JWT with a specific
tenant_id. You should only see users associated with that tenant.https://my-honc-service.<your-name>.workers.dev/api/users -
Tenants Endpoint: Similarly, this endpoint's access will be filtered based on the
tenant_idin the JWT.https://my-honc-service.<your-name>.workers.dev/api/tenants
-
generate-keys.ts: This script generates the RSA key pair used for signing and verifying JWTs.src/index.ts: This is the main application logic. It defines the API endpoints, generates custom JWTs, and interacts with the Neon database using Drizzle ORM. Notice how theauthTokenis included when creating the Drizzle client, allowing Neon RLS to enforce RLS based on the JWT claims.src/db/schema.ts: Defines the database schema using Drizzle ORM, including the Row-Level Security policies that restrict access based on theauth.session()->>'tenant_id'andauth.user_id()values derived from the JWT.seed.sql&seed.ts: These files contain the initial data and the script to seed the database. Pay attention to theGRANTstatement inseed.sql, which grants permissions to theauthenticatedrole, the role under which RLS policies are enforced.
- Custom JWT Generation: The demo showcases how to create and sign JWTs directly within your application, giving you full control over the claims included.
- Neon RLS Integration: The project demonstrates how to configure Neon RLS to trust your custom JWTs by providing the JWKS URL.
- Row-Level Security (RLS): The database schema includes RLS policies that use the
auth.session()andauth.user_id()functions (provided by thepg_session_jwtextension, which Neon RLS leverages) to enforce access control based on the claims in the JWT. - Multi-Tenancy: The example demonstrates a basic multi-tenant scenario where users and data are associated with specific tenants, and access is restricted accordingly.
- Explore Different JWT Claims: Experiment with adding more custom claims to your JWTs and create corresponding RLS policies to enforce different access control rules. Currently the JWT uses hardcoded
tenant_idandsubclaims, but you will likely want to use dynamic values based on the authenticated user. - Implement User Authentication: Integrate a proper authentication mechanism to issue these custom JWTs to authenticated users.
Contributions are welcome! Please feel free to submit a Pull Request.



