Product-Independent Authentication Infrastructure
Reusable Auth Kit
Authentication infrastructure that several products can share without sharing accounts, permissions, or data — provider-neutral identity contracts, a web adapter, an explicit API client, and independent token verification on the server.
- Status
- Early reusable infrastructure
- Stack
- TypeScript
- React
- Next.js
- Python
- FastAPI
- JWT
- JWKS
- Supabase adapter
The problem
Every product needs sign-in, a session, an authenticated API client, and token verification on the server. When each product builds those for itself, the same things go wrong in slightly different ways:
- Duplicated provider logic. Each codebase learns the provider's SDK and its quirks again.
- Inconsistent identity types. One product's "user" has fields another's does not, and the provider's user object leaks into application code.
- Verification drift. Backends validate tokens with different rules, or trust the frontend more than they should.
- Coupling. Authentication ends up tangled with profiles, roles, and billing, so it cannot be reused without dragging a business model along.
Reusable Auth Kit answers one question — who is this? — in a form several products can share, and deliberately stops there.
Core principle
- Identity is shared.
- Accounts are independent.
- Business data is isolated.
- Authorization belongs to each product.
Authentication answers who you are. Authorization answers what you may do, and that depends on the product. A shared identity layer should let products trust the same sign-in without being forced to share accounts, roles, billing, or records.
This is a repository rule, not just a description. The kit's own contribution rules forbid adding account models, profiles, organizations, subscriptions, or role and permission logic.
Architecture
Identity provider
Identity provider
Handles sign-in and issues asymmetrically signed tokens.
normalized by
Reusable Auth Kit
Contracts
Provider-neutral identity, session, state, and errors.
Web adapter
Provider SDK behind the contracts, plus React bindings.
API client
Explicit auth mode per request.
Server verifier
Independent token verification for FastAPI.
produces
Shared
AuthIdentity
A stable subject and verified facts about it. No roles, no profile.
mapped by each product to
Owned by each product
Product A
Its own account, authorization rules, and data.
Product B
Its own account, authorization rules, and data.
Contracts
The contracts package is the centre of the design. It is small on purpose: AuthIdentity, AuthSession, AuthState, a typed AuthError, the AuthClient interface an adapter implements, and an AccessTokenProvider that anything needing a token depends on.
Stable contracts are what make the rest possible:
- Provider independence. Application code imports the contract, never the provider's user or session type.
- Frontend and backend agree. The same identity shape exists in TypeScript and in the Python verifier.
- Testability. Contracts and models are unit-tested with no network and no provider.
- Reuse. A second product adopts the same types instead of inventing its own.
One example of what normalization buys: whether an email is verified is a three-state value. It is true only when the provider has confirmed it, false when an email exists without confirmation, and absent when there is no email at all. It is never inferred from an email string being present, and user-editable metadata is never treated as proof of anything.
Web and React integration
The web package contains a Supabase adapter that implements the AuthClient contract. Supabase is an adapter here, not the domain model: the adapter validates its configuration, maps provider users and sessions into the kit's identity and session types, and normalizes provider errors into the kit's error codes.
Sign-in support is stated as precisely as the repository states it. Email magic link and Google have been verified end to end. Additional provider adapters for Apple, Facebook, X, GitHub, and Microsoft exist in the codebase but have not yet been live-verified.
Each product declares which providers it allows. A sign-in attempt with a provider the product has not enabled is rejected at runtime, before any redirect happens. Upstream provider tokens are not kept in client state or forwarded to APIs.
React bindings — a provider, hooks for auth state and session, and a guard for protected routes — consume the same contract. A component never touches a provider-specific API.
Backend verification
A signed-in browser does not authorize a backend request. The server verifies every token itself.
In the API
- then
Bearer token
Extracted from the request.
- then
Signature
Checked against the provider's public keys.
- then
Claims
Issuer, audience, expiry, not-before, subject.
AuthIdentity
Handed to the route as a dependency.
- Asymmetric algorithms only. The verifier allows ES256 and RS256. Symmetric algorithms are rejected in configuration, so a backend never holds a shared signing secret.
- Keys come from JWKS. Signing keys are resolved by key id from the provider's published key set and cached in memory with a time-to-live, which is what lets keys rotate without a deploy. An unknown or missing key id fails closed.
- Transport is checked too. A remote key-set URL must be HTTPS, and certificate and hostname verification are always on.
- Claims are validated strictly. Issuer and audience must match configuration; expired and not-yet-valid tokens are rejected; a subject is mandatory.
- Failures are typed. Missing credentials, expired token, wrong issuer, wrong audience, missing claim, unknown key, and unsupported algorithm are distinct errors.
A FastAPI route receives the result through a dependency. It gets an AuthIdentity or the request is rejected; there is no path where a route sees an unverified token.
API client
The API client makes authentication behaviour explicit per request, with three modes:
- Required — a token must be available, or the request fails before it is sent.
- Optional — a token is attached when there is one.
- None — no credentials are sent.
Around that sit a few deliberate refusals. The client only attaches a token to requests for its configured origin and rejects cross-origin targets before calling fetch. A caller-supplied Authorization header in an authenticated mode is treated as a conflict and rejected. Remote endpoints must use HTTPS. And tokens are not cached in the client: it asks the token provider on every request, so a refreshed or revoked session takes effect immediately.
Attaching credentials silently to everything is simpler. Making the caller choose is what stops a token being sent somewhere it was never meant to go.
Product boundary
| The kit owns | Each product owns |
|---|---|
| Identity, session, state, error contracts | Its account model and profile |
| The access-token provider interface | Roles and permissions |
| Provider adapters and their normalization | Organizations, teams, workspaces |
| React auth bindings | Subscriptions and billing |
| Server-side token verification | Product-specific authorization |
| The authenticated API client | All business data |
A product consumes the kit by mapping the verified subject to its own account record. The subject is opaque and stable; everything that makes someone a customer, an admin, or a member lives on the product's side of that mapping.
End to end
- then
Sign in
Session and identity in the browser.
- then
Request
Fresh token, explicit auth mode.
- then
Verify
Server checks signature and claims.
Product
Account lookup, then its own authorization.
The repository proves this with running code rather than a diagram: a Next.js example with a protected route and an authenticated request, a FastAPI example with a protected endpoint, and a second, separate consumer application built against the same packages. The kit was designed so that products such as CareerNeed can reuse authentication while keeping accounts and authorization local.
Engineering decisions
Shared identity, independent accounts
- Why
- Products can reuse authentication without coupling their business models to each other.
- Tradeoff
- Every product has to own and maintain its own identity-to-account mapping.
Provider adapters behind contracts
- Why
- Application code should not depend on one provider's user and session types.
- Tradeoff
- Each adapter needs normalization work and has to be kept current with its provider.
The backend verifies independently
- Why
- Frontend authentication state is not a trust boundary. Only a verified signature and claims are.
- Tradeoff
- Each service needs issuer, audience, and key-set configuration, and verification infrastructure.
Explicit auth mode per request
- Why
- Not every request should carry credentials, and none should carry them across origins.
- Tradeoff
- Callers must state what each request needs instead of relying on a default.
Authorization stays outside the kit
- Why
- Roles and permissions are product-specific. A universal model would fit none of them well.
- Tradeoff
- The kit cannot answer what a user may do. Each product builds that itself.
Trust boundaries
Stated as concrete properties of the implementation:
- Tokens are never accepted without signature verification, and never with a symmetric algorithm.
- Issuer, audience, expiry, and not-before are all enforced; user-editable metadata is never authoritative.
- Bearer tokens are confined to the configured origin and are not cached by the client.
- Provider objects, provider tokens, and raw provider errors do not cross into application code.
- Products restrict sign-in to an explicit allow-list of providers.
- The kit contains no account, role, or permission logic.
These are boundaries the code enforces. They are not a claim that an application built on the kit is secure by default; authorization, session policy, and data access remain the product's responsibility.
Status
Reusable Auth Kit is early reusable infrastructure. The contracts, the Supabase adapter with React bindings, the API client, and the FastAPI verifier are implemented and tested. Supabase is the only provider adapter so far, and the additional social sign-in providers have not yet been live-verified. It is not presented as production-ready.