Security
Security rules for frontend development. The backend remains the security boundary; the frontend limits the exploitation surface.
Tokens (JWT)
Section titled “Tokens (JWT)”- Persistent storage (
localStorage, validated product decision — see Authentication): an injected script could read the tokens; strict rotation and deduplicated refresh limit exploitation. - Never logged: no
console.*, no error transport, no UI field. - No token in URLs,
.env, or headers other than the bearer.
Network access
Section titled “Network access”- Everything goes through the
core/api/http.tswrapper: no rawfetch()(uniformity of bearer, refresh, error envelope, timeout). - Base URL configured by
VITE_API_URL(relative/api/v1default; in dev the Vite proxy relays to the dev backend). Nothing else is prefixed by the dev backend in production without validation.
Permissions
Section titled “Permissions”- The frontend model (
core/permissions) mirrors the real backend — never invent a prefix/verb. - UI guards (
requirePermissions,useCan) are a convenience, not protection: the backend allows or denies on every endpoint.
Injection & content
Section titled “Injection & content”- React escapes text by default. Never use
dangerouslySetInnerHTMLon unapproved content (backend messages included). - Never pass user values into request URLs without encoding — the wrapper uses fixed relative paths (no path interpolation from the user).
Dependencies & secrets
Section titled “Dependencies & secrets”- No secret in the client bundle (anything bundled is public).
- New dependencies justified and pinned by the lockfile.
Devtools / prod
Section titled “Devtools / prod”- Devtools (Router/Query) are only mounted under
import.meta.env.DEV(also stripped by the@tanstack/devtools-viteplugin at build).
Don’ts
Section titled “Don’ts”- ❌ Send tokens to the error server / monitoring.
- ❌
dangerouslySetInnerHTMLon backend or user content. - ❌ Reduce storage without a validated product decision.
- ❌ Assume hiding a button removes the backend permission.
