Organizations and teams
The tenant model. Workspace is the organization wall. Team is the active work scope.
The Viewport tenant model is single-database, row-level, with workspace as the organization tenant. Users are global. Memberships are explicit. Teams are org-local work scopes. Every customer-data row carries a workspace_id; team-scoped product data also carries a team_id and is filtered through the active team context.
If you've used Slack or Vercel: same shape. One person, many workspaces, each workspace is a hard wall. Inside that wall, a team is the scope you choose before creating workflows, reviewing artifacts, or attaching context.
The shape
User (global)
├── membership → Workspace A (role: owner)
│ ├── Team: Platform
│ │ ├── Workflows, routes, artifacts, context
│ │ └── Review packets and inbox decisions
│ ├── Team: Reviewers
│ ├── Runners (org-scoped compute)
│ └── Billing, members, integrations, audit
├── membership → Workspace B (role: admin)
│ ├── Team: Engineering
│ └── Workflows, plans, vaults, inbox …
└── membership → Workspace C (role: viewer)
└── …Workspace = the tenant
A workspace is the hard wall. It owns:
- Members (workspace_memberships).
- Teams (org-local groups).
- Billing and plan state.
- Machines and runners.
- Integrations, webhooks, settings.
- Audit events.
Workspaces cannot see each other. Resources cannot be shared across workspaces. A team in workspace A cannot receive an ACL entry for a workspace B resource.
Internally referenced by ULID. The slug is display-only.
User = global identity
One user account per email. The same user can belong to many workspaces with different roles in each. There's no users.tenant_id. Sessions cookies are tenant-agnostic; the current org is a per-request context.
Active organization context
For every API request that touches tenant data, the platform resolves the active organization:
- Route binding: if the route has
{workspace}, that wins. - Request header:
X-Viewport-Organization: <ULID>. - Token claim: if the auth token is org-bound, that's used.
- Else: fail closed. The request is denied or the page asks the user to pick an org.
The platform verifies the actor has a membership row in the chosen org. No membership = 403 tenant_forbidden.
This means:
- Web, mobile, CLI, daemon, integrations. They all pass org context in the same way.
- One user can have web on org A and mobile on org B simultaneously without confusing the server.
- API tokens can be org-bound (recommended for CI) or user-bound (require explicit header per request).
Team = active work scope
Teams are workspace-scoped collaboration groups. They're not tenants. They're ACL principals and the active scope for most product work.
Rules:
- A team has exactly one
workspace_id. Cannot move. - Same slug can repeat across orgs (each "Platform" team is its own thing).
- A team member must already be an org member. The platform enforces this at write time.
- Team grants on resources are workspace-scoped: an ACL entry where
team.workspace_id != resource.workspace_idis rejected. - Teams are never auto-created. An org starts at zero teams.
- Workflows, workflow routes, workflow runs, artifacts, review packets, inbox items, context packages, context vaults, and Team Resource exports are team-scoped.
- If a page requires team context and none is selected, the app asks you to choose or create a team before continuing.
Runner = org-level compute
Runners belong to the organization, not to a team. They are simple compute that execute workflow assignments:
- A runner advertises capabilities such as agents, shell access, integrations, runner pool, and access mode.
- The router picks a team-scoped workflow run; the org-level runner claims it if policy and capabilities match.
- The same runner can execute work for multiple teams in the same organization.
- Provider credentials and repo access stay on the runner.
That split keeps capacity simple: teams own the work, organizations own the compute.
Team Resource = Git-backed record
A Team Resource points a team at a Git repository, branch, and base path. It is the durable place for portable exports:
.viewport/
team.json
workflows/definitions.json
workflows/routes.json
reviews/packets.json
artifacts/workflow-run-artifacts.json
context/packages.json
context/vaults.json
context/events.json
context/candidate-applications.jsonViewport remains the control-plane source of truth. The Git resource gives teams reviewable evidence, historical diffs, and a familiar recovery path.
Roles
| Layer | Roles |
|---|---|
| Org | owner, admin, member, viewer |
| Team | lead, member |
| Resource | owner, admin, editor, reviewer, viewer |
Roles compose: a workspace admin who's also a team member can do everything an admin can plus whatever team membership grants. Multiple grant paths give a user the most permissive resulting role.
The daemon's org binding
A daemon can be paired to one or more orgs. Each org binding has its own:
- Crypto identity (separate keypair file).
- Machine ID (different ULIDs in different orgs).
- Install ID, runtime target ID, relay endpoint.
- Bound directories.
So the same physical laptop can stream from ~/work to Acme and from ~/personal to Personal without either org ever seeing the other org's metadata or machine identity. Managed workflow runners use the same org boundary, but are intentionally team-agnostic compute. See Concepts: Machines and pairing and Concepts: Trust and privacy.
What multitenancy gets you
- Personal data stays personal. A consultant working on three clients keeps three clean inboxes, three audit logs, three sets of policies.
- A compromise of one org doesn't cascade. Audit log compromise in Acme reveals nothing about Personal.
- Different teams, different rules. Aggressive auto-approval for OSS, strict gates for the production org.
- Clean offboarding. Removing a user from one org leaves their other memberships untouched.
What multitenancy does not do (today)
- Schema-per-tenant or DB-per-tenant isolation. Single shared DB with row-level scope. If that's a non-starter for your security posture, talk to us about on-prem.
- Org-to-org sharing. You can't share a workflow from org A into org B. Export/import is not available yet.
- Cross-org search. Each org search is its own. We don't have a global "search across all my orgs."