Z8 Docs
Technical Docs

Authentication

Better Auth integration, secure sessions, invitations, and organization-scoped access

Configuration

Z8 uses Better Auth as the system authentication layer. System secrets, base URLs, and default social-provider credentials are environment-backed. Organization-specific identity settings are configured inside the product for each organization instead of through tenant-specific environment variables.

Typical auth maintenance commands from the repo root use pnpm:

pnpm --dir apps/webapp run auth:generate
pnpm --dir apps/webapp run auth:migrate

In practice this means:

  • BETTER_AUTH_SECRET and related auth secrets stay in environment configuration.
  • Default social providers such as Google, GitHub, LinkedIn, and Apple can be enabled from system env vars.
  • Organization-scoped SSO and branded social OAuth are configured per organization in settings and resolved against the current org context at runtime.
  • SCIM is part of the enterprise auth stack, but the current product does not expose a dedicated self-serve SCIM settings page; managed rollout is coordinated outside a normal org settings flow.
  • Organization-specific credentials are entered through org settings and stored through the app's org-scoped secret handling instead of per-tenant env vars.

Better Auth is the source of truth for sessions, with database-backed session records and cookie-based web sessions. Z8 also uses secondary storage to speed up session lookups and auth rate limiting without moving revocation authority out of the database.

Key behaviors:

  • Web requests use secure, httpOnly session cookies through the Next.js Better Auth integration.
  • Desktop clients can use bearer-token auth where cookies are not the primary transport.
  • The mobile app is currently an Expo workspace in the monorepo, but it should not be documented as a finished bearer-token client until that runtime behavior exists.
  • Sessions carry activeOrganizationId, which is the current org context used by authorization and data-access layers.
  • Organization-scoped routes and services should trust session.session.activeOrganizationId rather than user-supplied org identifiers.
  • Email verification and password-reset links are rewritten to the correct organization-aware base URL when auth emails are sent.

The auth surface supports email/password, required email verification, passkeys, 2FA, admin tooling, and Better Auth organization features in one shared session model.

Invitation And Onboarding Flows

Invitation acceptance and onboarding are part of the auth system, not separate ad hoc flows. The goal is to preserve organization context from the first email through post-login onboarding.

The current flow is:

  1. An org admin sends an invitation for a specific organization.
  2. Z8 sends the invitation email using the organization's resolved app URL.
  3. The invited user signs up or verifies their email.
  4. Pending invitation state is preserved so the user returns to the correct accept-invitation flow after verification.
  5. Better Auth accepts the invitation, adds the member to the organization, and records that org relationship on the user session.
  6. Onboarding reads activeOrganizationId so employee creation, role-based steps, and defaults stay scoped to the invited organization.

This is also why callback handling matters for enterprise auth. Social OAuth, SSO, verification redirects, and invitation acceptance all need to land the user back in the correct organization context instead of dropping them into a generic global session.

OAuth Security Features

FeatureDescription
PKCEProof Key for Code Exchange prevents code interception
State signingHMAC-signed state prevents CSRF attacks
Nonce validationOIDC nonce prevents replay attacks
Secure cookieshttpOnly cookies store OAuth state

OAuth Flow

User clicks "Sign in with Google"

Detect organization from context

Load org-specific or default credentials

Redirect to provider with PKCE challenge

User authenticates with provider

Callback with code + state

Verify state signature + exchange code

Link/create user account

Establish session with organization context

Organization-Scoped Sessions

Sessions are scoped to the active organization:

// Get session with organization context
const session = await auth.api.getSession({
  headers: request.headers,
});

// session.session.activeOrganizationId
// session.user.id

Switching Organizations

// Switch active organization
await fetch('/api/organizations/switch', {
  method: 'POST',
  body: JSON.stringify({ organizationId }),
});

Native App Login Handoff

Desktop and mobile clients use a browser-based login handoff that returns a bearer session token to the native app:

/api/auth/app-login?app=mobile&redirect=z8mobile://auth/callback
/api/auth/app-login?app=desktop&redirect=z8://auth/callback

Flow summary:

  1. The native app opens /api/auth/app-login in the browser.
  2. If the user is not signed in, the route redirects to /sign-in with a callbackUrl back to the app-login request.
  3. After sign-in, the route validates app access and redirects back to the native deep link with a bearer session token.
  4. If access is denied, the route redirects back to the deep link with an error such as access_denied.

App Access Control

User-level app access flags remain part of the auth model:

PermissionDescription
canUseWebappAccess to the web application
canUseDesktopAccess to the desktop application
canUseMobileAccess to the mobile application

These permissions are evaluated after authentication so an authenticated user can still be denied a specific app surface while retaining access to other allowed clients.

Default Access

All permissions default to true. Administrators can restrict access per user.

Access Detection

The system detects app type from request headers:

  • Cookie auth → Webapp
  • Bearer token + X-Z8-App-Type: mobile → Mobile
  • Bearer token + X-Z8-App-Type: desktop → Desktop
  • Bearer token without explicit app header → Legacy fallback based on user agent for shared routes

Mobile-Only Routes

Mobile API routes under /api/mobile/* require a bearer token plus X-Z8-App-Type: mobile. Cookie-authenticated web requests are always treated as webapp, even if they send a spoofed app-type header.

Access Denied Handling

When access is denied:

  1. Audit log entry created
  2. User redirected to /access-denied page
  3. Clear message explaining which app is restricted

Content Security Policy

The app implements a strict CSP with nonce-based script execution:

// CSP headers are set automatically
// Scripts must include the nonce
<script nonce={nonce}>...</script>

CSP Reporting

CSP violations are reported to /api/csp-report for monitoring.

On this page