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.

Host App Integration

Overview

Integrating jampress auth into your Astro/Cloudflare Workers app involves four steps:

  1. Install packages
  2. Configure the auth instance
  3. Wire middleware
  4. Implement the HostHooks callback

Step 1: Install

# From your host app's directory
pnpm add @jampress/auth-core @jampress/auth-provider-magic-link @jampress/auth-storage-d1 @jampress/auth-astro-workers

Until published to npm, install via git URL:

{
  "@jampress/auth-core": "github:your-org/jampress#main&path=packages/auth-core",
  "@jampress/auth-provider-magic-link": "github:your-org/jampress#main&path=packages/auth-provider-magic-link"
}

Step 2: Configure

Create src/lib/auth.ts in your host app:

import {
  createAuthInstance,
  createZeptomailSender,
} from '@jampress/auth-astro-workers';
import { createMagicLinkProvider } from '@jampress/auth-provider-magic-link';
import { createD1Storage } from '@jampress/auth-storage-d1';
import { env as cloudflareEnv } from 'cloudflare:workers';

export function createAuth() {
  const storage = createD1Storage(cloudflareEnv.AUTH_DB);
  const email = createZeptomailSender({
    secret: cloudflareEnv.ZEPTOMAIL_SECRET,
    fallbackRelayUrl:
      cloudflareEnv.JAMPRESS_MAIL_RELAY_URL || 'https://api.jampress.pw/mail/send',
    domain: 'myapp.com',
    fromAddress: 'no-reply@myapp.com',
    fallbackFromAddress: 'no-reply@jampress.pw',
  });

  const provider = createMagicLinkProvider({
    storage,
    email,
    buildMagicLinkUrl(token, redirectTo) {
      const url = new URL(redirectTo || '/api/auth/verify', 'https://myapp.com');
      url.searchParams.set('token', token);
      return url.toString();
    },
  });

  return createAuthInstance({
    provider,
    storage,
    email,
    session: {
      cookieName: 'myapp_session', // default: 'jp_session'
      ttlSeconds: 60 * 60 * 24 * 7, // default: 7 days
    },
    routes: {
      requestLogin: '/api/auth/request',  // default
      completeLogin: '/api/auth/verify',  // default
      logout: '/api/auth/logout',         // default
      confirm: '/confirm',                // for subscriber auth widget
    },
    csrf: {
      allowedOrigins: ['https://myapp.com', 'http://localhost:4321'],
    },
  });
}

Step 3: Wire middleware

In your src/middleware.ts:

import { createAuth } from './lib/auth';
import { env as cloudflareEnv } from 'cloudflare:workers';

export const onRequest = (context, next) => {
  // Called on every request to resolve the viewer
  return createAuth().middleware({
    resolveOrCreateViewer: async (identity) => {
      // Look up or create the user in YOUR product tables.
      // This is where product-specific logic lives.
      const user = await findOrCreateUser(cloudflareEnv.DB, identity.email);
      const account = await findOrCreateAccount(cloudflareEnv.DB, user.id);

      return {
        user: {
          id: user.id,
          email: user.email,
          displayName: user.displayName,
          role: user.role,
        },
        account: {
          id: account.id,
          accountName: account.name,
          status: account.status,
        },
        auth: {
          provider: identity.provider,
        },
      };
    },
  })(context, next);
};

Astro 6 on Cloudflare Workers no longer exposes Astro.locals.runtime.env. Use import { env } from 'cloudflare:workers' inside server-side modules instead. The framework adapter copies the session cookie into the x-jampress-session-id header before calling the provider, and then fills viewer.auth.sessionId on Astro.locals.viewer.

If ZEPTOMAIL_SECRET is absent, auth email falls back to the shared jampress relay at api.jampress.pw and sends from no-reply@jampress.pw. Register your domain once with POST https://api.jampress.pw/domains and the SDK can use the relay with no per-app relay secret.

Step 4: Use in your app

In any Astro page or API route:

// Pages
const viewer = Astro.locals.viewer;
if (!viewer) {
  return Astro.redirect('/get-started');
}
// viewer.user.email, viewer.account.id, etc.

// API routes
export async function POST({ locals }: APIContext) {
  if (!locals.viewer) {
    return new Response('Unauthorized', { status: 401 });
  }
  // Use locals.viewer.user, locals.viewer.account
}

D1 setup

The auth storage adapter needs its own tables. You have two options:

Option A: Dedicated auth D1 database

Create a separate D1 database for auth tables. Cleanest separation.

# wrangler.toml
[[d1_databases]]
binding = "AUTH_DB"
database_name = "myapp-auth"
database_id = "..."

Option B: Shared D1 database

Use your app's existing D1 database. Auth tables are namespaced with auth_ prefix.

# wrangler.toml
[[d1_databases]]
binding = "DB"  # same binding your app uses
database_name = "myapp"
database_id = "..."
createD1Storage(cloudflareEnv.DB)  // shares the app's D1

Either way, run the auth migrations:

for migration in ../../packages/auth-storage-d1/migrations/*.sql; do
  pnpm exec wrangler d1 execute AUTH_DB --local --file "$migration"
done

Subscriber auth widget

To add the dual-purpose subscribe/auth form:

import { createSubscribeHandler, createConfirmHandler } from '@jampress/auth-widget-subscribe';
import { createAuth } from './lib/auth';

const hooks = {
  resolveOrCreateViewer,
  onNewSubscriber: async (email) => {
    // Add to your mailing list: D1 table, Mailchimp, ConvertKit, webhook, etc.
    await addSubscriber(email);
  },
};

export const POST = (context) => {
  const auth = createAuth();
  return createSubscribeHandler({
    provider: auth.provider,
    storage: auth.storage,
    email: auth.email,
    hooks,
    confirmUrl: `${context.url.origin}/confirm`,
  })(context);
};

export const GET = (context) => {
  const auth = createAuth();
  return createConfirmHandler({
    authInstance: auth,
    hooks,
    authRedirect: '/dashboard',
    subscribeRedirect: '/?subscribed=1',
  })(context);
};

The widget renders a single email input. Add it to your footer layout:

<SubscribeForm action="/api/subscribe" placeholder="Enter your email" buttonText="Subscribe" />

Both known users and new subscribers see the same UI response. The email they receive looks identical — same subject, same template. The confirm link either logs them in or confirms their subscription.

Migrating from existing auth

If your app already has inline auth (like zip.wtf's current setup):

  1. Install the SDK packages
  2. Create the auth config pointing at your existing D1
  3. Run the auth migration to add auth_identities and auth_sessions tables
  4. Migrate existing user records into auth_identities
  5. Replace your middleware's resolveViewer() with the SDK's middleware
  6. Replace your route handlers with the SDK's route handlers
  7. Update your get-started page to use the SDK's request endpoint
  8. Test the full flow: request → email → verify → session → dashboard
  9. Remove old auth code

The viewer contract stays the same — your app pages shouldn't need any changes.