19 July 2026

The contract: what keeps a platform portable

Portability isn't a migration tool you bolt on later — it's a four-clause contract you commit to on day one. Here's the exact interface the whole platform runs on.

architecturekubernetesportability12-factorpostgres


Every platform that offers to run your app for you eventually asks you to write your app its way. The convenience and the lock-in arrive in the same box: the SDK that makes your deploys feel magical is the same SDK you can't leave.

So when I set out to build a platform that runs your SaaS and lets you walk away from it, the first design decision wasn't a tool or a framework. It was a border — the smallest possible line between "your code" and "my platform."

In the last post I argued that sovereignty and portability are really the same problem: both come down to who controls the exit. This post is the mechanism. Portability is not a migration script you write the day you want to leave — by then it's already too late. It's an interface you commit to on day one and never violate. The counterintuitive part is that its power comes from how little it demands.

What you'll get from this post

The exact four-clause contract that every app on the platform meets, why each clause earns its place, the things I deliberately refuse to add, and the tradeoff that small contract buys you.

The whole contract, in one sentence

The platform runs anything that is a container that listens on $PORT, speaks Postgres, exposes liveness and readiness checks, and reads its config from environment variables.

Four clauses. That is the entire API between your application and my platform. Honour those, and I can run it, scale it, observe it, back it up, and move it between substrates — without ever needing to know what's inside.

Why each clause is there

A container. This is the unit of portability. Not "a Next.js app," not "a function" — a container, because a container runs identically on my laptop, on a five-euro box, and on a fifty-node cluster. It's the one packaging format every substrate I'd ever want to run on already understands.

Listening on $PORT. The platform decides the port; the app obeys. That single inversion is what lets the same image run behind a dev proxy, a single-node ingress, or a cloud load balancer with no code change. The app stops caring where it lives.

Speaking Postgres. One database, spoken over a standard wire protocol with a standard client — not a proprietary SDK. Postgres because it is the most boring, most universally hosted database there is: in-cluster today, managed EU Postgres tomorrow, your own instance later, all with zero code change. The moment you reach for a vendor's bespoke data primitive instead, you've pinned yourself to that vendor; the standard client is the thing that travels.

Liveness and readiness. Your app tells the platform two different truths. Am I alive? — if not, restart me. Am I ready for traffic? — if not, route around me. Keeping those separate is what lets me operate your app without knowing anything about its internals: self-healing, zero-downtime rollouts, and a database blip that marks a pod not ready instead of triggering a restart loop. The health checks are how your app hands the operating over to me.

Config from environment variables. The same image runs in every environment; nothing is baked in at build time. Move from staging to production, or from my cloud to yours, with no rebuild — endpoints and secrets are injected, not compiled. This is plain twelve-factor, and it's the difference between a portable artifact and a snowflake.

Opinion lives at the border, and nowhere above it

Below that line I am deliberately, unapologetically opinionated: Kubernetes-shaped, GitOps, observability on by default, EU regions only. That's not me being precious — operating those well is the service I'm selling.

Above the line, I get out of the way. Your language, your framework, your libraries are yours. I bless a stack — a Next.js app and a Go service, both container-first — as a starting point for teams who want one. But a Django, Remix, or Rust team that meets the same four clauses is exactly as first-class. The blessed stack is an on-ramp, never a toll booth.

What's deliberately not in the contract

The discipline is in the no's. The contract does not require a framework, a proprietary SDK, an edge runtime, a platform-specific storage API, or a baked-in way to do auth or payments. Every one of those would make some feature easier to build — and every one would be a handcuff.

I keep the contract small on purpose, because each clause I add is a constraint I impose on every customer and every future substrate, permanently. A requirement I can't honour on a five-euro box and on managed Kubernetes alike is a requirement that quietly breaks portability. So the bar for adding anything is brutal: it has to be true everywhere, forever, or it doesn't go in.

The takeaway

A small contract is, in the end, a promise about the exit. Those four clauses are all things that are true on a laptop, a single cheap box, and managed Kubernetes alike — which is precisely why moving between them is a config change rather than a rewrite.

The tradeoff, stated honestly: you give up the seductive convenience of a vendor's clever primitives — the magic blob store, the edge cache, the one-line auth — and you write a little more boring glue yourself. In exchange, you are never hostage. For a team that wants to own its exit, that's an easy trade. For someone chasing maximum magic with no intention of ever leaving, it isn't — and they're simply not who this is for. The contract self-selects the right customers in and the wrong ones out, which is exactly what a good contract should do.

What tripped me up

The constant temptation is "just one more" clause — require Redis, mandate a message queue, standardise on a single migration tool. Each feels harmless in isolation, and each quietly raises the floor for portability. Saying no, over and over, is most of the work.

And the clause people get most wrong is liveness versus readiness. Collapse them into one "health" check and a brief database outage becomes a restart storm: every pod fails its check, gets killed, and thunders straight back into the same outage. Two checks, two meanings. Readiness gates traffic; liveness only ever kills a truly dead process.

Where this leaves us

The contract is the spine, and everything that comes later — GitOps, ingress, the database, observability, the tier ladder — hangs off it without ever changing it. That's the test I'll hold every future decision against: does it respect the border?

Enough theory. Next post we stop talking and build: an immutable Talos Linux cluster on a cheap German box, created entirely from OpenTofu — no SSH, nothing clicked in a console — the first concrete rung of the ladder.


I'm building this in the open, and I'll run it for EU teams who'd rather ship product than hire a platform engineer. If that's you, follow along via RSS — every build post lands there first — or start with what this project is about.