Turn login into paid access in minutes.

You’ll have a working, gated login in minutes. No billing until you see it work.

Magic link + Stripe + a simple gate in your app.

Observe Mode preset: verify the gate first, then activate enforcement when you’re ready.

Setup status

[ ] Magic link verified
[ ] Stripe connected
[ ]
[ ]

Step 1 — Create tenant / get keys

Minimum required to run the callback and place your first gate.

Optional / Advanced

Only needed if you want admin tools, cookie overrides, or strict redirect allowlists.

ADMIN_API_KEY is optional; gate placement and testing work without it.

Setup & Verification (Observe Mode — no enforcement)

Step 2 — Install the Tenant Callback Adapter (required)

JamAuth redirects back to your app after magic link login. The callback endpoint exchanges the grant and sets a first-party session cookie. Without the callback adapter, login cannot complete.

Choose your callback adapter:

What you configure:
  • Callback URL you will set in JamAuth tenant settings
  • Required env vars: JAMAUTH_CLIENT_ID, JAMAUTH_RESOLVE_URL (and JAMAUTH_CLIENT_SECRET for secret callback)
netlify-secret.js

Setup Instructions

Required Dependencies

What does this callback do?

Where to place this file: netlify/functions/jamauth-callback.js

Step 3 – Add your first gate / access check

Pick the fastest win. Page/Route is preselected and ready to go.

Advanced / Why it works →

Infra-first details and deeper patterns.

Jump to Extended Patterns

Step 3 — Add the gate (copy/paste) Gate: not placed

Put this at the top of the protected page/route. It asks JamAuth “allowed?” and blocks if not.

Gate snippet (server-side)
// Put this at the top of a protected page/route
async function jamauthGate(req, res, next) {
  const token = req.cookies[process.env.COOKIE_NAME];
  if (!token) return res.redirect('/login');

  // Ask JamAuth if access is allowed
  // Replace JAMAUTH_VERIFY_URL with your verify endpoint
  const verified = await fetch(process.env.JAMAUTH_VERIFY_URL, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  if (!verified.ok) return res.status(403).send('Access denied');
  req.user = await verified.json();
  next();
}

app.get('/protected', jamauthGate, (req, res) => {
  res.render('protected', { user: req.user });
});
Examples (Next.js / Express)
// middleware.ts
import { NextResponse } from 'next/server';

export async function middleware(req) {
  const token = req.cookies.get(process.env.COOKIE_NAME);
  if (!token) return NextResponse.redirect(new URL('/login', req.url));
  // TODO: verify token with JamAuth
  return NextResponse.next();
}
// app.js
app.get('/pro', jamauthGate, (req, res) => {
  res.render('pro', { user: req.user });
});

Step 3 — Add the gate (API endpoint)

API guards use bearer tokens or cookie sessions. For now, use the page/route gate or jump to advanced examples.

Step 3 — Add the gate (Feature/UI element)

Feature gates are coming next. Start with a Page/Route gate or use the widget in Advanced.

Demo Complete

You logged in using a real magic link.

Authentication is live

Your app now issues JamAuth sessions.

Step 4 – Test it (first dopamine hit) 🎯

In Observe Mode, JamAuth evaluates access and returns decisions, but does not block users yet.

Quick checklist:

Configure your tenant, validate the setup, and test your magic link flow. This is the complete onboarding loop: Save → Validate → Test.

From Step 1 credentials
Only needed for admin actions (saving tenant config / health checks). You can place a gate and test without it.
Optional: Admin Tools (save config / health checks)
Save Configuration

Update your tenant settings before testing. Configure app type, domain, and redirect URIs.

Determines validation requirements
Required for web/hybrid apps
Custom schemes for mobile, https:// for web
Validate Configuration

Check your tenant health and catch misconfigurations before testing.

Tenant active
Domain configured
Redirect URIs
PKCE enabled (v4)
Stripe connected (optional)
Configuration valid
3️⃣ Test Login Flow

Generate a test magic link to verify end-to-end authentication.

Email to receive magic link
What happens when I click the magic link?
  1. Token verification: complete-login validates the magic token
  2. Grant code creation: Creates temporary grant code in Redis (5min TTL)
  3. Callback redirect: Redirects to your tenant callback URL with grant code
  4. Token exchange: Your callback calls resolve-grant (server-to-server)
  5. Session established: Your callback sets first-party cookie on your domain
  6. User redirect: User lands on API dashboard

After clicking the link, check your browser DevTools → Application → Cookies to confirm the session cookie was set.

Try a live sample instead

Live Demo Deployment

Deploy a demo, receive a real magic link, and verify the flow.

🪄 Live Demo Deployment

You'll receive a real magic link and log in to a live app.

Deploy to Netlify →

This deploys a starter with 3 tiny serverless functions:
• Config endpoint (public)
• Callback (PKCE, sets secure cookie)
• Session validator (server-side check)

After deploy: Set 2 environment variables (JAMAUTH_HOST + JAMAUTH_CLIENT_ID) and visit /protected.html

Next: Add this to your actual site (5 minutes)

After testing the demo, copy the gate snippet into any HTML page on your site. The snippet fetches config from your Netlify functions and injects the JamAuth widget.

What to copy:

  • Keep the 3 functions in netlify/functions/
  • Paste the gate snippet from the starter README into your pages
  • Deploy and you're done

Full instructions: Starter README

Test multi-user / multi-tenant flows

Try emails from multiple teams and environments. Inspect req.user on your callback to confirm tenant_id, scope, and other claims are scoped correctly.

Step 5 – Activate Enforcement (Billing)
Stripe signal: not connected Enforcement is optional. Your app won’t block anything until you choose to enable it.

JamAuth never stores Stripe keys in your browser. Webhook secret is used server-side to verify Stripe events. This page does not persist secrets.

Paste your Stripe webhook signing secret (whsec_…). JamAuth generates a unique webhook URL for your tenant. Add it in Stripe → Developers → Webhooks.

Found in Stripe Dashboard → Developers → Webhooks → (select endpoint) → Signing secret.

Advanced: Optional Stripe API key (for reconciliation later)

Optional. Stored encrypted. Not required for v1. Used later for reconciliation.

Before activating billing, make sure you’ve seen:
  • ✓ Magic-link login works
  • ✓ Your app receives an access decision
  • ✓ A gated page or route responds correctly
Ready

Billing starts when enforcement is enabled.

Keep testing in Observe Mode (no billing yet)

ℹ️ If you regenerate, update the endpoint in Stripe.

Advanced: How it works

Session adapters, env vars, and deeper infra paths are tucked here.

SSR / API tip

Use middleware to guard server routes, e.g. app.use('/api/*', jamauthSession), and pass req.user through to your handlers.

Multi-app / microservices tip

Verify session JWTs locally in each service using your JamAuth public key. Share tenant secrets via Vault/SSM, and avoid cross-service calls for auth.

🔑 Before you continue

This onboarding assumes you've already received your client_id and client_secret from the one-time setup link.

Your client_secret is shown only once during setup. Store it securely. This page doesn't need your secret.

Extended Patterns (Optional)

Power-user setup paths, adapters, and advanced integrations.

Other Setups

🪄 Quick win: Protect your first page (3 minutes)

If you're starting with a static site, you can add JamAuth in minutes.

  1. Add the JamAuth script to your page
  2. Enter your email to test
  3. Your page is now gated with passwordless login

For the quick win demo, JamAuth hosts the callback. For production, install the tenant callback adapter above.

Advanced Setup (when you're ready)

✅ 5-Minute Success Path

  1. Paste your callback route (Step 2)
  2. Paste a route guard (Extended → Protect server routes)
  3. Test login (Step 4)
  4. (Optional) Stripe gating (Stripe activation)

Do those core steps and you're done. Everything else is optional.

App Type (optional)

JamAuth supports web apps (cookie sessions) and mobile apps (PKCE + bearer tokens). Select your app type to see the right integration code.

Choose your protection method

Same login + entitlement rules. Only the transport changes (cookie vs bearer).

Web (cookie session)

Callback sets an HttpOnly cookie session. Protect pages by requiring the cookie.

Go to cookie guard snippet →
Mobile/API (token mode)

Use token mode (bearer tokens). Clients send Authorization: Bearer <token>. Protect APIs by verifying the token server-to-server.

Go to token guard snippet →
Paste a route guard (choose one)

Optional – Domain verification

Recommended when you want a clean auth.yourapp.com domain. You can skip this for now and come back later.

This step doesn't affect the login flow; it only improves DX and branding.

Create a CNAME: auth → cname.jamauth.com then click "Check Verification".
Awaiting check…
When to use CNAME vs TXT?

CNAME is best for most setups and required if we'll host UI/assets on your subdomain. Use TXT only if your DNS flattens CNAMEs or a proxy hides it.


Advanced (optional)

A) Protect APIs / server-rendered pages (SSR)

Bearer token guard (mobile/API)

For mobile apps and APIs: extract the bearer token from Authorization header, verify it server-to-server with JamAuth.

// Minimal bearer token middleware example: function jamauthBearer(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing token' }); } const token = auth.substring(7); // Verify with JamAuth: // const verified = await fetch(JAMAUTH_VERIFY_URL, { // method: 'POST', // headers: { 'Authorization': `Bearer ${token}` } // }); // req.user = verified.user; req.user = { /* decoded JWT payload */ }; next(); } app.get("/api/protected", jamauthBearer, (req, res) => { // req.user is verified and trusted res.json({ data: 'protected', user: req.user }); });

B) Custom entitlements providers

JamAuth separates authentication (who the user is) from validation (whether they're allowed right now). Stripe is the recommended default (configured in Step 5). Use these alternatives for custom access control logic.

Stripe subscription check RECOMMENDED
Gate access based on Stripe subscriptions. Configure webhook URL in Step 5 above.
Custom webhook endpoint ADVANCED
Delegate the access decision to your service. JamAuth applies the result.
Allow all authenticated users NO ENTITLEMENT CHECK
No billing or entitlement checks performed.
Handle validation in my middleware ADVANCED / ESCAPE HATCH
JamAuth provides identity only; you apply access logic yourself.

C) Session behavior

Control how long sessions last and whether they can be refreshed without forcing users to click a new magic link.

How this maps to your backend

JamAuth exposes these as environment variables (or config) that your backend can use when issuing and verifying session tokens, e.g.:

SESSION_LIFETIME_HOURS=24 SESSION_REFRESH_ENABLED=true SESSION_REFRESH_WINDOW_DAYS=7

D) Enterprise

Webhook endpoint (optional)

Use webhooks to invalidate sessions or adjust access when subscriptions or roles change.



Idle…
How to handle this on your side
// Example webhook handler (Node/Express) app.post("/webhooks/jamauth", (req, res) => { const event = verifyJamAuthWebhook(req, process.env.JAMAUTH_WEBHOOK_SECRET); if (event.type === "subscription.updated" || event.type === "subscription.cancelled") { revokeSessionsForTenant(event.tenant_id); } res.sendStatus(200); });

Webhooks, scopes & fleets

For larger teams, wire JamAuth into your existing security and observability stack: subscriptions, scopes, audit logs, and fleets of services.

// Invalidate sessions when a subscription changes app.post("/webhooks/jamauth", (req, res) => { const event = verifyJamAuthWebhook(req); if (event.type === "tenant.subscription_updated") { revokeSessionsForTenant(event.tenant_id); } res.sendStatus(200); }); // Example scope checks app.get("/admin", jamauthSession, requireScope("admin"), handler);

Multi-tenant setup

Use JamAuth as a shared auth layer across many tenants, apps, or teams.

  • • Each tenant gets its own client_id/client_secret pair.
  • • Sessions are automatically scoped to tenant_id in req.user.
  • • Optionally use separate Redis namespaces or DB schemas per tenant.
Manage tenant creation and rotation from the JamAuth dashboard or your CI/CD pipeline.

All set ✅

You now have login + protected access with Stripe as the default signal.

This flow is one JamAuth pattern. You can swap Stripe for any signal, or enforce access server-to-server, without re-authing users. Explore infra-first options →

Open JamAuth dashboard See full integration examples View public keys / JWKS