Kinotic
Persistence

Multi-Tenancy

Multi-tenant data isolation in Kinotic with automatic tenant filtering.

Kinotic supports multi-tenant data isolation at the persistence layer. Data from different tenants is stored together but automatically filtered so each tenant only sees their own data.

Defining a Multi-Tenant Entity

Add @TenantId to a field and set the entity to MultiTenancyType.SHARED.

import { Entity, AutoGeneratedId, TenantId, MultiTenancyType } from '@kinotic-ai/persistence'

@Entity(MultiTenancyType.SHARED)
export class Person {
    @AutoGeneratedId
    id: string | null = null

    @TenantId
    tenantId: string = ''

    firstName: string = ''
    lastName: string = ''
}

The @TenantId field is populated automatically based on the authenticated user's tenant context. You do not need to set it manually when saving entities.

Where a user's tenant comes from

Declaring the entity is only half of it. A user's tenant is assigned when the user is created, and who assigns it is the application's tenantManagement setting: PLATFORM gives every user a tenant of their own at creation, TENANT_ADMINISTRATORS leaves tenants to the people who create them — a customer signing up, or the organization in the portal — whose administrators decide who joins.

Application tenantManagementEntityResult
TENANT_ADMINISTRATORSMultiTenancyType.NONEOne shared dataset — every user sees every other user's rows
PLATFORMMultiTenancyType.NONEStill one shared dataset; the setting has nothing to act on
TENANT_ADMINISTRATORSSHARED + @TenantIdA user created into no tenant has none, and its writes fail with tenantId cannot be null or blank for shared multi tenancy
PLATFORMSHARED + @TenantIdEach user isolated in their own tenant

Choose it when the application is created — createApplicationIfNotExist takes it as an argument, defaulting to TENANT_ADMINISTRATORS — and change it afterwards only from the portal, under Application → Settings → Tenants.

PLATFORM applies to users created while it is in force. Existing users are never backfilled, so choosing it for an application that already has users leaves those users without a tenant, and every write they make to a SHARED entity fails. Settle it before the application has users.

Tenants and their sites

Under TENANT_ADMINISTRATORS a tenant is a Tenant of the application, created by your organization in the portal or through TenantService (createTenantIfNotExist(applicationId, name, description)), whose id is its slugified name: lowercase letters, digits and single dashes, at most 24 characters so it leads a hostname label, and unique within your organization, so a name whose slug a tenant of another of your applications already holds is refused. The tenant's users carry that id as their tenantId. Each of the application's published UIs is also served for the tenant at <tenantId>--<applicationId>--<uiName> under your organization's subdomain of the sites domain, and a sign-in from that site is to the tenant: it admits the tenant's users only, and offers the tenant's own identity providers (TenantOidcConfiguration, added for the tenant on the application's Settings → Sign-in providers tab in the portal) before the application's — see Authentication. A tenant whose users were created with a tenantId no Tenant row names has no login of its own until the row is created.

Under PLATFORM tenants are minted per user and have no rows or sites; the user signs in at the application's own sites.

Tenants and services

A tenant comes from the authenticated participant, and only an application-scope participant carries one. A microservice running in a project's deployment connects at organization scope, so it cannot read or write a SHARED entity through the tenant-scoped repository — it uses the admin repository below, naming the tenants it means to act on. Per-user writes driven by the user belong on a client connected as that user.

Tenant-Isolated Repository

The standard generated Repository automatically filters all operations to the current tenant. No additional configuration is needed.

const service = new PersonRepository()

// Only returns people belonging to the current tenant
const people = await service.findAll({ page: 0, size: 10 })

All CRUD operations (save, find, update, delete, search, count) are scoped to the current tenant transparently.

Admin Repository

When you run kinotic sync on an entity with a @TenantId field, the CLI generates both a tenant-scoped repository (PersonRepository) and an admin repository (PersonAdminRepository) that can access data across tenants. The admin repository is useful for administrative dashboards, reporting, and cross-tenant operations.

const adminService = new PersonAdminRepository()

Access All Tenants

const allPeople = await adminService.findAll(['*'], { page: 0, size: 10 })

Access a Specific Tenant

const tenantPeople = await adminService.findAll(['tenant-123'], { page: 0, size: 10 })

Access Multiple Tenants

const multiPeople = await adminService.findAll(['tenant-123', 'tenant-456'], { page: 0, size: 10 })

Tenant Selection

Admin repository methods accept a tenant selection array as the first argument:

SelectionBehavior
['*']Access data from all tenants
['tenant-123']Access data from a single tenant
['tenant-123', 'tenant-456']Access data from multiple tenants

Security Considerations

A participant that belongs to a tenant is confined to it. Through the admin repository, including its named queries, it may select only its own tenant; naming another tenant, or ['*'], is refused with an authorization error. On save, a blank @TenantId is filled with the participant's tenant and any other value is rejected. Selecting other tenants is possible only for a participant without a tenant, which is how a microservice in a project's deployment connects (organization scope), and such a participant must name the tenant of every entity it saves.

The standard tenant-isolated repository is the default and should be used for regular application logic.

Copyright © 2026