Skip to content

Four Integration Archetypes

This is a decision you make once, early — before you write any integration code — and then live with. Once you’ve decided who owns each user column, the next question is structural: should a local users table exist at all, and if so, what belongs in it? Every integration is one of four shapes, and each shape is an answer to that question. Naming the shape up front tells you what your schema, your sync logic, and your failure modes will look like.

The four shapes fall out of two independent questions:

  • Where does the user row live? — Shadow-User (a local users table) vs. Federated-IDs (no local table).
  • Where do permissions live? — Identity-Only (your app decides) vs. Full-Delegation (Auther decides).

The two are orthogonal: you answer them separately, and any combination is valid. Get them straight and the rest of the integration is mechanical rather than improvised.

A. Shadow-User — a local table that mirrors Auther

Section titled “A. Shadow-User — a local table that mirrors Auther”

Choose this when users.id is referenced by foreign keys from several other tables, or joins on the user row are common in your queries.

A local users table still exists, but it stops being the source of truth for identity. Its primary key is literally Auther’s user ID (the JWT sub), and it gets populated two ways:

  1. Just-In-Time (JIT) — on the first valid JWT carrying a sub you’ve never seen, you insert a shadow row from the claims.
  2. Webhooks — Auther’s lifecycle events (user.created, user.updated, user.deleted) keep that row current and cascade deletions.

Because the row still exists with the same primary key, all of your existing foreign keys keep working. INNER JOIN users still works. Cascade deletes still work. The only thing that changes is that you stop writing to the identity columns yourself — email, name, and the rest now flow in from Auther, and your row caches them.

Shadow-User sync
Rendering diagram…

The Hono template — the running example throughout Part 2 — hits exactly that case: its users.id is referenced by sessions, accounts, two_factors, notifications, push_tokens, notification_preferences, members, and audit_logs. With that much foreign-key coupling, dropping the table is not worth the rewrite.

Choose this when your service is stateless and never joins on a user — it just carries an opaque ID through.

The opposite shape: drop the users table entirely. Every downstream table keeps a user_id column, but it’s plain text with no foreign key behind it. When you need profile data — a display name, an email — you fetch it from Auther’s /userinfo endpoint and cache the result.

The appeal is that there’s a single source of truth and no sync logic to maintain. The cost is that you give up foreign-key integrity, you lose INNER JOIN users, and every query that needs to display a user now requires an external lookup.

That tradeoff pays off for stateless services that never join on a user — microservices, event processors, and data pipelines that only ever carry an opaque ID through. It is the wrong shape for the Hono template, which leans on those joins constantly.

C. Identity-Only — Auther for authN, your own authZ

Section titled “C. Identity-Only — Auther for authN, your own authZ”

Choose this when you already have a working authorization system you want to keep — Auther just tells you who the user is, you decide what they can do.

This axis is about authorization, not the user row. In the Identity-Only shape, Auther issues tokens and you trust them for authentication — you extract sub and email from the JWT — but every permission decision runs through your app’s own authorization system. It is almost always paired with Shadow-User (the two axes are independent).

The Hono template already ships packages/authorization — a working ReBAC-style engine with createAuthorize, registry.can(), principals, and resource loaders. Keeping that engine and feeding it an identity from Auther is precisely Identity-Only.

D. Full-Delegation — Auther runs authN and authZ

Section titled “D. Full-Delegation — Auther runs authN and authZ”

Choose this when centralized authorization is the explicit goal — typically a multi-app SaaS where every product shares one permission model.

The other end of the authorization axis: let Auther resolve permissions too. The JWT carries a permissions claim with pre-resolved permissions per entity type, and your app simply reads those claims. For the permissions that depend on runtime context — the ABAC-gated ones that can’t be baked into a token — your app calls /api/auth/check-permission instead.

It also works for the Hono template if you want to retire packages/authorization and let Auther own that responsibility — but that’s a deliberate choice to delete working code, not a default.

The two axes are independent, so an integration is a point on a grid rather than a pick from a menu. The left column says where the user row lives; the right column says where permissions are decided.

SituationUsers tableAuthorization
Hono template (this course’s main example)ShadowIdentity-Only (keep the package)
Monolith with deep FK user dependenciesShadowIdentity-Only
Stateless API microserviceFederatedFull-Delegation
Multi-tenant SaaS with per-tenant modelsShadowFull-Delegation
Microservice mesh, each service owns its domainFederatedFull-Delegation + local hot cache
Existing app with complex custom authzShadowIdentity-Only
Greenfield app, minimum codeShadowFull-Delegation

For the Hono template specifically, the answer is Shadow + Identity-Only for exactly that reason: it has deep foreign-key coupling and a working local authorization package, so throwing away either one buys you nothing.