Appearance
Entity: group
Entity Type: Database table
Description: A named, tenant-scoped collection of users used as the primary recipient of permissionSet grants for tenant-side users (see permissionSetGroupAssignment). A group belongs to exactly one tenant for its entire lifetime — it cannot be reassigned to a different tenant. Within a given tenant, a user's group membership is exclusive: a user can belong to at most one group per tenant (see groupMembership), though the same user can belong to a different group in a different tenant.
Groups exist only for tenant-side access. Porsenna platform users are never granted access via a group — they are always granted directly (see permissionSetUserAssignment), including when a single Porsenna user needs access across several tenants (one direct grant per tenant). A group-based, one- click, cross-tenant grant for Porsenna staff, and a Platform-managed "template" mechanism for pre-populating a new tenant's groups, were both considered and explicitly deferred — see Users & Access.
Some groups are auto-created, not administrator-created. When a permissionSet is created with visibility = organization and restrictedToTenantId left null (or when a tenant later becomes active while such a set already exists), one group per relevant tenant is provisioned automatically — named after the Permission Set, empty, holding a fresh permissionSetGroupAssignment for that set — see permissionSet ("Auto-provisioned across every tenant when created unrestricted"). This is narrower than, and distinct from, the deferred group-templating idea referenced above: it reuses one shared Permission Set across tenants (exactly the existing restrictedToTenantId = null reuse case), it doesn't clone Permission Set content per tenant. Once created, an auto-provisioned group is an ordinary group in every respect — nothing tracks that it was system-created, and its members are added the same way as any other group's (see Users & Access). There is no group-management UI in MVP, for a tenant admin or anyone else — a group can't be renamed or deleted through the product at all until that screen exists (🚫 not in MVP).
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 |
| tenantId | The tenant this group belongs to. Set once at creation and never changed. | UUID | - | Yes | No | UUID v7 | Foreign Key → tenant; must exist. Immutable after insert — a group cannot be moved between tenants. | name: idx_groups_tenant, type: btree (active records) | 018fa51f-fda1-79f4-8461-2cb8f1cabc10 |
| name | Human-readable name of the group, shown in the UI (e.g. "Admins", "Accountants"). | String | - | Yes | Yes (per tenant) | - | Non-empty | - | Accountants |
| 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. Null means the group is active. Once set, immutable. | Timestamp with time zone | - | No | No | ISO 8601 | Immutable once set. Active records: WHERE deletedAt IS NULL | name: idx_groups_deleted_at, type: btree (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: (tenantId, name) is unique among active (non-deleted) records, enforced by unique index uidx_groups_tenant_name, type: btree — two groups in the same tenant cannot share a name. This constraint is an analysis-added convention (not stated in the original requirement) to keep a tenant's group list unambiguous; flag for confirmation if a different rule is wanted.
Deletion: A group with any live member cannot be deleted — see DELETE /v1/groups/{groupId} — so deleting a group (soft delete) never touches its members' user records, their groupMembership rows, or their derived tenantUser membership: by the time a group can be deleted, it has none. The group's own permissionSetGroupAssignment rows are soft-deleted along with it.
Audited fields
Recorded (created/deleted: full set; updated: changed fields only): name.
Excluded:
id,createdAt,updatedAt,deletedAt,createdBy,updatedBy— redundant with the audit entry's ownentityId/createdAt/createdBy, which already identify the record and the write.tenantId— immutable after insert, and redundant with the audit entry's owntenantId(RLS scope column).