Skip to content
Updated Sep 24, 2026 by Barča Dvořáková · Owner: analysisactiveentity Edit on GitHub

Entity: groupMembership ​

Entity Type: Database table

Description: Records that a user belongs to a group. Membership is the authoring fact that drives access — it is not itself read at evaluation time. Adding a membership row propagates, synchronously and in the same transaction, one permissionSetUserAssignment row per Permission Set the group holds (each carrying sourceGroupId set to this group), copying this row's own restrictions onto each propagated row; removing membership deletes those propagated rows the same way, unless a given row has since been claimed by a genuine direct grant (see the collision rule on permissionSetUserAssignment). Restrictions live here — not on the group, and not on the Permission Set assignment — because two members of the same group can be restricted differently (e.g. two Accountants each restricted to a different set of buildings) even though they share the same group and the same granted Permission Sets; a restriction change on this row is propagated by deleting and recreating the affected permissionSetUserAssignment row(s) with the new restriction, rather than patching them in place.

Because a group belongs to exactly one tenant for its entire lifetime (see group), this record's tenant is fixed the moment it is created and is carried on this row (tenantId) purely as a derived, denormalized value for fast tenant-scoped queries — it is set once, from the group's own tenantId, and is never independently writable or re-synced. It must never be treated as authoritative on its own for an authorization decision; the actual permission check always resolves tenant scope via the live group, and tenantId here exists only to make listing and constraint-checking fast.

Membership in a tenant's group is exclusive: a user can belong to at most one group within a given tenant at a time (enforced below), though the same user can belong to a different group in a different tenant, or hold a direct permissionSetUserAssignment alongside group membership.

Data Attributes Table ​

Attribute NameDescriptionData TypeDefault ValueRequired (= Nullable)UniqueFormatValidationsIndexExample
idPrimary key of the entity.UUIDGenerated in code (app layer)YesYesUUID v7-Primary Key018ed0b3-c298-7c7a-96d5-8b36f5a7f8d2
userIdThe user who is a member of the group.UUID-YesNoUUID v7Foreign Key → user; must existname: idx_group_memberships_user_tenant, type: btree (composite with tenantId, active records)018fa51f-fda1-79f4-8461-2cb8f1cabc10
groupIdThe group this user belongs to.UUID-YesNoUUID v7Foreign Key → group; must exist. Immutable after insert — revoking membership is a soft delete, changing groups is a new row, never an update of this column.name: idx_group_memberships_group, type: btree (active records)018fa51f-fda1-79f4-8461-2cb8f1cabc15
tenantIdDerived from group.tenantId at insert time (BEFORE INSERT trigger); immutable thereafter, since neither a group's tenant nor this row's groupId can change after creation. Kept only for fast tenant-scoped queries and the uniqueness constraint below — never trusted alone for an authorization decision.UUIDDerived from group.tenantId — set in triggerYesNoUUID v7Must equal the referenced group's tenantId; enforced by trigger, not independently writable.name: idx_group_memberships_user_tenant, type: btree (composite with userId, active records)018fa51f-fda1-79f4-8461-2cb8f1cabc10
restrictionsRestriction entries narrowing this member's access within the group's granted Permission Sets. Empty array means unrestricted.JSON array[]YesNoSee restriction collection--see JSON-DAT sibling
createdAtTimestamp of when the record was created. Immutable after insert.Timestamp with time zonenow() — set in codeYesNoISO 8601Cannot be null; cannot be modified after creation.-2026-09-01T00:00:00Z
updatedAtTimestamp of the last update (e.g. a restriction change). Set on insert (equal to createdAt) and updated on every change.Timestamp with time zonenow() — set in codeYesNoISO 8601Cannot be null.-2026-09-01T00:00:00Z
deletedAtTimestamp of soft deletion (membership revoked). Null means the membership is active. Once set, immutable.Timestamp with time zone-NoNoISO 8601Immutable once set. Active records: WHERE deletedAt IS NULLname: idx_group_memberships_deleted_at, type: btree (active records)-
createdByIdentifier of the actor who created the record.String-YesNotype:actorNon-empty.-user:018e...
updatedByIdentifier of the actor who last updated the record.String-YesNotype:actorNon-empty.-user:018e...

Uniqueness: (userId, tenantId) is unique among active (non-deleted) records, enforced by unique index uidx_group_memberships_user_tenant, type: btree — this is exactly what makes group membership exclusive per tenant: a user cannot hold two live memberships whose groups share a tenant, regardless of which two groups they are.

Derived data: Creating (or soft-deleting) a groupMembership row updates the user's tenantUser membership for that tenant, exactly as a permissionSetUserAssignment does — a user is a tenant member if they hold either kind of grant there.

Audited fields ​

Recorded (created/deleted: full set; updated: changed fields only): groupId, restrictions.

Filed under subject: userId is not duplicated in this set — every entry for this entity sets the audit row's own subjectEntityType = "user" / subjectEntityId = userId, so one member's full group-membership history (across every group) is a single indexed read, per the Audit Log's own subject mechanism.

Excluded:

  • id, createdAt, updatedAt, deletedAt, createdBy, updatedBy — redundant with the audit entry's own entityId/createdAt/createdBy, which already identify the record and the write.
  • userId — filed as the audit row's subject reference instead (see above).
  • tenantId — derived from group.tenantId, redundant with the audit entry's own tenantId (RLS scope column) and with group's own trail.