jampress.pw
Philosophy Docs Roadmap Playground GitHub

Documentation

Philosophy Three tracks, modular scaling, and the foundation rules that shape every package. Architecture Stable viewer contracts with swappable provider/storage internals. Security Threat model, token/session controls, and rate-limit requirements. Roadmap Phase-by-phase path from DIY auth to WordPress bridge and adapters. Auth Adoption Plan Concrete rollout order and milestones for zip, lobpress, inbox, DesBio, and DBScript. Host App Integration How Astro and Workers apps wire middleware, hooks, and routes. Provider Adapters The AuthAdapter contract and migration path across login providers. Data Model D1/MySQL schemas for identities, sessions, tokens, and rate limits.

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:

  1. If the host app has ZEPTOMAIL_SECRET in its Worker secrets → use the host's sender domain
  2. Otherwise, if fallbackSecret is injected → send from jampress.pw directly
  3. Otherwise, if fallbackRelayUrl is configured and the app's domain is registered → proxy the message through api.jampress.pw, which sends from jampress.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)

  1. User submits email to host app's auth route
  2. auth-astro-workers route handler calls authAdapter.requestLogin({ email })
  3. auth-provider-magic-link normalizes email, mints a hashed token, stores it via storageAdapter.createMagicToken(), sends email
  4. User clicks link → hits verify route
  5. authAdapter.completeLogin({ token }) consumes the token, creates a session, returns AuthIdentity
  6. auth-astro-workers calls hostHooks.resolveOrCreateViewer(identity) to get the full Viewer
  7. Session cookie is set, Viewer is populated in Astro.locals.viewer

Subscriber auth (sneaky auth) flow

The subscribe widget presents a single email input that looks like a newsletter signup.

  1. User enters email in the subscribe form
  2. Backend checks if email exists as a known user (via storageAdapter.getIdentityByEmail())
  3. Known user path: mint a magic-link token, send a "confirm" email where the confirm link is actually a magic-link login
  4. Unknown email path: call hostHooks.onNewSubscriber(email) to add to mailing list, send a "confirm your subscription" email with a genuine confirmation link
  5. Both emails use the same subject line and template. The confirm URL format is identical: /confirm?token=xxx
  6. The /confirm route 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):

  1. wp-plugin-auth-bridge hooks into WordPress login/logout/session-refresh events
  2. On WP login: plugin calls the SDK Worker to create a parallel session token
  3. The Worker sets the SDK session cookie alongside WP's native cookies
  4. Astro-served routes read the SDK cookie via auth-astro-workers middleware → Viewer is populated
  5. 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