flowchart LR
subgraph UNTRUSTED["Untrusted"]
NET[network and caches]
DEV[device storage]
end
subgraph EDGE["Edge"]
W[Worker<br/>rate limit · CSP · proxy secret]
end
subgraph CORE["Trusted core"]
API[API]
PG[(Postgres)]
KEY[content signing key<br/>CI only]
end
NET --> W --> API --> PG
KEY -. signs .-> M[manifest]
M --> NET
| Threat | Control |
|---|---|
| Tampered lessons from a cache or proxy | Ed25519-signed manifest, keys pinned in the build, SHA-256 per file |
| Stolen signing key | Key lives only in CI secrets; rotation by key id without an emergency release |
| Spoofed client IPs on direct API calls | The forwarded IP is trusted only with the Worker's proxy secret, compared in constant time |
| Abuse and scraping | Per-IP limit of 300 requests a minute on /api/*; attempt limits on sign-in codes |
| Session theft | Random tokens in httpOnly cookies, stored only as hashes; logout revokes; sign-in codes hashed, 5 tries, one day at most |
| Script injection | Enforced Content Security Policy: scripts only from the app's own origin, no framing, no plugins; violations reported to Sentry |
| Replayed or duplicated writes | Every mutation id is recorded before it is applied |
| Secrets in the repository | None: tokens, keys and signing files live in CI and platform secret stores |
user_private; every other table holds an id.consents, and lake rows carry the flag.audit_log.warning Push and email providers receive only what they must: a device token, or an address and a sign-in code. No learning data leaves the platform.
A formal review against India's Digital Personal Data Protection Act (consent notices, data-principal requests, retention schedule) is planned before the store launch. See Roadmap.
${sch.nav()}