Authentication
Authentication system with Better Auth, supporting OAuth providers, magic links, organizations, and teams.
Overview
ZeroStarter uses Better Auth for authentication, configured in the @packages/auth package. It supports multiple sign-in methods, multi-tenant organizations with teams, and session management with cross-subdomain cookie support. The Better Auth OpenAPI plugin is also enabled, exposing an auth API reference at /api/auth/reference.
Sign-In Methods
Magic Link
The sign-in UI includes an email magic-link flow (authClient.signIn.magicLink, registered via the magicLinkClient() plugin in web/next/src/lib/auth/client.ts). The matching server-side magicLink plugin and email sender are not enabled by default, so the flow is not functional out of the box. To turn it on, add the magicLink plugin to packages/auth/src/index.ts and implement sendMagicLink to deliver the email.
OAuth Providers
Two social providers are configured:
- GitHub — Requires
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET - Google — Requires
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET
Both redirect to /dashboard after successful authentication.
Agent Sign-In (local only)
For local development and AI agents, POST /api/agents/sign-in-as signs in as a fixed AgentZero user (agent@zerostarter.dev) and mints a session cookie directly. It is gated to the local environment and a trusted Origin header. See api/hono/src/routers/agents.ts.
Adding OAuth Providers
- Create OAuth credentials with the provider
- Add the client ID and secret to your
.envfile - Configure the provider in
packages/auth/src/index.ts:
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},- Add the environment variables to
packages/env/src/auth.ts
Organizations & Teams
The Better Auth Organizations plugin provides multi-tenant support.
Features
- Organization creation — Users can create organizations from the dashboard sidebar
- Organization switching — Switch between organizations via the sidebar dropdown
- Last used org persistence — The last selected organization is saved in a cookie and restored on next login
- Teams — Teams can be created within organizations
- Member roles — Members have roles (default: "member") within organizations
- Invitations — Invite users to organizations via email
Database Schema
The organization system adds these tables:
| Table | Purpose |
|---|---|
organization | Organization metadata (name, slug, logo) |
member | Links users to organizations with a role |
team | Teams within an organization |
teamMember | Links users to teams |
invitation | Pending org invitations with status, email, role, expiresAt |
The session table includes activeOrganizationId and activeTeamId fields to track the current context.
Client Usage
import { authClient } from "@/lib/auth/client"
// List user's organizations
const { data: orgs } = authClient.useListOrganizations()
// Get active organization
const { data: activeOrg } = authClient.useActiveOrganization()
// Switch organization
await authClient.organization.setActive({ organizationId: "..." })
// Create organization
await authClient.organization.create({ name: "Acme Inc.", slug: "acme" })Session Management
Sessions are stored in the session database table with:
- Token-based authentication via secure cookies
- Cross-subdomain cookies — Automatically configured when using subdomains
- IP address and user agent tracking for security
- Session expiration with automatic cleanup
Server-Side Session Access
In Next.js server components and layouts:
import { auth } from "@/lib/auth"
const session = await auth.api.getSession()
if (!session?.user) redirect("/")Client-Side Session Access
import { authClient } from "@/lib/auth/client"
const { data: session } = authClient.useSession()Protected Routes
The (protected) layout in web/next/src/app/(protected)/layout.tsx handles route protection server-side. It checks for a valid session and redirects unauthenticated users to the home page.
API routes are protected using the auth middleware in api/hono/src/middlewares/auth.ts, which validates the session from request headers.
Rate Limiting
API routes are rate-limited using hono-rate-limiter with Arcjet IP detection.
| Setting | Default | Environment Variable |
|---|---|---|
| Requests per window | 60 | HONO_RATE_LIMIT |
| Window duration | 60,000ms (1 min) | HONO_RATE_LIMIT_WINDOW_MS |
Authenticated requests use a per-user limiter set to twice the configured limit (HONO_RATE_LIMIT * 2, 120 by default). Rate limit keys are resolved in order: authenticated user ID, API key, then IP address.
Environment Variables
| Variable | Description |
|---|---|
BETTER_AUTH_SECRET | Secret key for signing tokens (openssl rand -base64 32) |
GITHUB_CLIENT_ID | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | GitHub OAuth app client secret |
GOOGLE_CLIENT_ID | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
HONO_APP_URL | Backend URL (used as Better Auth base URL) |
HONO_TRUSTED_ORIGINS | Allowed CORS origins |