No origin server required to ship.
Running in edge mode · zero monthly cost
Learning infrastructure that admits its own decay
MetaX is a multi-academy platform where every claim carries a half-life, every edit is
attributable, and every payment is idempotent. It runs whole on Cloudflare — D1 for
relational truth, R2 for blobs, Workers for identity, money and certificates — and it
moves to a MySQL origin by changing vps_url from an empty string,
with no redeploy and no downtime beyond three minutes.
Currently teaching
Stateless encrypted session cookies.
Exactly the free-tier ceiling, by design.
Only when Arabic PDFs or writes demand it.
Two rules make the whole thing safe
Every other decision in the platform follows from these. They are stated plainly because ambiguity here is what turns an outage into a data-loss incident.
Identity never depends on the origin
Sessions are self-contained encrypted cookies: a 30-minute access token carrying a compact ACL, and a 30-day opaque refresh token. A dead origin cannot log anybody out, and the hot path costs zero storage reads.
Trade-off stated honestly: a stolen access cookie is valid for up to thirty minutes.
Mitigated by the short TTL, a sess_ver counter, encryption
rather than mere signing, and a strict CSP on every project.
Money is never queued
A checkout, a webhook, a payout or a refund either completes against the true store or returns a clear 503. Progress, snapshots, referral clicks and content drafts are queueable; orders, entitlements, ledger entries and commissions are not.
The user-facing consequence is a deliberate wording split: “Saved — will sync when the connection returns” never appears on a money surface.
One platform, many academies
Each academy is a tenant with its own host, branding, pricing, revenue terms and role ladder. Being a member of the root site grants nothing anywhere else.
Part 4 · Worker
The router never knows which store answered
Both backends implement an identical action list. That single fact is what makes the switch possible: the dispatcher resolves a mode, then calls either the edge handlers or the signed remote, and returns the same shape either way.
D1 — relational truth
28 tables, one logical schema in two dialects. 500 MB per database, 50 queries per
invocation, 100 bound parameters per query — so bulk inserts collapse into a single
json_each() statement.
R2 — every blob
Learner snapshots, rendered certificates, published content bundles and nightly JSON
backups. Exposed through cdn.metax.academy so Cloudflare's
cache serves content overrides at no per-view cost.
Workers Crypto — money
HMAC-SHA512 for Binance request signing, RSASSA-PKCS1-v1_5 with SHA-256 for webhook verification, AES-GCM for the session seal. Payments work fully in edge mode.
HTMLRewriter — sanitiser
Editor HTML is sanitised at save time against a tag and attribute allowlist. Inline SVG
and data:image/svg+xml are refused outright; IDs are
namespaced; external links are forced to noopener.
Cron — three triggers
*/5 reconciles payments and drains the outbox;
0 * expires tokens and republishes content;
0 3 releases holds, rolls up facts and backs up to R2.
Every job is cursor-chunked.
Outbox — the seam
In mirror mode every mutation also lands in an outbox the origin collects. Delivered rows are pruned after seven days, because that table holds a plaintext record of every mutation including OAuth profile data.
Action metadata drives everything
Each action declares whether it writes, whether it mirrors, whether it may be queued, and whether it touches money. The dispatcher needs nothing else.
// src/store/actions.js
export const ACTIONS = {
'auth.upsert': { write: 1, mirror: 1, queue: 0 },
'acl.bundle': { write: 0, cacheable: 1 },
'content.save': { write: 1, mirror: 1, queue: 1 },
'pay.checkout': { write: 1, mirror: 1, queue: 0, money: 1 },
'order.finalize': { write: 1, mirror: 1, queue: 0, money: 1 },
'progress.save': { write: 1, mirror: 1, queue: 1 },
};
Part 1 · One config object
Five modes, derived never set
The effective mode is computed from vps_url, the handover state,
the freeze window and the VPS failure streak. Nobody sets it by hand, which is why it cannot
disagree with reality.
| Condition | Effective mode | Truth | Behaviour |
|---|---|---|---|
vps_url empty |
edge | D1 + R2 | Full standalone Cloudflare operation |
| set, handover unfinished | mirror | D1 + R2 | Every mutation also lands in the outbox |
now < freeze_until |
freeze | — | Safe writes queue; money writes are refused |
| handover done, VPS healthy | origin | MySQL | D1 kept as a frozen fallback snapshot |
| handover done, 3+ failures | degraded | MySQL (down) | Reads fall back to D1 marked stale; money refuses |
The config object
{
"vps_url": "",
"mode": "auto",
"handover": { "state": "idle", "table": null, "cursor": 0 },
"vps_last_ok": 0,
"vps_fail_streak": 0,
"freeze_until": 0,
"features": { "pdf": "auto", "sync": "ask", "payments": true }
}
Stored in KV under cfg:core, editable from the console
without a redeploy, cached in-isolate for fifteen seconds.
Try the mode engine
The same derivation the Worker runs, in your browser. Nothing is written anywhere.
Subsystems, and where each one lives
auth.
Co-branded consent screen, three providers, four states, silent refresh.
Identityai.
An academy: catalogue, free-first-two access, inline editing overlay.
Tenantme.
Profile, progress, snapshot sync consent, certificates, affiliate.
Learnerpay.
Binance Pay checkout, status polling, the unauthenticated webhook.
Moneycertificate.
Public verification: verified, revoked, tampered or not found.
Trustadmin.
Health strip, orders, revenue, affiliate review, Core storage panel.
OperatorPart 9 · Free tier
The numbers that actually bind
D1 row writes are the real ceiling — not requests, not CPU. Three defences are built in: per-user watermarks so a repeat sync writes almost nothing, content-hash dedupe that rejects identical snapshots, and a hard cap of 2,000 facts per snapshot.
Build order that keeps each step verifiable
- D1 schema and the Worker with
vps_urlempty. Nothing else can be tested until truth exists. - Auth and the co-branded consent screen. Every later subsystem needs a session.
- ACL and the content overlay. Partial page management is the differentiator; prove it early.
- Learner sync. Watermarks and consent before any volume arrives.
- Payments in sandbox. Replay the same webhook twenty times and assert nothing changes.
- Certificates. SVG for every script at the edge; PDF for Latin-1.
- The admin console. You cannot operate what you cannot see.
- The VPS, deliberately last. The whole point of the switch is that you never need it to ship.
Start where the evidence is
The first two items of every series are open without an account. Read them, disagree with them, then decide.