Aprender para Todos
A free study platform for Brazilian K-12 and the national university entrance exam, built with no backend and no accounts: progress lives on the student's device.
Context
A social project: a free study platform organized by school stage and grade, covering pre-school through secondary education, including the ENEM national entrance exam and its essay. The audience is students, families following along, exam candidates, and teachers or NGOs, most of them arriving on a phone.
Challenge
Sustain a complete education platform at an operating cost close to zero, without collecting personal data from students, many of them minors. Removing the backend solves cost and privacy in one move, but leaves two open questions: where does a student's progress live, and how do you author content without a CMS.
My role
Architecture, development, education domain modeling, accessibility, testing, infrastructure, and deployment.
Responsibilities
- Application and content-model architecture.
- Education domain modeling: stages, grades, areas, learning paths, modules, lessons, and quizzes.
- Local persistence layer over IndexedDB.
- Accessibility as a first-class preference.
- Test suite and CI pipeline.
- AWS infrastructure and two-environment deployment.
Constraints
- A free project: the operating cost has to fit whoever maintains it, indefinitely.
- The audience includes children: accounts and sign-up mean collecting a minor's personal data.
- Mobile first, on a possibly modest device: every kilobyte of JavaScript is paid for by the user.
- A broad content base (multiple stages, grades, and subjects) that has to grow without turning into a mess.
Architecture
- Next.js 16 with the App Router and static export: no application server, no runtime database.
- No runtime dependencies beyond the framework itself: next, react, and react-dom. No state library, no UI kit, no IndexedDB wrapper.
- On-device persistence: a hand-rolled IndexedDB wrapper with one repository per domain (progress, quizzes, error notebook, study plan, drafts, history).
- Small synchronous preferences in localStorage, kept separate from IndexedDB so they never block first paint.
- Content as TypeScript code, with branded IDs so the compiler rejects a wrong cross-entity reference.
- An editorial pipeline inside the domain: pure validators returning structured issues, a coverage report, and explicit editorial states.
- A local editorial console running in the browser, with no CMS and no server.
- A versioned export/import envelope, validated on the way in, as the alternative to server-side sync.
Main technical decisions
No backend: student progress lives on the device, in IndexedDB.
Why
Accounts and a database would mean collecting children's personal data, owning the duty to protect it, and paying for a server forever. They would also put a sign-up screen between the student and the lesson. Without a backend, a free project's operating cost drops to static hosting, and there is no minor's data to leak.
Trade-offs
- Progress does not sync across devices: a new phone starts from zero.
- Clearing browser data erases the study history.
- With no server there is no aggregate telemetry, no visibility into how the content is actually used.
- Partial mitigation: user-controlled data export and import through a versioned envelope.
Zero runtime dependencies beyond the framework.
Why
Every kilobyte of JavaScript is paid for by exactly the modest device this product targets. And every dependency is future maintenance a free project cannot fund long term. React Context and the native IndexedDB API cover what the product needs.
Trade-offs
- More code of our own to maintain. The IndexedDB wrapper, for one, is ours.
- Gives up the conveniences of mature libraries (devtools, caching, utilities).
- In exchange: a small bundle, a near-zero supply-chain surface, and no breakage arriving from a third party.
Content as versioned code, with tests guarding the content base itself.
Why
A CMS would bring back the server and the database the first decision removed. With content in TypeScript, the compiler enforces its shape and CI validates coverage and gaps before deploy. A content error becomes a red build rather than a production bug.
Trade-offs
- Authoring now requires a build and Git, which does not serve a non-technical author.
- Hence the local editorial console: authoring in the browser, with drafts in IndexedDB and export, without reintroducing a server.
- A large content base turns into a lot of TypeScript files to navigate.
Implementation
- Learning paths by stage, grade, and subject, with a canonical lesson and quiz per module.
- Error notebook, study plan, progress, and metrics, all local.
- An ENEM section, including the essay.
- Multiple profiles on the same device, for when the phone is shared.
- Accessibility across three axes (reduced motion, high contrast, and text size), defaulting from the operating system's own settings with a persisted user override.
- Device data export and import, validated and previewed before it is applied.
Production concerns
- Static export published to a private S3 bucket, served by CloudFront with OAC.
- Clean URLs resolved at the edge by a CloudFront Function on viewer-request.
- Infrastructure provisioned by idempotent, versioned scripts, with a per-environment IAM user.
- Cache split by asset class: revalidated HTML, immutable hashed assets.
- CI runs tests, lint, build, and an export validation before any deploy.
- Two environments: staging automatically on merge, production only by manual dispatch with an explicit confirmation.
- No application server in production: there is nothing to operate, patch, or scale.
Results
- Operating cost limited to static hosting, which is what makes a free project sustainable.
- No student data leaves the device: no accounts, no database, no analytics.
- Content integrity protected by tests running in CI, covering persistence, domain logic, and the content base itself.
- Accessibility treated as part of the architecture rather than a later adjustment: it inherits the system preference and lets the user override it.
- Every content bug became a permanent test instead of a one-off fix.
Stack
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS 4
- IndexedDB
- Vitest
- AWS S3
- AWS CloudFront
- CloudFront Functions
- GitHub Actions