Each decision lists what was chosen, what was rejected and the price paid.
decision Offline-first with an outbox. Rejected: online-first with a cache. Why: learners use the app on trains and patchy mobile data; nothing they do may be lost. Price: a sync engine and idempotent mutations.
decision Sync logs, derive state. Rejected: syncing card state with conflict resolution. Why: union of logs merges without conflicts and every device computes the same schedule. Price: replaying logs costs CPU, cached on the server in
learner_state.
decision Content as signed data. Rejected: lessons compiled into the app. Why: daily content without store review, instant rollback. Price: a signing key to protect, and compatibility rules for old builds.
decision One React codebase with Capacitor. Rejected: separate native apps, React Native. Why: one small team, one codebase, the web as a first-class platform. Price: native feel depends on careful CSS and a few plugins.
decision Modular monolith on Postgres. Rejected: microservices, a document store. Why: few moving parts, transactions across modules, SQL for analysis. Price: scaling the database is the main future project (see Scaling Plan).
decision Edge in front of everything. Rejected: serving from the API region. Why: about 55 ms to first byte worldwide, cheap egress, rate limits and link previews in one place. Price: two deploy targets.
decision First-party analytics and a Parquet lake. Rejected: a third-party analytics SDK, a warehouse. Why: privacy control, no extra SDK weight, near-zero cost. Price: writing views and an export job.
decision Transparent policies with logged propensities. Rejected: an opaque recommender from day one. Why: explainable choices now, and offline evaluation of better policies later. Price: more data per decision.
decision Shared packages for client and server. Rejected: duplicated logic. Why: due dates, experiment variants and protocol checks can never disagree. Price: packages must stay small and free of heavy dependencies to protect the first download.
${sch.nav()}