Skip to content

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

js
// _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:

js
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' }
  })
}

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:

js
// 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:

js
// 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:

js
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.

Sprint Mode LLC — Internal Platform Documentation