Skip to content

Portal Setup

This guide covers setting up an SM product portal — the app at yourproduct.sprintmode.ai or a custom domain.

Portal Types

TypeExampleAuth
Client portalstudios.sprintmode.aiSM session cookie
Investor portalinvestors.sprintmode.aiSM session cookie + investor_profile_id
Product portalmode.sprintmode.aiSM session cookie

All portals use the same sm_client cookie on .sprintmode.ai. A user authenticated on any portal is authenticated on all others.

Required Files

Every portal needs:

your-portal/
├── src/
│   ├── main.jsx          ← App entry point
│   ├── app.css           ← Product accent override
│   └── pages/            ← Page components
├── public/
│   └── assets/
│       ├── logo-horizontal.png       ← light logo
│       └── logo-horizontal-dark.png  ← dark logo
├── index.html
├── vite.config.js
├── _worker.js            ← CF Worker auth gate
└── package.json

Environment Variables

Set these on the CF Pages project:

VariableDescription
SESSION_SECRETJWT signing secret — same value across all SM portals
SM_API_CLIENT_IDCF Access service token ID (for server-to-server SM API calls)
SM_API_CLIENT_SECRETCF Access service token secret

SESSION_SECRET must be the same on all portals — this is what makes the .sprintmode.ai cookie readable everywhere.

CF Pages Build Config

SettingValue
Build commandnpm run build
Output directorydist
Node version20

SPA Routing

For React Router to work on CF Pages, add public/_redirects:

/*    /index.html    200

This tells CF Pages to serve index.html for all routes (letting React Router handle navigation).

Calling SM API from a Portal

The portal worker makes server-to-server calls with the CF Access service token:

js
// In _worker.js
async function callSMApi(path, env, options = {}) {
  const res = await fetch('https://api.sprintmode.ai' + path, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'CF-Access-Client-Id': env.SM_API_CLIENT_ID,
      'CF-Access-Client-Secret': env.SM_API_CLIENT_SECRET,
      ...(options.headers || {})
    }
  })
  return res.json()
}

From the browser (React), use the api() helper which sends cookies:

js
import { api } from '@nomadahq/sm-ui'

// Calls api.sprintmode.ai with credentials: 'include'
const { data } = await api('/api/companies', { product: 'studios' })

Verifying Setup

After deploy, run this checklist:

  • [ ] yourportal.domain.com/auth/login renders Login component
  • [ ] Google SSO completes without redirect loop
  • [ ] After login, useSession() returns { id, email, products: [...] }
  • [ ] A test SM API call returns data (not 401 or 403)
  • [ ] Dark mode logo shows correctly (open DevTools → Rendering → Emulate dark)
  • [ ] Mobile viewport looks correct (375px wide)

Sprint Mode LLC — Internal Platform Documentation