Multi-Tenancy
How organizations, spaces, members, end-users and API keys fit together, and how Appstrate isolates tenants on every request.
Appstrate is multi-tenant by design. Every request is evaluated in a strictly scoped context, and every row carries its tenant keys. This page gives the map. The pages it links to give the detail.
The hierarchy
Organization (UUID)
├── Members (owner | admin | member | guest)
├── Custom roles, models, provider credentials, proxies, package catalog
├── Space spc_xxx (team spaces, one is the default)
│ ├── Space members (role per person) and visibility
│ ├── Packages placed here and which are active (agents, skills, integrations)
│ ├── Runs, files, schedules, memory, notifications
│ ├── Integration connections (per member or per end-user)
│ ├── End-users eu_xxx
│ ├── API keys apst_xxx
│ └── Webhooks (space level)
└── Personal space of each member (private)| Level | What it is | Page |
|---|---|---|
| Organization | Membership, billing and shared infrastructure boundary. | Organizations |
| Space | Where agents run. Scoping and access boundary inside the organization. | Spaces |
| Member | A person with an organization role and, per space, a space role. | Roles and permissions |
| End-user | An identity from your own product, scoped to a space. | End-Users |
| API key | A headless credential bound to one space. | API Keys |
The platform does not use row-level security in the database. Isolation is enforced by the application: every query filters by organization, and by space for space-scoped resources. There is no query that crosses tenants implicitly.
How a request is scoped
Each authenticated request goes through the same steps, in order:
- Authentication. Module strategies (OAuth tokens) are tried first, then an API key (
apst_), then the cookie session. - Organization context. The organization comes from the key, or from the
X-Org-Idheader for a session, and the caller must be a member. With an API key theX-Org-Idheader is ignored. - Permissions. The organization role is resolved, then the credential's ceiling (key scopes) applied.
- Space context. For space-scoped routes (agents, runs, schedules, end-users, API keys, notifications, packages, integrations, files, uploads), the space comes from the key, or from
X-Space-Idfor a session. AnX-Space-Idthat contradicts the key is a403. The caller's role in that space is resolved and the effective permissions become(organization ∪ space) ∩ ceiling. No role in the space is a403 not_a_space_member, or a404for a private space. - API version and idempotency, then the route handler.
Routes of the /api/orgs/{orgId} family carry the organization in the path instead of a header. Webhooks name their own space in the body or query. See Spaces for the transports.
Acting for an end-user
Send Appstrate-User: eu_xxx with an API key to act as one of your own customers. The call is then attributed to the end-user, uses its connections and memory, and only sees its runs. It is refused for cookie sessions and for end-users of another space. See End-Users.
Realm isolation
Sessions carry a realm. Dashboard users have the realm platform. When the oidc module signs in the end-users of a space, those sessions get the realm end_user:<spaceId>. A guard rejects an end-user session on platform routes and the reverse, so a stolen end-user session cannot reach an organization's admin API even though both live on the same instance.
Credential isolation
Credentials (OAuth tokens, API keys, custom fields) are encrypted at rest with CONNECTION_ENCRYPTION_KEY and stay scoped to a space and to their owner, a member or an end-user. At run time the agent never receives a credential. The run's sidecar injects it on outbound requests (see Sandbox and Sidecar). A member's connection is theirs. Others in the space use it only when it is explicitly shared or pinned by an admin. The details are in Isolation and Security.
Audit
Three records answer "who did what":
- Run records. Every run stores its organization, space, the key, member, end-user or schedule that launched it, and the model and proxy it used. See Runs.
- Structured logs. Authentication decisions, permission denials and every
Appstrate-Userimpersonation are emitted as JSON log lines. - Audit table. State-changing operations (organization, space, member, key, webhook, package and connection changes) are appended to an
audit_eventstable with actor, resource, before and after values, IP and request id. Writes are best effort so that an audit failure never blocks a change. The table is not exposed through the API, so export it from the database or forward the logs for retention and tamper-evidence.
Mapping your product
| Your product | Appstrate |
|---|---|
| Your company | An organization |
| An environment or a product line | A space |
| Your backend | An API key bound to a space |
| One of your customers | An end-user, with your id in externalId |
| A customer triggers an agent | Your backend calls the API with Appstrate-User |
| A customer's Gmail or Slack account | A connection owned by the end-user |
| Your support team using the dashboard | Members with space roles |
One organization with one space is usually enough. Add spaces when you need separate agents, schedules, keys and connections between environments or teams, and separate organizations when customers need full isolation.