The starting situation
Sites were run on a shared spreadsheet and the phone. Three people typed the same information, and every subsidy file had to reach the scheme operator with the right papers in the right order, or be rejected.
What was delivered
The software that turns an enquiry into a paid job. A file moves along a six-stage track, from submission to compliance inspection, and at each stage the expected paperwork assembles itself: invoice, assignment contract, statutory annexes, site survey, photo report. It all downloads as one archive. Fitters are scheduled on a calendar, payments are tracked per beneficiary, and a submitted file locks so it can't drift. The spreadsheet is gone.
What it changes
The project as the people using it every day see it.
01
Information typed once
Client, site, appointments, amounts: it all comes from one file. Three people no longer copy the same rows into a spreadsheet, and nobody wonders which version is the real one.
02
Fewer files sent back
Which documents are due depends on where the file stands. What's missing shows before submission, not three weeks later when the scheme operator rejects it.
03
Knowing where every job stands, at any time
A list, statuses, colours. The owner sees at a glance what's moving, what's stuck and what's waiting on a signature, without calling anyone.
The screens, one by one
Each capture stays put while you read what it demonstrates, and each is paired with its phone version.


01 / 03
The file's paperwork
The documents the scheme operator demands assemble as the file advances. Whatever can be produced from the file is; the rest is uploaded. It all leaves as one archive.
Under the hood
Every document type is declared and typed. pdf-lib fills the statutory forms, PDFKit composes the rest, archiver assembles the bundle.
On a phone
The site survey is filled in from the phone, on site, and triggers the same generations.


02 / 03
The file's track
Six stages, from submission to compliance inspection, with a live eligibility checklist. You see what's left to do at a glance.
Under the hood
Constrained transitions rather than a free-text field: that's what lets the system know which documents are due at any point.
On a phone
The same file on a phone screen: status, eligibility and installation scheduling, with nothing dropped.


03 / 03
Administration
Accounts, batches, invoicing, payments, the operator's Drive connection, the GDPR register and the activity log.
Under the hood
Five roles and six permissions granted individually. Public sign-up is closed: only the admin creates accounts.
On a phone
The nine modules stack into a single column, with no hidden menu and no feature removed.
The rest of the software
Click to enlarge: a dense interface can't be read at postage-stamp size.
The same software, on a phone
Not a stripped-down version: the same screens, the same data, the same freedom to act. That's what lets a file be filled in from the site.
The decisions, and why
Each decision carries its reason, then what would break without it.
01
Business rules live outside the interface
Eligibility, CAE scale sheets, payment calculations, the status track: all of it sits in a domain layer that depends on neither the framework nor the database. It borrows only types from the schema, erased at compile time. Amounts go through a Money object rather than floating-point numbers.
Without it: A CAE rule change would mean reopening screens and retesting them. Here it's one file and its test.
02
Identity numbers are encrypted at rest
AES-256-GCM, and the GCM part isn't incidental: it authenticates as well as encrypts, so tampering is detected rather than passing unnoticed. A silently altered ID number would produce a false document in a public-subsidy file. The key is held outside the code and outside the database.
Without it: A copy of the database, stolen or mislaid in a backup, would hand over every client's identity document.
03
Documents are served through expiring links
Nothing is public: every file is reached through a signed URL valid for one hour. These documents hold identity cards and photographs of people's homes.
Without it: A public URL, once known, is known forever. A link forwarded by mistake stays open for years.
04
The database isn't exposed to the internet
App and database run as containers on a single server, deployed by Coolify from a Dockerfile. The database port is open only on the internal network; migrations go through an SSH tunnel.
Without it: A database reachable from outside hands over a whole class of attacks: port scanning and continuous authentication attempts.
05
Everything is hosted in Europe
Server and database at Hetzner in Germany, files on Cloudflare R2 in the EU-restricted region. The data concerns Spanish individuals: staying inside the Union removes the international transfer, and with it the contractual apparatus that would otherwise have to be justified to the AEPD.
Without it: Hosting outside the EU would mean documenting and defending every transfer. The processing register is already written in the AEPD's format.
06
Six permissions on top of five roles
The role sets the baseline, permissions are granted individually: batches, invoicing, payments, accounts, locking, financial figures. A fitter, besides, only sees the files assigned to them.
Without it: With roles alone, a secretary who must invoice but not unlock fits no box, and you end up inventing a role per exception.
07
A submitted file locks
Once sent to the scheme operator, the file becomes read-only and unlocking is a deliberate act, recorded in the log.
Without it: A well-meant correction after submission desynchronises the local file from the operator's, and it's the local one that looks right.
08
Documents are kept for years
The CAE scheme requires supporting documents to be kept at least five years after operations end, six for Spanish tax obligations. Hence the complete per-file archive and the log that records who produced what, and when.
Without it: An inspection three years later finds a lost spreadsheet and documents never gathered.
Performance
Measured on the app, before and after. Desk software opened fifty times a day is judged on how it feels, not on a tool's score.
24 to 48 ms6 ms
The file page
It loaded its eight blocks one after another, each waiting on the last. One query is enough for the access checks; the other seven now leave together. It's the most opened page in the software.
21 ms3 to 5 ms
The dashboard
Seven counters computed single file, now fired at once. On top sits an in-memory cache whose lifetime depends on the role: five minutes for an admin, two for someone in the field.
Full scanIndex read
Filtering and sorting
Fifty-two indexes declared in the schema, including composite ones on the pairs actually used: status and date, a user's unread notifications. Writes cost about 5% more; at this volume that's painless.
30 ms3 ms
Bulk actions
Locking ten files fired ten successive updates. One does the job. The same reasoning applies to grouped inserts of payments, documents and invoice lines.
- Our role
- Scoping, architecture, build, ongoing
- Type
- Business software
- Technologies
- Next.js 16
- TypeScript
- PostgreSQL 17
- Drizzle ORM
- Better Auth
- Cloudflare R2
- pdf-lib / PDFKit
- FullCalendar
- next-intl
Tell us what you are trying to build.
Message us on WhatsAppOther work

Terres de Villas
Luxury real estate · France
A young agency in a market where credibility is decided before the first appointment. Facing names established for twenty years, it had to look settled from day one, and exist in English without doubling the typing.
Read the case study

SAVY
Personal finance · Studio product
Keeping a budget together happens in a shared spreadsheet, or not at all. Someone fronts an expense, nobody knows where the month stands, and the conversation always comes back to the same place: who paid for what.
Read the case study

Regarde-moi
Vocational training · France
A Qualiopi-certified beauty training centre near Avignon whose visibility rested on Instagram and word of mouth. Someone searching for “eyelash extension training Avignon” didn't find it, and her first question, how to pay for it, was answered nowhere.
Read the case study

