Other services · custom software & apps
Get software your team actually owns.
Web apps, mobile apps and the internal systems that run the business. Built fast, handed over completely, priced on demand.
144ms
p95 API response
99.9%
uptime target
72%
code you own
Today · 6 jobs
Offline queue
2 reports saved on device. They will sync the moment signal returns.
What we build
Four surfaces, one system underneath.
A customer portal, an app on a van, an admin tool and the integrations between them are four windows onto one database, one set of rules and one audit trail. Build them as four products and they will disagree by Christmas.
Web apps
The product your customers log into. Dashboards, portals, booking, checkout, whatever the business actually is.
Mobile apps
iOS and Android from one codebase, shipped to both stores, with push, deep links and an offline queue included.
Internal systems
The tool your team currently runs as a spreadsheet with forty tabs. Roles, approvals, exports, audit trail.
Integrations
Your CRM, your billing, your calendar, your warehouse. One system of record instead of five that disagree.
The same request, sent twice
the customer is charged once. the retry is a lookup, not a repeat. this is the difference between a support ticket and a refund.
nothing slow ever happens inside a request. a failed job retries with backoff, then lands in a dead letter queue with its payload attached.
The plumbing, where products quietly go wrong
Idempotency keys on every write
The phone loses signal mid-request and retries. The same key returns the same order instead of charging twice. Keys are stored with the response for 24 hours, so a retry is a lookup rather than a repeat.
Background jobs, never a blocked request
Anything slower than the user's patience goes to a queue: PDFs, exports, imports, emails, reindexing. Retries with exponential backoff, a dead letter queue, and a job you can requeue by hand at 2am.
Webhooks you can actually verify
Outbound events are signed with HMAC and a timestamp, retried on a schedule you can read, and replayable from the dashboard. Inbound webhooks get signature checks and a five minute replay window.
Rate limits that tell the truth
A token bucket per API key and per IP, the remaining budget in the response headers, and a 429 that carries Retry-After instead of leaving an integrator guessing.
Uploads that never touch the API
The browser asks for a signed URL and posts straight to Cloud Storage. Your server never streams a two gigabyte file, and a stalled upload cannot take a request thread with it.
Websockets only where they earn it
Live cursors, a dispatch board, a chat thread: yes. A dashboard somebody refreshes twice an hour: no. A socket you do not need is a reconnection bug you will have anyway.
Authentication & authorisation
It starts with who is allowed in.
Every serious product is a permissions problem wearing a user interface. So the roles get drawn before the screens do, and the matrix below is a real artefact from a real build, not a diagram.
Permission matrix · field operations build
Six roles, six capabilities, thirty-six decisions made on purpose.
| Role | View jobs | Edit jobs | Approve invoices | Export data | Manage users | View payroll |
|---|---|---|---|---|---|---|
| Ownerthe account holder | ||||||
| Ops managerruns the schedule | ||||||
| Field techon the van | own rows | own rows | ||||
| Financepays and bills | ||||||
| Client contactoutside the company | own rows | own rows | ||||
| Auditorread only, time boxed |
Scoping enforced in the database, not only in the code
the tenant filter lives in Postgres. a new endpoint that forgets its WHERE clause returns nothing, instead of returning everybody.
a refresh token is single use. if a burnt one ever comes back, the only explanation is theft, so the whole family dies.
Sessions or tokens, and when each is the right answer
Sessions vs JWT
Server sessions
JSON web tokens
server side, one row per session
inside the token the client is holding
delete the row, gone on the next request
impossible until it expires, unless you keep a deny list and lose the point
one lookup, cached in memory
a signature check, no round trip at all
one backend and a browser, which is most products
mobile, service to service, and anything crossing a trust boundary
httpOnly, Secure, SameSite=Lax cookie, 30 day sliding window
15 minute access token with a rotating refresh token beside it
Passwords hashed with argon2id
Memory hard by design, tuned to 64 MiB and three passes so a stolen table is worthless at scale. bcrypt at cost 12 where a legacy system forces it, rehashed transparently on the next successful login.
Refresh tokens rotate, and reuse is fatal
Every refresh mints a new token and burns the old one. If a burnt token ever comes back, the whole family is revoked and the session dies, because the only way that happens is a stolen token.
MFA that is not just SMS
TOTP apps and WebAuthn passkeys, recovery codes shown exactly once, and step up prompts on the actions that matter rather than on every single login.
SSO when the buyer is an enterprise
SAML 2.0 and OIDC against Okta, Entra ID or Google Workspace, with SCIM provisioning so an offboarded employee loses access from the HR system, not from a memo.
Row-level security in the database itself
The tenant filter lives in Postgres, not only in the ORM. A forgotten WHERE clause in a new endpoint returns nothing instead of returning everybody's data.
The boring hardening nobody demos
Login throttling per account and per IP, single use reset tokens that expire in fifteen minutes, no email enumeration on any endpoint, and a session list the user can revoke from.
Data · Postgres by default
Then with where the truth gets kept.
The schema outlives the framework, the design system and probably us. It is the one decision that is genuinely expensive to change later, so it gets drawn on paper before anybody opens an editor.
Schema sketch · the first hour of a build
Four tables, every relation named, every tenant boundary visible.
tenants
- pkid uuid
- ··name text
- ··plan text
- ··created_at timestamptz
users
- pkid uuid
- fktenant_id uuid
- ··email citext unique
- ··role text
- ··mfa_enabled bool
jobs
- pkid uuid
- fktenant_id uuid
- fkassignee_id uuid
- ··status text
- ··scheduled_for timestamptz
job_events
- pkid bigint
- fkjob_id uuid
- ··kind text
- ··payload jsonb
- ··at timestamptz
Every table that holds customer data carries tenant_id, and every one of them is covered by a row level security policy. Events are append only, so the history of a job is a fact rather than a reconstruction. Money is stored in integer minor units, because floating point currency is a bug with a delay on it.
Why Postgres, unless you have a reason
Transactions that actually hold, so an invoice and its line items are either both there or neither is.
JSONB when a column is genuinely shaped like a document, indexed, in the same query as the relational columns.
Full text search built in, which is one fewer service to run until the day you truly need one.
Constraints, checks and foreign keys, so the data cannot go wrong in the first place rather than being cleaned later.
Extensions when you need them: PostGIS for maps, pgvector for embeddings, pg_stat_statements for finding the slow query.
Thirty years old, boring, and supported by every host on earth including the one you already use.
We will happily reach for something else when the shape demands it: Redis for caching and rate limits, BigQuery when analytics outgrow the transactional database, a search engine when full text stops being enough. What we will not do is start there.

One index, and the two and a half seconds it gave back
one composite index, created concurrently so nothing locked, and the screen everybody stares at all day stopped being the slow one.
Connection pooling
5,000 : 20
client connections in front, server connections behind, in transaction mode. Postgres falls over long before 5,000 backends.
Read replicas
3 regions
reads served from the nearest replica, writes always to one primary, lag monitored and alerted on.
Slow query log
over 200 ms
anything past the budget is captured with its plan, so tuning starts from evidence.
Migrations that nobody has to schedule at midnight
Zero downtime migrations · expand then contract
Five deploys instead of one outage.
01
Expand
Add the new column nullable, with a default that costs nothing. Nothing reads it yet.
02
Backfill in batches
Ten thousand rows at a time, in its own transaction, throttled so replicas keep up.
03
Dual write
The application writes both shapes while both are still valid. Deployable and revertible at any point.
04
Switch reads
Behind a flag. If the numbers disagree, the flag goes back and nobody has a bad evening.
05
Contract
Drop the old column in a later release, once no running instance still remembers it.
Backups, and the restore we have actually run
- Base backupnightly
encrypted, stored in a separate project from the database
- Write ahead logshipped continuously
which is what makes point in time recovery possible at all
- Recovery pointunder 60 seconds
the most data any incident should be able to cost you
- Last restore drill12 minutes
full restore to a scratch instance, row counts compared, quarterly
a backup nobody has restored is a folder of hope. this one gets proven every quarter, on the clock.
Ownership · the part nobody reads until it hurts
Rented software is somebody else's asset.
Your repo, your cloud account, your data, your domains, your keys. We build inside your walls and hand the whole thing back, documented, on the day we finish.
Infrastructure · Google Cloud, in your project
Which is why it ships to your cloud, not ours.
One Google Cloud project, in your organisation, paid on your card. We hold access while we build and hand it over when we are done. Nothing here is a wrapper only we understand.
A deploy, from the console it actually prints to
the old revision keeps serving until the new one is healthy and holding traffic. rolling back is one command against a digest that never changed.
The rollout, watched
- t + 010% of traffic
error rate 0.02%
- t + 2 min50% of traffic
error rate 0.02%
- t + 5 min100% of traffic
error rate 0.01%
Autoscaling underneath it: minimum one instance so nothing a customer touches starts cold, maximum a hundred so a bad Monday cannot become a bad invoice, eighty concurrent requests per instance. Staging scales to zero, because nobody is waiting.
Which Google Cloud services, and what each one is for
The bill of materials
Every one of these is a managed service you could hire anybody to operate. That is the point of picking them.
Runs the container. Scales from zero to hundreds of instances on request volume, and back down when the day ends.
no servers to patchThe managed primary, with a high availability standby in a second zone and automated failover.
managed, not magicFiles, uploads, exports and backups. Signed URLs in and out, lifecycle rules to cold storage.
your bucket, your billEvery container image we ever deploy, immutable and digest addressed, scanned for CVEs on push.
rollback is a digestEvery credential the app uses, versioned, IAM controlled, injected at runtime and never written to a repo.
no .env in git, everOne anycast address worldwide, TLS terminated at the edge, static assets cached in the city the user is in.
one IP, 100+ points of presenceStructured logs with a request id, latency and error dashboards, distributed traces through every hop.
you can see it breakThe event backbone and the cron. Fan out, retries and dead letter topics without a queue server to babysit.
nothing to run at 3amthe environment can be rebuilt from an empty project. that is the only honest test that it is documented.
Infrastructure as code, not as memory
Every bucket, service, database, secret and IAM binding is declared in Terraform and reviewed in a pull request. The environment can be rebuilt from an empty project, which is the only real test that it is documented.
Staging is a mirror, not a mood board
Same regions, same runtime, same migrations, same infrastructure code, on anonymised data. A change that works there is not a promise, but a change that fails there never reaches your customers.
Zero downtime deploys, every time
A new revision boots, passes its health check, then takes traffic in steps while the old one still serves. If error rate moves, traffic goes back in one command and nobody has to rebuild anything.
Alerts that wake somebody
Error rate, p95 latency, queue depth, failed jobs, certificate expiry, budget. Each with a threshold, a window, and a human on the other end. An alert nobody is paged for is a dashboard, not an alert.
What wakes a human up
Alert policies · observability that ends in a phone call
Logs, traces and dashboards tell you what happened. These are the lines that tell somebody it is happening now.
page on-call, roll traffic back to the previous revision
page on-call, check pool saturation and slow queries
scale workers, notify the channel
ticket raised automatically with the payload attached
ticket raised, renewal is automated anyway
Worldwide speed
And it answers fast from every timezone you sell into.
An average is a comforting number. The person who leaves is in the tail, so the budget is written at the ninety-fifth and checked at the ninety-ninth percentile.
measured from the application, on real requests, from wherever the person happened to be standing.
The budget, agreed before the first line of code
- API response, p95under 200 ms
measured on real requests, not on your laptop
- Cold startunder 400 ms
and kept warm entirely where latency is revenue
- Time to interactiveunder 2 s on 4G
on the phone your customer actually owns
- Uptime99.9%+
with a status page you can point a client at
A budget only counts if something fails when it is broken. These are asserted in CI on a synthetic run and alerted on in production.
Reads follow the user, writes go to one place
One primary, replicas where your customers are
Multi-region reads without the two answers problem.
Every write in the system lands here, in order, once. A standby in a second zone takes over automatically if the primary disappears. There is exactly one version of the truth, which is the entire reason this is not four databases.
| Cached at the edge | For how long | Where it is served from |
|---|---|---|
| Hashed JS, CSS, fonts | immutable, one year | edge + browser |
| Images and public media | 30 days, revalidated | edge |
| Public catalogue and pricing pages | 60 s, stale while revalidate | edge |
| Anything behind a login | never cached at the edge | origin only |
| Anything scoped to one tenant | never cached at the edge | origin only |
Edge caching is free speed right up until it serves one customer another customer's data. So the rule is blunt: if a response depends on who is asking, it never leaves the origin.
Reads follow the user, writes do not
One primary owns every write, so two people editing the same record never end up with two different answers. Read replicas sit in the regions you actually sell into, and read only queries are routed to the nearest one.
Replica lag is a number, not a hope
Lag is monitored and alerted. Any read that must be perfectly fresh, such as the screen right after a save, is pinned to the primary on purpose rather than by accident.
Cold starts, handled honestly
A container from cold is 380 ms and warm is 22 ms. Minimum instances stay above zero on anything a customer touches, and scale to zero on staging where nobody is waiting.
The database is usually the slow bit
Every endpoint over budget gets EXPLAIN ANALYZE, not a bigger instance. Ninety percent of the time it is one missing index, one N+1 loop, or a query fetching columns nobody reads.

Mobile · one codebase, two stores
Even in a pocket, with no signal at all.
A basement, a lift, a rural job, a plane. The app cannot stop working because the network did, so the network is treated as an optional extra from the first commit rather than as a repair job later.
The offline queue, and the moment it drains
Sync queue · 4 changes waiting
Written to the device
4.1 MB held locally, each change with its own id. Nothing is lost if the app is killed, the battery dies, or the phone stays in a basement until Thursday.
the queue drains in order, every call carries its idempotency key, and the one genuine conflict is resolved by a rule somebody agreed to in the spec.
Shipping an update
Most changes never touch a store queue.
Deep links, push, and the store review nobody plans for
One codebase, two stores
React Native and Expo, so iOS and Android ship from the same repository and the same pull request. Native modules where a platform genuinely differs, which is far less often than people expect.
Offline first, not offline tolerant
Writes land in a local queue with a client generated id, the UI updates immediately, and the queue drains when signal returns. Conflicts resolve by rule, and anything ambiguous is shown to a human instead of silently overwritten.
Push that respects the person
Tokens registered per device and cleaned up when they go stale, notifications carrying the payload the screen needs, quiet hours honoured, and a preferences screen that actually turns things off.
Store review, planned for
Account deletion in the app because Apple requires it, privacy labels that match what the app really collects, Sign in with Apple whenever another social login exists, and a first build submitted early so review is never on the critical path.
Deep links · one tap, the right screen
fieldops://job/4821
the job detail screen, signed in
https://app.example.com/job/4821
the same screen, or the web app if no app is installed
fieldops://invite?t=…
accept invite, then straight into the account
A push notification, an email and a QR code all land on the same screen, signed in, with the record already open. Anything less and the notification is just an interruption.

Security, ownership & handover
So here is exactly what you get handed.
Security is not a section at the end of a proposal. It is ten specific failure modes with ten specific answers, and every one of them is a decision made while the code is being written.
The OWASP top ten, in practice
OWASP top ten · what we actually do about each one
Deny by default, checks on the server for every route, plus row level security in Postgres underneath.
TLS 1.3 everywhere, encryption at rest on disks and buckets, argon2id for passwords, no home made crypto.
Parameterised queries only, no string built SQL, output escaped by React, a content security policy on top.
Threat modelling in the spec, rate limits and quotas designed in, abuse cases written next to the happy path.
Infrastructure as code, least privilege service accounts, no default credentials, staging locked down like production.
Dependabot on the repository, image scanning in Artifact Registry, a patch window that is days not quarters.
MFA, throttling, rotating refresh tokens, sessions that expire, no enumeration on login or reset.
Signed webhooks, pinned dependencies with a lockfile, CI that builds the artefact once and promotes the same digest.
Structured logs with a request id, alerts wired to a human, audit trail on every write that matters.
Outbound calls allowlisted, metadata endpoints blocked, user supplied URLs never fetched from inside the network.
one day somebody will ask who changed the total on invoice 8841. this is that answer, with the previous value still attached.
GDPR shape · retention, written down
| Data | Kept for | Lawful basis |
|---|---|---|
| Account and profile | life of the account, then 30 days | contract |
| Job and service history | 6 years | legal obligation |
| Application and access logs | 90 days | legitimate interest |
| Marketing preferences | until withdrawn, then proof kept | consent |
| Backups containing any of it | 35 day rolling window | documented in the processing record |
Export and deletion are buttons in the admin tool, not a three week engineering favour. Deletion cascades through backups on the documented window, and the processing record is a real document your lawyer can read.
Security, by default
- Secrets live in Secret Manager, injected at runtime, rotated on a schedule. A commit that contains a key is blocked before it merges.
- Encrypted in transit with TLS 1.3 and at rest on every disk, bucket and backup, with keys you can bring yourself.
- Dependencies scanned on every push and every image, with a patch window measured in days.
- Audit logging on every write that matters, because one day somebody will ask who changed it and when.
- GDPR shaped by default: export, deletion, retention windows, and a processing record that is a real document.
- Penetration test friendly: we will fix what a tester finds, and we would rather they find it than a customer does.
What is yours on day one
- The repository is yours, in your organisation, from commit one.
- The cloud account is yours. We build inside it, we do not rent it back to you.
- Domains, DNS and certificates stay in your name, on your registrar.
- Documentation, environment variables and runbooks handed over in writing, not in somebody's head.
- No proprietary lock-in layer that only we can maintain. Standard React, standard Postgres, standard Google Cloud.
- A handover call with your next engineer, whoever they turn out to be.
How an engagement runs
How engagements run · priced on demand
01
Scope
One call, then a written spec: users, flows, data, integrations, and the non-goals in writing.
02
Prototype
A clickable build in weeks, not a slide deck. You use it on real screens before we scale it.
03
Flat quote
One number for the build. Change requests priced before they start, never after.
04
Ship and hand over
Deployed to your cloud, documented, with the keys and the runbook in your hands.
Software is not a SKU. One flat number for the build lands after the first call, and it does not move unless the scope does.
Bring the problem.
Leave with a plan.
One call, thirty minutes. What the software has to do, who touches it, what it must connect to. You leave with a scope and a number, whether or not we build it.
4.9/5 from 40+ organizersKeep going · the rest of the stack
Software is one desk. The rooms still need filling.
The marketing site that sells the product we built.
Paid acquisition once the product is ready for volume.
Lifecycle email wired straight into the app's own events.
The same team fills B2B event rooms for a living, which is why we care so much about products that hold up under a real audience. See the flagship: virtual event marketing








