Documentation Index
Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
Use this file to discover all available pages before exploring further.
A Membership represents a user’s participation in a workspace, binding a Person (identified by their Firebase auth identity) to a Workspace with a specific role and lifecycle state. It is the central RBAC artifact in the multi-tenant architecture: every permission check, invitation flow, and workspace-access decision resolves through this record. Each Membership carries a role (owner, admin, member, guest), a status (pending or active), and a provenance flag that marks which workspace is the user’s default. A soft-deleted Membership means access has been revoked; the row is retained for audit purposes.
| Naming | Value |
|---|
| Object | Membership |
Resource type (JSON:API type) | membership |
| Collection / records root | memberships |
| REST base | /v1/memberships |
| Entity class | Membership |
API operations
| Operation | Method & path | Status |
|---|
| List | GET /v1/memberships | ✅ Implemented |
| Retrieve | GET /v1/memberships/{id} | ✅ Implemented |
| Create | POST /v1/memberships | ✅ Implemented |
| Update | PATCH /v1/memberships/{id} | ✅ Implemented |
| Delete | DELETE /v1/memberships/{id} | ✅ Implemented |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|
| membership_id | string, UUID | ✅ Yes | unique; generated via gen_random_uuid() on insert | — | Public stable identifier for this membership. Used in all API responses and external references. The internal pk (auto-increment integer) is never exposed. |
| firebase_id | string | ⚪ No | nullable; part of the composite index idx_memberships_firebase_workspace_deleted (firebase_id, workspace_pk, deleted_at) | — | Firebase Authentication UID of the user. Populated when the membership belongs to a Firebase-authed user. Used as the cross-Person identity key: a single Firebase user may have multiple Person rows (one per invited email) but should have exactly one active is_default membership across all of them. |
| membership_role | string (MembershipRole enum) | ⚪ No | nullable; constrained in application code to MEMBERSHIP_ROLES values; part of the composite index idx_memberships_workspace_role_deleted (workspace_pk, membership_role, deleted_at) | owner | admin | member | guest | The RBAC role granted to this user within the workspace. Determines which actions the user may perform. Role assignment happens at invitation acceptance, not at invite creation. Parent-workspace admin retention is automatic on workspace hierarchy changes. |
| status | string (MembershipStatus enum) | ✅ Yes | default: pending; non-nullable | pending | active | Lifecycle state of the membership. Newly created memberships (on invitation) start as pending; they transition to active upon the invitee accepting. Pending memberships confer no workspace access. |
| is_default | boolean | ✅ Yes | default: false; non-nullable. Business invariant: exactly one active, non-deleted membership per firebase_id must have is_default = true. Enforced by application logic and backfilled by Migration20260424100000. | true | false | Flags which workspace is the user’s default landing workspace. Used to redirect the user after login when no explicit workspace is specified. Exactly one active membership per firebase_id should be marked true; zero or multiple defaults indicate a corrupted state (see Migration20260424100000_backfill_membership_default_flag). |
| invite_token | string, UUID | ⚪ No | nullable; unique (partial — uniqueness enforced across non-null values only) | — | One-time token distributed in the invitation email. Present while the membership is in pending status; nulled out (or the row is reused) upon acceptance. Serves as the authentication credential for the invitation acceptance flow. Re-sending an invite to the same email reuses the existing pending membership row rather than creating a new one. |
| created_at | Date, 🔒 system | ✅ Yes | set by @Property onCreate hook; non-nullable | — | Timestamp when the membership record was created (i.e., when the invitation was issued). |
| updated_at | Date, 🔒 system | ⚪ No | set by @Property onCreate and onUpdate hooks; nullable in the DB column definition | — | Timestamp of the last mutation to this row (e.g., status transition from pending to active, role change). Auto-managed by the ORM lifecycle hook. |
| deleted_at | Date | ⚪ No | nullable; part of composite indexes for firebase_id and workspace+role hot-path lookups. Soft-delete sentinel: when non-null the membership is revoked. | — | Soft-delete timestamp. A non-null value means the membership has been revoked. The row is retained for audit history. All active-membership queries filter deleted_at IS NULL. |
Relationships
| Name | Type | Required | Description |
|---|
| person | to-one (people) | ✅ Yes | The Person record that holds this membership. A Person is the email-scoped identity (one Person per invited email address). The membership binds this Person to the workspace. Not nullable. |
| workspace | to-one (workspace) | ✅ Yes | The Workspace this membership grants access to. The workspace is the multi-tenant boundary; every permission check, data-view query, and notification scopes to this workspace. Not nullable. |
| invited_by | to-one (people) | ⚪ No | The Person who issued this invitation. Nullable — absent for memberships created by the system (e.g., workspace creator’s own bootstrapped membership) or for legacy rows predating this column. Provides audit provenance for the invite grant. |
System-computed
- membership_id is generated via gen_random_uuid() as a database default and also seeded via randomUUID() in the entity constructor, ensuring the UUID is available before the first flush.
- created_at is set by the @Property onCreate lifecycle hook and is never subsequently modified.
- updated_at is set by both the @Property onCreate and onUpdate lifecycle hooks; it reflects the most recent mutation to the row.
- deleted_at is the soft-delete sentinel. Setting it to a non-null timestamp revokes access. Queries scoped to active memberships always filter deleted_at IS NULL.
- is_default invariant: at most one active (status = active, deleted_at IS NULL) membership per firebase_id may have is_default = true. MembershipService.acceptInvitation computes isFirstWorkspace based on the firebase_id scope (not the Person scope) to determine whether to set is_default = true on the new membership. Migration20260424100000_backfill_membership_default_flag repairs any existing rows where this invariant was violated.
- invite_token is a UUID generated at invitation creation and serves as a single-use bearer credential. The invitation flow is idempotent: re-sending an invite to the same email reuses the existing pending membership row rather than inserting a duplicate.
- status transitions: pending (created at invite issuance) -> active (set atomically at acceptance). A failed mid-flow acceptance must leave the row in pending status with no membership access granted.
- Two composite database indexes are maintained for hot-path queries: idx_memberships_firebase_workspace_deleted (firebase_id, workspace_pk, deleted_at) for per-user workspace resolution, and idx_memberships_workspace_role_deleted (workspace_pk, membership_role, deleted_at) for owner/admin lookup within a workspace (added in Migration20260416000000).
Example
{
"data": {
"type": "membership",
"id": "b3e2f1a0-4c7d-4e9f-8a1b-2d3c5e6f7890",
"attributes": {
"membership_id": "b3e2f1a0-4c7d-4e9f-8a1b-2d3c5e6f7890",
"firebase_id": "VmN9QkR2TpU8xLdWoAhJzKcFgYbE1s3i",
"membership_role": "admin",
"status": "active",
"is_default": true,
"invite_token": null,
"created_at": "2025-09-14T10:22:00.000Z",
"updated_at": "2025-11-03T08:45:12.000Z",
"deleted_at": null
},
"relationships": {
"person": {
"data": { "type": "people", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
},
"workspace": {
"data": { "type": "workspace", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" }
},
"invited_by": {
"data": { "type": "people", "id": "11223344-5566-7788-99aa-bbccddeeff00" }
}
}
}
}
Source: apps/api/src/database/entities/Membership.ts · domain: workspace · tier: Platform