Runtime contract
Everything your container must do to run on the platform. This page is the difference between a one-click launch and a day of debugging — each rule below maps to a real mistake that has cost a team hours. Follow it exactly.
1. Listen on 0.0.0.0:$PORT
The platform injects PORT and probes your container on it. Bind 0.0.0.0 (all interfaces) on $PORT — never 127.0.0.1/localhost and never a hardcoded port. Serve in every mode (don't skip the server in a "stub" path).
2. Health & readiness
| Endpoint | Returns | Meaning |
|---|---|---|
GET /health | 200 as soon as the process is up | Liveness. Don't gate this on the DB — if it 503s while warming, the pod is killed. |
GET /ready | 200 only when DB + deps are reachable; 503 while warming | Readiness. 503 during startup is correct, not a failure — traffic is held until you're ready. |
3. Image names (exact)
Push to these repositories — hyphenated, never nested with a slash:
registry.sage7.ai/<slug>-api:latest # always — your backend / SSO + API registry.sage7.ai/<slug>-frontend:latest # only if you run a separate web tier
<slug>/api (a nested path) instead of <slug>-api is the #1 cause of ImagePullBackOff: the platform pulls <slug>-api and never finds your image.4. Topology — one image or two
The platform supports both. Pick one and tell the platform team:
| Topology | What you ship | Routing |
|---|---|---|
| Single (default) | one <slug>-api image that serves the SSO contract, /api, and your UI | all of <slug>.bizbasics.ai → your image |
| Two-tier | <slug>-api (contract + /api) and <slug>-frontend (your UI on port 3000) | /api, /auth/*, /health, /ready, /bootstrap → api; everything else → frontend |
Serve all backend paths under /api — in both topologies
It's a single host split by path, never a separate API origin (this is the Vercel/Netlify-style split). The only bare paths are the four SSO-contract endpoints — /auth/sso, /health, /ready, /bootstrap. Everything else your backend serves must live under /api/…:
- your app's business API —
/api/envelopes, not/envelopes - any public / customer-facing API —
/api/v1/…, not/v1/… - your inbound webhook receiver — register it at
/api/webhooks/…, not/webhooks/…
/ routes to your frontend. A business endpoint at a bare path (/envelopes) therefore hits the frontend and 404s — not your API. In single-image mode the API receives every path, so bare paths happen to work there — which hides the bug until you switch topology. Keep business under /apiin both and the same image is topology-agnostic: you can flip single ↔ two-tier without re-routing anything.API-only product (no UI — like a signing API your customers call)? Ship a single image; there's no frontend to serve. Keep your endpoints under /api anyway — it costs nothing and keeps the door open to a UI later.
5. Detect platform mode from the injected env
Run in platform mode when the platform variables are present — key off BIZBASICS_JWKS_URL. Do not require your own custom flag (e.g. MYAPP_MODE=platform): the platform doesn't set it, so you'll silently fall into standalone mode and never enable SSO.
6. Environment the platform injects
Pre-filled into your <slug>-secrets:
DATABASE_URL, REDIS_URL, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET # scoped to you PORT = 8088 BIZBASICS_API_URL = https://api.bizbasics.ai BIZBASICS_AUTH_URL = https://auth.bizbasics.ai BIZBASICS_JWKS_URL = https://auth.bizbasics.ai/.well-known/jwks.json BIZBASICS_PRODUCT_ID = <slug> APP_URL = https://<slug>.bizbasics.ai # YOUR public base URL BIZBASICS_APP_URL = https://<slug>.bizbasics.ai
You add three of your own (see Onboarding): your bbas_ as BIZBASICS_INTERNAL_KEY, your bbk_ as BIZBASICS_API_KEY, and an APP_SESSION_SECRET (≥32 random bytes) to sign your own session.
7. Sessions are httpOnly cookies — never localStorage
After the SSO handoff, set your own session as an httpOnly cookie and redirect into your app. Your frontend must establish auth by calling your API (e.g. GET /api/me or /bootstrap) with credentials: "include" — the browser sends the cookie, your API validates it. Never read or store the session token in localStorage.
localStorage for a token, finds nothing (JS can't read httpOnly cookies), and bounces back to login — forever. Derive auth from the cookie via an API call, not from JS-readable storage. This is also a hard platform security rule.8. Build redirects & links from APP_URL
Your post-SSO landing, email links, and absolute URLs must come from APP_URL (injected). Never hardcode localhost — a left-over http://localhost:3000 default will redirect real users to their own machine after a successful login.
9. Credentials — three different things
| Credential | Direction | Use |
|---|---|---|
bbas_ → BIZBASICS_INTERNAL_KEY | you → platform (internal) | X-Internal-Key on verify-app-token, quota, webhooks. Scoped to your product. |
bbk_ → BIZBASICS_API_KEY | you → platform (data) | Authorization: Bearer on the workspace-records / quota API. |
| Your own API keys | your customers → you | Keys you mint for your customers. The platform is not involved — never hand out bbk_/bbas_. |
bbas_/bbk_, the old one stops working immediately — you must update your secret with the exact new value (shown once). A stale bbas_ is the usual cause of an "invalid platform token" at SSO.10. Surface the real error
When a platform call fails, pass through the upstream status and detail — don't collapse everything into a single generic message. A 403 "Invalid internal credential" tells you the credential is wrong; a 401 "Invalid or expired app token" tells you the launcher token expired. Hiding that turns a one-line fix into a long dig.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Pod ImagePullBackOff | Wrong image repo name (nested slug/api) | Push to <slug>-api:latest (hyphen). §3 |
Pod CrashLoopBackOff, probes "connection refused" | Not listening on 0.0.0.0:$PORT | Bind 0.0.0.0 on $PORT, serve /health+/ready. §1–2 |
/ready stays 503 | Can't reach DB/deps, or readiness never flips | Use the injected DATABASE_URL/REDIS_URL; flip /ready to 200 once connected. §2 |
SSO succeeds then redirects to localhost | Hardcoded localhost redirect | Build the redirect from APP_URL. §8 |
| SSO succeeds then loops back to the platform login | Frontend reads token from localStorage, not the httpOnly cookie | Validate the session via an API call with credentials. §7 |
invalid or missing platform token | Stale/mismatched bbas_, or launcher token expired | Re-issue the credential + update your secret; surface the upstream error. §9–10 |
Business API 404s in two-tier (but works in single) | Endpoints at bare paths route to the frontend, not your API | Move business endpoints under /api/…; keep only the SSO contract bare. §4 |
/ returns API JSON, not your UI | Single-image but the image only serves the API | Serve your UI from the same image, or switch to two-tier. §4 |
| App card is greyed / "Upgrade to unlock" | Your org isn't entitled to the product | External apps need an entitlement (the platform team grants it). Not a code issue. |