Published July 2026 · Infrastructure notes from 3DN
DutchBud is our personal banking companion — an Android app talking to bank.dutchie.org over a JSON API. Getting it from “works on my laptop” to “works on my phone over WireGuard at 02:00” touched DNS, OTAP deploy pipelines, a deliberate Laravel-to-Go API split, and a regression harness that compares every Acceptatie release against production-shaped data. This post is the tour.

Setting up DutchBud end to end
The stack is deliberately boring — which is a compliment:
- Android client — Kotlin app using token auth against
/api/*endpoints. - Laravel web app — still serves the browser UI, migrations, and OTAP deploy scripts in a dedicated web root per environment.
- Go
bank-api— production JSON API on paired application servers as a systemd service; nginx routes/api/through HAProxy to Go while Laravel keeps everything else. - Edge access — WireGuard clients resolve internal names via our authoritative DNS;
bank.dutchie.orgreaches the Thailand DMZ front on the private network.
Early phone testing surfaced a classic ops trap: a rogue BIND instance on the gateway answering DNS with broken AAAA behaviour, so WireGuard resolution looked fine until it wasn’t. Pointing clients at the correct authoritative resolver, stopping the stray service, and fixing port forwarding restored predictable resolution. Login 422s turned out to be reachability, not bad credentials — once DNS and routing were honest, the app came back.

Why we moved the API from Laravel to Go
Laravel is excellent for product velocity. It is less excellent when every API call pays a full PHP framework bootstrap tax on a path the Android app hits dozens of times per session. We measured on the same hosts before committing:
- Laravel framework bootstrap alone: ~100 ms before your controller runs.
- Go
GET /api/bank-accounts: ~12 ms on localhost. - Go
GET /api/payments?page=1: ~150 ms (database-bound, not framework-bound). - Go
POST /api/login: ~280 ms — bcrypt still dominates; the win is elsewhere.
Decision criteria were practical, not ideological:
- Latency on hot JSON paths — accounts and list endpoints must feel instant on mobile.
- Memory footprint — a small Go binary per node beats PHP-FPM pools for a read-heavy API.
- Operational simplicity — one static binary, structured logging, configuration on the API host.
- Keep Laravel where it shines — Blade UI, Eloquent migrations, existing OTAP playbooks unchanged.
/api/, Laravel on the web surface.OTAP CI — from dev push to production gate
The bank application is our OTAP pilot. GitLab CI on a dedicated runner deploys by branch:
- dev → Ontwikkel, then fast-forwards tst → Test.
- Commits must carry
Refs #N; the pipeline posts “Ready for Test” on the linked issue. - acc → manual Acceptatie deploy after customer approval on Test.
- master → manual Productie.
Deploy is a hardened SSH git checkout on the application host: fetch, hard reset to the pipeline SHA, fix permissions, run environment setup when present. No rsync mysteries — just git truth on every stage.
acc branch.Regression testing on real production shape
Acceptatie is not a toy database. After manual deploy:acceptatie, CI runs:
- sync:prod-db-to-acc — copy production data into the acceptance database plus receipt storage sync.
- migrate:acc — apply pending Laravel migrations on ACC.
- regression:check — hammer the live ACC API and compare against a committed baseline.
The harness lives in the bank-api repository. Four scenarios — login, bank-accounts, budget-posts, payments page 1 — each run 20 sequential iterations (~6 seconds total). For every scenario we record HTTP status, a shape hash of the JSON payload (structure and keys, not volatile values), and p95 latency. Initial baseline after prod sync: login p95 598 ms, bank-accounts 116 ms, budget-posts 184 ms, payments 178 ms, against 58 payments and 3 accounts.
The check job fails the pipeline if shape changes or p95 exceeds baseline × tolerance (1.25× default, 1.5× for login). Need a new baseline after an intentional API change? Run manual regression:capture and commit the updated baseline in the bank-api repo. Results land in the GitLab job log — no external SaaS required.
What we learned
Mobile apps punish small infrastructure sins. DNS that almost works is worse than DNS that clearly fails. Splitting API runtimes is fine when the boundary is nginx and the contract is JSON. OTAP only earns trust when Test and Acceptatie are mechanically different — manual promotion plus automated regression on prod-shaped data is that difference.
DutchBud is personal software on professional rails. The next time you see a green accounts screen load in under a second over WireGuard, that is Go, PowerDNS, GitLab CI, and a baseline file saying “still the same shape as yesterday.”
Questions about our infrastructure or hosting approach? Get in touch.
Leave a Reply