Toast
Developer

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"}'

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

EndpointMethodDescription
/api/auth/sign-up/emailPOSTCreate account
/api/auth/sign-in/emailPOSTSign in with password
/api/auth/magic-linkPOSTSend magic link email
/api/auth/sign-outPOSTEnd session
/api/auth/get-sessionGETGet 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.com

The API sets:

  • Access-Control-Allow-Origin: the ADMIN_URL origin
  • Access-Control-Allow-Credentials: true
  • Access-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=true

This 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

RoleDescriptionDefault
adminFull access to everything
editorContent management + member viewing
authorCreate and edit own content
memberNo management permissionsYes

New users are assigned the member role by default.

Permission Matrix

PermissionAdminEditorAuthorMember
content.createYesYesYes
content.edit_ownYesYesYes
content.edit_allYesYes
content.publishYesYes
content.deleteYesYes
members.viewYesYes
members.manageYes
site.settingsYes
site.billingYes
site.deleteYes
User managementYes
ImpersonationYes

Middleware Chain

Protected API routes use a three-layer middleware chain:

  1. session() — Runs on all /api/* routes. Extracts user and session from the cookie. Does not reject unauthenticated requests (public endpoints need to pass through).

  2. requireAuth() — Returns 401 Unauthorized if no valid session. Applied to routes that need a logged-in user.

  3. requirePermission() — Returns 403 Forbidden if 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 cookie

Multi-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

VariableRequiredPurpose
BETTER_AUTH_SECRETYesToken signing secret (min 32 characters)
BETTER_AUTH_URLYesBase URL for auth callbacks
ADMIN_URLYesAdmin panel URL (CORS + trusted origins)
CROSS_SITE_COOKIESNoSet to true for cross-domain deployments

For the full API reference including request/response schemas, see Auth endpoints.

On this page