Skip to content

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

0 Edge-native

No origin server required to ship.

0 KV writes Per login

Stateless encrypted session cookies.

0 Cron triggers

Exactly the free-tier ceiling, by design.

$0 Upgrade / month

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.

Tenancy model

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.

A mode change takes up to about a minute to reach every colo — the 15-second isolate cache plus KV eventual consistency. The cutover runbook is built around that window rather than pretending it does not exist.
ConditionEffective modeTruthBehaviour
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.

Effective mode edge Full standalone Cloudflare operation — D1 is the system of record.

Part 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.

Total cost in edge mode is zero. Turning on the VPS adds roughly $4.50/month and removes the write ceiling, the CPU ceiling and the Unicode PDF limitation — an upgrade you take when it pays for itself, not a prerequisite to ship.

Build order that keeps each step verifiable

  1. D1 schema and the Worker with vps_url empty. Nothing else can be tested until truth exists.
  2. Auth and the co-branded consent screen. Every later subsystem needs a session.
  3. ACL and the content overlay. Partial page management is the differentiator; prove it early.
  4. Learner sync. Watermarks and consent before any volume arrives.
  5. Payments in sandbox. Replay the same webhook twenty times and assert nothing changes.
  6. Certificates. SVG for every script at the edge; PDF for Latin-1.
  7. The admin console. You cannot operate what you cannot see.
  8. 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.