Skip to main content
Version: v1.0.0(int)

Platform

Identity, tenant catalog, membership, invitations, and API tokens. Lives in the platform schema (no org_id filter applies). Consumed by the user, organization, membership, invitation, and API-token workflows.

Tables owned​

TablePurpose
platform.usersOne row per Firebase identity. Profile fields only.
platform.organizationsTenant catalog. Identity-only (name, slug).
platform.organization_usersMembership rows. Role lives here.
platform.user_invitationsPending / accepted / declined / revoked invitations.
platform.api_tokensPersonal Access Tokens (PATs). Hash + prefix only; never the plaintext.

Operations​

Every op takes a conn as its first parameter and explicit ids — Platform does not use the app.org_id GUC.

Users​

  • createUser(conn, { firebaseUid, email, displayName? }) → User. Inserts the platform user record after Firebase sign-up.
  • updateUser(conn, userId, patch) → User. Edits display name / profile fields. Empty patch returns the row unchanged.
  • deleteUser(conn, userId) → void. Hard-deletes the user row; FKs cascade memberships, API tokens, and invitations sent. L3 (W004) purges solely-owned orgs and the Firebase identity around it.
  • touchUserLastLogin(conn, userId) → void. Stamps last_login_at = NOW(). Called from W002 (GET /user) on every authenticated hit.
  • getUserById(conn, userId) → User | null.
  • getUserByFirebaseUid(conn, firebaseUid) → User | null. Looked up on every authenticated request.
  • userExistsByEmailInOrg(conn, email, orgId) → boolean. True iff there is an active membership in orgId whose user has this email. Used by W020 to block invites to existing members.

Organizations​

  • createOrganization(conn, { name, slug }) → Organization. Creates a tenant catalog row.
  • updateOrganization(conn, orgId, patch) → Organization. Edits name / slug.
  • deleteOrganization(conn, orgId) → void. Hard-deletes the org row (FKs are ON DELETE RESTRICT; the L3 purge clears tenant rows first).
  • getOrganizationById(conn, orgId) → Organization | null.
  • listOrganizationsForFirebaseUid(conn, firebaseUid) → Array<{ organization: Organization, role: OrgRole }>. One-query join across platform.users + organization_users + platform.organizations. Used by W002 to assemble the user-plus-orgs payload on app load.

Memberships​

  • addMembership(conn, { userId, orgId, role }) → OrganizationUser. Inserts a (user, org, role) row.
  • updateMembershipRole(conn, { userId, orgId, role }) → OrganizationUser. Promotes / demotes a member.
  • removeMembership(conn, { userId, orgId }) → void. Drops a membership.
  • touchMembershipLastActive(conn, { userId, orgId }) → void. Stamps last_active_at = NOW(). Called from request middleware on every org-scoped request.
  • getMembership(conn, { userId, orgId }) → OrganizationUser | null.
  • listMembersByOrg(conn, orgId) → OrgMember[]. Memberships joined with the user's identity ({ userId, email, displayName, role, isActive, joinedAt, lastActiveAt }), ordered by join date. Used by W016.
  • countOwners(conn, orgId) → number. Count of active owner-role memberships. Used by W018 and W019 to guard the last owner.

Invitations​

  • createInvitation(conn, { orgId, invitedBy, email, role, expiresAt? }) → UserInvitation. Inserts a pending invitation. token is a DB default (gen_random_uuid()); expiresAt defaults to NOW() + 7 days.
  • resendInvitation(conn, invitationId) → UserInvitation | null. Rotates the token and sets expires_at = NOW() + 7 days iff still pending; returns the row or null. Used by W021.
  • acceptInvitation(conn, invitationId) → boolean. Sets accepted_at = NOW() iff still pending. Used by W010 inside its locked transaction.
  • revokeInvitation(conn, invitationId) → boolean. Sets revoked_at = NOW() iff still pending. Used by W022.
  • declineInvitation(conn, invitationId, callerEmail) → boolean. Sets revoked_at = NOW() iff still pending and LOWER(email) = LOWER(callerEmail). Used by W011. Decline and revoke share the revoked_at column — the distinction lives at the call boundary, not in the data.
  • getInvitationById(conn, invitationId) → UserInvitation | null.
  • getInvitationByToken(conn, token) → UserInvitation | null. For the public lookup (W009); returns null for non-UUID tokens so L4 can 404 cleanly.
  • findPendingInvitation(conn, orgId, email) → UserInvitation | null. Pending invitation matching (orgId, email). Used by W020 for resend-on-duplicate.
  • listInvitationsByOrg(conn, orgId, statuses?) → UserInvitation[]. Org-side list (W023).
  • listInvitationsByEmail(conn, email, statuses?) → UserInvitation[]. The invitee's "my pending invitations" view (W008).

API tokens​

  • createApiToken(conn, { userId, orgId, name, tokenPrefix, tokenHash, expiresAt }) → ApiToken. Inserts a PAT row (hash + prefix; the plaintext is never persisted). Used by W005.
  • getApiTokenByHash(conn, hash) → ApiToken | null. Lookup for the auth middleware. Returns the row regardless of revoked / expired state — the caller decides, so failures surface as a clean 401.
  • listApiTokensForUser(conn, userId) → ApiToken[]. Active (non-revoked) tokens, newest first. Used by W006.
  • revokeApiToken(conn, tokenId, userId) → boolean. Sets revoked_at = NOW() iff the token belongs to userId and is still active. Used by W007.
  • touchApiTokenLastUsed(conn, tokenId) → void. Fire-and-forget; called from the auth middleware on every successful PAT request.

Notes​

  • This is the only component that operates with no org_id filter — its tables are platform-wide. The app.org_id GUC is irrelevant here.
  • L2 only does the DB work. Firebase calls (verify / create / delete identity) and email sending live in L3 workflows, not here.