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.
requestUploadPathcreates jobs; clients cannot create a job directly.- Owners can read their jobs and edit only presentation fields.
deleteSessionperforms owner-checked deletion of both the document and Storage artifacts.- Video objects are owner-scoped, below 2 GB, and must have
video/mp4content 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โ
- Update both server authorization and direct-access Rules where applicable.
- Add positive and negative tests, including unverified-email and cross-owner cases.
- Run Firestore/Storage emulator tests.
- Check private-site CSP and session-cookie tests when changing docs or launchpad delivery.
- 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.