Skip to Content

Auth

Shipmore Core uses a dual auth model: Payload’s built-in auth for the admin panel, and Better Auth  for frontend customer sign-in and sessions.

Overview

ConcernSystemCollectionURL
Admin loginPayload authusers/admin
Customer sign-inBetter Authcustomers/auth (per tenant)

Admin users manage tenants, pages, and products. Customers sign in on the frontend to access dashboards, make purchases, and manage subscriptions.

Better Auth setup

Better Auth is integrated via the payload-auth plugin, which bridges Better Auth with Payload’s database and collections.

Providers

Out of the box, three sign-in methods are configured:

  • Email + password — standard credentials-based sign-in.
  • Magic link — passwordless sign-in via email link.
  • Google OAuth — social sign-in with Google.

Configuration

Auth options live in src/domains/auth/lib/options.ts. The key settings:

  • baseURL: undefined — Better Auth infers the base URL from each request’s host. This is critical for multi-tenant setups where each tenant has a different domain.
  • Session — cookie-based with a 5-minute cache for performance.
  • Account linking — enabled for Google and email-password. If a user signs in with Google and later with email using the same address, the accounts are linked.

Google OAuth

To enable Google sign-in:

  1. Go to the Google Cloud Console APIs & ServicesCredentials.
  2. Create an OAuth 2.0 Client ID (Web application type).
  3. Add Authorized redirect URIs for each tenant domain, e.g.:
    • http://localhost:3000/api/auth/callback/google (local dev)
    • https://mysite.com/api/auth/callback/google (production)
    • Add one redirect URI per tenant domain you want Google sign-in on.
  4. Set the env vars:
GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id # used client-side

If these vars are not set, the Google sign-in button won’t appear.

Multi-tenant auth

Trusted origins

Better Auth needs to trust each tenant domain for CORS and cookie handling. The configuration automatically adds:

  • Origins from env vars (NEXT_PUBLIC_BETTER_AUTH_URL, BETTER_AUTH_URL, NEXT_PUBLIC_SERVER_URL).
  • The current request’s host — so any tenant domain is automatically trusted at runtime.

OAuth callbacks per tenant

For social sign-in (Google), the OAuth callback URL must match the domain the user is signing in from. A middleware in the auth configuration overrides the baseURL for /sign-in/social and /callback/* routes using the request’s Host header. This ensures the OAuth flow redirects back to the correct tenant domain.

In practice, this means Google OAuth works on any tenant domain as long as you’ve added the redirect URI for that domain in the Google Cloud Console.

Auth cookies are scoped to the domain they were set on. A customer signed in on tenant-a.com is not signed in on tenant-b.com. This is expected browser behavior and provides proper tenant isolation.

Session handling

Server-side

In server components, API routes, and server actions, read the session from request headers:

import { getSession } from '@/domains/auth/lib/session' const session = await getSession(headers) // session.user contains the authenticated customer

Under the hood, this calls payload.betterAuth.api.getSession({ headers }).

Client-side

In client components, use the auth client:

import { createAuthClient } from '@/domains/auth/lib/client' const authClient = createAuthClient() // authClient.useSession(), authClient.signIn.email(), etc.

The client uses window.location.origin as its base URL, so it automatically points to the current tenant’s domain.

Protected routes

Server actions use zsa procedures for route protection:

  • authProcedure — requires an active session. Returns 401 if unauthenticated.
  • stripeCustomerProcedure — extends authProcedure and ensures the customer has a Stripe customer record.
  • paywallProcedure — extends further with paywall checks (credits, subscription tier).

Frontend pages like /dashboard check the session at the page or layout level and redirect unauthenticated users to /auth.