Authentication
Auth flows, sessions, CORS, and role-based access control.
Toast uses Better Auth for authentication with a Drizzle ORM adapter and PostgreSQL. All authenticated endpoints require a valid session cookie.
Auth Flows
Toast supports two authentication methods:
Email + Password
The standard sign-in flow. Users provide an email and password, and receive a session cookie on success.
# Sign up
curl -X POST https://api.example.com/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"name": "Jane", "email": "jane@example.com", "password": "secure-password"}'
# Sign in
curl -X POST https://api.example.com/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "password": "secure-password"}'Magic Link
Passwordless authentication via email. The user receives a link that signs them in directly.
curl -X POST https://api.example.com/api/auth/magic-link \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com"}'Magic links expire after 10 minutes. This flow requires a configured email driver.
Sessions
Authentication is session-based using HTTP cookies. After a successful sign-in, the API sets a session cookie that the browser includes automatically on subsequent requests.
Session Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/auth/sign-up/email | POST | Create account |
/api/auth/sign-in/email | POST | Sign in with password |
/api/auth/magic-link | POST | Send magic link email |
/api/auth/sign-out | POST | End session |
/api/auth/get-session | GET | Get current session/user |
Checking Authentication
curl https://api.example.com/api/auth/get-session \
-H "Cookie: better-auth.session_token=..."Returns the current user and session if valid, or an error if not authenticated.
Cross-Origin Configuration
When the admin panel and API are on different origins (which is typical), you need to configure CORS correctly.
API-Side CORS
The API allows requests from the origin specified in ADMIN_URL:
# In your .env or Railway environment
ADMIN_URL=https://admin.example.comThe API sets:
Access-Control-Allow-Origin: theADMIN_URLoriginAccess-Control-Allow-Credentials:trueAccess-Control-Allow-Methods:GET, POST, PUT, PATCH, DELETE, OPTIONS
Cross-Site Cookies
On platforms where the API and admin panel are on different registrable domains (e.g., Railway's *.up.railway.app which is on the Public Suffix List), browsers treat them as separate "sites." This means SameSite=Lax cookies won't be sent cross-origin.
To fix this, set CROSS_SITE_COOKIES=true on the API service:
CROSS_SITE_COOKIES=trueThis changes the session cookie to SameSite=None; Secure, which allows cross-site cookie delivery over HTTPS. Only enable this when the API and admin panel are genuinely on different registrable domains.
Trusted Origins
Better Auth's CSRF protection rejects requests from unknown origins. The API automatically adds the ADMIN_URL as a trusted origin, so no extra configuration is needed as long as ADMIN_URL is set correctly.
Roles and Permissions
Toast uses role-based access control with four built-in roles. Roles are defined as code presets — no database queries are needed to resolve permissions.
Roles
| Role | Description | Default |
|---|---|---|
admin | Full access to everything | |
editor | Content management + member viewing | |
author | Create and edit own content | |
member | No management permissions | Yes |
New users are assigned the member role by default.
Permission Matrix
| Permission | Admin | Editor | Author | Member |
|---|---|---|---|---|
content.create | Yes | Yes | Yes | |
content.edit_own | Yes | Yes | Yes | |
content.edit_all | Yes | Yes | ||
content.publish | Yes | Yes | ||
content.delete | Yes | Yes | ||
members.view | Yes | Yes | ||
members.manage | Yes | |||
site.settings | Yes | |||
site.billing | Yes | |||
site.delete | Yes | |||
| User management | Yes | |||
| Impersonation | Yes |
Middleware Chain
Protected API routes use a three-layer middleware chain:
-
session()— Runs on all/api/*routes. Extracts user and session from the cookie. Does not reject unauthenticated requests (public endpoints need to pass through). -
requireAuth()— Returns401 Unauthorizedif no valid session. Applied to routes that need a logged-in user. -
requirePermission()— Returns403 Forbiddenif the user's role lacks the required permission. Applied to routes with specific access requirements.
Request → session() → requireAuth() → requirePermission() → Handler
│ │ │
│ │ └─ 403 if missing permission
│ └─ 401 if not authenticated
└─ Extracts user (or null) from cookieMulti-Tenancy
Toast is multi-tenant by design. Every user, session, and content record is scoped to a siteId. The session middleware extracts the siteId from the authenticated session and makes it available to all downstream handlers.
This means:
- Users can only access content belonging to their site
- Email addresses are unique per site, not globally
- A single database can serve multiple independent sites
Auth Environment Variables
| Variable | Required | Purpose |
|---|---|---|
BETTER_AUTH_SECRET | Yes | Token signing secret (min 32 characters) |
BETTER_AUTH_URL | Yes | Base URL for auth callbacks |
ADMIN_URL | Yes | Admin panel URL (CORS + trusted origins) |
CROSS_SITE_COOKIES | No | Set to true for cross-domain deployments |
For the full API reference including request/response schemas, see Auth endpoints.