How simstudioai/sim Uses Better Auth for Shared One-Time Token Verification Across Services
The simstudioai/sim repository implements a distributed authentication system where the main Next.js application runs a full Better Auth instance while auxiliary services use a minimal verification-only instance that shares the same PostgreSQL database and secret to validate one-time tokens.
SimStudio AI's SIM is an open-source project that leverages Better Auth to handle authentication across multiple services. The architecture separates token issuance from token verification, allowing the main application to manage user sessions while specialized services like the realtime server can securely verify identities without duplicating authentication logic.
The Dual-Instance Architecture
SIM's authentication model relies on two tiers of Better Auth instances. The main Next.js application in apps/sim hosts a complete Better Auth configuration capable of user login, session management, password hashing, and OAuth flows.
Auxiliary services, such as the realtime server in apps/realtime, instantiate a streamlined version that only handles token verification. This design follows the principle of least privilege—services that merely need to validate tokens do not initialize unnecessary authentication features, reducing the attack surface while maintaining access to the shared verification table in PostgreSQL.
The Verification Helper in packages/auth/src/verify.ts
The core of the distributed verification system resides in packages/auth/src/verify.ts. This file exports the createVerifyAuth function, which constructs a minimal Better Auth instance configured specifically for one-time token validation.
The function accepts a VerifyAuthOptions interface containing the shared secret and base URL:
export interface VerifyAuthOptions {
/** Better Auth shared secret. Must match the apps/sim Better Auth secret. */
secret: string
/** Public-facing Better Auth URL (usually same as NEXT_PUBLIC_APP_URL). */
baseURL: string
}
When invoked, createVerifyAuth returns a Better Auth instance configured with the oneTimeToken plugin set to expire tokens after 24 hours:
export function createVerifyAuth(options: VerifyAuthOptions) {
return betterAuth({
baseURL: options.baseURL,
secret: options.secret,
database: drizzleAdapter(db, {
provider: 'pg',
schema,
}),
plugins: [
// Enables creation & verification of short-lived one-time tokens
oneTimeToken({ expiresIn: 24 * 60 * 60 }), // 24 h
],
})
}
One-Time Token Flow
The authentication flow spans multiple services but remains unified through the shared database schema. The process operates in five distinct stages.
Token Issuance: The main application in apps/sim generates a one-time token using its full Better Auth instance. The oneTimeToken plugin stores this token in the shared verification table with a 24-hour expiration timestamp.
Client Distribution: The client receives this token, typically via URL query parameters or response headers, and presents it when connecting to auxiliary services.
Service Initialization: Services like the realtime server initialize their auth client by importing createVerifyAuth from @sim/auth/verify and passing the required environment variables:
import { createVerifyAuth } from '@sim/auth/verify'
const realtimeAuth = createVerifyAuth({
secret: process.env.BETTER_AUTH_SECRET!, // must match apps/sim
baseURL: process.env.NEXT_PUBLIC_APP_URL!,
})
Token Verification: The service verifies the token using the verify method. The minimal instance checks the token against the shared PostgreSQL database, validating both its existence and expiration status.
Session Establishment: Upon successful verification, the service extracts the user ID from the token payload and establishes an authenticated session for the request.
Database Schema and Shared Configuration
Consistency across services depends on shared infrastructure. The BETTER_AUTH_SECRET environment variable must remain identical between the main application and all auxiliary services. This secret enables HMAC validation of tokens across the distributed system.
The database schema, defined in packages/db/schema.ts, provides the common structure for the verification table. Both the full and minimal Better Auth instances connect to this shared PostgreSQL schema using the Drizzle adapter, ensuring that tokens created by apps/sim are immediately visible to apps/realtime and other services.
Implementation Example in the Realtime Server
The realtime server in apps/realtime/src/auth.ts demonstrates practical implementation of this pattern. The service imports createVerifyAuth to instantiate its auth client, then uses it within request handlers to validate incoming tokens.
When handling WebSocket connections, the server extracts bearer tokens from authorization headers and delegates verification to the Better Auth instance:
export async function handleWsConnection(req) {
const token = req.headers.get('authorization')?.replace('Bearer ', '')
if (!token) throw new Error('Missing auth token')
const { success, data } = await realtimeAuth.verify(token)
if (!success) throw new Error('Invalid or expired token')
// `data` now holds the user ID and any extra payload stored in the token
const userId = data.userId
// …continue handling the WebSocket connection as an authenticated user
}
This approach allows the realtime server to authenticate users without maintaining its own user database or session management logic.
Summary
- Dual-instance architecture: The main Next.js application runs full Better Auth while auxiliary services use minimal verification-only instances from
packages/auth/src/verify.ts. - Shared infrastructure: All services connect to the same PostgreSQL database schema and use identical
BETTER_AUTH_SECRETenvironment variables. - One-time token pattern: The
oneTimeTokenplugin creates 24-hour expiring tokens stored in the sharedverificationtable. - Least-privilege design: Auxiliary services initialize only the Better Auth features required for token verification, reducing security exposure.
- Cross-service compatibility: Tokens generated in
apps/simvalidate seamlessly inapps/realtimeand other services through thecreateVerifyAuthhelper.
Frequently Asked Questions
How does the minimal Better Auth instance in auxiliary services access user data?
The minimal instance configured via createVerifyAuth connects to the same PostgreSQL database as the main application using the Drizzle adapter. Because both instances share the database schema defined in packages/db/schema.ts, the minimal instance can read the verification table and validate tokens without duplicating user data storage.
What happens if the BETTER_AUTH_SECRET differs between services?
Token verification will fail. The createVerifyAuth function requires the same secret parameter used by the main application's Better Auth instance. This secret is used for cryptographic validation of token signatures. If auxiliary services use a different secret, HMAC validation fails and the verify method returns an unsuccessful result.
Can auxiliary services generate tokens or only verify them?
Auxiliary services using the createVerifyAuth helper can only verify tokens. The minimal instance configuration in packages/auth/src/verify.ts exclusively includes the oneTimeToken plugin for verification purposes. Token generation requires the full Better Auth instance running in apps/sim with complete session management and user authentication capabilities.
How long do one-time tokens remain valid?
Tokens expire after 24 hours according to the configuration in packages/auth/src/verify.ts. The oneTimeToken plugin is initialized with expiresIn: 24 * 60 * 60 seconds. After this period, the verification method automatically rejects the token as expired, requiring the user to obtain a new token from the main application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →