Architecture
Core design principle
Stable host-facing contract, swappable internals.
Jampress owns reusable capability architecture, not every independently distributed
product assembled from it. See CAPABILITY_FOUNDRY.md for
the upstream package, downstream repository, release, and licensing boundaries.
Every host app interacts with auth through a single Viewer object populated by middleware. App pages and product API routes never import provider-specific code, never write SQL against auth tables, and never handle cookies directly. One bootstrap module composes the chosen provider/storage/email adapters; everything else stays behind the adapter seam.
Package dependency graph
auth-core (contracts only, zero deps)
├── auth-provider-magic-link (implements AuthAdapter)
├── auth-provider-firebase (implements AuthAdapter, future)
├── auth-provider-betterauth (implements AuthAdapter, future)
├── auth-storage-d1 (implements StorageAdapter)
├── auth-storage-mysql (implements StorageAdapter)
├── auth-astro-workers (imports auth-core types, wires middleware)
└── auth-widget-subscribe (imports auth-core for token minting)
wp-shared (PHP utilities, auto-updater wrapper — bundled into all WP plugins)
wp-plugin-auth-bridge (PHP, depends on wp-shared, calls auth-core via Worker HTTP)
wp-plugin-ssg (depends on wp-shared, independent of auth)
wp-plugin-gravity-bridge (depends on wp-shared, optional auth context)
wp-plugin-woo-headless (depends on wp-shared + wp-plugin-auth-bridge for session)
fleet-schema (operator-side fleet.yaml contract; no runtime or Cloudflare API access)
└── blueprint-cli and Astro sites consume its normalized output
content-contracts (site-scoped editorial values)
└── studio-core (authorization + atomic repository command boundary)
└── studio-cloudflare (local-candidate D1/R2 + HTTP adapter)
└── host composes studio-ui (editor HTML/JS) and current auth
The site-fleet hierarchy and planned package boundary are documented in
SITE_BLUEPRINTS.md. Fleet configuration is deliberately
separate from the auth runtime: current authentication remains one host per D1,
and accepting a collection-D1 topology in configuration does not implement the
future realm/site-scoped auth contract needed to run it safely.
content-contracts and studio-core define scoped editorial values and commands;
they do not implement the auth realm/site boundary. Future /studio and fleet
control-plane adapters may call the same service contract. The host must
authenticate the caller, check current site capabilities, and derive their
allowed actor and scope on every call; this service cannot establish that trust
from the supplied values. Storage
adapters must include both collection and site IDs in every query and perform
revision, mutable-head, optimistic-version, idempotency, and audit writes in one
transaction.
The caller supplies a stable idempotencyKey for retries of one logical
mutation. A generated requestId only correlates one attempt. Replay and
conflict behavior are obligations of the atomic repository, documented in
studio-core. The current service supplies
input validation and role policy, not persistence or production isolation.
The new local Cloudflare adapter
implements that persistence boundary. Its loopback example
proves the editorial flow and stopped-state recovery, without claiming a production
auth realm, deployment, or immutable package release.
The Viewer contract
This is the single most important type in the system. Every host app page, API route, and middleware depends on it:
type Viewer = {
user: {
id: string;
email: string;
displayName?: string | null;
role?: string;
};
account: {
id: string;
accountName?: string;
status?: string;
};
auth: {
provider: string;
sessionId?: string;
claims?: Record<string, unknown>;
};
};
Host apps extend this through the HostHooks.resolveOrCreateViewer(identity) callback, which maps an auth identity into the app's own user/account model. This is where product-specific logic lives (creating accounts, assigning pricing tiers, setting roles).
The AuthAdapter interface
Every provider implements this four-method interface:
type AuthAdapter = {
requestLogin(input: { email: string; redirectTo?: string; request: Request }): Promise<{ redirectTo?: string }>;
completeLogin(input: { token: string; request: Request }): Promise<{ identity: AuthIdentity; session: unknown }>;
resolveSession(request: Request): Promise<AuthIdentity | null>;
logout(request: Request): Promise<void>;
};
Framework adapters pass the active session ID to providers through the
x-jampress-session-id request header. This keeps providers cookie-agnostic.
The StorageAdapter interface
Every storage backend implements this interface for session and token CRUD:
type StorageAdapter = {
createMagicToken(email: string, hashedToken: string, expiresAt: number): Promise<void>;
consumeMagicToken(hashedToken: string): Promise<{ email: string } | null>;
createSession(sessionId: string, identity: AuthIdentity, expiresAt: number): Promise<void>;
getSession(sessionId: string): Promise<AuthIdentity | null>;
deleteSession(sessionId: string): Promise<void>;
upsertIdentity(identity: AuthIdentity): Promise<void>;
getIdentityByEmail(email: string): Promise<AuthIdentity | null>;
consumeRateLimit?(endpoint: string, key: string, threshold: number, windowSeconds: number): Promise<RateLimitState>;
checkRateLimit(endpoint: string, key: string, threshold: number, windowSeconds: number): Promise<RateLimitState>;
incrementRateLimit(endpoint: string, key: string, windowSeconds: number): Promise<RateLimitState>;
};
D1 and MySQL implement this with their own schemas and migration files owned
within their respective packages. consumeRateLimit is the preferred operation:
it counts and decides in one storage transaction, closing concurrent
check-then-increment races. The provider retains the two older methods only as
a compatibility fallback for adapters that have not implemented the atomic
operation. The built-in D1 adapter is atomic.
Email sending
The SDK abstracts email sending through a simple interface:
type EmailSender = {
send(options: { to: string; subject: string; html: string; from?: string }): Promise<void>;
};
The included implementations support Zeptomail with a per-application
ZEPTOMAIL_SECRET and Cloudflare Email Sending with a host-owned send_email
binding. Host apps may provide another authenticated EmailSender, but there is
no unauthenticated shared fallback and no automatic cross-provider retry.
Sender construction is intentionally non-fatal. Missing mail credentials should not break
read-only page renders; the sender throws only when send() is called.
Session flow (magic-link)
- User submits email to host app's auth route
auth-astro-workersroute handler callsauthAdapter.requestLogin({ email })auth-provider-magic-linknormalizes email, mints a hashed token, stores it viastorageAdapter.createMagicToken(), sends email- User clicks link → GET confirmation interstitial; the token is not consumed
- User explicitly submits the same-origin POST form to the verify route
authAdapter.completeLogin({ token })consumes the token, creates a session, returnsAuthIdentityauth-astro-workerscallshostHooks.resolveOrCreateViewer(identity)to get the fullViewer- Session cookie is set,
Vieweris populated inAstro.locals.viewer
Subscriber auth (sneaky auth) flow
The subscribe widget presents a single email input that looks like a newsletter signup.
- User enters email in the subscribe form
- Backend checks if email exists as a known user (via
storageAdapter.getIdentityByEmail()) - Known user path: mint a magic-link token, send a "confirm" email where the confirm link is actually a magic-link login
- Unknown email path: call
hostHooks.onNewSubscriber(email)to add to mailing list, send a "confirm your subscription" email with a genuine confirmation link - Both emails use the same subject line and template. The confirm URL format is identical:
/confirm?token=xxx - The
/confirmroute resolves the token type and either creates a session (auth) or confirms subscription (newsletter)
From the outside, there is no visible login flow anywhere on the site.
WordPress auth bridge
For sites running WordPress alongside Astro (e.g., DesBio/DBScript decoupling):
wp-plugin-auth-bridgehooks into WordPress login/logout/session-refresh events- On WP login: plugin calls the SDK Worker to create a parallel session token
- The Worker sets the SDK session cookie alongside WP's native cookies
- Astro-served routes read the SDK cookie via
auth-astro-workersmiddleware →Vieweris populated - On WP logout: plugin calls the Worker to revoke the SDK session
The WordPress user table remains the source of truth for identity. The bridge doesn't migrate users out of WordPress — it translates WP auth events into the SDK's session format.
Cross-site auth (future, opt-in)
Documented in packages/auth-federation/README.md. Not built in v1. The concept:
- A dedicated "identity registry" Worker with its own D1
- Knows which email addresses have accounts on which sibling projects
- Can broker SSO sessions across projects that explicitly opt in
- Each project still owns its own user record and product data
- The registry only stores email ↔ project mappings, never product-level data