Worker Auth Setup
Every SM portal has a _worker.js (CF Pages Function) that runs before any page is served. It handles auth gating — if the user has no session cookie, they get redirected to login.
Minimal Auth Worker
// _worker.js
import { getSession, verifyJWT } from '@nomadahq/sm-ui/auth'
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url)
// Always public: static assets, auth routes
if (
url.pathname.startsWith('/assets/') ||
url.pathname.startsWith('/auth/') ||
url.pathname === '/favicon.ico'
) {
return env.ASSETS.fetch(request)
}
// Require session for everything else
const token = getSession(request) // reads sm_client cookie
if (!token) {
return Response.redirect(
url.origin + '/auth/login?redirect=' + encodeURIComponent(url.pathname),
302
)
}
const session = await verifyJWT(token, env.SESSION_SECRET)
if (!session) {
// Token invalid or expired — clear cookie and redirect
return new Response(null, {
status: 302,
headers: {
'Location': '/auth/login',
'Set-Cookie': 'sm_client=; Max-Age=0; Path=/; Domain=.sprintmode.ai'
}
})
}
// Auth OK — serve the SPA
return env.ASSETS.fetch(request)
}
}Adding Product Access Check
If your portal is product-specific, verify the user has access:
const session = await verifyJWT(token, env.SESSION_SECRET)
if (!session) return Response.redirect('/auth/login')
// Check product access
if (!session.products?.includes('mode')) {
return new Response('You do not have access to Mode.', {
status: 403,
headers: { 'Content-Type': 'text/plain' }
})
}Setting the Session Cookie
After Google/Microsoft SSO, SM API sets the cookie. But if your portal has its own callback route (e.g. for cross-domain SSO), set it like this:
// Cookie spec — must match across all SM portals
const COOKIE = [
`sm_client=${jwt}`,
`Domain=.sprintmode.ai`, // ← shared across all *.sprintmode.ai
`Path=/`,
`HttpOnly`,
`Secure`,
`SameSite=None`, // ← required for cross-site embeds
`Max-Age=604800` // ← 7 days
].join('; ')
return new Response(null, {
status: 302,
headers: {
'Location': redirectTo,
'Set-Cookie': COOKIE
}
})SameSite=None requires Secure
SameSite=None only works over HTTPS. CF Pages enforces HTTPS automatically, but your local dev server needs to be HTTPS too (or omit SameSite=None in dev).
SESSION_SECRET
The SESSION_SECRET environment variable is used to sign and verify JWTs. It must be identical across all SM portals — this is what allows studios.sprintmode.ai to read a session set by api.sprintmode.ai.
Set it on each CF Pages project's environment variables. Copy the value from an existing portal (sprint-mode-clients → Settings → Environment Variables).
Logout
To log out:
// Clear the cookie
return new Response(null, {
status: 302,
headers: {
'Location': '/auth/login',
'Set-Cookie': 'sm_client=; Max-Age=0; Path=/; Domain=.sprintmode.ai; Secure; SameSite=None'
}
})Or call DELETE /auth/logout on SM API which does the same server-side.
API Key Auth (MCP / Agents)
If your worker needs to call SM API as a service (not on behalf of a user), use an API key:
const res = await fetch('https://api.sprintmode.ai/api/contacts', {
headers: {
'Authorization': 'Bearer ' + env.SM_API_KEY
}
})The SM API key is in project files as SM_API_Key. Set it as SM_API_KEY on the worker's environment.
