Multi-tenancy
Arkyc is multi-tenant from the ground up. Every scoped table carries a tenant_id (and a project_id where relevant), and every query, storage path, and route is tenant-scoped. Retrofitting isolation is explicitly avoided.
The hierarchy
Tenant ──< Project ──< VerificationSession ──< captures / checks / reviews
│ │
│ └──< ProjectMember, ApiKey, WebhookEndpoint
└──< TenantMember, Role, TenantInvitation- Tenant — an organization. Has a unique
slug(used for addressing in the dashboard),name,logo_url, and a free-formsettingsJSON. - Project — an application/environment within a tenant. Carries
environment,settings,branding, verificationthresholds, andstatus. Slug is unique within the tenant. - VerificationSession — scoped to both
tenant_idandproject_id.
Members
Membership is two-layered:
- TenantMember — the primary membership. One per
(tenant, user), with arole_idand astatusofactive/invited/suspended. Only active members can access a tenant. - ProjectMember — optional, narrower scoping. One per
(project, user), with its ownrole_id. A user can belong to a tenant without any project membership.
New members are added via invitations: an email + role + hashed token with an expiry. Accepting the invite creates the active TenantMember.
Active-tenant resolution
Dashboard routes are shaped /v1/dashboard/tenants/:tenantId/.... The resolveTenant middleware (after auth):
- Loads the tenant by id.
- Confirms the authenticated user is an active
TenantMember(rejectinginvited/suspendedwith403). - Attaches
req.tenantandreq.tenantMemberfor downstream handlers and thecan(...)permission guard.
The dashboard addresses tenants by slug in the URL and resolves the slug to the tenant id client-side.
Effective permissions per request
GET /v1/dashboard/tenants/:tenantId/me returns the caller's role_permissions, direct_permissions, and the deduplicated effective_permissions for the active tenant. The dashboard uses this to render permission-aware navigation and actions. See RBAC & permissions for how the effective set is composed.
Isolation guarantees
- Foreign keys + indexes on
tenant_id/project_idon every scoped table. - Cascade deletes from tenant → projects → sessions → captures/checks.
- Storage keys are namespaced
tenants/{tenantId}/projects/{projectId}/sessions/{sessionId}/…. - The public/client APIs resolve tenant + project from the API key / client token, so a key can only ever touch its own project's data.
