Multi-Tenancy
Overview
Sección titulada «Overview»Greentic multi-tenancy is built around explicit runtime context, not hidden global configuration. A bundle can expose the same application packs to many tenants, restrict packs per tenant or team, and pass tenant identity through every component and host capability call.
The current model is defined by greentic-types and greentic-interfaces:
greentic-types::TenantCtxis the Rust type shared across host-side crates.greentic:types-core@0.6.0defines the canonical WITtenant-ctxused by component interfaces.greentic:interfaces-types@0.1.0keeps compatibility fields for older host capability surfaces.greentic-bundlematerializes tenant/team access files and resolved manifests frombundle.yaml.
Runtime Scope
Sección titulada «Runtime Scope»Bundle workspace├── app packs # Digital worker applications and flows├── extension providers # Messaging, state, secrets, telemetry, etc.└── tenants/ └── <tenant>/ ├── tenant.gmap # Tenant-level access policy └── teams/ └── <team>/ └── team.gmap # Optional team-level policy overlayTenant and team directories are access scopes. They are not YAML tenant profiles and they do not store channel configuration directly. The runtime combines the selected tenant/team, access policy, app-pack mappings, setup state, and incoming message metadata to build the invocation context.
TenantCtx
Sección titulada «TenantCtx»TenantCtx is the context carried with invocations, host calls, state, secrets, telemetry, and messaging. In greentic-types, it is richer than the old three-field examples:
pub struct TenantCtx { pub env: EnvId, pub tenant: TenantId, pub tenant_id: TenantId, pub team: Option<TeamId>, pub team_id: Option<TeamId>, pub user: Option<UserId>, pub user_id: Option<UserId>, pub session_id: Option<String>, pub flow_id: Option<String>, pub node_id: Option<String>, pub provider_id: Option<String>, pub trace_id: Option<String>, pub i18n_id: Option<String>, pub correlation_id: Option<String>, pub attributes: BTreeMap<String, String>, pub deadline: Option<InvocationDeadline>, pub attempt: u32, pub idempotency_key: Option<String>, pub impersonation: Option<Impersonation>,}The duplicate pairs such as tenant / tenant_id, team / team_id, and user / user_id exist for compatibility with older interface surfaces. New code should keep them aligned by using the helpers such as TenantCtx::new(...), with_team(...), and with_user(...).
The canonical component WIT shape in greentic:types-core@0.6.0 uses the _id names:
record tenant-ctx { tenant-id: tenant-id, team-id: option<team-id>, user-id: option<user-id>, env-id: env-id, trace-id: trace-id, correlation-id: correlation-id, deadline-ms: u64, attempt: u32, idempotency-key: option<string>, i18n-id: string,}Bundle Workspace
Sección titulada «Bundle Workspace»bundle.yaml is the workspace state file. It records app packs, extension providers, capabilities, remote catalogs, and optional app-pack mappings:
schema_version: 1bundle_id: support-bundlebundle_name: Support Bundlelocale: enmode: createapp_packs: - oci://ghcr.io/greenticai/packs/apps/support-agent:latestapp_pack_mappings: - reference: oci://ghcr.io/greenticai/packs/apps/support-agent:latest scope: tenant tenant: acme - reference: oci://ghcr.io/greenticai/packs/apps/field-service:latest scope: team tenant: acme team: opsextension_providers: - oci://ghcr.io/greenticai/packs/messaging/messaging-webchat-gui:latest - oci://ghcr.io/greenticai/packs/state/state-memory:latestcapabilities: - greentic.cap.bundle_assets.read.v1Mapping scopes are:
| Scope | Meaning |
|---|---|
global | The app pack is available to every tenant/team that policy allows. |
tenant | The app pack is mapped to one tenant. |
team | The app pack is mapped to one tenant team. |
The wizard stores these mappings in JSON AnswerDocuments using app_pack_entries[].mapping.scope. The generated workspace persists them in bundle.yaml.
Access Policy
Sección titulada «Access Policy».gmap files are policy files, not tenant metadata documents. Each non-comment line is:
<pack>[/<flow>[/<node>]] = public|forbiddenExamples:
_ = forbiddensupport-agent = publicsupport-agent/intake = publicsupport-agent/admin = forbiddensupport-agent/admin = publicsupport-agent/admin/delete-ticket = forbiddenEvaluation uses the most specific matching rule. Team policy overlays tenant policy: if a team rule matches, it wins; otherwise the tenant rule is used. The default generated policy is _ = forbidden, so a pack, flow, or node must be made public before it is available to that tenant/team.
Wizard AnswerDocuments express the same policy as access_rules:
{ "answers": { "delegate_answer_document": { "answers": { "access_rules": [ { "tenant": "acme", "policy": "public", "rule_path": "support-agent" }, { "tenant": "acme", "team": "ops", "policy": "public", "rule_path": "support-agent/admin" } ] } } }}Resolved Manifests
Sección titulada «Resolved Manifests»When the bundle workspace is synchronized, Greentic writes resolved manifests for each tenant/team:
resolved/├── acme.yaml└── acme.ops.yaml
state/resolved/├── acme.yaml└── acme.ops.yamlThose files summarize the effective bundle, policy sources, extension providers, capabilities, and app-pack policy decisions:
version: 1tenant: acmeteam: opsbundle: bundle_id: support-bundle bundle_name: Support Bundlepolicy: default: forbidden source: tenant_gmap: tenants/acme/tenant.gmap team_gmap: tenants/acme/teams/ops/team.gmapapp_packs: - reference: oci://ghcr.io/greenticai/packs/apps/support-agent:latest policy: publicextension_providers: - oci://ghcr.io/greenticai/packs/messaging/messaging-webchat-gui:latestcapabilities: - greentic.cap.bundle_assets.read.v1State And Sessions
Sección titulada «State And Sessions»State and session host interfaces receive the tenant context. In WIT compatibility surfaces, state operations accept an optional tenant-ctx:
read: func(key: state-key, ctx: option<tenant-ctx>) -> result<list<u8>, host-error>;write: func(key: state-key, bytes: list<u8>, ctx: option<tenant-ctx>) -> result<op-ack, host-error>;delete: func(key: state-key, ctx: option<tenant-ctx>) -> result<op-ack, host-error>;The important rule is that state keys are resolved together with TenantCtx. Components should pass the context they receive from the host and avoid embedding tenant IDs into ad hoc string keys.
Secrets
Sección titulada «Secrets»Secrets use the canonical SecretScope from greentic-types:
pub struct SecretScope { pub env: String, pub tenant: String, pub team: Option<String>,}Setup and extension packs can declare SecretRequirements with an expected scope. Runtime storage commonly uses a URI-like namespace such as:
secrets://{env}/{tenant}/{team-or-_}/{provider}/{key}The scope is environment + tenant + optional team. Use the structured scope where possible; treat URI strings as storage addresses, not as the source of truth for authorization.
Messaging And Events
Sección titulada «Messaging And Events»Ingress providers normalize platform-specific payloads into Greentic messages/events and attach tenant context before the flow runs. A WebChat, Teams, Slack, webhook, or timer event should all end up with the same identity fields:
env/env_idtenant/tenant_idteam/team_iduser/user_idwhen knownsession_idfor conversation continuityprovider_id,flow_id,node_id- trace, correlation, attempt, deadline, and idempotency metadata
Messaging channels are therefore not the tenant hierarchy. They are ingress surfaces that resolve to a tenant/team context.
Component Responsibilities
Sección titulada «Component Responsibilities»Components should trust the host-provided TenantCtx as the authority for the current invocation and should not accept tenant override fields from user payloads.
fn handle(input: Input, ctx: TenantCtx) -> Result<Output, Error> { let identity = ctx.identity(); audit("tenant", identity.tenant_id.as_str());
// Use ctx for state, secrets, telemetry, and outbound calls. // Do not derive tenant scope from untrusted input. process_for_current_scope(input, ctx)}This keeps a component portable across single-tenant, shared multi-tenant, local, and cloud deployments.
Deployment Models
Sección titulada «Deployment Models»Single Tenant
Sección titulada «Single Tenant»Run one bundle workspace per customer. This is simple operationally and useful for strict data residency or customer-managed deployments.
Shared Multi-Tenant
Sección titulada «Shared Multi-Tenant»Run one Greentic deployment with multiple tenant/team scopes. App packs and extensions can be shared, while access policy, secrets, state, sessions, and telemetry remain scoped by TenantCtx.
Run shared infrastructure for lower-risk tenants and dedicated workspaces for regulated tenants. The same pack can move between models because tenant identity is passed through the same TenantCtx and WIT contracts.
Best Practices
Sección titulada «Best Practices»- Use
gtc wizard --schema- Generate tenant/team access and app-pack mappings from the current JSON AnswerDocument schema. - Keep
.gmapfor policy only - Do not put tenant profiles, channel settings, or secrets intenant.gmaporteam.gmap. - Default deny - Keep
_ = forbiddenand explicitly publish the packs, flows, or nodes a tenant/team may run. - Pass
TenantCtxunchanged - State, secrets, telemetry, HTTP, and messaging calls should use the host-provided context. - Keep compatibility fields aligned - If you construct
TenantCtxmanually, keeptenantwithtenant_id,teamwithteam_id, anduserwithuser_id. - Use structured scopes - Prefer
SecretScope,TenantIdentity, andTenantContextover ad hoc path strings.