Use case — Choosing a storage layout
The library never owns your users table, your database, or your migration tooling. It reaches storage through small store.* substore interfaces, and you decide how to back them. This page is the map: pick your entry point, and it routes you to the concrete guide.
Mental model
Two questions settle almost everything: where do the OIDC tables live (one backend, or split across two), and do you already have a schema you must fit into. The substore interfaces are identical in every case — only the backing changes.
Pick your entry point
| Your situation | Layout | Guide |
|---|---|---|
| Greenfield — no schema yet | One SQL backend, adapter-owned tables | Persistent storage (SQL) |
| AWS / DynamoDB — you want an AWS-native durable backend | One DynamoDB table per substore | DynamoDB storage |
Existing database — you already run a users table and migrations | Adapter-owned oidc_* tables (renamed to fit), your identity data projected in | BYO store backend · BYO user store |
| Scale / high churn — you want volatile state off the durable tier | composite split: durable SQL + volatile Redis | Hot/cold split |
The third branch has one trap. Authorization codes and the PAR request_uri are short-lived, so they look like volatile state, but they still belong on the durable side: every member of composite.TxClusterKinds must resolve to a single backend.
Greenfield
The sql adapter ships the entire schema, so you do not design the OIDC tables. New(db, dialect) builds the store against SQLite, MySQL 8.0+, or PostgreSQL 14+ via the matching SQLite() / MySQL() / Postgres() dialect, and the adapter owns its own oidc_* tables.
- Development / examples: call the store's
Migrate(ctx)to apply the embedded schema to a fresh live connection. It is a convenience for demos and tests, not an upgrade path for existing tables. - Production: call the store's
Schema()to get the dialect-specific DDL — with anyWithNamingoverrides already applied — and feed that string into your existing migration tooling. The DDL is exposed verbatim so a review can diff the adapter's expectations against your production schema.
For an existing database, apply the additive columns and indexes in schema/MIGRATIONS.md before the first request. The SQL guide lists the exact allowed_scopes, MFA row_version, and retention-index names; storage maintenance gives the rollout and GC order.
Existing database
You keep your database and your migrations. Two facts shape how the adapter fits alongside them.
- The adapter owns fixed-shape tables.
WithNamingrenames the physical tables (oidc_clients→ whatever your convention is), but the columns are fixed — the adapter builds its queries against a known column set. An unknown logical key makesNewfail, so a typo surfaces at construction time rather than at first query. - Your real
userstable stays yours. The adapter'soidc_userstable (subject,claims,updated_at, plus optionalusername/password_hash) is a projection target, not a replacement. If you already have a richuserstable, implement astore.UserStore— andstore.UserPasswordStorewhen you support the password grant — that reads your columns and returns astore.User. You never migrate your user data into the adapter's shape.
So "reuse my existing tables" splits in two: the OIDC protocol tables (auth codes, refresh chains, grants, …) are the adapter's — rename them with WithNaming and let your migrations create them — while your identity data stays behind a bring-your-own UserStore.
Which substores are worth bringing your own
Any substore can be BYO, but the one embedders almost always own is the user projection (UserStore / UserPasswordStore), because the user record is your domain. The protocol substores (codes, tokens, grants) rarely benefit from a hand-written backend — use the sql adapter for those. See BYO store backend for a from-scratch implementation and BYO user store for the identity-only case.
Splitting for scale (hot/cold)
When one backend is no longer the right shape — durable rows don't need the QPS the volatile state generates, and volatile rows don't need durable guarantees — the composite adapter routes each substore to a durable or a volatile backend. This is the standard production shape: SQL durable, Redis volatile. Full walkthrough on the Hot/cold split page.
What may go to Redis — the rule
The Redis-or-SQL choice is not "is this data short-lived." It follows one invariant: substores that must commit atomically together (composite.TxClusterKinds) have to share a single backend, so they stay on the durable tier. Data lifetime only decides the remainder.
| Bucket | Substores | Why |
|---|---|---|
| Must be durable (SQL) | AuthorizationCodeStore, RefreshTokenStore, GrantStore, PushedAuthRequestStore, AccessTokenRegistry, OpaqueAccessTokenStore, GrantRevocationStore | Members of the atomic-routing cluster — they commit or CAS in one consistency domain. The Redis adapter returns nil for these, so composite cannot route them off the durable tier. |
| Good fit for Redis (volatile) | InteractionStore, ConsumedJTIStore | Short-lived, high-churn, losable in isolation, and outside the cluster. |
| Your call | SessionStore | Route to either tier; declare logout-trigger/session-snapshot durability with WithSessionDurabilityPosture. BCL targets remain grant-derived, and bcl.no_sessions_for_subject records a session-bearing logout with zero eligible RP targets. |
The counter-intuitive member is PAR: its request_uri is short-lived and looks volatile, but the OP consumes it inside the authorization-code path, so it belongs to the cluster and stays durable. The authoritative per-substore table — and the exact op.New guardrails for when a required backend is nil — live on the Hot/cold split page.
Redis safety floor
redis.New refuses to start without TLS (rediss://) and AUTH. The dev-only escape hatch is redis.WithDevModeAllowPlaintext; shipping it in production is a security regression you have to type out by hand.
Keep the layout healthy
Before launch, apply the schema and verify the writer compatibility boundary. During operation, schedule SQL retention GC, monitor its GCStats, and treat DynamoDB TTL as reclamation rather than expiry enforcement. See storage maintenance, backup & disaster recovery, and the backend-specific SQL, DynamoDB, and BYO store guides.