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.

    We also ship the site

    144ms

    p95 API response

    99.9%

    uptime target

    72%

    code you own

    9:41
    Field Opsv2.4.0

    Today · 6 jobs

    08:30Site survey · Camden
    11:00Install · Shoreditchlive
    14:15Handover · Kings Cross

    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.

    ReactTypeScriptPostgres

    Mobile apps

    iOS and Android from one codebase, shipped to both stores, with push, deep links and an offline queue included.

    React NativeExpoApp Store + Play

    Internal systems

    The tool your team currently runs as a spreadsheet with forty tabs. Roles, approvals, exports, audit trail.

    Admin UIRole-based accessAudit log

    Integrations

    Your CRM, your billing, your calendar, your warehouse. One system of record instead of five that disagree.

    REST + webhooksStripeSSO

    The same request, sent twice

    POST /v1/orders · the same request, twiceidempotent
    $ POST /v1/orders Idempotency-Key: 8f2c-41d9
    201 Created order_9f21 · card charged · once
    # the phone lost signal mid-request and retried the identical call
    $ POST /v1/orders Idempotency-Key: 8f2c-41d9
    200 OK order_9f21 · replayed from the key store · not charged again
    $ POST /v1/orders Idempotency-Key: 4b70-92aa
    201 Created order_9f22 · a new key is a new order, as intended
    X-RateLimit-Limit: 600 Remaining: 583 Reset: 37s
    429 Too Many Requests Retry-After: 12 · never a silent drop

    the customer is charged once. the retry is a lookup, not a repeat. this is the difference between a support ticket and a refund.

    workers · queue depth 41 · 8 concurrentbackground jobs
    jobqueueattemptstatetook
    invoice.renderdefault1 / 5done1.2 s
    report.export.csvheavy1 / 5running18 s
    webhook.deliveroutbound3 / 8retryingbackoff 4 m
    email.receiptdefault1 / 5done310 ms
    search.reindexheavy1 / 3done42 s
    # exports, PDFs, imports, emails, reindexing. all of it, off the request path.

    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.

    RoleView jobsEdit jobsApprove invoicesExport dataManage usersView payroll
    Ownerthe account holder
    Ops managerruns the schedule
    Field techon the vanown rowsown rows
    Financepays and bills
    Client contactoutside the companyown rowsown rows
    Auditorread only, time boxed
    allowed everywhereown rowsallowed, but only on the rows this account owns denied, and denied by default

    Scoping enforced in the database, not only in the code

    psql · row level security on the jobs tableenforced below the app
    -- the request sets its tenant before it touches a row
    SET LOCAL app.tenant = '7c1f9a20…';
    ALTER TABLE jobs ENABLE ROW LEVEL SECURITY;
    CREATE POLICY tenant_isolation ON jobs
    USING (tenant_id = current_setting('app.tenant')::uuid);
    -- an endpoint that forgot to filter
    SELECT count(*) FROM jobs;
    1,842 -- this tenant only
    2,391,664 -- what it would have returned without the policy

    the tenant filter lives in Postgres. a new endpoint that forgets its WHERE clause returns nothing, instead of returning everybody.

    auth · refresh token rotation, and a stolen onerotation + reuse detection
    $ POST /auth/refresh rt_a1f…
    200 OK access 15m · new refresh rt_b7c… · rt_a1f burnt
    $ POST /auth/refresh rt_b7c…
    200 OK access 15m · new refresh rt_c93… · rt_b7c burnt
    # a copy of an already used token arrives from another device
    $ POST /auth/refresh rt_b7c…
    401 reuse detected
    token family revoked · 3 sessions ended · user emailed
    argon2id (m=64MiB, t=3, p=4) · bcrypt cost 12 only where a legacy system insists

    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

    Where the truth lives

    server side, one row per session

    inside the token the client is holding

    Revoking access

    delete the row, gone on the next request

    impossible until it expires, unless you keep a deny list and lose the point

    Cost per request

    one lookup, cached in memory

    a signature check, no round trip at all

    Where it wins

    one backend and a browser, which is most products

    mobile, service to service, and anything crossing a trust boundary

    What we ship by default

    httpOnly, Secure, SameSite=Lax cookie, 30 day sliding window

    15 minute access token with a rotating refresh token beside it

    There is no universally correct answer, only a correct answer for your shape. A browser product with one backend gets cookies and sessions, because logging somebody out should not take fifteen minutes. A mobile app talking to several services gets short access tokens with rotating refresh beside them.

    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.

    A desk with dashboards and reports open across two screens
    the query plan is read here, before anybody suggests a bigger instance.

    One index, and the two and a half seconds it gave back

    psql · EXPLAIN ANALYZE on the jobs list, 2.4 million rows2,410 ms to 12 ms
    $ EXPLAIN ANALYZE SELECT * FROM jobs
    WHERE tenant_id = $1 AND status = 'open'
    ORDER BY scheduled_for LIMIT 50;
    Seq Scan on jobs (rows=2,391,664 width=184)
    Filter: (tenant_id = $1 AND status = 'open')
    Rows Removed by Filter: 2,389,822
    Execution Time: 2,410.338 ms ·· before
    $ CREATE INDEX CONCURRENTLY jobs_tenant_status_sched
    ON jobs (tenant_id, status, scheduled_for);
    Index Scan using jobs_tenant_status_sched (rows=1,842)
    Execution Time: 12.044 ms ·· after · same query, same hardware

    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

    restore drill · quarterly · not a slideverified
    $ gcloud sql instances clone prod-pg drill-pg
    --point-in-time 2026-07-24T14:32:07Z
    restoring base backup … 41.2 GB
    replaying write ahead log to 14:32:07 …
    ready in 12m 04s
    $ psql drill-pg -f verify.sql
    jobs 1,842,551 rows match
    invoices 212,904 rows match
    checksums all green
    # recovery point 47 s · recovery time 12 m · both inside target

    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

    gcloud run deploy api · production0 s downtime
    $ gcloud run deploy api --image …pkg.dev/acme/api@sha256:9f21c4
    Deploying container to service [api] in region [europe-west1]
    image already in Artifact Registry, nothing rebuilt
    revision api-00147-fjr created
    health GET /healthz 200 OK in 1.8 s
    migrations 1 applied, expand phase, reversible
    Routing traffic
    api-00146-b2k 100% → 90% → 50% → 0%
    api-00147-fjr 0% → 10% → 50% → 100%
    ✓ live · 0 dropped requests · 0 s downtime
    # and if the error rate had moved
    $ gcloud run services update-traffic api --to-revisions api-00146-b2k=100
    ✓ rolled back in 9 s

    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.

    Cloud Run

    Runs the container. Scales from zero to hundreds of instances on request volume, and back down when the day ends.

    no servers to patch
    Cloud SQL for Postgres

    The managed primary, with a high availability standby in a second zone and automated failover.

    managed, not magic
    Cloud Storage

    Files, uploads, exports and backups. Signed URLs in and out, lifecycle rules to cold storage.

    your bucket, your bill
    Artifact Registry

    Every container image we ever deploy, immutable and digest addressed, scanned for CVEs on push.

    rollback is a digest
    Secret Manager

    Every credential the app uses, versioned, IAM controlled, injected at runtime and never written to a repo.

    no .env in git, ever
    Cloud Load Balancing + CDN

    One anycast address worldwide, TLS terminated at the edge, static assets cached in the city the user is in.

    one IP, 100+ points of presence
    Cloud Logging, Monitoring, Trace

    Structured logs with a request id, latency and error dashboards, distributed traces through every hop.

    you can see it break
    Pub/Sub + Cloud Scheduler

    The event backbone and the cron. Fan out, retries and dead letter topics without a queue server to babysit.

    nothing to run at 3am
    infra/run.tf · reviewed, then applied by CIinfrastructure as code
    resource "google_cloud_run_v2_service" "api" {
    name = "api"
    location = "europe-west1"
    template {
    scaling { min_instance_count = 1 max_instance_count = 100 }
    containers {
    image = "…/api@sha256:9f21c4"
    env { name = "DATABASE_URL" secret = "db-url" }
    }
    }
    }
    # terraform plan · 0 to add, 1 to change, 0 to destroy
    # staging is the same file with a different variable set

    the 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.

    5xx rate above 1%5 minutes

    page on-call, roll traffic back to the previous revision

    p95 latency above 500 ms10 minutes

    page on-call, check pool saturation and slow queries

    Queue depth above 5,00015 minutes

    scale workers, notify the channel

    Failed jobs in dead letterany

    ticket raised automatically with the payload attached

    Certificate expiring21 days

    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.

    latency · real user monitoring · last 7 daysp95 under budget
    regionp50p95p99shape
    London183461
    Frankfurt244172
    New York213868
    Singapore3862104
    São Paulo4674119
    Sydney5283131
    # milliseconds, round trip, application response included

    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.

    europe-west14 ms away from London
    us-east16 ms away from New York
    asia-southeast15 ms away from Singapore
    writes
    europe-west1 · primary

    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 edgeFor how longWhere it is served from
    Hashed JS, CSS, fontsimmutable, one yearedge + browser
    Images and public media30 days, revalidatededge
    Public catalogue and pricing pages60 s, stale while revalidateedge
    Anything behind a loginnever cached at the edgeorigin only
    Anything scoped to one tenantnever cached at the edgeorigin 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.

    Two people on a sofa looking at the same phone screen
    they will never read a percentile. they only ever feel the half second it saved them.

    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

    9:41
    Field Opsoffline

    Sync queue · 4 changes waiting

    PATCHjob 4821 · status complete
    POSTreport 9903 · 6 photoswaiting
    POSTsignature 9903waiting
    PATCHjob 4822 · notesconflict

    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.

    sync · connectivity restored at 16:410 lost writes
    [16:41:02] connectivity : offline online
    [16:41:02] sync : 4 queued mutations, oldest 2h 14m
    [16:41:03] PATCH /jobs/4821 200 Idempotency-Key: 41d0…
    [16:41:06] POST /reports/9903 201 6 photos · 4.1 MB · resumable upload
    [16:41:07] POST /signatures/9903 201
    [16:41:07] PATCH /jobs/4822 409 conflict server changed at 16:38
    resolve · notes: keep the field edit, status: keep the server
    [16:41:08] queue empty · 0 lost writes · 0 duplicates

    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.

    Copy, layout, business logic, a new screenover the air updateminutes
    A new JavaScript dependencyover the air updateminutes
    A new native module, permission or SDKstore submission1 to 3 days
    App icon, name, entitlementsstore submission1 to 3 days
    Over the air updates are signed, staged to a percentage of devices first, and revertible from a dashboard. A build that crashes on launch is not a three day emergency.

    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.

    A hand holding a phone mid-tap
    one thumb, one hand, on a screen the size of a bank card.

    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

    A01Broken access control

    Deny by default, checks on the server for every route, plus row level security in Postgres underneath.

    A02Cryptographic failures

    TLS 1.3 everywhere, encryption at rest on disks and buckets, argon2id for passwords, no home made crypto.

    A03Injection

    Parameterised queries only, no string built SQL, output escaped by React, a content security policy on top.

    A04Insecure design

    Threat modelling in the spec, rate limits and quotas designed in, abuse cases written next to the happy path.

    A05Security misconfiguration

    Infrastructure as code, least privilege service accounts, no default credentials, staging locked down like production.

    A06Vulnerable components

    Dependabot on the repository, image scanning in Artifact Registry, a patch window that is days not quarters.

    A07Authentication failures

    MFA, throttling, rotating refresh tokens, sessions that expire, no enumeration on login or reset.

    A08Data integrity failures

    Signed webhooks, pinned dependencies with a lockfile, CI that builds the artefact once and promotes the same digest.

    A09Logging and monitoring failures

    Structured logs with a request id, alerts wired to a human, audit trail on every write that matters.

    A10Server side request forgery

    Outbound calls allowlisted, metadata endpoints blocked, user supplied URLs never fetched from inside the network.

    audit_log · jobs, invoices, users · never deletedappend only
    timeactorrecordfromto
    14:02:11s.patelinvoice 8841 · statusdraftapproved
    14:02:44s.pateljob 4821 · scheduled_for09:0013:30
    15:17:03m.okaforuser j.reid · rolefield_techops_manager
    16:40:29systemexport jobs.csv·18,402 rows
    # plus request id, IP and user agent on every row

    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

    DataKept forLawful basis
    Account and profilelife of the account, then 30 dayscontract
    Job and service history6 yearslegal obligation
    Application and access logs90 dayslegitimate interest
    Marketing preferencesuntil withdrawn, then proof keptconsent
    Backups containing any of it35 day rolling windowdocumented 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.
    If you fire us on a Friday, it still runs on Monday. That is the test.

    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.

    Pricing · on demand

    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.

    See how we ship the site too
    4.9/5 from 40+ organizers

    Keep going · the rest of the stack

    Software is one desk. The rooms still need filling.

    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

    Event staff greeting delegates at a registration desk