We just shipped add-mcp: think npx skills but for MCPs. One command to install MCPs across all your editors and agents
/Neon Auth/Authentication Flow

Authentication flow

Understanding the complete sign-in and sign-up process

Beta

The Neon Auth with Better Auth is in Beta. Share your feedback on Discord or via the Neon Console.

This guide explains the authentication flow: how sign-in works from SDK call to session creation.

note

Anyone can sign up for your application by default. Support for restricted signups is coming soon. Until then, consider adding a verification step by enabling email verification via verification link or verification code.

Architecture overview

Neon Auth is a managed REST API service built on Better Auth that connects directly to your Neon database. You use the SDK in your application and configure settings in the Console—no servers to manage.

Your App (SDK)
    ↓ HTTP requests
Neon Auth Service (REST API)
    ↓ connects to database
Your Neon Database (neon_auth schema)

All authentication data—users, sessions, OAuth configurations—lives in your database's neon_auth schema. You can query these tables directly with SQL for debugging, analytics, or custom logic.

Complete sign-in flow

Here's what happens when a user signs in, from your code to the database:

  1. User signs in

    Your application calls the SDK's sign-in method:

    const { data, error } = await client.auth.signIn.email({
      email: 'user@example.com',
      password: 'password',
    });

    The SDK posts to {NEON_AUTH_URL}/auth/sign-in/email. The Auth service validates credentials against neon_auth.account, creates a session in neon_auth.session, and returns the session cookie with user data.

    Response you receive:

    {
      data: {
        session: {
          access_token: "eyJhbGc...",  // JWT token
          expires_at: 1763848395,
          // ... other session fields
        },
        user: {
          id: "dc42fa70-09a7-4038-a3bb-f61dda854910",
          email: "user@example.com",
          emailVerified: true,
          // ... other user fields
        }
      }
    }
  2. The Auth service sets an HTTP-only cookie (__Secure-neonauth.session_token) in your browser. This cookie:

    • Contains an opaque session token (not a JWT)
    • Is automatically sent with every request to the Auth API
    • Is secure (HTTPS only, HttpOnly, SameSite=None)
    • Is managed entirely by the SDK—you never touch it

    Where to see it: Open DevTools → Application → Cookies → look for __Secure-neonauth.session_token

  3. JWT token is retrieved

    The SDK automatically retrieves a JWT token and stores it in session.access_token. You don't need to call /auth/token separately—the SDK handles this behind the scenes.

    What's in the JWT:

    {
      "sub": "dc42fa70-09a7-4038-a3bb-f61dda854910", // User ID
      "email": "user@example.com",
      "role": "authenticated",
      "exp": 1763848395, // Expiration timestamp
      "iat": 1763847495 // Issued at timestamp
    }

    The sub claim contains the user ID from neon_auth.user.id. This is what Row Level Security policies use to identify the current user.

  4. JWT is used for database queries

    When you query your database via Data API, the SDK automatically includes the JWT in the Authorization header:

    // JWT is automatically included in Authorization header
    const { data } = await client.from('posts').select('*');

    What happens:

    1. SDK gets JWT from session.access_token
    2. Adds Authorization: Bearer <jwt-token> header
    3. Data API validates JWT signature using JWKS public keys
    4. Data API extracts user ID from JWT and makes it available to RLS policies
    5. Your query runs with the authenticated user context

Sign-up flow

The sign-up flow creates a new user:

const { data, error } = await client.auth.signUp.email({
  email: 'newuser@example.com',
  password: 'securepassword',
  name: 'New User',
});

The SDK posts to {NEON_AUTH_URL}/auth/sign-up/email. The Auth service creates a new row in neon_auth.user, stores hashed credentials in neon_auth.account, and returns user data. If email verification is required, it creates a verification token in neon_auth.verification and may delay session creation until verification.

note

By default, anyone can sign up for your application. To add an additional verification layer, enable email verification (see Email Verification). Built-in signup restrictions are coming soon.

OAuth flow

OAuth authentication (Google, GitHub, Vercel, etc.):

await client.auth.signIn.social({
  provider: 'google',
  callbackURL: 'http://localhost:3000/auth/callback',
});

The SDK redirects to the OAuth provider. After the user authenticates, the provider redirects back with an authorization code. The SDK exchanges the code for an access token, then the Auth service creates or updates the user in neon_auth.user, stores OAuth tokens in neon_auth.account, and creates a session.

Database as source of truth

Neon Auth stores all data in your database's neon_auth schema:

  • Changes are immediate (no sync delays)
  • Query auth data directly with SQL
  • Each branch has isolated auth data
  • You own your data
SELECT id, email, "emailVerified", "createdAt"
FROM neon_auth.user
ORDER BY "createdAt" DESC;

Data API integration

When you enable the Data API, JWT tokens from Neon Auth are validated automatically. The user ID is available via the auth.uid() function, enabling Row-Level Security policies to grant data access based on the authenticated user.

Example RLS policy:

CREATE POLICY "Users can view own posts"
ON posts FOR SELECT TO authenticated
USING (user_id = auth.uid());

Learn more about securing your data:

What's next

Need help?

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

Last updated on

Was this page helpful?