Skip to content

Database Schema

PostgreSQL with the pgvector extension (required for face embedding storage).

Schema files live in packages/db/src/schema/ — one file per domain entity. After any schema change, run pnpm --filter=@repo/db db:generate and commit the generated migration.

All tables use:

  • UUID primary keys (uuid_generate_v4())
  • createdAt timestamp (default now)
  • updatedAt where rows are updated
  • deletedAt (soft delete) where applicable

Users & Auth

users

End user accounts.

ColumnTypeNotes
iduuid PK
namevarchar(255)
emailvarchar(255)unique
password_hashtextbcrypt
addresstextnullable
billing_addresstextnullable
created_attimestamp

operators

Operator accounts (manage projects, photographers, pricing).

ColumnTypeNotes
iduuid PK
namevarchar(255)
emailvarchar(255)unique
password_hashtextbcrypt
currencyvarchar(3)ISO 4217, default from IP geo
localevarchar(10)BCP 47
timezonevarchar(64)IANA timezone
profile_picture_urltextnullable
created_attimestamp

photographers

Photographers belong to an operator.

ColumnTypeNotes
iduuid PK
operator_iduuid FK → operatorsindexed
namevarchar(255)
emailvarchar(255)unique
password_hashtextbcrypt
addresstextnullable
created_attimestamp

Projects

projects

ColumnTypeNotes
iduuid PK
operator_iduuid FK → operatorsindexed
namevarchar(255)
typeproject_type enumprivate / public — default private
statusproject_status enumdraft / live / closed / archived — default draft
location_namevarchar(255)nullable
start_attimestampnullable
end_attimestampnullable
project_linktextnullable
terms_and_conditionstextnullable
timezonevarchar(64)nullable; falls back to operator timezone if null
created_attimestamp

project_members

Users who have joined a project (via QR).

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
user_iduuid FK → usersindexed
joined_attimestamp
terms_accepted_attimestampnullable
terms_version_iduuid FK → project_termsnullable, indexed — the exact T&C version accepted at join time

Unique index on (project_id, user_id). terms_version_id is nullable for backwards compatibility with rows created before versioning was introduced.

project_terms

Versioned snapshots of a project's Terms & Conditions. A new row is created when the first user joins (version "1") and whenever an operator publishes updated T&C (version "2", "3", …).

Storing a snapshot at join time ensures users can always retrieve exactly the text they agreed to, even if the operator later changes the T&C.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
versionvarchar(50)sequential per project: "1", "2", …
contenttextfull T&C text at the time this version was created
effective_attimestampwhen this version came into effect (default now)
created_attimestamp

Unique index on (project_id, version).

project_photographers

Photographers assigned to a project.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
photographer_iduuid FK → photographersindexed

project_discounts

Discounts linked to a project (composite PK).

ColumnTypeNotes
project_iduuid FK → projectsindexed
discount_iduuid FK → discountsindexed

project_coaches

Coaches associated with a project.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
namevarchar(255)

Media

photos

Project photos uploaded by photographers.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
photographer_iduuid FK → photographersindexed
storage_keyvarchar(512)R2 key for original
thumbnail_keyvarchar(512)nullable, 400px thumb
watermarked_keyvarchar(512)nullable
statusvarchar(20)processing / ready / failed
taken_attimestampnullable — EXIF date extracted during thumbnail generation
uploaded_attimestamp

selfies

User selfies for face recognition.

ColumnTypeNotes
iduuid PK
user_iduuid FK → usersindexed
storage_keyvarchar(512)R2 key
embeddingvector(512)pgvector, 512-dim from Retina clustering service
quality_scorerealnullable, Retina face detection score (0–1)
statusvarchar(20)pending / approved / rejected
rejection_reasontextnullable
created_attimestamp

photo_tags

Face recognition results linking users to photos.

ColumnTypeNotes
iduuid PK
photo_iduuid FK → photosindexed
user_iduuid FK → usersindexed
confidencerealface detection score × 100 (0–100 scale)
sourcevarchar(20)auto / manual
review_statusvarchar(20)confirmed / pending / rejected
created_attimestamp

Unique index on (photo_id, user_id).

WARNING

Only confirmed tags are visible to end users. pending tags are in the manual review queue.

photo_favorites

Photos bookmarked by a user ("favourited"). Used for the Favourites filter in the main-web photo grid.

ColumnTypeNotes
iduuid PK
user_iduuid FK → usersindexed
photo_iduuid FK → photoscascade delete
created_attimestamp

Unique index on (user_id, photo_id).

photo_tag_reports

User-submitted tag accuracy reports.

ColumnTypeNotes
iduuid PK
photo_iduuid FK → photosindexed
user_iduuid FK → usersindexed
report_typevarchar(20)incorrect_tag / missing_tag
created_attimestamp

Unique index on (photo_id, user_id, report_type) — one report per type per user per photo.


Pricing & Promotions

pricing_rules

Per-photo price set by an operator. A rule with project_id IS NULL is the operator default; a rule with a project_id overrides the default for that project. The project override wins.

ColumnTypeNotes
iduuid PK
operator_iduuid FK → operatorsindexed
project_iduuid FK → projectsnullable, indexed — null = operator default
price_per_photo_centsintegerin cents, not null
compare_at_price_centsintegernullable — shown as strikethrough original price
valid_fromtimestampdefault now
valid_untiltimestampnullable — null = no expiry
is_activebooleandefault true
created_attimestamp

discounts

Operator-defined discounts that can be linked to projects.

ColumnTypeNotes
iduuid PK
operator_iduuid FK → operatorsindexed
namevarchar(255)display name
typeenumpercentage / fixed / bundle
valueintegernullable — pct points or cents depending on type
bundle_min_photosintegernullable — required for bundle type
valid_fromtimestampdefault now
valid_untiltimestampnullable
is_activebooleandefault true
created_attimestamp

Discount types:

  • percentagevalue % off subtotal (e.g. value=20 → 20% off)
  • fixedvalue cents off subtotal (e.g. value=200 → $2.00 off)
  • bundlevalue % off if photoCount >= bundleMinPhotos

Only one discount is applied per purchase (the one with the highest savings).

promo_codes

Redeemable codes that apply a specific discount at checkout.

ColumnTypeNotes
iduuid PK
operator_iduuid FK → operatorsindexed
codevarchar(50)unique
discount_iduuid FK → discountsindexed
max_usesintegernullable — null = unlimited
max_uses_per_userintegerdefault 1
used_countintegerdefault 0, atomically incremented on paid purchase
valid_fromtimestampdefault now
valid_untiltimestampnullable
is_activebooleandefault true
created_attimestamp

purchases

ColumnTypeNotes
iduuid PK
user_iduuid FK → usersindexed
project_iduuid FK → projectsindexed
photo_countintegernumber of photos in this purchase
subtotal_centsintegerphotoCount × pricePerPhotoCents before discount
discount_applied_centsintegerdiscount savings, default 0
total_centsintegersubtotalCents - discountAppliedCents
applied_discount_iduuid FK → discountsnullable
applied_promo_codevarchar(50)nullable — promo code string if used
stripe_payment_intent_idvarchar(255)nullable, partial unique index
statusvarchar(20)pending / paid / failed / refunded
created_attimestamp

Indexes: user_id, project_id, status, partial unique on stripe_payment_intent_id WHERE NOT NULL.

purchase_downloads

Tracks which photos a user has access to after a purchase.

ColumnTypeNotes
iduuid PK
purchase_iduuid FK → purchasesindexed
user_iduuid FK → usersindexed
photo_iduuid FK → photosindexed
created_attimestamp

user_consents

Biometric processing consent records.

ColumnTypeNotes
iduuid PK
user_iduuid FK → usersindexed
consent_typevarchar(50)e.g. biometric_processing
consent_versionvarchar(20)
given_attimestamp
revoked_attimestampnullable — null means active

invites

Pending email invitations sent to users who opted in to receive photos from a project. Created by the bulk invite feature in the backoffice.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
emailtextrecipient address (lowercase)
tokenuuidunique — embedded in the invite link
statustextpending / used / expired
expires_attimestamp30 days from creation
used_attimestampnullable — set when the user registers via the invite link
created_attimestamp

qr_tokens

Tokens issued when a user joins a project (via QR or shareable link). Used for event check-in.

ColumnTypeNotes
iduuid PK
project_iduuid FK → projectsindexed
user_iduuid FK → usersindexed
tokenvarchar(255)unique
expires_attimestamp30 days from issue
used_attimestampnullable
created_attimestamp

password_resets

One-time password reset tokens issued when a user requests a password reset via email.

ColumnTypeNotes
iduuid PK
user_iduuid FK → usersindexed
token_hashtextSHA-256 hash of the reset token; unique
expires_attimestamp1 hour from creation
used_attimestampnullable — set when the password is successfully reset
created_attimestamp

Wairo — Internal Documentation