Warden

Multi-Tenancy

How Warden isolates authorization data per tenant.

Tenant Scoping

Every Warden operation is scoped to a tenant. This ensures that roles, permissions, policies, and relations from one tenant cannot leak to another.

Need a hierarchy inside one tenant? See Namespaces — the second scope dimension layered on top of tenant_id. Tenants are a hard wall; namespaces are a soft hierarchy with cascading inheritance for org charts that aren't flat.

With Forge

When running inside Forge, tenant context is automatically available via forge.ScopeFrom(ctx):

// Forge middleware sets scope automatically
// from Authsome JWT or Keysmith API key
ctx = forge.WithScope(ctx, forge.Scope{
    AppID:    "myapp",
    TenantID: "tenant-123",
})

Standalone Mode

Without Forge, set tenant context explicitly:

import "github.com/xraph/warden"

ctx = warden.WithTenant(ctx, "myapp", "tenant-123")

How Isolation Works

Store Layer

All store queries include tenant ID as a filter. The PostgreSQL store uses tenant_id columns on every table:

SELECT * FROM warden_roles
WHERE tenant_id = $1 AND id = $2;

Engine Layer

The engine extracts tenant scope from context before every operation:

result, err := eng.Check(ctx, &warden.CheckRequest{
    Subject:  warden.Subject{Kind: warden.SubjectUser, ID: "user-42"},
    Action:   warden.Action{Name: "read"},
    Resource: warden.Resource{Type: "document", ID: "doc-123"},
})
// Only evaluates roles, policies, and relations for the current tenant

DSL: tenant declared in source

The same scope can be set in .warden source via the tenant header (or --tenant / --var TENANT=... at apply time):

warden config 1
tenant ${TENANT}    // bound at apply time via --var TENANT=acme

role admin { name = "Admin" }

Apply across environments without rebuilding the source:

warden apply -f config/ --var TENANT=acme    --store $PROD_DSN
warden apply -f config/ --var TENANT=staging --store $STAGE_DSN

The applier wires this scope into every entity it creates — roles, permissions, policies, resource types, and relation tuples all land in the named tenant. See Variables for the full template substitution rules.

Single-Tenant Apps: Skip the Tenant Entirely

Tenant scope is optional on both sides. When an app never calls warden.WithTenant and the DSL omits the tenant header, every entity lands in the global scope (tenant_id = "") and every Check uses the same empty scope — they match cleanly.

warden config 1
// No `tenant` declaration — applies to the global scope.

permission "doc:read" (document : read)

role viewer {
  name   = "Viewer"
  grants = ["doc:read"]
}
warden apply -f config/ --store sqlite:./warden.db
# No --tenant flag — entities are written with tenant_id = "".

This is the right path for single-tenant deployments. Multi-tenant apps should always pass a tenant explicitly (via source, --tenant, or --var) so entities never accidentally land in the global bucket.

Cross-Tenant Prevention

  • Store methods that omit tenant ID return warden.ErrMissingTenant
  • There is no API to query across tenants
  • TypeIDs are globally unique, but data is partitioned by tenant

Tenant-Scoped Operations

All CRUD operations on entities are automatically scoped:

// Creates a role for tenant-123 only
store.CreateRole(ctx, role)

// Lists roles for tenant-123 only
roles, _ := store.ListRoles(ctx, filter)

// Deletes all data for tenant-123
store.DeleteByTenant(ctx, "tenant-123")

On this page