Appearance
Entity: tenantUser
Entity Type: Database table
Description: Records that a user belongs to a given tenant — the list a tenant switcher reads from. This is a derived record: it is created and maintained automatically whenever a user gains a tenant-scoped grant through either permissionSetUserAssignment (a direct grant with tenantId set) or groupMembership (joining a group, which always belongs to exactly one tenant) — and it is never written directly by any feature. It holds no role or permission information itself — what the user may do in the tenant is always determined by their direct assignments and group memberships, not by this table.
A user remains a tenant member as long as at least one of these sources is live; only when the last live direct assignment and the last live group membership for that tenant are both gone does this record's status move to removed.
Data Attributes Table
| Attribute Name | Description | Data Type | Default Value | Required (= Nullable) | Unique | Format | Validations | Index | Example |
|---|---|---|---|---|---|---|---|---|---|
| id | Primary key of the entity. | UUID | Generated in code (app layer) | Yes | Yes | UUID v7 | - | Primary Key | 018ed0b3-c298-7c7a-96d5-8b36f5a7f8d2 |
| userId | The user who belongs to the tenant. | UUID | - | Yes | No | UUID v7 | Foreign Key → user; must exist | name: idx_tenant_users_user_id, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc10 |
| tenantId | The tenant the user belongs to. | UUID | - | Yes | No | UUID v7 | Foreign Key → tenant; must exist | name: idx_tenant_users_tenant_id, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc10 |
| status | Membership status of this user within this tenant. | Enum (TenantUserStatus) | invited | Yes | No | - | One of: invited, active, removed | name: idx_tenant_users_status, type: btree (active records) | active |
| createdAt | Timestamp of when the record was created. Immutable after insert. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 | Cannot be null; cannot be modified after creation. | - | 2026-06-08T00:00:00Z |
| updatedAt | Timestamp of the last update. Set on insert (equal to createdAt) and updated on every change. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 | Cannot be null. | - | 2026-06-08T00:00:00Z |
| deletedAt | Timestamp of soft deletion. Null means the record is active. Once set, immutable. | Timestamp with time zone | - | No | No | ISO 8601 | Immutable once set. Active records: WHERE deletedAt IS NULL | Indexed (active records) | - |
| createdBy | Identifier of the actor who created the record. | String | - | Yes | No | type:actor | Non-empty. | - | system:trigger |
| updatedBy | Identifier of the actor who last updated the record. | String | - | Yes | No | type:actor | Non-empty. | - | system:trigger |
Uniqueness: (userId, tenantId) is unique among active (non-deleted) records, enforced by unique index uidx_tenant_users_active, type: btree.
Audited fields
Recorded (created/deleted: full set; updated: changed fields only): status.
Filed under subject: userId is not duplicated in this set — every entry sets the audit row's own subjectEntityType = "user" / subjectEntityId = userId, so one person's full tenant-membership history is a single indexed read, the same pattern as groupMembership.
Excluded:
id,createdAt,updatedAt,deletedAt,createdBy,updatedBy— redundant with the audit entry's ownentityId/createdAt/createdBy, which already identify the record and the write.userId— filed as the audit row's subject reference instead (see above).tenantId— always present (never null, unlike a platform-levelpermissionSetUserAssignment), so it carries no information beyond the audit entry's owntenantId(RLS scope column).