Z8Z8 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 and is configured through Enterprise Identity Setup at /settings/enterprise/identity-setup after org-scoped SSO is configured.
  • 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 with a one-time authorization code:

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

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 redirects back to the native deep link with a one-time code.
  4. The client exchanges the code and its verifier through /api/auth/app-exchange for a bearer session token.

Native Client Requests

Native clients authenticate API requests with bearer session tokens. The app exchange endpoint uses X-Z8-App-Type to identify the client for its code exchange.

Mobile-Only Routes

Mobile API routes under /api/mobile/* require a bearer token plus X-Z8-App-Type: mobile.


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