Skip to main content

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.
NamingValue
ObjectMembership
Resource type (JSON:API type)membership
Collection / records rootmemberships
REST base/v1/memberships
Entity classMembership

API operations

OperationMethod & pathStatus
ListGET /v1/memberships✅ Implemented
RetrieveGET /v1/memberships/{id}✅ Implemented
CreatePOST /v1/memberships✅ Implemented
UpdatePATCH /v1/memberships/{id}✅ Implemented
DeleteDELETE /v1/memberships/{id}✅ Implemented

Data model

Attributes

FieldTypeRequiredConstraintsAllowed valuesDescription
membership_idstring, UUID✅ Yesunique; generated via gen_random_uuid() on insertPublic stable identifier for this membership. Used in all API responses and external references. The internal pk (auto-increment integer) is never exposed.
firebase_idstring⚪ Nonullable; 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_rolestring (MembershipRole enum)⚪ Nonullable; 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 | guestThe 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.
statusstring (MembershipStatus enum)✅ Yesdefault: pending; non-nullablepending | activeLifecycle 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_defaultboolean✅ Yesdefault: 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 | falseFlags 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_tokenstring, UUID⚪ Nonullable; 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_atDate, 🔒 system✅ Yesset by @Property onCreate hook; non-nullableTimestamp when the membership record was created (i.e., when the invitation was issued).
updated_atDate, 🔒 system⚪ Noset by @Property onCreate and onUpdate hooks; nullable in the DB column definitionTimestamp 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_atDate⚪ Nonullable; 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

NameTypeRequiredDescription
personto-one (people)✅ YesThe 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.
workspaceto-one (workspace)✅ YesThe 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_byto-one (people)⚪ NoThe 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