Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesLegalLegal — Data Model

Legal — Data Model

Two tables. Defined in libs/db/src/lib/schema/legal.ts.

ColumnTypeNotes
iduuid PK
typelegal_document_type NOT NULLterms_of_use, privacy_policy, fitness_waiver, cookie_policy, acceptable_use, member_terms, external_payment_acknowledgement
versionvarchar(20) NOT NULLSemver-ish string, e.g. '2.0.0'. Compared by published_at not by parsing this field
localevarchar(5) NOT NULLhe, en, ru
urltext NOT NULLCanonical URL on marketing site
published_attimestamptz NOT NULLSource of truth for “latest” ordering
created_attimestamptz NOT NULL DEFAULT now

Unique: (type, version, locale).

ColumnTypeNotes
iduuid PK
user_iduuid NOT NULLFK users(id) (no ON DELETE — see PII)
organization_iduuidFK organizations(id) nullable — required acceptance scope for external-payment acknowledgement; audit context for other types
document_iduuid NOT NULLFK legal_documents(id) — immutable evidence of the exact version and locale accepted
consent_contextconsent_context NOT NULLowner_onboarding, member_onboarding, reconsent, profile_update
ip_addressvarchar(45)IPv6-capable
user_agenttextFull UA string
consented_attimestamptz NOT NULL DEFAULT now
revoked_attimestamptzNULL = active. No UI today

Index: (user_id, document_id).

Relationships

legal_documents 1 ─── * legal_consents users 1 ─── * legal_consents organizations 1 ─── * legal_consents (external acknowledgement scope; otherwise optional audit context)

Multi-org isolation

Documents are a platform-wide versioned catalog. Privacy, health acknowledgement, platform Terms, and member terms acceptance is platform-level; organization_id records where it happened. external_payment_acknowledgement acceptance is instead scoped to an organization: active evidence and dedup must match (user_id, document_id, organization_id), and acceptance requires a target organization. Evidence from one gym never satisfies another gym’s external-payment acknowledgement. The existing (user_id, document_id) index is non-unique; append-only version evidence remains possible.

The required set is derived from the selected organization role, not stored on a document. Members require privacy + Fitness & Health Acknowledgement (fitness_waiver) + current member_terms; owners require only terms_of_use; admins and coaches require nothing until staff-specific terms are introduced. Member join flows additionally require external acknowledgement for a server-resolved plan payment-link URL or active external payment provider. Pending-only member onboarding may require it for an active external provider, but active-member backstop requirements never include it. Existing members accept only missing current types through /accept-terms; already-current evidence remains valid. Staff creation and invitation never synthesize member consent.

UsersService.getLegalRequirements(userId, organizationId?) returns { types, organizationId }. Selection matches the web client: valid explicit organization first, then membership role priority; stale/omitted selections fall back. Requirements use that one selected role rather than combining roles across organizations. GET /legal/consents/status?organizationId=… preserves the per-type response array and adds isRequired and the resolved organizationId. Missing required acceptance is exactly isRequired && requiresReconsent; clients must not reconstruct it from roles or flags.

The displayed title Fitness & Health Acknowledgement does not rename the existing fitness_waiver type or slug. Its terms preserve mandatory, non-waivable rights.

Localized publication

Every document used by owner or member onboarding must have a current row in each supported locale (he, en, ru). recordConsent fails closed when any requested type is absent in the submitted locale; it never returns after a partial legal write.

0125_needy_zuras.sql publishes member_terms and external_payment_acknowledgement version 1.0.0 in all three locales while transactionally expanding the enum. 0127_legal_document_locales.sql adds the missing English and Russian publications for the existing Terms of Use, Privacy Policy, and Fitness & Health Acknowledgement versions.

The latter migration also repairs the specific partial evidence created before the fail-closed check: a member_onboarding member-terms row proves the unified checkbox was accepted because the join API required both acceptedLegal and acceptedMemberTerms before recording it. The migration copies that row’s scope, IP, user agent, and timestamp into only the absent current privacy and fitness evidence. It does not infer consent from membership or create evidence for users without that explicit member-terms acceptance.

Hebrew canonical URLs have no /he prefix. Publication does not enable a rollout flag or synthesize acceptance outside the narrowly repairable partial write above.

Migration safety

0125_needy_zuras.sql replaces the legal-document enum inside one transaction because PostgreSQL cannot consume a newly added enum value until commit. Existing document IDs and consent foreign keys are preserved. The operation takes an ACCESS EXCLUSIVE lock and rewrites legal_documents; review and schedule that lock before applying.

0127_legal_document_locales.sql is insert-only and idempotent: document rows use the (type, version, locale) conflict key, and repaired consent rows use an active current-version NOT EXISTS guard.

PII handling

  • ip_address and user_agent are plaintext audit evidence.
  • No special encryption.
  • revoked_at exists for soft-revocation. No “right to be forgotten” cascade — the foreign key prevents deleting a referenced user until consent records are handled explicitly.

Soft vs hard delete

  • Documents: never deleted by app code.
  • Consents: soft-revocable via revoked_at. Active rows have revoked_at IS NULL — every read filters on this.