Appearance
Entity: permissionSetGroupAssignment
Entity Type: Database table
Description: Grants one permissionSet to one group. This is the normal, expected granting mechanism for tenant-side access — a tenant user gets access by being a member of a group that holds one or more of these grants, rather than by a direct per-user grant (see permissionSetUserAssignment, reserved for the edge case).
Because a group belongs to exactly one tenant for its entire lifetime, this grant's tenant is always the group's own tenant — there is no independent tenantId to set or keep in sync. tenantId is carried on this row purely as a derived, denormalized value (set once at insert from group.tenantId by trigger, never independently writable) to make tenant-scoped listing fast; the actual permission check always resolves tenant scope via the live group, never by trusting this column alone.
A group may hold more than one Permission Set at once (additive, same as direct assignments). A row here can also be created automatically rather than by an administrator's own action: creating an unrestricted organization-visibility permissionSet (or a tenant later becoming active while one already exists) auto-provisions a group per tenant together with exactly one of these rows, linking that group to the set — see permissionSet ("Auto-provisioned across every tenant when created unrestricted"). Such a row is otherwise ordinary — createdBy records the actor who triggered the provisioning (the set's creator, or whoever activated the tenant), not a synthetic system actor, same as everywhere else in this model.
This entity is not read at evaluation time. It is the authoring source for a group's intended grants — an administrator adds or removes a Permission Set here — but the actual access a member holds is materialized as a row on permissionSetUserAssignment (sourceGroupId set to this group), kept in sync synchronously whenever this table, or the group's membership, changes: adding a Permission Set here propagates a new permissionSetUserAssignment row (with sourceGroupId set) to every current member; removing one deletes the corresponding rows. Evaluation reads only permissionSetUserAssignment; it never joins through this table or through groupMembership any more. A user's effective access from a group is still, in effect, the union across every group they belong to — it just now shows up as the union of their own materialized rows rather than a live join.
A user's tenant membership (see tenantUser) is derived from group membership and genuine direct grants together, and is never assigned directly.
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 |
| groupId | The group this Permission Set is granted to. | UUID | - | Yes | No | UUID v7 | Foreign Key → group; must exist | name: idx_permission_set_group_assignments_group, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc15 |
| tenantId | Derived from group.tenantId at insert time (BEFORE INSERT trigger); immutable thereafter. Kept only for fast tenant-scoped queries — never trusted alone for an authorization decision. | UUID | Derived from group.tenantId — set in trigger | Yes | No | UUID v7 | Must equal the referenced group's tenantId; enforced by trigger, not independently writable. | name: idx_permission_set_group_assignments_tenant, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc10 |
| permissionSetId | The Permission Set being granted. | UUID | - | Yes | No | UUID v7 | Foreign Key → permissionSet; must exist, be active, and (restrictedToTenantId is null or equals this group's tenantId) | name: idx_permission_set_group_assignments_set, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc11 |
| 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-09-01T00: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-09-01T00:00:00Z |
| deletedAt | Timestamp of soft deletion (i.e. the grant was revoked). Null means the grant 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. | - | user:018e... |
| updatedBy | Identifier of the actor who last updated the record. | String | - | Yes | No | type:actor | Non-empty. | - | user:018e... |
Uniqueness: (groupId, permissionSetId) is unique among active (non-deleted) records, enforced by unique index uidx_permission_set_group_assignments, type: btree — the same Permission Set cannot be granted twice to the same group. Tenant is implied by groupId, so it does not need to appear in this constraint.
Audited fields
Recorded (created/deleted: full set; updated: changed fields only): permissionSetId.
Filed under subject: groupId is not duplicated in this set — every entry sets the audit row's own subjectEntityType = "group" / subjectEntityId = groupId, so one group's full Permission Set grant history is a single indexed read.
Excluded:
id,createdAt,updatedAt,deletedAt,createdBy,updatedBy— redundant with the audit entry's ownentityId/createdAt/createdBy, which already identify the record and the write.groupId— filed as the audit row's subject reference instead (see above).tenantId— derived fromgroup.tenantId, redundant with the audit entry's owntenantId(RLS scope column) and withgroup's own trail.