> ## 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.

# Membership

> 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

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

```json theme={null}
{
  "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" }
      }
    }
  }
}
```

<sub>Source: `apps/api/src/database/entities/Membership.ts` · domain: workspace · tier: Platform</sub>
