Skip to content

How it works for a school group (architecture & tenancy)

This page is written for the buyer’s technical advisor. It explains how MySentinel is built, where the trust boundary sits, and — most importantly — exactly how one school’s data is kept separate from another’s. We have written the tenancy section to be checkable: nothing here is gloss. Where a capability is shipped-and-proven we say so; where it is planned we mark it Roadmap.

The short version: MySentinel is a set of small Cloudflare Workers, each owning its own database, sitting behind a single gateway that is the only thing the browser talks to. Tenant isolation today is logical and code-enforced — every school-scoped query is filtered by a school_id that comes only from the verified login token, never from anything the browser can set. That isolation runs on a single live database shard. The system is built to add more shards without a rewrite, but it is not physically sharded today.

The platform: Cloudflare, end to end

MySentinel runs entirely on Cloudflare — Workers (serverless compute), D1 (SQLite databases), KV, R2 (object storage), Queues, Durable Objects, Cron Triggers, and Static Assets. There are no servers to run and no second cloud. This is a deliberate, load-bearing decision (ADR-001): one billing and operations surface, a global low-latency edge, and no local infrastructure for a South African pilot to maintain. The trade-off we accept is living within Cloudflare’s primitives (D1 size/SQLite semantics, Workers CPU limits) as design constraints.

Capability services, not a monolith

The backend is roughly 24 small Workers (“capability services”), each doing one job — identity-service (learners, badges, guardians), movement-service (validate and record check in/out), tenant-service (schools, settings, memberships), notification-service (delivery fan-out), auth-service (login, passkeys, sessions), operator-service (onboarding), and so on. Each service:

  • Owns its own D1 database. No service reads another service’s database directly (ADR-003). If service B needs data service A owns, it calls A’s typed RPC over a Cloudflare service binding — never a shared table.
  • Ships a typed contract. Request/response shapes live in packages/rpc-types and are shared by both the services and the PWA, so a breaking change surfaces at compile time rather than in production (ADR-014).

The reason for this shape is documented in the project philosophy: smaller services are safer to change, with a smaller blast radius and explicit contracts at every boundary.

The gateway is the only door

The browser (the SvelteKit PWA used by officers, admins, class teachers, and operators) talks to exactly one thing: the gateway Worker. Every backend service is internal — reachable only via service bindings, never directly from the public internet (ADR-005). The gateway owns:

  • CORS and routing.
  • Authentication — it verifies the JWT on every request.
  • RBAC — every route declares which roles and scope may call it (requireAuth(roles, scope)). An officer hitting an /admin route, or an admin hitting an /operator route, is bounced. This is shipped and verified.
  • Tenant context propagation — it injects the trusted X-School-Id and actor headers downstream from the verified token.
  • Step-up — sensitive admin actions (safety lockdown, POPIA erasure, school/provider config, staff credential changes) require a WebAuthn passkey assertion. Route completeness here is enforced by a test, not a hand-kept list (ADR-025), so a new mutating route cannot ship ungated by omission.

Because the gateway is the single trust boundary, there is exactly one place to reason about auth, tenancy, and CORS.

Operator console

How work fans out: queues, DLQs, and real-time

A gate scan must return fast. So the officer’s confirmation is a synchronous, immutable write plus a handful of queue enqueues — it never waits on downstream bookkeeping. Cross-service fan-out runs on Cloudflare Queues (movement-events, movement-projections, movement-attendance, movement-postcommit, emergency-broadcasts), each with a dead-letter queue (DLQ) and idempotent consumers (ADR-004). A consumer crash retries; a poison message lands in the DLQ for replay rather than wedging the pipeline.

Real-time dashboard updates use a Durable Object, SchoolStreamHub, hosted in the gateway (ADR-010). Services publish per-school “hints” after their own writes; the browser treats those as nudges and refetches the D1 projection for truth, falling back to polling if the stream drops. The stream is never the source of truth — the projections are.

Long-running, multi-step jobs run on Cloudflare Workflows (ADR-020/021): school onboarding (provision → drip emails over 30 days → day-30 go-live evaluation), POPIA erasure (30-day cool-off then a per-store cascade), scheduled report runs, and academic-year rollover. These survive Worker restarts, sleep between steps, and retry individual steps durably. You can watch the onboarding steps tick green live in the operator console.

Onboarding workflow progress

Tenant isolation — the honest detail

This is the section a technical advisor should read most carefully.

How separation works today: logically, in code. Multi-tenancy is enforced by a single hard rule (ADR-006): every school-scoped table carries a school_id, and that school_id is always taken from the verified JWT. A browser-supplied X-School-Id header is ignored. Every school-scoped query filters by the token’s school_id, and the rule is regression-tested in the gateway’s golden suite. A user authenticated to school A cannot read school B — the school identifier is never something the client chooses.

What this is NOT (yet): it is not physical, database-per-tenant isolation. All schools share each service’s single D1 database, partitioned logically by school_id. Event-heavy services route their D1 through @mysentinel/shard-router, which is built to spread schools across multiple shards by hashing school_id. But only one shard is live todayshard_0. The router’s own default ring is literally { shards: ["shard_0"] }. So:

AspectStatus today
school_id-from-token enforcementShipped — code-enforced, regression-tested
Per-query school scopingShipped on every school-scoped table
Isolation typeLogical / code-enforced, not physical per-tenant DBs
Horizontal shardingShard-ready — router live, but only shard_0 provisioned
Cross-school operator/audit readsBound to shard_0 today
Adding shard_1+Possible without a service rewrite (config + provisioning), but it is an operational step that has not been taken

The honest summary: the safety guarantee (no cross-tenant reads) is real and tested; the mechanism is logical scoping on one shared database, with the path to physical sharding designed in but not yet exercised. A technical advisor should treat “shard-ready” as “no rewrite required,” not “already sharded.”

What “operator” means — and what a group owner does NOT get yet

It is important to be precise about roles for a chain owner.

  • operator is Cybertron platform staff — the people who run MySentinel. An operator’s token carries platform scope and sees every school on the platform, for cross-school onboarding and health. The operator console lets staff onboard a new campus (a durable Workflow), view platform health, and manage schools.

Operator schools list

  • There is no group-scoped owner tenant — Roadmap. A chain owner cannot today log in to a tenant scoped to only their own campuses. The two real scopes are school (one school) and platform (everything). A multi-school owner role that sees a subset of schools, and a consolidated cross-campus academic report, do not exist yet. The only cross-campus aggregates that exist today are channel-usage and billing-style operational rollups in the operator console — there is no consolidated academic report across campuses.

Other honest boundaries a DD review will check

  • WhatsApp and SMS are Preview / per-school provider-gated — until a school’s provider is connected, the channel silently demotes to email. App push and email are the live channels.
  • Web push is real and RFC-8291-proven in code, but needs VAPID secrets and a real-device proof per environment before you should call it end-to-end.
  • Branded PDF / SAR rendering needs the Cloudflare Browser Rendering binding provisioned; local development uses a deterministic stub.
  • Append-only audit is enforced by a database trigger — it is not a hash-chained / WORM ledger, and audit appendEvent is at-least-once (benign, traceable duplicates are accepted; ADR-019).
  • Encryption at rest: per-school AES-GCM photo keys are shipped; guardian/visitor contact encryption (sealed + hash) is shipped for new writes, with existing-row backfill and plaintext null-out as supervised per-environment steps.
  • Production is currently a hollow shell (no secrets provisioned). Demo and evaluate on UAT; production is not yet functionally equivalent. POPIA, trilingual en/af/zu, and the South African context are first-class throughout.

The one-paragraph takeaway

MySentinel is a clean, Cloudflare-native, capability-service system with a single enforced trust boundary at the gateway and genuine, tested tenant isolation by school_id-from-token. The architecture is built to scale horizontally and to support richer ownership models, but today that isolation is logical (one shared shard, not per-campus databases), and “operator” is platform staff rather than a group owner. A scoped owner tenant, physical sharding, and a cross-campus academic report are all on the roadmap, not in the build — and we would rather you knew that before you signed than after.