Architecture
Core design principle
Stable host-facing contract, swappable internals.
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)
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>;
checkRateLimit(endpoint: string, key: string, threshold: number, windowSeconds: number): Promise<RateLimitState>;
incrementRateLimit(endpoint: string, key: string, windowSeconds: number): Promise<RateLimitState>;
};
D1 and MySQL both implement this with their own schemas and migration files owned within their respective packages.
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>;
};
Default implementation uses Zeptomail. The sender domain is determined by environment detection:
- If the host app has
ZEPTOMAIL_SECRETin its Worker secrets → use the host's sender domain - Otherwise, if
fallbackSecretis injected → send fromjampress.pwdirectly - Otherwise, if
fallbackRelayUrlis configured and the app's domain is registered → proxy the message throughapi.jampress.pw, which sends fromjampress.pw
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 → hits 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