Major update to our Free Plan: now with 1 GB of Postgres storage per project, 100 projects included

JWT

Authenticate using JSON Web Tokens (JWT) for external services

Managed Better Auth is built on Better Auth and provides support for JWT plugin APIs through the Neon SDK. You do not need to manually install or configure the Better Auth JWT plugin.

While Managed Better Auth primarily relies on secure, HTTP‑only cookies (sessions) for browser‑based authentication, certain scenarios require a raw token. In these cases, the JWT plugin is especially useful:

  • Microservices: Sharing identity between backend services.
  • Separate frontend and backend domains: Authenticating API requests from a domain different than your main application.
  • CLI tools: Enabling authentication from the command‑line interface.

Sessions vs. JWTs

This plugin is not a replacement for session management in web applications. For standard browser-based apps (Next.js, React, Vue, etc.), rely on the default session cookie mechanism provided by authClient.signIn and authClient.getSession.

Only use JWTs when you specifically need to authorize requests to services that cannot access the browser's cookie jar.

Prerequisites

  • A Neon project with Auth enabled.

Retrieve a Token

You can retrieve a JWT for the currently signed-in user using the Neon SDK.

Using the SDK method

To fetch a raw token string, use the authClient.token() method. This is the recommended approach for client applications that need to attach a token to an API request header manually.

src/get-token.ts
import { authClient } from './auth';

export async function getJwtToken() {
  const { data, error } = await authClient.token();

  if (error) throw error;

  // The token string (for example, "eyJhbGciOiJFZ...")
  return data.token;
}

If your app is served from a different origin than your Managed Better Auth URL (for example a Vite or SPA dev server on localhost talking to auth on *.neon.tech), configure the auth client to send the session cookie on cross-origin requests. Otherwise authClient.token() returns data.token as undefined and calls to your API fail with 401.

src/auth.ts
export const authClient = createAuthClient(NEON_AUTH_URL, {
  fetchOptions: { credentials: 'include' },
});

Cross-domain setups have further limitations, notably Safari ITP blocking third-party cookies, with reverse-proxy or shared-parent-domain workarounds. See Better Auth: Safari, ITP, and Cross-Domain Setups.

Using the session header

When you call authClient.getSession(), Managed Better Auth automatically includes a JWT in the response headers. If you are using a custom fetcher or need to intercept the token immediately after a session check:

await authClient.getSession({
  fetchOptions: {
    onSuccess: (ctx) => {
      const jwt = ctx.response.headers.get('set-auth-jwt');
      console.log('JWT:', jwt);
    },
  },
});

Example decoded JWT payload

A typical decoded JWT payload looks like this:

{
  "iat": 1766320685,
  "name": "User Name",
  "email": "user@email.com",
  "emailVerified": false,
  "image": null,
  "createdAt": "2025-12-20T11:04:41.437Z",
  "updatedAt": "2025-12-20T11:04:41.437Z",
  "role": "authenticated",
  "banned": false,
  "banReason": null,
  "banExpires": null,
  "id": "860dc360-609f-4b7d-9e70-ec93fe6414d3",
  "sub": "860dc360-609f-4b7d-9e70-ec93fe6414d3",
  "exp": 1766321585,
  "iss": "<YOUR_NEON_AUTH_URL_ORIGIN>",
  "aud": "<YOUR_NEON_AUTH_URL_ORIGIN>"
}

Verify a token

To verify the authenticity of a JWT, you need to validate its signature using the public keys provided by JWKS (JSON Web Key Set).

Managed Better Auth exposes a public JWKS endpoint that contains the public keys necessary to verify the signature of your JWTs.

The JWKS endpoint

Your Managed Better Auth JWKS endpoint is located at:

<YOUR_NEON_AUTH_URL>/.well-known/jwks.json

If you verify Neon Auth JWTs inside Neon Functions, the platform injects NEON_AUTH_BASE_URL and NEON_AUTH_JWKS_URL when Neon Auth is provisioned on the branch. Use NEON_AUTH_JWKS_URL directly instead of deriving it yourself. See Functions environment variables.

Verification example

The following examples demonstrate how to verify a Managed Better Auth JWT in several programming languages. No matter which language you use, the process is the same: fetch the JWKS from the provided endpoint and use it to validate the token’s signature and claims. If your preferred language isn’t included here, you can apply these same principles in your own environment.

Production Readiness

The following examples are provided for reference only and are not guaranteed to be production‑ready. Be sure to implement proper caching, error handling, and security best practices as required for your application.

  1. Install the library:

    npm install jose
  2. Use the following example code as a reference to verify a JWT:

    import { jwtVerify, createRemoteJWKSet } from 'jose';
    
    const NEON_JWKS_URL = `${process.env.NEON_AUTH_BASE_URL}/.well-known/jwks.json`;
    const JWKS = createRemoteJWKSet(new URL(NEON_JWKS_URL));
    
    export async function validateNeonToken(token: string) {
        try {
            const { payload } = await jwtVerify(token, JWKS, {
                issuer: new URL(process.env.NEON_AUTH_BASE_URL!).origin
            });
    
            return payload;
        } catch (error) {
            console.error('Token validation failed:', error);
            return null;
        }
    }
    
    validateNeonToken(<YOUR_JWT_TOKEN>).then((payload) => {
        console.log('Token is valid. Payload:', payload);
    }).catch(() => {
        console.log('Token is invalid.');
    });

    Replace <YOUR_JWT_TOKEN> with the actual JWT token you want to verify.

Limitations

Because Managed Better Auth is a managed service, certain server-side configurations available in the standalone Better Auth library are pre-configured by Neon and cannot be changed:

  • Signing algorithm: Managed Better Auth uses EdDSA (Ed25519) by default for high security and performance. Ensure your verification libraries support this algorithm.
  • Expiration: Tokens expire in 15 minutes (access tokens). You should implement logic to refresh the token using authClient.token() when it expires.
  • Custom claims: Currently, the JWT payload contains the default user information. Custom claims are not supported at this time.

Troubleshooting

Token rejection

If a token is rejected during verification, check the following:

  1. Verify that you are using the correct JWKS endpoint for your Managed Better Auth instance. The issuer of the token must match the origin of your Managed Better Auth URL. (for example, if your Managed Better Auth URL is https://ep-xx.aws.neon.tech/neondb/auth, the issuer should be https://ep-xx.aws.neon.tech).
  2. Confirm that your verification library supports EdDSA (Ed25519).
  3. Make sure the token has not expired.
  4. Check that the kid in the JWT header matches one of the keys in the JWKS response. If not, fetch the latest keys from the JWKS endpoint.

Need help?

Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.

Was this page helpful?
Edit on GitHub

On this page

Copy neon init command