reLabs build note. This is the story of how we moved from tenant isolation as a rule the application had to remember, to tenant isolation as something the platform enforces by default. It began as a practical problem in two products, Keep and Humanely, and became a repeatable pattern for building multi-tenant SaaS on Cloudflare Workers for Platforms.
Every multi-tenant product eventually reaches the same uncomfortable moment.
At the beginning, tenant isolation feels straightforward. Every row has a tenant id. Every query filters by it. Every request carries the right context. The team knows the rules, reviews the code, and builds the usual guardrails.
And for a while, that is enough. But as the product grows, the shape starts to feel fragile. More features touch data. More workflows run in the background. More automation gets added. More user-facing intelligence sits close to the product. Suddenly, isolation is not one decision; it is a thousand small decisions repeated across the codebase.
That was the problem we wanted to move away from. We did not want every future query, workflow, tool, migration, and product surface to keep remembering the tenant boundary. We wanted the boundary to be harder, cleaner, and more natural.
The answer we arrived at was simple: One tenant, one Worker.
Not one shared backend that receives a tenant id and promises to behave. Not one shared runtime where every path has to remember the current tenant. Each tenant is uploaded as its own Cloudflare Worker, with its own bindings, storage, and runtime boundary.
The tenant stops being a value passed through the system. It becomes part of the deployment.
The problem we had
We reached this architecture from two different starting points.
Keep, our AI-first delivery-management product, was greenfield on Cloudflare. It began with the shape we wanted: one Worker per tenant, with per-tenant storage and retrieval resources close to the runtime.
Humanely's Company OS got there through evolution.
The first Humanely backend was hosted on Google Cloud Run, backed by a Cloud SQL instance. Tenant segregation followed the usual SaaS pattern: every tenant-scoped record carried a tenant id, and the application made sure each request only touched rows for the current tenant.
That was a perfectly reasonable place to start. It was familiar. It worked. It let us move.
But the boundary lived in the shared application runtime.
A request came in, the application identified the tenant, carried that tenant id through the request, and every data access path had to apply the correct filter. The database could support discipline, but the real isolation still depended on the application doing the right thing every time.
As Humanely expanded, that started to feel like the wrong kind of risk. The issue was not that the pattern was broken. The issue was that the blast radius was too wide if anything went wrong.
The compute plane was shared. The schema was shared. The decision of "which tenant is this?" happened at runtime. A mistake in that seam would not be a single-tenant problem; it could become a fleet problem.
That is what we wanted to clean up.
What we changed
We moved Humanely from the Cloud Run and Cloud SQL shape to the same Workers-for-Platforms model that Keep already had.
The new pattern is easier to explain:
A request arrives at the tenant's domain.
A dispatch Worker validates the tenant and authenticates the request.
The request is routed into a dispatch namespace.
The dispatch namespace invokes that tenant's uploaded Worker.
The tenant Worker can only use the resources bound to it.
That last point is the whole architecture.

The Worker for Acme has Acme's database, Acme's bucket, Acme's retrieval resources, and Acme's stateful coordination objects. It does not have another tenant's resources. There is nothing to accidentally select.
The boundary is no longer just a convention in the application. The boundary is represented by what the Worker can physically reach.
This changed the feel of the system.
In the old shape, the application constantly had to ask, "Which tenant am I serving right now?"
In the new shape, the tenant Worker already knows. It is serving one tenant because it was deployed for one tenant. The runtime does not need to rediscover that fact on every request.
That makes the system easier to reason about. It also makes it easier to explain to customers, engineers, and ourselves.
How the architecture works
There are four parts to the pattern.
1. The dispatch Worker is the front door
The dispatch Worker owns the wildcard domain. It validates that the tenant exists, authenticates the request, and forwards it to the right tenant Worker.
It does not own tenant data. That is important.
Its job is to route, not to become a shared application layer. The control plane holds the registry and routing information. The tenant plane serves the tenant.
That separation keeps the control plane small and the tenant boundary clear.
2. The tenant Worker owns the boundary
Each tenant Worker is created from the same application template, but each one is uploaded with different bindings.
A Humanely tenant Worker can be bound to that tenant's database, object storage, stateful coordination objects, and AI gateway configuration. A Keep tenant Worker can be bound to D1, R2, Vectorize, encryption material, and workspace-level coordination state.
The specific storage choice can vary by product. The principle does not.
The Worker should only have the resources that tenant is allowed to use.
That is what makes the model clean. The boundary is not hidden in a helper function. It is visible in the deployment.
3. Provisioning builds the boundary
Once the tenant boundary becomes a deployment artifact, provisioning becomes a core part of the product.
Creating a tenant is not just inserting a row in a database. It is a small, retry-safe workflow:
Create a provisioning job.
Create the tenant's storage and runtime resources.
Run migrations and seed what is needed.
Upload the tenant Worker with the correct bindings.
Run a health check.
Mark the tenant active only when the Worker proves it can reach its own resources.
The failure path matters as much as the happy path. If a database, bucket, or Worker is created and a later step fails, cleanup must be part of the workflow. Otherwise, the system slowly fills with orphaned resources that nobody owns.
This is one of the less glamorous parts of the architecture, but it is where a lot of trust is earned. Provisioning is not an admin script. It is production code.
4. User access stays inside the tenant boundary
The user experience is still simple: a user opens their workspace and uses the product. The important part is what happens underneath.
The user's request flows into the tenant Worker. From there, the product can access only the resources bound to that tenant: operational data, files, retrieval indexes, stateful workflows, and model calls tagged through the AI gateway.
The user does not get a runtime selector that can accidentally point somewhere else. Workflows do not need to pass around a tenant id as a dangerous parameter. The tools available inside that runtime are already scoped to the tenant.
This becomes especially important as products get more intelligent. The more a system can do on behalf of a user, the more important it is that access is constrained by the environment, not by hope, prompt text, or convention.

Why Cloudflare fit this shape
AWS and Google Cloud both have strong building blocks for this kind of system.
On AWS, a team could assemble a version with Lambda, API Gateway, CloudFront, S3, DynamoDB or Aurora, IAM, Step Functions, and Bedrock or another model gateway. On Google Cloud, the equivalent might involve Cloud Run, Cloud Load Balancing, Cloud Storage, Cloud SQL or Firestore, Workflows, IAM, and Vertex AI.
Those are mature platforms. They can absolutely support serious architectures.
But our question was different: Which platform lets a lean team ship secure, global, AI-native multi-tenancy with the least platform code?
Cloudflare matched the shape we wanted unusually well.
Workers gave us lightweight compute. Workers for Platforms gave us tenant-level script uploads. Bindings made access explicit. Durable Objects gave us a natural place for stateful coordination. D1, R2, Vectorize, and AI Gateway sat close to the runtime. The result was not just a list of services; it was a smaller platform surface that mapped directly to the tenant boundary.
That mattered because we are intentionally lean. We do not want to build a large internal platform before the product earns it. We want the team focused on customer workflows, product quality, and useful intelligence, not on stitching together infrastructure for its own sake.
Cloudflare gave us enough platform power without forcing us into platform sprawl.

The benefits we get
The biggest benefit is not just stronger isolation. It is calmer engineering.
Smaller blast radius: A tenant Worker has that tenant's bindings and not another tenant's bindings. If something goes wrong, there are fewer places the failure can go. We still use logical guardrails where they make sense, but they are no longer the only wall.
Clearer application logic: The code no longer has to rediscover the tenant everywhere. The runtime is already scoped. That reduces repetition, mental overhead, and the number of places where a mistake can hide.
Safer user-facing workflows: When users interact with the product, their access flows through tenant-scoped capabilities. The system does not need to hand workflows a dangerous tenant selector. The tools available are already the tools for that tenant.
Faster onboarding: A new tenant becomes a provisioning workflow: create resources, attach bindings, upload Worker, run health checks, activate. That makes onboarding repeatable and easier to trust.
Better cost visibility: AI and automation costs need to be visible per tenant. With tenant-oriented provisioning and AI Gateway tagging, usage can be understood closer to the boundary where it belongs.
A platform a small team can operate: Per-tenant Workers do introduce fleet operations. Releases fan out. Migrations need care. Provisioning needs cleanup. But these are explicit problems we can build tools around.
That is a better trade than carrying silent risk in a shared runtime.
What others can learn
This is not a claim that every team should copy our exact stack.
The useful lesson is more general:
Make the tenant boundary concrete. A tenant should map to something the platform can enforce, not just a column the application remembers.
Separate the control plane from the tenant plane. The control plane provisions and routes. The tenant plane serves.
Avoid tenant selectors in runtime tools. Give users and workflows tenant-scoped capabilities instead.
Treat provisioning as product infrastructure. Idempotency, cleanup, retries, and health checks are part of the trust model.
Choose storage based on the data. Keep can use edge-local D1 and Vectorize. Humanely can use relational storage where the domain needs it. Isolation should be solved one layer above.
Be honest about operations. Per-tenant deployment moves complexity into release management, migrations, and fleet health. That is acceptable only if you build for it deliberately.
The sharp edges
This pattern has worked well, but it is not magic.
A few details matter:
Run the dispatch Worker before static assets, so tenant validation cannot be bypassed.
Keep provisioning retry-safe, and clean up partial resources.
Canary tenant releases before fan-out.
Treat schema changes as fleet operations.
Keep tenant selection out of runtime tools.
Use platform bindings as the access boundary, not just application convention.
Where we land
We did not set out to arrive at the same architecture twice.
Keep started on Cloudflare because it was greenfield and edge-native. Humanely started in a more familiar Google Cloud shape: Cloud Run, Cloud SQL, and tenant-id based segregation. That was the usual SaaS starting point, and it helped us move quickly.
But as the product matured, we wanted the tenant boundary to be cleaner than a convention repeated across the application. We wanted the platform to carry more of the responsibility.
Both systems converged on one idea: The deployment should hold the wall.
The database is chosen for the data. The Worker is scoped to the tenant. User access inherits that boundary. Cloudflare gives our lean team a way to build this without assembling a heavy platform first.
That is the takeaway for us: strong isolation does not have to mean heavy infrastructure. When the platform primitive maps cleanly to the product boundary, a small team can move quickly and still build with confidence.
Further reading
Cloudflare, Workers for Platforms: dispatch namespaces and per-tenant script uploads.
Cloudflare, Durable Objects: SQLite-backed, single-threaded stateful objects for long-lived coordination.
Cloudflare, D1 and Vectorize: edge-local SQLite and vector search bound directly to a Worker.
Cloudflare, R2: object storage with simple Worker bindings.
Cloudflare, AI Gateway: per-call tagging, caching, and rate-limit accounting in front of model providers.
Neon, Serverless driver and pgvector on Neon: Postgres from serverless environments, with embeddings beside relational data.
PostgreSQL, Row Security Policies: an in-database backstop for tenant-scoped access.