DevelopersAuthentication

Authentication

Orchard has three distinct auth surfaces. Don’t conflate them.

1. Server-to-server: API keys (Bearer)

Four key prefixes encode (mode, exposure):

PrefixModeWhere it’s safeScopes allowed
sk_live_…liveserver onlyany scope
pk_live_…livebrowser-saferead scopes + checkout:write
sk_test_…testserver onlyany scope
pk_test_…testbrowser-saferead scopes + checkout:write

Test-mode keys hit the same logical API but a separate Supabase project — use them for all development.

Scope catalogue

Each key carries an allow-list of {resource}:{action}:

products:read   content:read    inventory:read   search:read
cart:read       cart:write      checkout:read    checkout:write
orders:read     reviews:read    customer:read    customer:write
subscriptions:write             storefront:read

pk_* keys are restricted at creation time to browser-safe read scopes plus checkout:write. sk_* keys may hold any scope and must be treated as secrets — env vars, Vercel secrets, CI secrets. Never commit them; never ship them in a browser bundle.

Authorization: Bearer sk_live_ABCD...

How the tenant is resolved

For Bearer-authenticated requests, the tenant is derived from the key, server- side. Any x-tenant-slug header is ignored — this defeats leaked-key replay against another tenant. A bad key returns a generic 401 (the reason is never disclosed); a key missing the required scope returns 403.

Shoppers authenticate with a passwordless magic link (the customer-accounts module is opt-in and billable). The real endpoints:

StepEndpoint
Request a linkPOST /api/v1/customer/magic-link/request
Consume the token (from the email link)POST /api/v1/customer/magic-link/consume
Read the current sessionGET /api/v1/customer/me
Rolling refreshPOST /api/v1/customer/refresh
Sign outPOST /api/v1/customer/signout

The session JWT is HS256 with a per-tenant HKDF-derived secret, 15-minute TTL, kid = t:<tenant_id>:v<key_version>, and a 30-day rolling refresh (refresh tokens are sha256-hashed at rest).

Drift note: older docs cite POST /api/v1/customer-auth/request. That path does not exist — the live route is POST /api/v1/customer/magic-link/request.

3. Admin: your operator session

You and any staff you invite log into admin.theorchard.dev with email + bcrypt password + optional TOTP. Same admin UI every tenant uses; RBAC scopes you to your tenant. Invite staff at Settings → Users.

Public reads (no key)

Read-only catalog endpoints accept an anonymous request that identifies the tenant with the x-tenant-slug header:

curl https://api-test.theorchard.dev/api/v1/products \
  -H "x-tenant-slug: your-slug"

Anonymous reads are rate-limited per-tenant+IP; Bearer reads per-key. Honor the Retry-After header on a 429.