Authentication
Orchard has three distinct auth surfaces. Don’t conflate them.
1. Server-to-server: API keys (Bearer)
Four key prefixes encode (mode, exposure):
| Prefix | Mode | Where it’s safe | Scopes allowed |
|---|---|---|---|
sk_live_… | live | server only | any scope |
pk_live_… | live | browser-safe | read scopes + checkout:write |
sk_test_… | test | server only | any scope |
pk_test_… | test | browser-safe | read 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:readpk_* 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.
2. Customer-facing: passwordless magic-link
Shoppers authenticate with a passwordless magic link (the customer-accounts module is opt-in and billable). The real endpoints:
| Step | Endpoint |
|---|---|
| Request a link | POST /api/v1/customer/magic-link/request |
| Consume the token (from the email link) | POST /api/v1/customer/magic-link/consume |
| Read the current session | GET /api/v1/customer/me |
| Rolling refresh | POST /api/v1/customer/refresh |
| Sign out | POST /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 isPOST /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.