TL;DR
An offline, multi-user school management system with zero external dependencies, built on my own in 4 stages to replace scattered spreadsheets with a single place. It is currently being validated, ahead of a pilot with the school office.
Context and problem
I work as an apprentice in the administrative office of a school with around 535 students, from preschool to high school. Day-to-day work ran on loose Excel spreadsheets and Word documents, spread across several systems: the academic system (a desktop app), the admissions tool, the state's digital school records and the cafeteria system.
In practice, every task meant hunting for the right spreadsheet, copying data from one place to another and assembling documents by hand. Re-enrollment, missing documents, contracts, official letters, scholarships, billing checks and pickup authorizations all lived in different files, and none of them knew about the others.
My role
A personal project I started on my own, built solo: requirements, architecture, code, tests, documentation and installation. I use Claude Code as a pair programmer, with a living handoff document (decisions, pitfalls, roadmap) so that each session picks up exactly where the last one stopped.
Constraints
- No IT staff and regular Windows PCs, with no guarantee of being allowed to install software.
- It has to work offline: the school network isn't reliable.
- Data about minors: anything privacy-related (Brazil's LGPD) outweighs any convenience.
- Several people at once: one PC acts as the server and the others connect through the browser (one PC today, three is the goal).
- Integration through exports only: there's no guaranteed access to the academic system's
database, so data comes in through the
.xlsxfiles it already exports. - Whoever maintains it may not be a developer: installing and updating must take two clicks.
Technical decisions and trade-offs
| Decision | Alternative | Why |
|---|---|---|
| Local web app: one PC is the server | Cloud / SaaS | Minors' data stays off the internet, there's no subscription, and everything works offline. |
| Portable Node.js, no npm, no dependencies | npm + a framework (React, Express) | It runs with two clicks and no build step. There are no packages to update, break or be compromised. |
Built-in node:sqlite database in WAL mode | MySQL / PostgreSQL | A single file and no database server to install; hot backups with VACUUM INTO. |
Import the academic system's .xlsx exports | Read its MySQL database directly | Database access isn't guaranteed, and exporting is something the office already knows how to do. The .xlsx reader is hand-written, no libraries. |
One-button updates via git pull --ff-only | An installer or copying files by hand | Non-developers update it themselves: the app shows what changed, backs up, applies and restarts. It refuses if there are local changes. |
| Optimistic concurrency (a version per record) | Locking a record while someone edits it | Nobody gets blocked; a conflict only shows up when it actually happens, with a name and a time. |
Architecture
- 01Browsers on the office PCsplain HTML, CSS and JS; hash routing
- 02Portable Node.js serverHTTP, sessions, CSRF, CSP and audit log
- 03node:sqlite (WAL)one file, migrations on startup
Around it
- Encrypted backupsAES-256-GCM, to a USB drive or OneDrive
- Git-based updateschangelog, backup, apply and restart
- .xlsx exportsstudents, guardians and report cards
The server is a framework-free Node.js process. Routes live in per-area modules (rotas/*.js) and
receive a shared context. Every change goes through an audit log, and official documents are
generated from .xlsx templates filled in by the code itself.
Implementation highlights
Encrypted backups that travel on a USB drive
Backups are taken while the system is running (VACUUM INTO) and, when a password is set, they are
encrypted with AES-256-GCM: a key derived with scrypt, a random salt and IV, and a marker at
the start to recognize the file. Restoring happens in two steps: the app validates the backup,
schedules the swap and only replaces the database on the next startup, keeping a copy of the old one.
function cifrar(dados, senha) {
const sal = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
const chave = crypto.scryptSync(senha, sal, 32);
const c = crypto.createCipheriv("aes-256-gcm", chave, iv);
const corpo = Buffer.concat([c.update(dados), c.final()]);
return Buffer.concat([MARCA, sal, iv, c.getAuthTag(), corpo]);
}Two people editing the same record
The screen sends the version it loaded. If someone else saved in the meantime, the server answers 409 saying who and when. On the client, only the changed fields are sent: if each person edited a different field, both changes are kept.
function conferirVersao(atual, b, u) {
const versao = b._versao,
forcar = b._forcar;
delete b._versao;
delete b._forcar;
if (versao === undefined || forcar) return;
if ((atual.atualizado_em || null) === (versao || null)) return;
if (!atual.atualizado_por || atual.atualizado_por === u.login) return;
// ...looks up the name of whoever saved it
falha(409, `${nome} alterou isto enquanto você editava.`, {
conflito: { por: nome, em: atual.atualizado_em },
});
}The codebase is written in Portuguese on purpose: it's maintained by and for a Brazilian school office.
What else protects the data
- Per-person login with roles (admin and apprentice), a forced password change on first login, a server-enforced screen lock and a Content Security Policy.
- Sessions stored in the database (only the token hash): restarting the server logs nobody out. If it goes down, the screen shows an "offline" banner and what was typed is kept as a recoverable draft.
- Privacy (LGPD): a log of who opened each student record, an export of everything stored about a student, and disposal that anonymizes former students while keeping the statistics.
- Real data never reaches Git: templates are scrubbed by a script before any commit, and every test runs against a demo with fictional data.
Results and impact
- Version 5.8.x, continuously evolving, with about 875 KB of JavaScript written without a framework.
- Measured with a demo of ~530 fictional students: the API answers in under 30 ms and no screen takes more than 0.4 s. Lists that reached 30,000 pixels in height dropped to about 6,000 with progressive loading.
- A home-grown test suite takes the server down for real, simulates two people editing at once, restores backups and checks all 30 screens of the app.
What I'd do differently and next steps
I'd run the pilot first. I built four stages before a single module was running on real data. Today I'd start with one module in a two-week pilot, because that's what proves value and earns the trust of whoever decides. Without a pilot, the rest is just code.
Next steps, in this order:
- A real pilot of one small module (front desk, school gate or visits).
- Installation on the office's three PCs, tested on site.
- Checking the generated school transcripts with the person who signs them today.
And what I decided not to do: rewrite it in React (months lost, plus a build step and dependencies), move it to the cloud (minors' data on the internet multiplies the responsibility) and automate bulk WhatsApp messages (the official API is paid, and unofficial ones get the school's number banned).
Links
- Code: internal, since it's the school's system, with its own rules and templates.
- Video demo: TODO(kevin): LinkedIn video link