skip to content
cd/
projects

loopvet hub

What I use to bring a clinic's data into Loopvet. Before importing, it shows what would happen without touching anything.

tl;dr#

loopvet hub is the workbench I use to move vet clinics from other systems into Loopvet. It puts three tools together: an auditor that shows what an import would do with the files without writing a thing; an importer that pushes the file queue through the real admin panel; and a debts tool that brings over what each client owed in the old system. The hub just opens and closes them, and each one runs in its own process with its own guardrails. It's a personal project: nobody asked for it, I built it for my own work, on top of the Loopvet API. The code is private, and there's no clinic data here.

the problem#

migrating a clinic means bringing over years of records: clients, patients, weights, vaccines, visits, exams, appointments. the data shows up as spreadsheets, already converted, and it has to land in Loopvet without breaking anything.

before the hub, every step was its own manual grind:

and the worst part: importing into production has no undo.

the idea#

whatever only reads lives on one side, whatever writes lives on the other. that split is baked into the architecture, so it doesn't depend on anyone remembering.

            hub (launches and stops, knows nothing about clinics)
              │
   ┌──────────┼──────────────────┐
   ▼          ▼                  ▼
auditor    importer            debts
read-only  drives the panel    orders API
   │          │
   └── entities (shared package: what exists and in which order it goes up)

decisions#

each tool in its own process, no single app. the auditor and the importer promise opposite things: one never writes, the other writes to production. if everything ran in one server, that promise would just be a code rule someone could forget. kept apart, the auditor doesn't even have the browser-automation library installed.

the hub isn't Next. it stays on all day, so how heavy it is matters. I measured it on the machine: an idle Next server used 382 MB; the bare Bun hub, already serving, uses 22 MB.

measure before packaging. the first idea was to package everything as a desktop app so it "wouldn't be heavy". once I measured, the weight was in the editor and the browser (over 8 GB together), not in the tool. a desktop shortcut solved "click and open" without any packager.

open fast. switching the tools from next dev to next start cut the time from click to screen from 2283 ms to about 580 ms.

when in doubt, lock it. in production, without a password, no route answers. opening without one only works on the machine itself and through the hub, both at once. one alone isn't enough: on its own, it would be a switch that turns security off.

never kill an import halfway. closing the hub shuts the tools down, except the one that says it's busy. the importer with a running queue stays up and tells you, because a file cut off mid-upload ends up in a state nobody can figure out.

the clinic's ok is ticked, never guessed. a migration only goes to production after the clinic checks everything in the test environment and confirms. that ok comes from a conversation between people, so it's a field someone ticks. nothing in the flow gets to assume an "ok" because nobody answered or because time passed, especially right before writing somewhere with no undo.

one place for the truth. the eleven import entities were copied across two repos, with 179 lines identical byte for byte. I merged everything into a monorepo, keeping the history of both, and the entities became a package both tools use.

how I work on it#

every change starts as a written task: why it matters, the risk, what it depends on and when to stop and ping me. bigger decisions become ADRs. AI plays separate roles:

the bar is the same as CI: bun test, typecheck and lint on every push.

by the numbers#

what's next#

the hub already has a spot saved for Linear, Drive and the whole migration flow, from the issue to the production import. each one will come in as another tool, with its own process and guardrails. the hub keeps doing one thing only: opening and closing.