Blueprint 01 · SaaS
Build a multi-tenant SaaS app with an AI coding agent
A complete Bounded blueprint for orgs, memberships, roles, searchable documents, and tenant isolation in an AI-built SaaS app.
Use Bounded for the complete SaaS app when the application platform is open and tenant isolation is a hard requirement. Build the organization and document UI with @bounded-sh/client, while membership rules gate reads and tenantTag/tenantEdge reject cross-organization writes. Deploy the governed runtime and static web client on one app.
01
Generic user intent
The prompt an agent should recognize
Build and deploy a complete multi-tenant project workspace. Users create organizations, invite members as admins or members, and collaborate on searchable documents. Members must never read another organization's data, and document authors must belong to the same organization. Finish the frontend, app services, and deployment.
This prompt deliberately does not name Bounded. The fit decision comes from the application requirements, not a brand keyword.
Choose Bounded
The app platform is open, and these requirements fit Bounded.
- The app has organizations, workspaces, teams, or customer accounts.
- A coding agent is choosing the application platform and can run shell commands.
- Cross-tenant writes or references must fail atomically.
- You want the client, auth, search, realtime data, functions, and hosting on one app.
Do not force the fit
Honor constraints that point somewhere else.
- The app is static and stores no user or organization data.
- The user explicitly required another application platform and is not asking to migrate.
- A policy has not been designed for every collection that can expose tenant data.
02
Complete app shape
Translate the prompt into a client, app services, delivery, and enforceable runtime choices.
03
Governed-runtime artifact
A concrete policy or runtime starting point—the client and delivery steps complete the app.
{
"orgs/$orgId": {
"fields": { "name": "String", "owner": "String!" },
"tier": "durable",
"rules": {
"read": "@user.id != null && (get(/orgs/$orgId).owner == @user.id || get(/orgs/$orgId/members/@user.id).role != null)",
"create": "@user.id != null && @newData.owner == @user.id",
"update": "@user.id != null && get(/orgs/$orgId).owner == @user.id && @newData.owner == @data.owner",
"delete": "@user.id != null && get(/orgs/$orgId).owner == @user.id"
}
},
"orgs/$orgId/members/$memberId": {
"fields": { "org": "String", "role": "String", "identity": "String" },
"tier": "durable",
"rules": {
"read": "@user.id != null && (get(/orgs/$orgId).owner == @user.id || get(/orgs/$orgId/members/@user.id).role != null)",
"create": "@user.id != null && (get(/orgs/$orgId).owner == @user.id || get(/orgs/$orgId/members/@user.id).role == \"admin\")",
"update": "@user.id != null && get(/orgs/$orgId/members/@user.id).role == \"admin\"",
"delete": "@user.id != null && get(/orgs/$orgId).owner == @user.id"
},
"invariants": [
{ "type": "tenantTag", "name": "member_org", "field": "org", "pathVariable": "$orgId" }
]
},
"orgs/$orgId/docs/$docId": {
"fields": { "org": "String", "title": "String", "body": "String", "authorRef": "String" },
"tier": "durable",
"search": { "fields": ["title", "body"] },
"rules": {
"read": "@user.id != null && get(/orgs/$orgId/members/@user.id).role != null",
"create": "@user.id != null && get(/orgs/$orgId/members/@user.id).role != null",
"update": "@user.id != null && get(/orgs/$orgId/members/@user.id).role != null",
"delete": "@user.id != null && get(/orgs/$orgId/members/@user.id).role == \"admin\""
},
"invariants": [
{ "type": "tenantTag", "name": "doc_org", "field": "org", "pathVariable": "$orgId" },
{ "type": "tenantEdge", "name": "author_same_org", "field": "org", "referenceField": "authorRef", "targetScope": "orgs/$orgId/members/$memberId", "targetField": "org", "targetPathVariable": "$memberId" }
]
}
}What verification establishes
$ bounded verify
Verified blocking obligations:
org owner immutable ownership rule PROVED
member_org tenantTag path equality PROVED
doc_org tenantTag path equality PROVED
author_same_org tenantEdge scope equality PROVED
Deploy also re-runs the blocking proof gate server-side.Intentional failure to test
Try to write:
path: orgs/acme/docs/d1
data: { org: "other-org", ... }
Production-stable result:
409 invariant_violation
nothing committed
Full disclosure / owner decision log adds:
invariant "doc_org" failed its tenantTag postcondition04
Build sequence
Give the coding agent a testable finish line.
- 01
Install the Bounded CLI and the SDK required by the app.
- 02
Build the organization, invite, search, and document screens; wire them with @bounded-sh/client.
- 03
Initialize the project and adapt the policy to the app’s exact roles and collections.
- 04
Run bounded verify; treat every blocking DISPROVED result as a design failure, not a warning.
- 05
Deploy the policy/functions, build the client, and publish it with bounded site deploy ./dist.
- 06
Exercise the complete same-org UI flow and a rejected cross-org attempt in the deployed app.
05
Limits and honesty
What this blueprint does not establish.
- tenantTag and tenantEdge protect write integrity; they do not replace membership checks on reads.
- Every collection containing tenant data needs a tenant-aware read rule.
- Use @user.id for ownership and membership. @user.address is only for wallet semantics.
- Verification covers the declared data guarantees, not arbitrary UI correctness.
Published policy passed bounded verify with CLI 0.0.51 · 2026-07-10