Portal Setup
This guide covers setting up an SM product portal — the app at yourproduct.sprintmode.ai or a custom domain.
Portal Types
| Type | Example | Auth |
|---|---|---|
| Client portal | studios.sprintmode.ai | SM session cookie |
| Investor portal | investors.sprintmode.ai | SM session cookie + investor_profile_id |
| Product portal | mode.sprintmode.ai | SM 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.jsonEnvironment Variables
Set these on the CF Pages project:
| Variable | Description |
|---|---|
SESSION_SECRET | JWT signing secret — same value across all SM portals |
SM_API_CLIENT_ID | CF Access service token ID (for server-to-server SM API calls) |
SM_API_CLIENT_SECRET | CF 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
| Setting | Value |
|---|---|
| Build command | npm run build |
| Output directory | dist |
| Node version | 20 |
SPA Routing
For React Router to work on CF Pages, add public/_redirects:
/* /index.html 200This 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/loginrenders 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)
