Better Auth service
Configure the current Better Auth integration, share the Drizzle database, supply server secrets, and mount its authentication API.
Use defineBetterAuthService to add Better Auth credentials, accounts, and sessions to your application. RELKIT discovers it, activates
the application’s Drizzle connection first, and passes that native client to Better Auth’s Drizzle
adapter. Better Auth handles credentials, accounts, sessions, and its HTTP protocol.
The current integration requires exactly one Drizzle service, one Better Auth service, and one
filesystem ALL catch-all mount for that auth handler. Do not construct a second connection for auth
or register the descriptor as a member of an ordinary defineService.
Pre-setup
Use authentication setup to start local Docker PostgreSQL,
declare the database/auth fields with defineEnv, and supply a stable secret before startup.
If you completed that scaffold, skip installation below and customize the existing files.
Existing SQLite/MySQL applications keep their dialect and applied migration history.
Install compatible packages
Authentication is optional in a minimal app. Choose one setup path from the backend root. Keep existing database tables, patches and overrides when merging these fields.
Scaffold the auth baseline
bun run relkit add auth --adapter better-auth --database-dialect postgresqlThis creates src/services/auth/service.ts and src/routes/api/auth/[[...auth]]/route.ts,
merges auth environment declarations, and installs the selected adapter, native SDKs,
declaration peers and repairs. It reuses the existing Drizzle service or creates one when absent.
--database-dialect must match an existing database; when none exists, it chooses the new dialect.
For an existing database, the scaffold adds schema/auth.ts and exports its tables from the
schema index. Review those additions, preserving business tables and applied migrations.
Existing auth table names cause a collision instead of being overwritten. The scaffold does not run migrations,
generate a secret, or protect business routes. Configure those before accepting sign-ins.
Run this once when the app has no auth service. If it already exists, edit its service and mount.
With --no-install, run bun install and bun run check before continuing. See the
auth command reference and
complete sign-in guide.
Install the following packages before creating the service and mount below:
Before installing, merge these selected declaration repairs into the existing package manifest, preserving unrelated overrides:
{
"overrides": {
"@better-fetch/fetch": "1.3.2"
},
"pnpm": {
"overrides": {
"@better-fetch/fetch": "1.3.2"
}
}
}From this guide’s application/package root, install only its selected requirements:
bun add @relkit/better-auth@0.8.2 @relkit/drizzle@0.8.1 better-auth@1.7.1 drizzle-orm@1.0.0-rc.5-169397b
bun add --dev @cloudflare/workers-types@4.20250121.0 create-relkit@0.8.4 drizzle-kit@1.0.0-rc.5-ab785fcThe selected Drizzle version also needs the release’s declaration repair. The development-only create-relkit package supplies its exact asset; a dependency cannot register Bun patches for its consumer.
mkdir -p patches
cp node_modules/create-relkit/dist/patches/drizzle-orm.patch patches/drizzle-orm@1.0.0-rc.5-169397b.patchMerge this entry into the application’s existing patchedDependencies, preserving other patches. Keep the asset in source control:
{
"patchedDependencies": {
"drizzle-orm@1.0.0-rc.5-169397b": "patches/drizzle-orm@1.0.0-rc.5-169397b.patch"
}
}Then apply the registered repair before check/typecheck:
bun installKeep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.
These are owner-specific compatible versions, not a claim about the latest upstream release. The default setup uses Docker PostgreSQL. The service API also supports the existing SQLite example; its database dialect, schema, and migrations must stay consistent. See Database setup.
The backend requires no React package. @relkit/better-auth contains lazy declarations and session
types; generated servers select @relkit/better-auth/drizzle for native activation. Applications
using the native Effect layers or activateBetterAuthService directly import them from that same
/drizzle entrypoint. React belongs to @relkit/better-auth/react and is installed only for a
selected React client. Better Auth and Drizzle remain application-owned SDK dependencies; their
actual upstream dependencies are retained.
The generated setup includes the Better Fetch declaration override and application-owned Drizzle
patch. relkit add auth plans those selected fields automatically; manual consumers must merge them
and reinstall. The Cloudflare package above supplies the
real D1Database type imported by this SDK’s declarations; it is a development requirement, not a
Cloudflare runtime selection. These additions belong only to projects selecting authentication.
Define the service and environment
Create src/services/auth/service.ts:
/** Configures email/password Better Auth against the sole database, borrowing URL and secret from server configuration. */
import { defineBetterAuthService } from "@relkit/better-auth";
export default defineBetterAuthService({
baseURL: process.env.BETTER_AUTH_URL ?? "http://127.0.0.1:3000",
// Better Auth reads BETTER_AUTH_SECRET from the server process environment.
emailAndPassword: { enabled: true },
});This enables email/password sign-in. The descriptor exposes a stable handler, not a native
betterAuth() instance. It does not expose auth.api, an application-owned connection, or a login
function to call through context.services.
Add the environment fields to your existing configuration without replacing other settings:
/** Auth and Drizzle share one database; credentials remain in the backend environment. */
import { defineApp, defineEnv, env } from "@relkit/app/config";
export default defineApp({
env: defineEnv({
DATABASE_URL: env.secret(),
BETTER_AUTH_URL: env.url(),
BETTER_AUTH_SECRET: env.secret(),
}),
});Better Auth reads BETTER_AUTH_SECRET from the server process environment when it activates. The
example deliberately has no hard-coded secret or secret default. Keep the Docker setup’s
DATABASE_URL and the stable auth values in your backend’s untracked .env.local. Generate a
secret once if you have not completed the pre-setup:
openssl rand -base64 32Set BETTER_AUTH_SECRET to that value and BETTER_AUTH_URL to your backend origin, locally
http://127.0.0.1:3000. Keep these fields in the existing src/platform/env.ts when the app
imports its environment instead of declaring it inline. SQLite apps retain DATABASE_PATH
instead of the PostgreSQL DATABASE_URL field.
Store a stable, strong secret in your deployment’s secret manager; use the same value across replicas and restarts. Do not regenerate it on every startup. Never put it in a browser bundle, a public environment variable, source control, logs, or a copied example configuration.
Auth options are a plain object, not an ({ env }) factory. Declaring a field in defineEnv validates
the RELKIT environment; it does not inject parsed values into that object or replace process.env.
The URL here is read during module evaluation, so supply it before loading the app. The secret uses
Better Auth’s native process-environment lookup. A test harness env override alone does not set that
process value; see Testing.
Mount the native handler
/** Mounts native Better Auth once for all methods and protects the declared account, order and MCP paths. */
import { defineRoute } from "@relkit/app/routes";
import auth from "@app/services/auth/service.js";
export const ALL = defineRoute({
handler: auth.handler,
auth: { protected: ["/account/*", "/orders/*", "/mcp"] },
});RELKIT derives /api/auth from this route and preserves Better Auth’s native responses, including
cookies. The auth.protected paths are application paths that require a session, not endpoints created
by Better Auth. See Protect routes for matching and authorization rules.
Do not wrap auth.handler in a function or another handler. Mount the recognized handler directly so
the compiler can associate the auth service and its base path. You do not manually call
activateBetterAuthService in normal application startup. Calling the descriptor handler before
activation returns HTTP 503; importing the descriptor is not a standalone auth server.
Use this auth service from the frontend
Once the service and /api/auth mount are running, use Better Auth’s browser client for sign-in.
First complete the frontend auth installation:
import { createAuthClient } from "better-auth/client";
// Browser code: use the public backend origin, never the server secret or service.
export const authClient = createAuthClient({
baseURL: "http://127.0.0.1:3000",
basePath: "/api/auth",
fetchOptions: { credentials: "include" },
});
export async function signIn(email: string, password: string) {
const { data, error } = await authClient.signIn.email({ email, password });
if (error !== null) throw new Error(error.message ?? "Sign-in failed");
return data;
}
emailAndPassword on the service enables this signIn.email operation. Keep the public URL and
mount aligned with your backend, allow the browser to manage session cookies, and handle rejected
requests in the calling UI. These native auth endpoints are not RELKIT-generated RPC operations.
Never import the server service, database, or secret into the browser.
For application route hooks after sign-in, use
BetterAuthRelkitClientProvider as shown in
React authentication. Follow the
sign-in guide for the complete flow.
Know which settings RELKIT owns
| Setting | Owner and behavior |
|---|---|
database |
Reserved: supplied from the single Drizzle service; cannot be passed here |
basePath |
Reserved: derived from the auth mount; cannot be passed here |
baseURL |
Your public backend origin; configure it for each environment |
secret, native auth options |
Better Auth settings; this example uses its environment lookup for the secret |
drizzle |
Optional adapter settings, including schema mapping; provider is derived from the service dialect |
| Connection disposal | The Drizzle service’s dispose callback |
Better Auth writes through the native adapter, bypassing RELKIT portable CRUD, generated Zod parsers, and operation overrides. Do not use a table override as an auth policy or expose credential/session tables through generic CRUD routes. See Database integration.
Run bun run check and bun run typecheck, then verify a real sign-up and protected request using
Your first auth flow. Continue with Customization
before exposing a deployment to users.