Host App Integration
Overview
Integrating jampress auth into your Astro/Cloudflare Workers app involves four steps:
- Install packages
- Configure the auth instance
- Wire middleware
- Implement the
HostHookscallback
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):
- Install the SDK packages
- Create the auth config pointing at your existing D1
- Run the auth migration to add
auth_identitiesandauth_sessionstables - Migrate existing user records into
auth_identities - Replace your middleware's
resolveViewer()with the SDK's middleware - Replace your route handlers with the SDK's route handlers
- Update your
get-startedpage to use the SDK's request endpoint - Test the full flow: request → email → verify → session → dashboard
- Remove old auth code
The viewer contract stays the same — your app pages shouldn't need any changes.