Skip to main content

Security Model

AceSense uses Firebase Authentication for identity, Security Rules for direct client access, callable guards for privileged mutations, and private server-gated delivery for docs and launchpad content.

Trust boundariesโ€‹

Client input is untrusted even after sign-in. The Admin SDK bypasses Security Rules, so every callable must perform its own auth, ownership, and argument checks.

User and job accessโ€‹

  • Owners can read their user document and edit only profile, preference, onboarding, consent, and related UX fields allowed by Firestore Rules.
  • Entitlement, trial, usage, and account-state fields are server-owned.
  • requestUploadPath creates jobs; clients cannot create a job directly.
  • Owners can read their jobs and edit only presentation fields.
  • deleteSession performs owner-checked deletion of both the document and Storage artifacts.
  • Video objects are owner-scoped, below 2 GB, and must have video/mp4 content type under current Storage Rules. Result writes are server/admin-only.

Admin authorizationโ€‹

The preferred authorization signal is the server-issued admin: true custom claim. During migration, a verified ID-token email in the canonical four-email allowlist is accepted as a fallback. The allowlist is mirrored in Firestore Rules, Storage Rules, the callable guard, docs, and launchpad; changes must stay synchronized until the fallback is removed.

An email string in a user document is never an admin signal. The fallback also requires email_verified === true.

Admin callables call requireAdmin and append audit records. Firestore/Storage rules independently protect direct data access. See Admin Allowlist.

Private docs and launchpadโ€‹

Firebase Hosting rewrites every path to docsServer or launchpadServer. Built site assets live in the function package, not the public Hosting directory. After Firebase ID-token verification, the server issues a five-day __session cookie with HttpOnly, Secure, SameSite=Lax, and revocation checking on reads.

Each response receives a random CSP nonce. HTML <script> and <style> tags, including the inline Firebase sign-in bootstrap, are rewritten with that nonce. The policy denies objects, framing, base-tag changes, and inline script attributes; it allowlists only the sources needed for Firebase auth, fonts, PostHog, and same-origin assets. Other headers include HSTS, nosniff, frame denial, a strict referrer policy, a restricted Permissions Policy, and noindex.

Athlete app headers (app.acesense.io)โ€‹

The Flutter web app is static hosting, so it gets no per-response nonce. Its Content-Security-Policy lives in acesense-frontend/firebase.json and has been enforcing since 2026-08-16; before that it was report-only and had never been validated against a real page load.

script-src includes 'unsafe-inline'. This is deliberate. The Firebase web SDK injects an inline bootstrap <script> per module at runtime, so their hashes change on every SDK bump โ€” pinning them would break the app on a dependency update, in production only, with no local signal. Unlike the docs server there is no request path to attach a nonce to. The directives that still carry weight alongside it are connect-src (exfiltration), frame-ancestors 'none' (clickjacking), object-src 'none', base-uri and form-action.

The allowlist is not guesswork โ€” it is every origin the app was observed to request: gstatic (CanvasKit + the Firebase SDK modules), google.com and gstatic (reCAPTCHA for App Check), accounts.google.com (Google Sign-In), fonts.gstatic.com, Cloudflare Insights, the europe-west1 callables host, the Storage bucket, and โ€” for Firebase Auth โ€” acesense-prod.firebaseapp.com plus apis.google.com (the gapi loader, needed in script-src, connect-src and frame-src because it loads a script, XHRs and opens its own iframes).

Loading a script is not the same test as fetch. The first enforcing deploy broke Google sign-in: Firebase Auth's popup loads the gapi client from https://apis.google.com/js/api.js, that origin was missing from script-src, and every attempt failed with Authentication error: internal-error. The probe that had cleared the policy walked connect-src endpoints with fetch(), which never inserts a <script> element โ€” script-src-elem falls back to script-src and only fires on a real tag. Test script origins by inserting a tag and asserting the global it defines appears; test frames by framing.

When changing this policy, verify with a negative control. A blocked fetch and a CORS rejection both surface as TypeError: Failed to fetch, so "no violations" is indistinguishable from "the listener never fired". Probe each endpoint from the live origin with a securitypolicyviolation listener and a known-disallowed host; the control must be reported blocked for the result to mean anything. That method caught connect-src omitting the Firebase authDomain, which silently broke social sign-in โ€” a defect no page load would have shown, because it only fires when a user clicks Continue with Google.

Also note a warm browser tab replays the cached document with its old headers. Verify in a fresh tab on a cache-busted URL.

The last live verification (2026-08-16) found report-only policies on landing and admin. Their source configs were promoted to enforcement on 2026-08-21 after fresh builds, all-built-HTML inspection, Firebase Hosting emulator checks, and local browser CSP probes. This is not a deployment claim: re-run the negative-control probe above on the custom and web.app origins after the next deploy before recording live enforcement.

App Checkโ€‹

Callable wrappers are App Check-aware, but enforcement is intentionally controlled by ENFORCE_APP_CHECK=true. An unset or different value means tokens are not enforced. Follow Enable Firebase App Check and verify legitimate-client metrics before enabling it.

Upload and outbound-request controlsโ€‹

  • Metadata, extension, size, duration, magic bytes, and media probe results are checked at server boundaries.
  • AI-processing consent and entitlement are checked before a job is created.
  • Daily/monthly/concurrent quotas limit cost-amplification abuse.
  • Provider handoff uses short-lived signed URLs and records URL issuance.
  • Public REST/MCP URL ingestion accepts public HTTPS URLs only and applies SSRF validation and response-size/content checks.
  • API keys are stored as non-recoverable hashes; reads and mutations are owner/admin scoped through functions and rules.

Secretsโ€‹

Use Firebase/Google Secret Manager bindings or deployment environment settings declared by the owning service. Do not commit .env, service-account JSON, API keys, signed URLs, session cookies, or ID tokens. Rotate a suspected secret first, then investigate from audit logs.

Security-change checklistโ€‹

  1. Update both server authorization and direct-access Rules where applicable.
  2. Add positive and negative tests, including unverified-email and cross-owner cases.
  3. Run Firestore/Storage emulator tests.
  4. Check private-site CSP and session-cookie tests when changing docs or launchpad delivery.
  5. Deploy the smallest affected surface and verify with a non-admin account.

GPU implementation security is outside this audit; only the signed handoff and control-plane boundary are described here.