Skip to main content

๐Ÿ” Access-Control Audit

Scope. Every public-addressable surface across the AceSense platform โ€” landing site, admin panel, REST/MCP API, docs hub, launchpad, mobile/web client, and Cloud Functions. For every route, page, well-known endpoint, and callable, this audit records the intended audience, the gate that is currently in place, and any leak or contradiction discovered.

Sister documents: Upload Threat Model, DPIA, ADR-0007 Agent Commerce API.


Summaryโ€‹

Seven distinct hosting surfaces were audited end-to-end against source: acesense.io (landing, 153 routes/files), admin.acesense.io (12 routes, all gated), api.acesense.io (5 REST + 1 MCP endpoint), docs.acesense.io (~110 docs pages, admin-gated as of 2026-06-21), acesense-launchpad.web.app (19 routes, allowlist gated), the Flutter app (12 screens, auth gate at root), and Cloud Functions (4 onRequest + 13 onCall + 2 auth-trigger surfaces). Total addressable pages/routes counted: ~310. No customer-data leaks found on a public surface โ€” Firestore rules + Storage rules + admin allowlist all align. One cross-cutting issue remains: launchpad allowlist plus admin allowlist are duplicated in multiple places that drift independently. Historical docs-public findings below are marked resolved where fixed.


Surface 1: Landing (acesense.io) โ€” acesense-landing/โ€‹

React-router routes (rendered from dist/index.html SPA fallback)โ€‹

Source of truth: acesense-landing/src/AppRouter.tsx.

RouteAudienceGateNotes
/PublicNoneHomepage with App Store CTA and sample-report CTA; no browser Firestore write path
/blogPublicNoneBlog index
/blog/:slugPublicNone50 prerendered posts in dist/blog/
/comparePublicNoneHub: 6 entries
/compare/:slugPublicNoneswingvision, pb-vision, baseline-vision, playsight, onform, topcourt, pricing-grid
/alternativesPublicNoneHub
/alternatives/:slugPublicNoneswingvision, onform, topcourt
/use-casesPublicNoneHub
/use-cases/:slugPublicNoneadult-returners, club-players, coaches-async-review, junior-coaches, parents-junior-tennis
/featuresPublicNoneHub
/features/:slugPublicNoneball-tracking, coaching-report, court-heatmap, shot-detection, stroke-quality
/how-toPublicNoneHub
/how-to/:slugPublicNonefilm-your-tennis-match, record-your-serve, share-report-with-coach
/how-it-works, /accuracy, /pricing, /faq, /changelog, /android, /ios, /examples, /about, /biomechanics, /sweden, /agentsPublicNoneStatic markdown-backed marketing pages
*PublicNoneNotFoundPage; renders a real 404 (no Helmet duplicate)

Firebase Hosting rewrites + raw HTMLโ€‹

Source: acesense-landing/firebase.json.

PathAudienceGateNotes
/privacy โ†’ /privacy.htmlPublicNoneGDPR/CCPA-compliant policy
/terms โ†’ /terms.htmlPublicNone
/contact โ†’ /contact.htmlPublicNoneStatic contact form
/support โ†’ /contact.htmlPublicNoneAlias
/accessibility โ†’ /accessibility.htmlPublicNoneEAA/WCAG statement

Markdown twins + machine-readable surfacesโ€‹

PathAudienceGateNotes
/<route>.md (every content page)PublicNoneContent-Type: text/markdown, X-Robots-Tag: index, follow (firebase.json:78-82)
/llms.txtPublicNonetext/plain, indexed
/robots.txtPublicNoneAllow-all (intentional per ADR-0006)
/sitemap.xmlPublicNone
/openapi.jsonPublicNoneOAS 3.1 for the agent API; CORS *

/.well-known/* (RFC 8615 + agent-discovery)โ€‹

PathAudienceGateNotes
/.well-known/api-catalogPublicNoneRFC 9727 linkset
/.well-known/agent-skills/index.jsonPublicNoneCloudflare Agent Skills RFC v0.2.0
/.well-known/mcp/server-card.jsonPublicNonePoints to api.acesense.io/mcp
/.well-known/mcp.json, /.well-known/acp.json, /.well-known/mpp.jsonPublicNoneDiscovery cards
/.well-known/oauth-authorization-serverPublicNoneOAuth 2.0 metadata
/.well-known/oauth-protected-resourcePublicNoneRFC 9728
/.well-known/openid-configurationPublicNoneOIDC metadata
/.well-known/http-message-signatures-directoryPublicNoneRFC 9421 dir
/.well-known/ucpPublicNoneUniversal Content Profile
/.well-known/x402PublicNone402-payment discovery

Audit findingsโ€‹

  • OK โ€” All routes are public-by-design and contain only marketing/discovery material. No PII or customer data is rendered.
  • OK โ€” The landing page no longer writes waitlist documents from the browser; it renders public marketing/discovery pages only.
  • OK โ€” robots.txt (acesense-landing/public/robots.txt:5-7) explicitly notes that authed surfaces live on separate domains, matching what we found.
  • RESOLVED 2026-06-21 โ€” llms.txt line 78 said "Internal documentation at docs.acesense.io" while docs were public. The docs host now has an admin sign-in gate.

Surface 2: Admin (admin.acesense.io) โ€” acesense-admin/โ€‹

React-router routesโ€‹

Source: acesense-admin/src/App.tsx.

RouteAudienceGateNotes
/AdminApp.tsx:113 if (!user) return <LoginPage />Dashboard
/usersAdminAuth gateUser dashboard
/jobsAdminAuth gateJob dashboard (Firestore read goes through isAdmin() rule, see below)
/analyticsAdminAuth gate
/storageAdminAuth gate
/research-decisionsAdminAuth gate
/research-decisions/:idAdminAuth gate
/developerAdminAuth gate
/operationsAdminAuth gate
/audit-logAdminAuth gateReads admin_audit/*
/admin-api-keysAdminAuth gateManages api_keys/* (root-level, requires isAdmin() to read all keys)
/settingsAdminAuth gate

Hostingโ€‹

acesense-admin/firebase.json โ€” single rewrite ** โ†’ /index.html. No public assets, no robots.txt, no noindex meta tag in index.html.

Allowlistโ€‹

The Firestore-rules allowlist that backs every admin Firestore read is at acesense-frontend/firestore.rules:18-25:

function isAdmin() {
return isSignedIn() && (
request.auth.token.admin == true ||
request.auth.token.email == 'admin@acesense.io' ||
request.auth.token.email == 'akshay.sarode@anilata.com' ||
request.auth.token.email == 'akshay.sarode18@gmail.com'
);
}

Compare to the Cloud Functions allowlist at acesense-auth-function/shared/admin-auth.ts:32-34:

export const ADMIN_EMAIL_ALLOWLIST: ReadonlyArray<string> = [
"admin@acesense.io",
];

Audit findingsโ€‹

  • OK โ€” Root auth gate at App.tsx:113-115 is correct; without a signed-in Firebase user, only <LoginPage /> renders. Routes are only mounted under <AuthenticatedApp />.
  • OK โ€” Even if a route were brute-forced via deep-link, every Firestore read in admin pages is bound by the isAdmin() rule above, so a signed-in non-admin would only see empty results / permission-denied.
  • RECOMMEND (drift) โ€” The Firestore-rules allowlist lists three emails (admin@acesense.io, akshay.sarode@anilata.com, akshay.sarode18@gmail.com); the Cloud Functions allowlist lists only one (admin@acesense.io). That means the two non-canonical emails can read admin Firestore data but cannot invoke adminRetryJob/adminSetUserPlan/etc. The discrepancy is a P2 footgun โ€” admin actions silently 403 for users who can otherwise see the dashboard.
  • RECOMMEND โ€” acesense-admin/index.html has no <meta name="robots" content="noindex, nofollow"> and there's no X-Robots-Tag header in firebase.json. Search engines cannot crawl past the auth gate, but they can index the login page itself. Add noindex to be explicit.

Surface 3: API (api.acesense.io) โ€” acesense-auth-function/โ€‹

REST apiServer โ€” acesense-auth-function/api/index.tsโ€‹

EndpointAuthRate-limitAudienceNotes
GET /v1/healthzNoneNoPublicLiveness (api/index.ts:227-235)
GET /healthzNoneNoPublicAlias
GET /v1/pricingNoneNoPublicRate card (api/index.ts:237-240)
POST /v1/jobsBearer phk_...Yes (per-key sliding window)Paying agent callerCreates job + signed upload URL (api/index.ts:598-603)
GET /v1/jobs/:jobIdBearer phk_... + ownership (apiKeyId === key.keyId)YesPaying agent caller404 (not 403) if not owner โ€” by design, no enumeration (api/index.ts:453)
GET /v1/jobs/:jobId/resultBearer phk_... + ownershipYesPaying agent callerStreams result JSON from Storage
GET /v1/usageBearer phk_...YesPaying agent callerReturns balance + counts
OPTIONS *NoneNoBrowsers (CORS preflight)Universal allow-list for safe methods only
* (404)NoneNoAnything elseGeneric 404 envelope

CORS:

  • GET is Access-Control-Allow-Origin: * (read-only and per-key authed โ€” safe).
  • POST requires either no Origin (server-side caller) or an Origin listed on apiKey.allowedOrigins (api/index.ts:147-165). Origin is echoed only after a match โ€” never reflected.

MCP mcpServer โ€” acesense-auth-function/mcp/index.tsโ€‹

MethodPathAuthRate-limitNotes
OPTIONS/mcpNoneNoPreflight
GET/mcpNoneNoTiny health JSON pointing at server-card; intentionally no internals (mcp/index.ts:107-117)
POST/mcpBearer phk_...YesJSON-RPC dispatch (analyze_tennis_video, get_job_status, get_analysis_result, get_pricing, list_my_jobs)
Other methods/mcpNoneNo405 with JSON-RPC error envelope

Other public Cloud Run / Functionsโ€‹

FunctionSourceAuthNotes
healthindex.ts:80-87invoker: public (default โ€” no auth required at IAM level)Returns {status: "ok", region, timestamp, version} โ€” no PII
apiServerapi/index.ts:666-679Per-route (see above)invoker: public, max 50 instances
mcpServermcp/index.ts:88-200Per-route (see above)invoker: public, max 50 instances

Audit findingsโ€‹

  • OK โ€” Every authed endpoint returns a 404 (not 403) on cross-tenant access (api/index.ts:453, api/index.ts:495) so a stolen API key cannot enumerate other tenants' jobIds.
  • OK โ€” Rate-limit is fail-open with structured logging (api/index.ts:212-220). The billing meter (per-job spend deduction in quoteAndReserve) remains authoritative even when the rate limiter is wobbling.
  • OK โ€” GET /v1/pricing and /healthz return only static config โ€” no secret material.
  • RECOMMEND โ€” health Cloud Function (index.ts:80-87) has no rate-limiting and a public invoker. Acceptable for an uptime probe, but consider a 1 req/s/IP ceiling so it can't be used as a 200-OK amplifier.

Surface 4: Docs (docs.acesense.io) โ€” acesense-docs/โ€‹

Hostingโ€‹

acesense-docs/firebase.json โ€” single rewrite ** โ†’ functions/docsServer, with a global X-Robots-Tag: noindex, nofollow, noarchive header. functions/docsServer verifies an admin Firebase session cookie before serving the Docusaurus HTML or assets. Docusaurus sitemap generation and local search indexing are disabled.

Remaining caveat: Firebase Hosting is static, so hashed JavaScript chunks are still served. This gate blocks normal browsing/indexing; strict byte-level privacy needs Cloudflare Access or an authenticated server rewrite.

Published doc tree (selected directories that matter for access control)โ€‹

PathAudience as of sourceCurrentlyNotes
/intro"internal documentation" (intro.md:22)Admin-gatedSets the entire framing
/architecture/*EngineeringAdmin-gatedIncludes architecture/api-reference, deployment docs
/compliance/*Compliance officer / DPOAdmin-gatedDPIA, threat model, SOC2 readiness, breach response, AI model card, eu-uk-representative
/compliance/dpas/*Compliance officerAdmin-gatedFirebase DPA, RunPod DPA, Resend DPA notes
/compliance/incidents/*Compliance officerAdmin-gated2026-Q2-drill-1.md โ€” internal drill log
/postmortems/*EngineeringAdmin-gated2026-03-14-runpod-image-regression.md โ€” full incident timeline
/runbooks/*On-call engineerAdmin-gatedadmin-signin.md, runpod-errors.md, functions-deploy-failed.md, job-stuck.md
/decisions/*EngineeringAdmin-gated7 ADRs
/marketing-videos/*MarketingAdmin-gatedIncludes a teleprompter.html standalone tool
/admin/*, /frontend/*, /gpu-backend/*, /firebase-functions/*, /annotate/*, /landing/*, /designs/*, /help/*, /get-started/*, /reference/*EngineeringAdmin-gatedService-internal docs

Total addressable doc pages from website/build/: roughly 110.

Configuration footgun (resolved 2026-06-21)โ€‹

docusaurus.config.ts:7:

tagline: 'Architecture, API, and operator docs for the AceSense platform',

intro.md:22:

Welcome to the AceSense internal documentation hub.

The docs site used to be on the public web with no auth, a generated public sitemap, and a generated public search index. It now has a Firebase Auth admin gate, global noindex, no sitemap, and no local-search index plugin.

Audit findingsโ€‹

  • RESOLVED โ€” The site now self-describes as operator docs and requires admin sign-in before rendering.
  • RESOLVED โ€” compliance/incidents/, postmortems/, and runbooks/ are behind the same docs-wide gate and the host sends X-Robots-Tag: noindex, nofollow, noarchive.
  • OK โ€” The gpu-backend agent.md handoff log lives at acesense-gpu-backend/agent.md and is not included in the docs build (verified โ€” no agent.md file under acesense-docs/). The Docusaurus presets.docs.path: '..' config only walks acesense-docs/, so the sibling repo's handoff is not exposed.

Surface 5: Launchpad (acesense-launchpad.web.app) โ€” acesense-launchpad/โ€‹

React-router routesโ€‹

Source: acesense-launchpad/src/App.tsx.

Every route below is wrapped in <AuthGate> (acesense-launchpad/src/App.tsx:25).

RouteAudienceGateNotes
/Allowlist (3 emails)AuthGateDashboard
/marketAllowlistAuthGateTAM/SAM/SOM data
/pain-pointsAllowlistAuthGate
/competitorsAllowlistAuthGate
/acceleratorsAllowlistAuthGate
/grantsAllowlistAuthGate
/vcsAllowlistAuthGateVC contact list โ€” most sensitive
/phasesAllowlistAuthGate
/pitchAllowlistAuthGateFull pitch deck
/strategyAllowlistAuthGate
/toolsAllowlistAuthGate
/timelineAllowlistAuthGate
/repo-mapAllowlistAuthGate
/brand-hubAllowlistAuthGate
/operationsAllowlistAuthGate
/architectureAllowlistAuthGateCompany architecture โ€” overlaps docs hub
/os-docAllowlistAuthGateOperating doc
/playbooksAllowlistAuthGate
/playbooks/:idAllowlistAuthGate

Allowlist sourceโ€‹

acesense-launchpad/src/config/firebase.ts:22-26:

export const ALLOWED_EMAILS: ReadonlySet<string> = new Set([
'akshay.sarode18@gmail.com',
'admin@acesense.io',
'akshay@akshaysarode.com',
]);

Login UIโ€‹

acesense-launchpad/src/pages/Login.tsx โ€” when a non-allowlisted user signs in, the UI shows:

"{notAllowedEmail} isn't authorised. The launchpad is a personal fundraising tool restricted to a 3-account list. Sign in with a different account, or contact the founder."

Audit findingsโ€‹

  • OK โ€” AuthGate is wired at the router root and ladders through three states: loading โ†’ no-user (Login) โ†’ user-not-allowed (Login with notAllowedEmail) โ†’ app shell. Routes only render in the final state.
  • OK โ€” The exact email list is not rendered in the unauth UI (only the count "3-account list" is mentioned). The allowlist constants live in the bundled JS, but a determined inspector reading the bundle still cannot impersonate those accounts without their Firebase credentials. This is the deliberate trade-off documented in config/firebase.ts:16-21.
  • RECOMMEND โ€” The launchpad has no Firestore data layer (data is bundled at build time as src/data/*.ts), so there are no rules to back the allowlist server-side. If the project ever grows to write data (e.g. CRM-style notes), this becomes a hard requirement; document it in the launchpad's own README.md so it isn't forgotten.
  • RECOMMEND (drift) โ€” Three different allowlists now exist:
    • Firestore rules: admin@acesense.io, akshay.sarode@anilata.com, akshay.sarode18@gmail.com (firestore.rules:18-25)
    • Cloud Functions admin: admin@acesense.io only (shared/admin-auth.ts:32-34)
    • Launchpad: admin@acesense.io, akshay.sarode18@gmail.com, akshay@akshaysarode.com (config/firebase.ts:22-26)
    • Pick one source of truth (custom claim admin === true) and have all three reference it.

Surface 6: Flutter app (app.acesense.io + iOS + Android) โ€” acesense-frontend/โ€‹

Auth gateโ€‹

The Flutter app gates authenticated screens on Firebase Auth state before rendering the app shell:

  • null user โ†’ LoginScreen
  • signed-in + onboarding.completed != true โ†’ OnboardingFlow
  • signed-in + onboarded โ†’ MainNavigation

Deep-link routes (/login, /session/new, /session/:jobId) all pass through the same auth-state checks โ€” there is no path that bypasses Firebase Auth state.

Screensโ€‹

ScreenAuth requiredSource
LoginScreenNolib/screens/login_screen.dart
OnboardingFlowYeslib/screens/onboarding/onboarding_flow.dart
HomeScreenYeslib/screens/home_screen.dart
SessionsScreenYeslib/screens/sessions_screen.dart
PracticeScreenYeslib/screens/practice_screen.dart
ProfileScreenYeslib/screens/profile_screen.dart
SettingsScreenYeslib/screens/settings_screen.dart
StartSessionScreenYeslib/screens/start_session_screen.dart
JobStatusScreenYeslib/screens/job_status_screen.dart
AnalysisScreenYeslib/screens/analysis_screen.dart
UpgradeScreenYeslib/screens/upgrade_screen.dart
HowItWorksScreenYeslib/screens/how_it_works_screen.dart
PrivacyPolicyScreen, TermsOfServiceScreenYes (presented from inside the app)Public-content screens shown in-app

Server-side enforcementโ€‹

  • Firestore rules: acesense-frontend/firestore.rules โ€” jobs (owner read/delete + create with own uid + restricted update whitelist), users (owner-only or admin), sessions (owner-only), api_keys (owner/admin read, function-owned balance fields), and api_usage (admin-only).
  • Storage rules: acesense-frontend/storage.rules โ€” videos/{userId}/{sessionId}/{file} owner R/W with 2GB cap; results/{userId}/{sessionId}/{file} owner read, no client write (Admin SDK only); legacy paths implicitly denied.

Audit findingsโ€‹

  • OK โ€” Every authenticated screen depends on Firebase Auth state, and every server-side read/write is bound by Firestore + Storage rules that key on request.auth.uid. The jobs rules even enforce a hasOnly([...presentation fields...]) whitelist on client-side updates (firestore.rules:53-58) so a compromised client cannot forge status, resultPath, or userId.
  • OK โ€” Storage rules deny client writes to results/* so the GPU backend's outputs are tamper-proof from any client.
  • RECOMMEND โ€” The users collection rule grants the owner write access (firestore.rules:67) without a field whitelist. That means a savvy user can mutate plan, trialEndsAt, welcomeEmailSent, etc. from the client. Tighten to hasOnly([...presentation fields...]) matching the jobs pattern.

Surface 7: Cloud Functions โ€” acesense-auth-function/โ€‹

Auth-triggered (no callable surface)โ€‹

FunctionTriggerSource
createUserDocument (createUser)auth.user().onCreateuser/index.ts:77
deleteUserDocument (deleteUser)auth.user().onDeleteuser/index.ts:159

Both fire on Firebase platform events. There is no HTTP surface to abuse.

Storage-triggeredโ€‹

FunctionTriggerSource
processVideoOnUploadonObjectFinalized on videos/*video/index.ts:185
onResultUploadedonObjectFinalized on output/*video/index.ts:575

Trigger via Storage event only โ€” not callable from the client.

Callables (Firebase auth context)โ€‹

CallableAuthOwner-checkSource
requestUploadPathrequireAuth (shared/auth.ts)uses request.auth.uid for pathvideo/index.ts:726
mergeChunkResultsrequireAuthreads job by sessionId, asserts ownershipvideo/index.ts:869
markEmailVerifiedrequireAuthself-onlyuser/index.ts:201
sendPasswordResetCustomrequireAuthself-onlyuser/index.ts:253
exportUserDatarequireAuthself-only (GDPR portability)user/data.ts:49
requestAccountDeletionrequireAuthself-only (GDPR erasure)user/data.ts:248
createApiKeyrequireAuth + max-10-keys/user capself-onlyapi/keys.ts:49
listApiKeysrequireAuthself-onlyapi/keys.ts:96
revokeApiKeyrequireAuth + ownershipself-onlyapi/keys.ts:118
adminRetryJobrequireAdmin (shared/admin-auth.ts)adminadmin/index.ts:100
adminSetUserPlanrequireAdminadminadmin/index.ts:243
adminSetUserSuspendedrequireAdminadminadmin/index.ts:347
adminAdjustApiKeyBalancerequireAdminadminadmin/index.ts:437
adminRevokeSignedUrlrequireAdminadminadmin/index.ts:550

Public-invokable Cloud Run / onRequestโ€‹

Already covered in Surface 3 (apiServer, mcpServer, health). Each handles its own auth.

Audit findingsโ€‹

  • OK โ€” Every callable starts with requireAuth or requireAdmin as the first line. The only paths past auth are unauth health/pricing.
  • OK โ€” Admin callables write to admin_audit/{autoid} (admin/index.ts:60-76) before performing the privileged action. The audit row is the only audit trail; CONTRIBUTING.md and admin/index.ts:1-12 both call out that adding a new admin callable without an audit row is a contract violation.
  • OK โ€” processVideoOnUpload enforces prefix + extension filter; onResultUploaded filters by output prefix. Neither is reachable from a client request.
  • OK โ€” requireAdmin and the firestore-rules isAdmin() are both keyed on request.auth.token.email, which is set by Firebase Auth and not user-mutable.

Cross-cutting findingsโ€‹

Ordered by severity:

  1. Allowlist drift across 3 files (P2 โ€” drift footgun). Firestore rules, Cloud Functions admin, and Launchpad each maintain their own admin/allowlist. Drift means the same user can do A but not B. Single source: a custom claim admin === true set via Firebase Admin SDK.
  2. RESOLVED 2026-06-21 โ€” Docs site labelled "internal" but published publicly. The site now has an admin sign-in gate.
  3. RESOLVED 2026-06-21 โ€” Operational/incident docs were public on docs.acesense.io. The whole docs host is admin-gated and noindexed.
  4. users/{userId} Firestore rule lets owner write any field (P3 โ€” privilege creep). firestore.rules:65-68. A user can alter their own plan, trialEndsAt, etc. Tighten to a presentation-fields whitelist as jobs already does.
  5. Admin login page indexable by search engines (P4 โ€” minor info leak). No noindex on acesense-admin/index.html and no X-Robots-Tag in acesense-admin/firebase.json. The login form itself isn't sensitive, but indexing it surfaces the brand name pattern unnecessarily.
  6. RESOLVED 2026-06-21 โ€” llms.txt called docs internal while they were public. The docs site is now private.

Recommendationsโ€‹

Numbered. Severity in brackets matches the cross-cutting list.

  1. [P2] Unify admin allowlist behind a custom claim. Add a Cloud Function (admin-only) that calls admin.auth().setCustomUserClaims(uid, { admin: true }). Update firestore.rules:18-25 to only check request.auth.token.admin == true. Update shared/admin-auth.ts:32 to drop the email allowlist and rely on token.admin === true. Update launchpad allowlist to read the same claim (sign-in flow already gives Firebase a JWT; check token.admin === true instead of an email set). Once shipped, delete all three hard-coded lists.

  2. [P3] RESOLVED โ€” Auth-gate docs.acesense.io. The whole site now uses Firebase Auth, serves the Docusaurus bundle and assets through the docsServer function after admin session-cookie verification, removes the public local-search index, and sends a global noindex header.

  3. [P3] Tighten users/{userId} Firestore rule. Apply the affectedKeys().hasOnly([...]) pattern that already exists for jobs (firestore.rules:53-58) to the users collection. Allow only displayName, photoURL, preferences, onboarding, updatedAt, etc. โ€” never plan, trialEndsAt, welcomeEmailSent, subscription, or emailVerified. Server-owned fields stay in the Cloud Functions Admin SDK path.

  4. [P4] Add X-Robots-Tag: noindex, nofollow to the admin host. Update acesense-admin/firebase.json hosting.headers with a wildcard rule for ** setting the header. Mirror in acesense-launchpad/firebase.json.

  5. [P4] Rate-limit the health Cloud Function. Add a 1 req/s/IP cap at the function level (index.ts:80-87) so it cannot be co-opted as a 200-OK amplifier.

  6. [P4] Fix llms.txt line 78. Either change "Internal documentation at docs.acesense.io" to "Public engineering documentation at docs.acesense.io" (Option A in #2) or actually move the docs behind auth.


Appendix: count of addressable itemsโ€‹

SurfaceRoutes / pages / endpoints
Landing routes (React)22
Landing prerendered slugs86 (49 blog + 7 compare + 3 alternatives + 5 use-cases + 5 features + 3 how-to + 14 static-marketing)
Landing markdown twins~85
Landing /.well-known/*11
Landing other public files (robots.txt, sitemap.xml, llms.txt, openapi.json, etc.)6
Admin routes12
API REST endpoints8
API MCP endpoint (POST + GET + OPTIONS)3
Other Cloud Run / onRequest functions1 (health)
Docs pages (built)~110
Launchpad routes19
Flutter screens13
Cloud Functions callables15
Auth + Storage triggers4
Totalโ‰ˆ383

(The 310 figure in the executive summary excludes prerendered blog/marketing slug variants when collapsing identical content under one route; the 383 figure here counts each addressable URL.)