TL;DR
A developer can't get a credential that matches production, so local success doesn't predict published success.
The two credentials
-
A personal API token — the only kind an individual can mint. Full access to all of /v2.
-
A static app viewer token — what a published app actually runs on. Minted only server-side, handed to the app for a signed-in viewer, with a short (~15 minute) life. It allows exactly three operations:
GET /v2/notebooks/:id — block content stripped, last-run pointers nulled
POST /v2/runs — detached runs only
GET /v2/runs/:runId — the viewer's own run only, raw snapshot withheld
Everything else → 403 This endpoint is not available to static app tokens.
Why that's a DX problem
- No way to test the real surface. You cannot obtain a restricted token, so every local run necessarily has more authority than production. Publishing is the only functional test.
- The gap is silent. Off-allowlist calls 403, and well-written clients swallow that into an empty state — so a feature disappears rather than errors. Nothing points at permissions as the cause.
- Same URL, different payload. Even the allowed routes return less data in production (stripped content, nulled run pointers, no raw snapshot). A client can depend on a field that is always null once published — a 200 either way, so no error surfaces at all.
- Devs hardcode the policy. The workaround is guessing in client code —
if (isEmbedded) return in examples/local-runner/cloud-app/index.html, where isEmbedded is merely window !== window.parent. That's an assumption about server behavior baked into a client, free to drift silently.
The concrete instance
The cloud-app run-history panel worked all through development (older ?token= path, personal token) and vanished when published, because enumerating a notebook's runs is off the allowlist. No error, just a missing panel.
Two candidate fixes
- Emulate the restrictions in the CLI — duplicates the policy in a second place, where it drifts, and would almost certainly miss the field-level redactions.
- Issue a real restricted token to developers (the preferred direction) — local requests then traverse the actual production guard, so parity holds by construction, including the redactions. The server already performs the equivalent authorization check when it mints one of these tokens today, and such a token is less privileged than what the developer already holds, so it isn't an escalation. Main open question is lifetime, since ~15 minutes is painful for a dev loop.
Also worth noting
There's no capability discovery — even with a restricted token, a client learns its limits only by collecting 403s.
TL;DR
A developer can't get a credential that matches production, so local success doesn't predict published success.
The two credentials
A personal API token — the only kind an individual can mint. Full access to all of
/v2.A static app viewer token — what a published app actually runs on. Minted only server-side, handed to the app for a signed-in viewer, with a short (~15 minute) life. It allows exactly three operations:
GET /v2/notebooks/:id— block content stripped, last-run pointers nulledPOST /v2/runs— detached runs onlyGET /v2/runs/:runId— the viewer's own run only, raw snapshot withheldEverything else →
403 This endpoint is not available to static app tokens.Why that's a DX problem
if (isEmbedded) returninexamples/local-runner/cloud-app/index.html, whereisEmbeddedis merelywindow !== window.parent. That's an assumption about server behavior baked into a client, free to drift silently.The concrete instance
The
cloud-apprun-history panel worked all through development (older?token=path, personal token) and vanished when published, because enumerating a notebook's runs is off the allowlist. No error, just a missing panel.Two candidate fixes
Also worth noting
There's no capability discovery — even with a restricted token, a client learns its limits only by collecting 403s.