Skip to content
Built with

Authentication ​

Build & Deploy

Add sign-in to your app. The SDK sets up Better Auth for you: database connection, API routes and client. Start with email and password, then add social login providers if you need them.

On this pageTurn on email and password sign-in · Add a social login provider · Read the user on the server and in the browser

SDK apps only

SDK-managed auth requires a native SDK app on Cloudflare. For meta-frameworks or Node.js, Bun, and Deno, use Better Auth directly.

Quick start ​

1. Turn on auth ​

This step turns on email and password auth. Import auth from void/client. Use configuration to add social providers.

2. Sign up and sign in ​

This step signs users up and in with the auth client from void/client.

tsx
import { auth } from 'void/client';

// sign up
await auth.signUp.email({
  email: 'alice@example.com',
  password: 'example-password-123',
  name: 'Alice',
});

// sign in
await auth.signIn.email({
  email: 'alice@example.com',
  password: 'example-password-123',
});

3. Protect a server route ​

This step protects a route. Call requireAuth in the handler or loader. It returns the user, or throws 401.

ts
// routes/api/profile.ts
import { defineHandler } from 'void';
import { requireAuth } from 'void/auth';

export const GET = defineHandler((c) => {
  const user = requireAuth(c);
  return { email: user.email };
});

To allow anonymous access in a pages-mode loader, use getUser or getSession:

ts
// pages/dashboard.server.ts
import { defineHandler, type InferProps } from 'void';
import { getUser } from 'void/auth';

export type Props = InferProps<typeof loader>;

export const loader = defineHandler(() => {
  const user = getUser();
  return { user };
});

4. Sign out ​

tsx
await auth.signOut();

Config ​

Auth turns on automatically when you:

  • import anything from void/auth
  • import auth from void/client
  • add auth to void.config.ts
  • add a root-level auth.ts file

Email and password is on by default. This step adds social providers.

json
{
  "auth": {
    "providers": ["email", "google", "github"]
  }
}

Provider credentials come from AUTH_<PROVIDER>_CLIENT_ID and AUTH_<PROVIDER>_CLIENT_SECRET. cognito also needs a domain, region and user pool ID. Set AUTH_COGNITO_DOMAIN, AUTH_COGNITO_REGION and AUTH_COGNITO_USER_POOL_ID, or set them in auth.ts.

See the full provider list in the config reference.

For example, github uses:

  • AUTH_GITHUB_CLIENT_ID
  • AUTH_GITHUB_CLIENT_SECRET

Client usage ​

The auth client uses Better Auth's client API at /api/auth. The SDK picks the client for your framework.

For a custom client, import createAuthClient from void/client.

Server usage ​

void/auth provides the server-side helpers:

ts
import { getSession, getUser, requireAuth } from 'void/auth';
  • getUser() returns the current AuthUser | null
  • getSession() returns { user, session } | null
  • requireAuth(c) returns the authenticated user or throws 401

Import AuthUser and AuthSession from void/auth for type annotations. See their fields in the API reference.

Behavior ​

Auth sessions use the same database as your app. The SDK creates BETTER_AUTH_SECRET on deploy if missing. Later deploys reuse it.

Local development needs no secret setup. To preview a production build locally, provide BETTER_AUTH_SECRET.

Responses to signed-in requests get Cache-Control: no-store. No browser or shared cache keeps user data. A route keeps its own policy when it sets no-store, or private together with no-cache. To answer repeat requests with 304, use private, no-cache with Hono's etag() middleware. If you set the ETag yourself, make it differ per user, so the 304 doesn't reuse another account's page. The browser cache keeps a copy of a private, no-cache page, so the Back button can show it again after the user signs out. Keep the default no-store for pages that must not stay in the browser cache.

POST, PUT, PATCH and DELETE requests and WebSocket connections from another origin don't use the signed-in session unless that origin is in trustedOrigins. If a separate frontend calls your app, add its origin there (see Customization). If your routes use their own cookies, check the request Origin yourself, for example with Hono's csrf() middleware.

Deployed apps on Cloudflare limit auth requests per client IP with Better Auth's defaults, for example 3 sign-in attempts per 10 seconds. Each Worker instance keeps its own count. Change or turn off the limits with rateLimit in auth.ts. To share one count across instances, set rateLimit: { storage: 'database' }, then run void db generate for a direct Cloudflare deploy.

Proxies and end-to-end tests ​

All requests from one IP share one count. Two common setups reach the limits:

  • A server calls your auth endpoints for your users, such as a backend-for-frontend, a rewrite from Vercel or nginx, or another Worker's fetch. All of those users share that server's IP.
  • End-to-end tests sign up or sign in many times against a deployed app.

Do not read the client IP from x-forwarded-for. Any client can send that header. This step gives the trusted caller a secret header and skips limits only for it.

ts
import { defineAuth } from 'void/auth';

export default defineAuth(({ defaults, env }) => ({
  ...defaults,
  rateLimit: {
    customRules: {
      '/**': (request, rule) =>
        env.AUTH_TRUSTED_KEY && request.headers.get('x-auth-trusted-key') === env.AUTH_TRUSTED_KEY
          ? false
          : rule,
    },
  },
}));

'/**' matches every auth endpoint. Returning false skips the limit. Returning rule keeps the default for everyone else. Declare AUTH_TRUSTED_KEY: string().optional() in env.ts and set it with void secret put AUTH_TRUSTED_KEY (see Environment variables).

Your app no longer limits requests that carry the key, so:

  • A proxy that sends it must limit every auth request it forwards for each of its users, including sign-up, password reset and verification emails.
  • A test runner should send it only to your app. Never put the key in code that runs in your users' browsers.

Customization ​

This step adds advanced config. Create auth.ts at the project root and export defineAuth(...).

ts
import { defineAuth } from 'void/auth';

export default defineAuth(({ defaults }) => ({
  ...defaults,
  trustedOrigins: ['https://example.com'],
}));

defaults already includes the SDK's conventions. Extend it explicitly instead of expecting a deep merge.

For the full set of available options, see the official Better Auth options reference.

Database and migrations ​

Auth tables sit beside your app's tables. In local development, the SDK creates them.

SDK platforms create auth tables for you. This step adds them to your migrations for a direct Cloudflare deploy.

sh
void db generate

Example output: A new SQL migration file in db/migrations/.

Review and commit the SQL before deploying. The SDK includes your auth config and plugin tables. Do not duplicate them in db/schema.ts or run the Better Auth CLI.

Next steps ​

implemented.stream AI