# Security model of a vibe app What credentials exist, where each one lives, why, and what that means for you. Written for the reader of [iblai/vibe#167](https://github.com/iblai/vibe/issues/167) ("Auth tokens and tenant state stored in plain localStorage") and for anyone doing a security review of an app built from vibe-starter. ## Two kinds of credential | | Session tokens | Platform API Token | |---|---|---| | What | `axd_token`, `dm_token`, `edx_jwt_token` (+ `userData`, `tenants`, `current_tenant`) | `IBLAI_API_KEY` (`TOKEN` in `iblai.env`) | | Whose authority | the signed-in **user** — exactly their permissions in exactly their org | the **organization** — admin-level, org-scoped | | Where it lives | the browser's `localStorage` for the app's origin | the server's environment only (`.env.local`, the hosting platform's env); `config.apiKey()` returns `""` in the browser by construction | | Sent as | `Authorization: Token ` by the SDK; `Bearer` on the AXD host | `Authorization: Api-Token `; `Bearer` only on the OpenAI-compatible `/v1` | | Lifetime | short; `*_expires` companions; refreshed by the SSO flow | until deleted in the OS (Integrations → APIs) | | Threat it is exposed to | XSS on your origin (anything that runs JS on your page can read it) | server compromise, leaked env, a commit of `.env.local` | ## Why session tokens are in localStorage The platform's sign-in is a hosted SSO flow: `login.iblai.app` authenticates the user and returns them to your app at `/sso-login-complete` with the token set in the URL, which `SsoLogin` stores. Every ibl.ai front end (the OS, the LMS, vibe apps, the embeddable chat) shares this design: - **Cross-app sync.** `AuthProvider`'s `enableStorageSync` lets several ibl.ai front ends on related origins observe one sign-in/sign-out. - **No app server needed.** vibe apps can be fully static or run in a Tauri WebView with no session backend; the SDK's RTK Query layer attaches the token itself. - **Multi-org routing.** `tenants` (the orgs the user belongs to, with `is_admin`) is what `TenantProvider` uses to decide the user may enter the org the app is pinned to — before any API call. An httpOnly-cookie session would need a first-party backend on every origin, which is exactly what this design avoids. The trade-off is that **XSS is the threat model**, so the template invests in the mitigation for XSS: ## Content Security Policy `middleware.ts` calls the SDK's `applyCsp`, which sets a per-request, nonce-based CSP with `strict-dynamic`: - **Production builds enforce** by default (the SDK's own resolution). - **`next dev` runs report-only** so React Refresh / `eval()` and the error overlay work — the template passes `mode: 'report-only'` only when `NODE_ENV === 'development'`, which Next inlines per build command. - **Runtime override:** `CSP_MODE=report-only` (validated by the SDK; unknown values fall safe to report-only). Use it to diagnose a blocked third-party script in production, then remove it. - `app/layout.tsx` sets `dynamic = "force-dynamic"` because a statically prerendered page would ship nonce-less `