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.

Data Model

Design principles

  1. Auth tables are separate from product tables. Auth never writes to your billing, orders, or account tables. The only bridge is the host_user_id / host_account_id in auth_identities, populated by your resolveOrCreateViewer hook.

  2. The auth_identities table exists from day one. Even when magic-link is the only provider, identities are stored in a dedicated table. This makes future provider migration a data operation, not a schema migration.

  3. Tokens are hashed at rest. Raw magic-link tokens are never stored. SHA-256 hash of the token is stored; the raw token exists only in the email URL.

  4. Sessions are database-backed, not KV. D1 (or MySQL) provides transactional guarantees for token consumption. KV's eventual consistency creates a window where a token could be consumed twice.

Tables (D1)

auth_identities

Maps auth provider identity to host app's user/account.

CREATE TABLE auth_identities (
  id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
  provider TEXT NOT NULL,          -- 'magic-link', 'firebase', 'better-auth'
  provider_user_id TEXT NOT NULL,  -- provider-specific user ID
  email TEXT NOT NULL,
  email_verified INTEGER NOT NULL DEFAULT 0,
  host_user_id TEXT,               -- populated by resolveOrCreateViewer
  host_account_id TEXT,            -- populated by resolveOrCreateViewer
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
  UNIQUE(provider, provider_user_id),
  UNIQUE(provider, email)
);

CREATE INDEX idx_auth_identities_email ON auth_identities(email);
CREATE INDEX idx_auth_identities_host_user ON auth_identities(host_user_id);

auth_sessions

Active sessions.

CREATE TABLE auth_sessions (
  id TEXT PRIMARY KEY,             -- cryptographically random session ID
  identity_id TEXT NOT NULL REFERENCES auth_identities(id),
  provider TEXT NOT NULL,
  expires_at TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
  ip_address TEXT,
  user_agent TEXT
);

CREATE INDEX idx_auth_sessions_identity ON auth_sessions(identity_id);
CREATE INDEX idx_auth_sessions_expires ON auth_sessions(expires_at);

auth_magic_tokens

Pending magic-link tokens (consumed and deleted on use).

CREATE TABLE auth_magic_tokens (
  id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
  email TEXT NOT NULL,
  token_hash TEXT NOT NULL UNIQUE,  -- SHA-256 of the raw token
  expires_at TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_auth_magic_tokens_hash ON auth_magic_tokens(token_hash);
CREATE INDEX idx_auth_magic_tokens_expires ON auth_magic_tokens(expires_at);

auth_rate_limits

Rate limiting counters.

CREATE TABLE auth_rate_limits (
  key TEXT PRIMARY KEY,            -- e.g., 'request:email:user@example.com' or 'verify:ip:1.2.3.4'
  count INTEGER NOT NULL DEFAULT 1,
  window_start TEXT NOT NULL DEFAULT (datetime('now')),
  window_seconds INTEGER NOT NULL
);

Tables (MySQL — for WordPress)

Same schema adapted for MySQL syntax:

  • TEXT primary keys become VARCHAR(64)
  • INTEGER booleans become TINYINT(1)
  • datetime('now') becomes NOW()
  • randomblob becomes UUID() or HEX(RANDOM_BYTES(16))
  • Indexes are the same

Each storage adapter package (auth-storage-d1, auth-storage-mysql) owns its own migration files.

Token lifecycle

Generate raw token (32 bytes, hex-encoded)
  → Hash with SHA-256
  → Store hash in auth_magic_tokens with TTL
  → Send raw token in email URL
  → User clicks link
  → Hash the incoming token
  → Look up hash in auth_magic_tokens
  → If found and not expired: delete row (atomic), create session
  → If not found or expired: reject

The raw token never touches storage. The hash is a one-way function — a database leak doesn't expose usable tokens.

Session lifecycle

completeLogin produces AuthIdentity
  → Upsert auth_identities row
  → Generate session ID (32 bytes, hex-encoded)
  → Insert auth_sessions row with TTL
  → Set session cookie (HttpOnly, Secure, SameSite=Lax)
  → On each request: read cookie → load session → check expiry → return identity
  → On logout: delete session row, clear cookie

Identity mapping during provider migration

When a project migrates from magic-link to Firebase:

  1. User's existing identity: auth_identities(provider='magic-link', email='user@example.com', host_user_id='usr_abc')
  2. User logs in via Firebase: new identity row auth_identities(provider='firebase', provider_user_id='firebase-uid-123', email='user@example.com', host_user_id='usr_abc')
  3. Both rows map to the same host_user_id
  4. resolveOrCreateViewer receives either identity and returns the same Viewer
  5. During dual-session window, old magic-link sessions still resolve via the old identity row
  6. After migration window, magic-link identity rows can be archived or retained as history

Subscriber auth data

For the subscribe widget, unknown emails go to the host app's mailing list, not to auth tables. The onNewSubscriber hook is responsible for storage — the SDK doesn't prescribe where subscriber data lives.

For the token minting path (known users), the flow uses the same auth_magic_tokens table as regular magic-link auth. The only difference is the email template and the post-confirmation redirect.