Case study · Platform

JetCustoms: the whole shop on one system, in three months

Client
JetCustoms
Sector
Automotive
Engagement
Turnkey project
Duration
3 months, first conversation to production
Team
One senior engineer
Stack
Next.js · TypeScript · Postgres
On corvale.pt

The challenge

Ten users, five business domains, and no system of record. Jobs, hours, parts and client statements each lived in a separate spreadsheet or hand-made PDF, so the same entity existed several times over with no key joining the copies and no constraint preventing them from diverging. Every aggregate a manager asked for — cost per vehicle, margin per project, hours booked against a job — was recomputed by hand from documents that could already disagree, and nothing in the process was able to detect that they did.

What we did

One application over one Postgres database, with the five domains modelled as bounded contexts inside a single schema rather than as separate services. Each fact — an hour, a part, a vehicle, a settlement line — is written once and referenced by key; aggregates are derived by the engine rather than assembled by a caller. Authorisation is a schema concern, not a UI concern, so the same predicates hold for the application, an admin path and an ad-hoc query alike. Shipped with a handbook and an engineering guide, so the shop is not dependent on us to change it.

The decision that shaped every other one: a cross-domain question should be a join, not an integration.

One database rather than five services buys referential integrity enforced by the engine, cross-domain reads inside a single transaction, and no inter-service contract to version. What it costs is independent deployability: a migration touching finance ships with everything else, and the module boundaries are a review discipline rather than a wall the runtime enforces. At ten users with one engineer that trade is correct by a wide margin. The boundaries are drawn in the schema from the first migration rather than discovered later, which is what keeps the decision reversible if the shape of the problem changes.

What it covers

ModuleWhat it ownsReplaced
ERPProjects, vehicles, hours, materials, approvalsExcel, WhatsApp
FinanceSettlement notes, rates per section, PDF statementsHand-made PDFs
HRTimesheets, schedules, holidays and time offPaper, reprinted
CRMClients, key account owners, automatic follow-upsMemory
CMSPublic content in two languages; drafts, preview, one-click publishAn agency

How it is built

AreaImplementation
AuthorisationRow-level security policies on every table. Five domain roles, many-to-many with users, so one account can hold more than one. The predicates apply to the application, admin paths and an ad-hoc session equally — there is no caller trusted by its origin
AuthenticationPassword or magic link behind a swappable provider. A new principal authenticates but reads nothing until approved, so the default state of an unknown user is no rows rather than whatever the interface remembered to hide
Data integrityTotals are generated columns and triggers, never application arithmetic. Settlement lines are append-only once issued and a correction is a new line, so the history of a figure survives being corrected. A parity test asserts every total equals the sum of its lines
File transferSingle-use signed URLs issued by the application, redeemed directly against object storage. No file bytes traverse the app server, so upload size is not a deployment concern and an expired link is simply expired
Schema changesDev first, always. Ordered, recorded migrations, never hand-edited against the live schema. Types are generated from the schema and ship in the same commit, and CI fails the build when code and database disagree about shape
TestsUnit tests, an RLS matrix and the parity test, all run in CI against a real database rather than a mock — the behaviour under test belongs to Postgres. Lint at zero warnings. Destructive maintenance scripts are tested to refuse when the target is production
EnvironmentsTwo: production, and a disposable clone of its schema and data for development. Copies flow one way only. The clone is noindex so it never competes with the live site in search
Build and deployOne workflow per branch, same ordered steps: checks, migration, type regeneration, SQL tests, build, deploy. The pipeline triggers the deploy as its final act and the host's git auto-deploy is off, because a build that starts on push races the migration it depends on

Start with a straight answer.

A fixed-price technical assessment, with no commitment afterwards. Or simply book a call.