Alternative System Shapes
The file-by-file Hono walkthrough is one concrete shape: an FK-heavy monolith that
already owns its users table. Most other applications are a subset or a variation
of it, and you don’t need a new mental model for each one. Find the shape closest
to your system below, then apply the same Shadow-User and archetype ideas in the
proportions it demands. Pick the right row of the
archetype matrix and reuse the mechanisms — JWT
verification, the Shadow-User sync, the API-key exchange — that the
core flows already lay out. This is a map, not a set of
deep dives: each shape opens with a one-line what makes it different, then lists
which pieces it keeps and which it drops.
Pure-API backend (stateless microservice)
Section titled “Pure-API backend (stateless microservice)”Different because: there is no users table to mirror, so Shadow-User disappears entirely.
A service that reads and writes orders, is called only by other services, never
renders UI, and has no reason to keep a users table.
- Archetype: Federated-IDs + Full-Delegation.
- Users table: none.
- Middleware: verify the JWT, then attach
userIdandpermissionsto the request context. - JOINs against users: they never happen. When a display name is needed, a
backend-for-frontend higher up the stack fetches it from Auther’s
/userinfoendpoint. - Code you write: a single middleware, on the order of 100 lines. That is the whole integration.
// Works with chi, gin, or echo.func JWT(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") claims, err := verifyToken(token) // uses go-jose JWKS auto-refresh if err != nil { http.Error(w, "unauthorized", 401) return } ctx := context.WithValue(r.Context(), "claims", claims) next.ServeHTTP(w, r.WithContext(ctx)) })}SPA with no backend
Section titled “SPA with no backend”Different because: there is no confidential server, so the browser itself runs the public PKCE flow.
A pure-browser Vue or React app that calls third-party APIs directly, with no server of its own to hold tokens.
- Archetype: public PKCE client.
- Tokens: the access token lives in memory; a refresh token only if Auther issues one for public clients.
- Silent refresh: a hidden iframe hitting
/authorizewithprompt=noneandresponse_mode=query, or the refresh-token flow. - Code you write: roughly a 150-line
authClient.tsexposinglogin,logout,getToken, andisAuthenticated. Reach foroidc-client-tsrather than hand-rolling it.
Monolith with FK-heavy user dependencies
Section titled “Monolith with FK-heavy user dependencies”Different because: this is the full case — you already own a users table threaded with foreign keys.
This is the shape the rest of Part 2 covers in full: a monolith whose schema is
threaded with foreign keys back to a local users table. The
Hono walkthrough is the worked example, from
shrinking the schema through to the safe cutover, so there is nothing new to add
here — Shadow-User plus Full-Delegation is the answer.
Microservice mesh
Section titled “Microservice mesh”Different because: no service owns user rows, so the question is verifying tokens cheaply as requests fan out.
Three services — billing, inventory, notifications — sharing one Auther, where no service owns user rows because they all rely on federated IDs.
- Archetype: Federated-IDs in each service + Full-Delegation, backed by a shared verification library.
- Shared package: an
@company/auth-middlewarepackage that bundles the JWKS cache, the verify step, and the context attach. Every service imports the same one. - API gateway: an edge gateway (Envoy, Traefik, or a managed API Gateway)
validates the JWT once and forwards trusted headers such as
x-user-idandx-permissions. Downstream services trust those headers because the gateway is the only ingress, which saves a JWT verification on every service hop. - Cross-service calls: relay the user’s existing JWT rather than inventing a separate service-to-service scheme. For system-to-system calls that carry no user context, use the API-key exchange flow instead.
- Hot-path authorization: read permissions straight from the JWT claims locally.
ABAC decisions go through
/check-permission, but a well-designed hot path avoids ABAC-gated permissions on principle.
Multi-tenant SaaS
Section titled “Multi-tenant SaaS”Different because: each tenant has its own users and permission model, so isolation matters while the JWT stays small.
A B2B SaaS where each customer is a tenant with its own users, resources, and permission model. Auther’s client-scoped entity types and per-tenant authorization models do most of the work.
- Archetype: Shadow-User + Full-Delegation, with a separate authorization model per tenant inside Auther.
- Per-tenant entity types: scope them to the client, for example
tenant_acme:project,tenant_acme:invoice, andtenant_acme:report. - Per-tenant client: give each tenant its own OAuth client for stronger isolation, or use one shared client with tenant-routed JWT claims for a simpler setup.
- Users in multiple tenants: the JWT
permissionsclaim reflects only the currently active tenant, so switching tenants re-issues the token (the new claims are injected by an Auther pipeline). This keeps the token small. - Registration contexts: one per tenant, keyed to an email domain. A user with
an
@acme.comaddress who authorizes through the acme client can be auto-grantedtenant_acme:project:vieweracross all projects. - Invites: lean on Auther’s platform invite table, which issues HMAC-signed, email-locked tokens.
- Shadow sync: the ordinary lifecycle webhooks, unchanged.
Mobile app with a backend
Section titled “Mobile app with a backend”Different because: it is the monolith shape plus device-specific token handling — PKCE and secure on-device storage.
An iOS or Android client talking to a Hono-style API: PKCE on the device and refresh tokens held in secure storage.
- Archetype: same as the FK-heavy monolith, plus on-device PKCE and refresh tokens.
- Client: a public PKCE client with a custom-scheme redirect such as
myapp://auth/callback. - Tokens: keep the access token in memory or the Keychain; keep the refresh token in the Keychain or EncryptedSharedPreferences.
- Rotation: Auther may rotate the refresh token on each use, so your mobile SDK must persist the new one every time it refreshes.
- Offline mode: cache the last token; if it has expired and there is no network, fall back to a “reconnect” UX.
- Biometric gate: gate reading the refresh token from the Keychain behind a local FaceID or TouchID check. This is entirely on-device — Auther never sees it.
Background jobs, cron, and CI
Section titled “Background jobs, cron, and CI”Different because: there is no interactive user, so machines authenticate via the API-key-to-JWT exchange.
A nightly “send weekly summary” job, a CI pipeline that deploys by calling your API, or a data pipeline.
- Archetype: API key exchanged for a JWT.
- One key per job: name each key for what it does —
cron:weekly-summary,ci:deploy-pipeline— so the audit trail reads clearly. - Minimum tuples: grant only the access the job needs, for example
(notifications, *, sender, apikey, <id>). - In the job code: exchange the key for a JWT, cache it for about 14 minutes, and refresh on demand.
- Audit: Auther records the API-key activity, and your app’s audit log carries
the
principal.apiKeyIdalongside it.
Legacy migration with gradual cutover
Section titled “Legacy migration with gradual cutover”Different because: an existing auth system has to keep working while you cut over, so the middleware runs dual-path.
A Rails-plus-Devise monolith with 500k users that has to move onto Auther with zero downtime. Dual-path middleware accepts both the old and new credentials at once, so the cutover is a sequence of safe, reversible phases rather than a single switch.
-
Stand up Auther and bulk-import users. No user-facing change yet; the new identity provider runs alongside the old one.
-
Deploy dual-path middleware. It accepts both a Devise session and an Auther JWT, and you flip the login button to point at Auther.
-
Monitor and drain. Let the old Devise sessions expire naturally over the following days while watching the metrics.
-
Remove the old path. Delete the Devise middleware, drop the Devise gems, and drop the legacy auth tables.
# Phase 2: middleware accepts either credential.def authenticate if (jwt = bearer_token(request)) @principal = verify_auther_jwt(jwt) # new path elsif (session = devise_session) @principal = principal_from(session) # legacy path, drained in phase 3 else head :unauthorized endendEvery shape on this page is the same exercise — choose the matching row of the archetype matrix, then apply the mechanisms from the Shadow-User pattern in the proportions that shape calls for.