Skip to content
© Code by Kevin · Home
All projects

Case study · 01 / 035 min read

SEK · Gestão Escolar

Offline, multi-user school management system with zero dependencies

Role
Sole author: requirements, architecture, code, installation and support
Team
Solo
Year
2026
Status
In daily use
Stack
Node.js · node:sqlite · JavaScript · HTML · CSS · PowerShell

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. Today the whole school office uses it.

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.
  • Integration through exports only: there's no guaranteed access to the academic system's database, so data comes in through the .xlsx files it already exports.
  • Whoever maintains it may not be a developer: installing and updating must take two clicks.

Technical decisions and trade-offs

DecisionAlternativeWhy
Local web app: one PC is the serverCloud / SaaSMinors' data stays off the internet, there's no subscription, and everything works offline.
Portable Node.js, no npm, no dependenciesnpm + 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 modeMySQL / PostgreSQLA single file and no database server to install; hot backups with VACUUM INTO.
Import the academic system's .xlsx exportsRead its MySQL database directlyDatabase 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-onlyAn installer or copying files by handNon-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 itNobody gets blocked; a conflict only shows up when it actually happens, with a name and a time.

Architecture

Main flow
  1. 01Browsers on the office PCsplain HTML, CSS and JS; hash routing
  2. 02Portable Node.js serverHTTP, sessions, CSRF, CSP and audit log
  3. 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.

app/lib/backup.js
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.

app/server.js
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

  • Used by the whole school office. Tasks that used to take hours or even days, such as issuing school transcripts, tracking missing documents and building organization spreadsheets, now take a few minutes. That gain is what the team reports, not a formal measurement yet.
  • 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:

  1. Measure the time each task takes before and after, to replace the team's account with numbers.
  2. Keep evolving from what the office uses most, one module at a time.

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.