This is a development version of the documentation. Content may change without notice.
Voke Documentation
Concepts

Organizations & tenancy

How tenancy, membership, roles, and the active-org contract work in Voke.

What is an organization?

An organization is the tenancy boundary in Voke. Most operational resources — plants, sub-devices, API keys, and trading-partner configs — are owned by exactly one organization. Data from one organization is never visible to another.

Device templates are the major exception: they are a global SuperAdmin-managed catalogue shared by all organizations.

Users can belong to multiple organizations simultaneously. Each membership carries an independent role scoped to that organization.


Roles

Organization membership is governed by OrgRole (from @cpi/shared/types/enums):

RoleDescription
ORG_ADMINFull control: manage members, plants, templates, API keys, and trading config.
OPERATORRead/write access to plants and telemetry; cannot manage members or org settings.
VIEWERRead-only access to plants and telemetry.

SuperAdmin is a system-wide flag (isSuperAdmin: boolean on AuthenticatedUser), not an OrgRole value. SuperAdmins see every organization, receive a synthetic ORG_ADMIN role in all org lists, and bypass per-org RBAC checks. They are still required to pass a valid x-org-id header on org-scoped requests — see below.


Active organization resolution

Every auth response includes the user's organizations and a server-selected currentOrganization. The canonical TypeScript shape, from packages/shared/src/types/auth.ts:

export interface AuthOrganization {
  id: string;
  name: string;
  slug: string;
  role: OrgRole;
}

export interface AuthenticatedUser {
  id: string;
  email: string;
  name: string;
  role: UserRole;
  isSuperAdmin: boolean;
  defaultOrganizationId: string | null;
  /**
  currentOrganization: AuthOrganization | null;
  organizations: AuthOrganization[];
}

The admin SPA does not treat currentOrganization as the durable source of truth anymore. The active org flow is:

  1. useOrgStore persists the selected org in localStorage under voke:current-org.
  2. OrgBootReconciler validates that stored org against every fresh /auth/me response.
  3. If the stored org is still accessible, it wins, including after page refresh.
  4. If the stored org is no longer accessible, the client clears it, falls back to user.currentOrganization ?? user.organizations[0] ?? null, and shows a toast.
  5. apiClient reads only voke:current-org when attaching x-org-id. It intentionally does not fall back to voke:auth-user.currentOrganization.

defaultOrganizationId remains useful as cross-device preference, updated on org switch through PATCH /auth/default-org, but refresh durability in the admin UI is localStorage-first.

Mid-session 403 responses with ORG_MEMBERSHIP_REQUIRED, ORGANIZATION_NOT_FOUND, or ORG_CONTEXT_REQUIRED trigger the registered org-access-revoked handler: the client clears the stored org, refetches /auth/me, and lets the reconciler choose the fallback.


x-org-id header

All org-scoped API calls require the x-org-id request header set to the UUID of the target organization.

GET /api/v1/plants HTTP/1.1
x-org-id: 9f4e2a1b-3c5d-4e6f-8a9b-0c1d2e3f4a5b
Authorization: Bearer <token>

The header is validated by OrgContextGuard before any service or database query runs:

ConditionError code
Header missing on an org-scoped endpointORG_CONTEXT_REQUIRED
Header value is not a valid UUIDINVALID_UUID
Caller is not a member of the specified orgORG_MEMBERSHIP_REQUIRED
Caller's role is insufficient for the operationINSUFFICIENT_ORG_PERMISSIONS

SuperAdmins must also supply a valid x-org-id; they are not exempt. The guard skips the membership check for SuperAdmins but still requires the header to be present and valid so downstream services always receive a non-null org context.

See Error codes for the full error schema.


SuperAdmin semantics

SuperAdmins are relevant to integrators in two ways:

  • API key scope. A SuperAdmin can create API keys for any organization, but each key belongs to exactly one org. There is no organization-wildcard API key — every request still requires x-org-id.
  • Organization list. getUserOrganizations(userId, isSuperAdmin=true) returns every organization in the system, each with a synthetic ORG_ADMIN role, ordered by createdAt ASC. This list drives the org switcher in the admin UI.

RBAC is otherwise unchanged: OrgRolesGuard (the guard protecting role-gated endpoints) short-circuits on isSuperAdmin and grants access without checking membership.


Implications for partners

An API key is tied to exactly one organization. VCP AMQP queues follow the naming scheme vcp.{queuePrefix}.*, where queuePrefix usually equals the organization slug. See Per-org AMQP queues for the full queue lifecycle and authentication contract.

On this page