Flow Platform — Database Schema

Last updated: 2026-05-28 — Added marketing lead tables waitlist (consumer pre-registration) and partner_leads (B2B: organizers/venues, merchants, artists, influencers), powering the web /waitlist and /partners pages. Both are server-only (service_role), RLS-enabled with no anon policies. Also reconciled git↔DB migration drift: migrations applied directly to prod (drop_legacy_event_columns, drop_squads_tables_use_crews_only, decouple_event_likes_from_attendance, security/index fixes, event-series cleanup) are now committed under their exact ledger versions.

Earlier (2026-05-08) — Added event_likes table + events.like_count column to decouple like (popularity) from attendance (event_attendees). Migration decouple_event_likes_from_attendance. See feat-event-details-v3 for the rationale and app-layer changes.

Earlier (2026-05-07): DB recon during event-details v3 refactor surfaced several undocumented tables (event_areas, event_lineup_slots) and column drift on events. Squads tables dropped (use crews only).

Earlier note (2026-04-28): event series Flutter integration complete + full mock data seeded (10 users, 4 series, 18 events, 31 ratings, connections, attendees). Series aggregates recalculated manually after replica-mode seed.

This document reflects the live Supabase schema (project hshqhtnkcsbenxorvlmm). Verified via migration files + MCP queries. Before any schema work, read 2026-04-27-architecture-refactor-master-guide §1 first.


Database Architecture Overview

Flow uses a Supabase-only architecture: a single Postgres database holds all operational truth, protected by RLS on every table.

TechnologyRole
PostgreSQL (Supabase)Authoritative source for all data: users, events, venues, chat, social graph, notifications
PostGISGeospatial queries (geo_point column)
pg_trgmFuzzy text search
RLSAuthorization at DB level — no application layer re-verification needed

Legacy architecture (historical)

Until 2026-03-29 Flow used a dual-database architecture (Postgres + MongoDB + Redis). The Node.js microservices in the legacy backend/ repo described that system. The Supabase-only migration eliminated MongoDB and Redis. See 2026-03-29-supabase-only-migration-design.


PostgreSQL / Supabase Schema

Security Model

All tables have Row-Level Security (RLS) enabled. The general pattern is:

  • Authenticated users can read/write their own rows
  • Admins and super-admins can access all rows via the is_admin() helper function
  • Service role (backend) bypasses RLS entirely

is_admin() and is_super_admin() helpers

-- SECURITY DEFINER: runs as postgres, bypassing RLS on profiles.
-- This prevents infinite recursion when admin policies need to check profiles.role.
SELECT public.is_admin();       -- true if current user has role 'admin' or 'super_admin'
SELECT public.is_super_admin(); -- true if current user has role 'super_admin'

Why SECURITY DEFINER? A policy on profiles that queries profiles to check the role creates infinite recursion. The SECURITY DEFINER function runs with elevated privileges and bypasses RLS, breaking the cycle. See 20260324000008_db_audit_fix.sql Section 1 for details.


Core Tables

profiles

Extends auth.users (Supabase Auth) with application-level profile data.

ColumnTypeNotes
idUUID PKFK → auth.users(id) ON DELETE CASCADE
emailTEXT UNIQUE
usernameTEXT UNIQUE
first_nameTEXT
last_nameTEXT
phone_numberTEXT
profile_pictureTEXTURL
bioTEXT
date_of_birthTIMESTAMPTZ
locationTEXTHuman-readable location string
latitudeFLOAT8
longitudeFLOAT8
geo_pointGEOGRAPHY(Point,4326)PostGIS point for spatial queries
websiteTEXT
interestsTEXT[]Array of interest strings
settingsJSONBUser preferences/settings
roleTEXTCHECK: user, moderator, admin, super_admin, vendor, partner, influencer, pr, dj_artist — default user
statusTEXTCHECK: active, suspended, banned, pending — default active
is_activeBOOLEANLegacy flag — kept for backward compat, backfilled into status
is_email_verifiedBOOLEAN
is_phone_verifiedBOOLEAN
is_verifiedBOOLEANPlatform verification badge
karma_scoreNUMERIC(3,2)0.00–5.00, default 3.00
karma_reviews_countINTEGER
followers_countDropped in migration 000010 — no follower model
following_countDropped in migration 000010 — no follower model
events_createdINTEGERMaintained by trg_events_created on events
events_attendedINTEGERMaintained by trg_events_attended on event_attendees
friends_countINTEGERMaintained by trg_friends_count on connections
xpINTEGER DEFAULT 0Gamification XP score
levelINTEGER DEFAULT 1CHECK (level >= 1)
vibe_scoreINTEGER DEFAULT 0
flow_scoreINTEGER DEFAULT 0
badgesJSONB DEFAULT ’[]‘Legacy — prefer user_badges junction table
ban_reasonTEXTDenormalized from user_bans for quick admin display
ban_expires_atTIMESTAMPTZNULL = not banned or permanent
banned_atTIMESTAMPTZ
banned_byUUID FK → profiles(id)
last_activeTIMESTAMPTZ
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZAuto-updated via trigger

RLS policies:

  • "Users can read own full profile" — SELECT where auth.uid() = id
  • "Authenticated users can read safe profile fields" — SELECT for all authenticated (use public_profiles view for unauthenticated reads)
  • "Users can update own profile" — UPDATE where auth.uid() = id
  • "admin_read_all_profiles" — SELECT via is_admin()
  • "admin_update_any_profile" — UPDATE via is_admin()

⚠️ Counter columns (posts_count, followers_count, etc.) are all 0 because there are no triggers to update them. Compute these via JOIN instead of reading the denormalized columns. A future migration should either add triggers or drop these columns.


events

Core event data with geospatial support.

ColumnTypeNotes
idUUID PK
organizer_idUUIDFK → profiles(id)
venue_idUUIDFK → venues(id) — nullable
titleTEXT
descriptionTEXT
image_urlTEXT
image_urlsTEXT[]
start_dateTIMESTAMPTZ
end_dateTIMESTAMPTZ
locationJSONB{address, city, country, lat, lng, ...}
location_pointGEOGRAPHY(Point,4326)PostGIS for radius queries
latitudeFLOAT8
longitudeFLOAT8
price_centsINTEGER NOT NULLPrice in eurocents. 0 = free. Canonical monetary field (§2.2 no-float rule).
price_currencyTEXTISO 4217 code, default EUR. CHECK ^[A-Z]{3}$
max_attendeesINTEGERMaximum capacity (never use capacity — that column is on venues)
attendee_countINTEGERDenormalized count — maintained by trigger
is_publicBOOLEAN
is_freeBOOLEAN
typeTEXTCHECK: in_person, virtual, hybrid — default in_person
categoryTEXTCHECK: social, nightlife, music, arts, food_drinks, clubbing, party, live_music, tech, sports, wellness, culture, networking, generale — default generale
statusTEXTCHECK: draft, active, published, cancelled, reported, deleted — default draft
series_idUUIDFK → event_series(id) ON DELETE SET NULL — links instance to its series
deleted_atTIMESTAMPTZSoft-delete timestamp — NULL = not deleted
deleted_byUUIDFK → profiles(id) — admin who soft-deleted
ticket_urlTEXT
dress_codeTEXT
age_requirementINTEGER
additional_infoJSONB
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZAuto-updated via trigger

⚠️ The column is max_attendees, NOT capacity. Early frontend code mistakenly used capacity — this caused null values in the admin portal events table.


venues

Physical venue data.

ColumnTypeNotes
idUUID PK
owner_idUUIDFK → profiles(id)
nameTEXT
descriptionTEXT
addressTEXT
cityTEXT
countryTEXT
latitudeFLOAT8
longitudeFLOAT8
location_pointGEOGRAPHY(Point,4326)PostGIS
capacityINTEGERPhysical venue capacity
categoryTEXT
image_urlTEXT
imagesTEXT[]
websiteTEXT
phoneTEXT
emailTEXT
social_linksJSONB
amenitiesTEXT[]
opening_hoursJSONB
is_verifiedBOOLEAN
is_activeBOOLEAN
ratingNUMERIC(3,2)
review_countINTEGER
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Social Graph

connections

The canonical friend/connection table. Used by all social RPCs.

ColumnTypeNotes
idUUID PK
user_idUUIDFK → profiles(id) ON DELETE CASCADE
friend_idUUIDFK → profiles(id) ON DELETE CASCADE
statusTEXTCHECK: pending, accepted, rejected, blocked
connection_depthINTEGER DEFAULT 00=acquaintance, 1=friend, 2=buddy, 3=crew — updated by compute-connection-depth Edge Function
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Counter trigger (fixed 2026-04-27): trg_friends_count fires AFTER INSERT OR UPDATE OR DELETE and updates profiles.friends_count for both user_id and friend_id. INSERT branch was missing before this fix.

friendshipsDropped in migration 008. It was a duplicate of connections with 0 rows.


Social model — friends only

Flow uses a mutual-friend model exclusively (connections table). There is no follower/following asymmetric model. The followers table was dropped in migration 000010 (2026-04-27). All social features use connections with status = 'accepted'.


Events Social Graph

event_attendees

ColumnTypeNotes
idUUID PK
event_idUUIDFK → events(id)
user_idUUIDFK → profiles(id)
statusTEXTgoing, interested, not_going
ticket_idUUID
created_atTIMESTAMPTZ

event_tags

Junction table linking events to tags (preferred over events.tags TEXT[]).

ColumnType
event_idUUID FK → events(id)
tag_idUUID FK → tags(id)

tags

ColumnTypeNotes
idUUID PK
nameTEXT UNIQUE
categoryTEXTENUM-equivalent CHECK
parent_idUUIDSelf-referential FK for sub-tags
usage_countINTEGERIncremented on INSERT to event_tags or profiles.interests
translationsJSONB DEFAULT ’{}‘{"it": "...", "en": "..."} — added migration 20260414000001
created_atTIMESTAMPTZ

event_series

The “brand” behind a recurring event. “La Prosperosa”, “Pullup”, etc. are series — each occurrence at any venue is a separate events row linked via series_id.

Searching by name finds the series → shows upcoming instances (next dates + venues) + past edition photos + aggregated star rating.

ColumnTypeNotes
idUUID PK
organizer_idUUIDFK → profiles(id) ON DELETE SET NULL
nameTEXT NOT NULL”La Prosperosa”
slugTEXT UNIQUEURL-safe, e.g. la-prosperosa
descriptionTEXT
cover_image_urlTEXT
categoryTEXTSame enum as events.category
recurrence_typeTEXTirregular, weekly, biweekly, monthly, custom
recurrence_interval_daysINTEGEROnly when recurrence_type = 'custom'
avg_ratingNUMERIC(3,2)Aggregated from event_ratings across all instances — maintained by trg_sync_series_rating
total_ratingsINTEGER
total_editionsINTEGERCount of all events in this series — maintained by trg_sync_series_editions
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Querying a series:

-- Upcoming instances
SELECT e.* FROM events e
WHERE e.series_id = '<series_id>'
  AND e.start_date > now()
ORDER BY e.start_date ASC;
 
-- Past edition photos (from any edition)
SELECT ep.* FROM event_photos ep
JOIN events e ON e.id = ep.event_id
WHERE e.series_id = '<series_id>'
  AND e.start_date < now();

event_ratings

Stars-only rating (1–5) per user per event. No text reviews. Ratings aggregate to event_series.avg_rating via trigger.

ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → profiles(id) ON DELETE CASCADE
event_idUUID NOT NULLFK → events(id) ON DELETE CASCADE
starsINTEGERCHECK (stars BETWEEN 1 AND 5)
created_atTIMESTAMPTZ

Unique constraint: (user_id, event_id) — one rating per user per event.

reviewsDropped in migration 000010. Text reviews replaced by event_ratings.


event_likes

User → event “like” (bookmark / popularity signal). Independent of event_attendees since 2026-05-08 — a user can like an event without RSVPing, and vice-versa.

ColumnTypeNotes
user_idUUID FK → profiles(id) ON DELETE CASCADEComposite PK part 1
event_idUUID FK → events(id) ON DELETE CASCADEComposite PK part 2
created_atTIMESTAMPTZ NOT NULL DEFAULT now()

Indexes: (event_id) and (user_id) separately for both directions of fan-out.

RLS: public SELECT (popularity is a public signal); INSERT/DELETE only on own row (user_id = auth.uid()). FORCE RLS enabled.

Counter trigger event_likes_count_trg maintains events.like_count on INSERT/DELETE. Use events.like_count for the popularity stat — never count event_likes rows in user-facing reads.

Legacy

Before this table existed, “like” was mapped to event_attendees.status='interested'. The 2026-05-08 migration backfilled all such rows into event_likes and deleted them from event_attendees. The legacy column events.interested_count is no longer maintained and should be dropped in a future cleanup.


event_areas

Physical zones inside an event (“Main Stage”, “Garden”, “Underground”). Used by the lineup section.

ColumnTypeNotes
idUUID PKuuid_generate_v4()
event_idUUID FK → events(id) ON DELETE CASCADENOT NULL
nameTEXT NOT NULLDisplay name
descriptionTEXT
capacityINTEGEROptional per-area capacity
sort_orderINTEGER NOT NULL DEFAULT 0
created_at/updated_atTIMESTAMPTZ

RLS: public SELECT; organizer + admin write. sort_order is the canonical render order.

event_lineup_slots

Time-bounded performance/activity slots, optionally tied to an event_areas row.

ColumnTypeNotes
idUUID PK
event_idUUID FK → events(id) ON DELETE CASCADENOT NULL
area_idUUID FK → event_areas(id)Nullable — null = “general” slot, not tied to an area
titleTEXT NOT NULL
artistTEXT
descriptionTEXT
start_timeTIMESTAMPTZ NOT NULL
end_timeTIMESTAMPTZ
sort_orderINTEGER NOT NULL DEFAULT 0Tiebreaker when start_time is equal
created_at/updated_atTIMESTAMPTZ

CHECK constraint: end_after_start (end_time IS NULL OR end_time > start_time).

The mobile app groups slots by area_id and within each area sorts chronologically by start_time. Slots with area_id = NULL render in a single “General” group.

Legacy events.lineup jsonb

The events.lineup jsonb column (default []) is the predecessor of this table. The mobile app no longer reads it. Drop pending confirmation that no production rows depend on it.


Crews

squads / squad_members tables were dropped 2026-05-07 (migration drop_squads_tables_use_crews_only). They had existed with 0 rows — the previous note (“do not exist”) was incorrect. The mobile app’s Squad Dart class and SquadService are naming-misleading: they already query the crews table internally. A future cleanup pass should rename them to align with the schema.

crews

ColumnTypeNotes
idUUID PK
nameTEXT
creator_idUUIDFK → profiles(id)
chat_idUUIDFK → chats(id)
event_idUUIDFK → events(id)
max_sizeINTEGER
statusTEXT
created_atTIMESTAMPTZ

crew_members

ColumnType
idUUID PK
crew_idUUID FK → crews(id)
user_idUUID FK → profiles(id)
roleTEXT
joined_atTIMESTAMPTZ

crew_feedback

Karma reviews between crew members.

ColumnType
idUUID PK
crew_idUUID FK → crews(id)
reviewer_idUUID FK → profiles(id)
reviewee_idUUID FK → profiles(id)
ratingINTEGER (1-5)
commentTEXT
created_atTIMESTAMPTZ

Messaging

chats

ColumnTypeNotes
idUUID PK
nameTEXTNULL for DMs (direct chats), set for groups
typeTEXTCHECK: direct, group, crew, squad, venue
created_byUUIDFK → profiles(id)
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Direct message chats have name = NULL. Before migration 008, they incorrectly had name = 'Direct Chat'.

chat_participants

ColumnTypeNotes
idUUID PK
chat_idUUIDFK → chats(id)
user_idUUIDFK → profiles(id)
roleTEXTCHECK: owner, admin, member
statusTEXTCHECK: active, muted, banned, left
last_read_atTIMESTAMPTZ
joined_atTIMESTAMPTZ

messages

ColumnTypeNotes
idUUID PK
chat_idUUIDFK → chats(id)
sender_idUUIDFK → profiles(id)
contentTEXT
typeTEXTtext, image, system
metadataJSONB
is_deletedBOOLEANSoft delete
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

Campaigns & Marketing

campaigns

ColumnTypeNotes
idUUID PK
creator_idUUIDFK → profiles(id) — nullable (system campaigns have NULL)
titleTEXT
descriptionTEXT
typeTEXTCHECK: social, environmental, music, health, education, business, art, sports, culture, other
statusTEXTCHECK: active, completed, cancelled, draft
target_audienceJSONB
participant_countINTEGER
banner_urlTEXT
icon_urlTEXT
start_dateTIMESTAMPTZ
end_dateTIMESTAMPTZ
created_atTIMESTAMPTZ

There are currently 23 seed/system campaigns with creator_id = NULL. The FK was added in migration 008 (ON DELETE SET NULL). Enforce creator_id NOT NULL at the application layer.

waitlist

Consumer beta pre-registration captured by the marketing site /waitlist page (web/app/api/waitlist/route.ts). Email is the only required field; the rest enrich the lead for launch prioritisation and attribution.

ColumnTypeNotes
idUUID PK
emailTEXT NOT NULLUnique via lower(email) index
first_nameTEXTOptional
cityTEXTLaunch-prioritisation signal
platformTEXTCHECK: ios, android
referral_sourceTEXTCHECK: instagram, tiktok, friend, search, other — marketing attribution
age_bandTEXTCHECK: under_18, 18_24, 25_34, 35_plus — soft 18+ signal
user_agent / source_ipTEXTTriage / abuse metadata
statusTEXTCHECK: pending, invited, joined, spam — default pending
created_atTIMESTAMPTZ

RLS enabled with no policies: server-only access via service_role (same pattern as contact_messages). Migration 20260528000001_waitlist.

partner_leads

B2B / pro partnership leads captured by /partners (web/app/api/partners/route.ts). Distinct funnel from waitlist.

ColumnTypeNotes
idUUID PK
roleTEXT NOT NULLCHECK: organizer_venue, merchant, artist_dj, influencer
emailTEXT NOT NULLUnique via (lower(email), role) index
contact_nameTEXT
business_nameTEXTVenue / brand / stage name / handle
cityTEXT
websiteTEXT
instagramTEXTHandle or URL
audience_sizeTEXTFree-form range (mostly influencers)
messageTEXT”What you offer / what you’re looking for”
user_agent / source_ipTEXTTriage / abuse metadata
statusTEXTCHECK: new, contacted, qualified, won, spam — default new
created_atTIMESTAMPTZ

RLS enabled with no policies: server-only via service_role. Optional Resend team notification on insert. Migration 20260528000002_partner_leads.


Platform Administration

app_config

Key-value store for runtime-configurable application settings.

ColumnType
idUUID PK
keyTEXT UNIQUE
valueJSONB
descriptionTEXT
updated_atTIMESTAMPTZ

Current keys (as of 2026-03-24):

KeyValueDescription
event_rate_limit_user10Max events a user can create per day
event_rate_limit_venue50Max events a venue can create per day
feedback_survey_pct20% of users shown feedback survey
max_crew_size10Default max crew size
max_squad_size8Default max squad size
karma_min_score1.0
karma_max_score5.0
karma_default3.0

RLS: only admins can write. All authenticated users can read.

app_issues

User-reported issues / bug reports submitted from the app.

ColumnTypeNotes
idUUID PK
user_idUUIDFK → profiles(id)
titleTEXT
descriptionTEXT
categoryTEXT
statusTEXTopen, in_progress, resolved, closed
priorityTEXT
assigned_toUUIDFK → profiles(id) — admin assignee
resolved_byUUIDFK → profiles(id) — admin who resolved
resolved_atTIMESTAMPTZ
metadataJSONBDevice info, app version, etc.
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

assigned_to, resolved_by, resolved_at were added in migration 008 for issue workflow tracking.


Verification & Moderation

verification_requests

Users requesting platform verification badge.

ColumnTypeNotes
idUUID PK
user_idUUIDFK → profiles(id)
statusTEXTpending, approved, rejected
submission_dataJSONB
reviewed_byUUIDFK → profiles(id)
reviewed_atTIMESTAMPTZ
created_atTIMESTAMPTZ

moderator_permissions

ColumnTypeNotes
idUUID PK
moderator_idUUIDFK → profiles(id)
permissionTEXT
granted_byUUIDFK → profiles(id)
created_atTIMESTAMPTZ

commentsDropped in migration 000010. Comments removed from product.


PR / Influencer Layer

pr_profiles

Extended profile data for users with the pr role.

ColumnType
idUUID PK
user_idUUID UNIQUE FK → profiles(id)
agencyTEXT
specializationsTEXT[]
contact_emailTEXT
portfolio_urlTEXT
created_atTIMESTAMPTZ

venue_prs

Links PR users to venues they manage.

ColumnType
idUUID PK
venue_idUUID FK → venues(id)
pr_user_idUUID FK → profiles(id)
roleTEXT
created_atTIMESTAMPTZ

Public Views

public_profiles

A safe public projection of profiles — excludes sensitive columns (email, phone_number, date_of_birth, latitude, longitude). Also excludes posts_count (dropped 2026-04-27).

Always use this view for unauthenticated or public-facing profile reads. Has anon + authenticated SELECT grants.

Columns exposed: id, username, first_name, last_name, profile_picture, bio, location, website, interests, role, account_type, verification_status, status, xp, vibe_score, flow_score, karma_score, karma_reviews_count, badges, gender, hometown, last_active, created_at, profile_visibility, values_visibility, followers_count, following_count, events_created, events_attended, friends_count. Visibility-gated: drink_preferences, music_energy.


Dropped Tables

TableMigrationReason
_deprecated_posts202603240000080 rows, no active RLS policies
friendships20260324000008Exact duplicate of connections, 0 rows
followers20260427000010Asymmetric follow model removed — friends only (connections)
comments20260427000010Comments feature removed from product
reviews20260427000010Text reviews replaced by event_ratings (stars only)
squads, squad_membersdrop_squads_tables_use_crews_only (2026-05-07)0 rows, never used by the app — crews is canonical

Known Technical Debt

IssueStatusNotes
Counter columns in profiles unreliable✅ Fixed 2026-04-27Triggers added for all 5 counters; posts_count dropped (no source table)
connections FK references auth.users✅ Fixed 2026-04-27Migrated to profiles(id)
squads / squad_members tables✅ N/AThese tables don’t exist in the live DB
user_follows / saved_events tables✅ N/AThese tables don’t exist in the live DB; followers is canonical
events.tags TEXT[]✅ Dropped 2026-04-27Dropped in migration 000008. App uses event_tags junction table.
campaigns.creator_id nullableOpen23 system/seed campaigns have NULL. Enforce NOT NULL once data is populated.
Social model — friends only✅ Resolved 2026-04-27followers dropped in 000010. Only connections (mutual) exists.

Migration History (PostgreSQL)

Migrations are in supabase/migrations/ ordered by timestamp prefix.

FilePurpose
20250601000000_mvp_foundations.sqlInitial schema: profiles, events, venues, crews, tags, RLS
20260101000001_chat_system.sqlchats, chat_participants, messages
20260101000002_campaigns.sqlcampaigns table
20260101000003_app_config.sqlapp_config key-value store
20260101000004_app_issues.sqlapp_issues bug reporting
20260101000005_verification_moderation.sqlverification_requests, moderator_permissions
20260324000006_profiles_status_column.sql⚠️ SUPERSEDED by 008
20260324000007_events_nullable_defaults.sql⚠️ SUPERSEDED by 008
20260324000008_db_audit_fix.sqlComprehensive security + integrity audit fix — see audit doc

For full details on what migration 008 fixed, see Database Audit 2026-03-24.


Security & Moderation (post-2026-04-20)

These tables were added after the last schema doc revision and are now documented:

user_bans

Full ban history with lift tracking. GDPR-friendly ON DELETE SET NULL on admin FKs.

ColumnType
idUUID PK
user_idUUID FK → profiles(id)
banned_byUUID FK → profiles(id) ON DELETE SET NULL
lifted_byUUID FK → profiles(id) ON DELETE SET NULL
reasonTEXT
expires_atTIMESTAMPTZ — NULL = permanent
lifted_atTIMESTAMPTZ
created_atTIMESTAMPTZ

admin_actions_log

Audit trail for all admin operations. Polymorphic target via target_type + target_id.

ColumnType
idUUID PK
admin_idUUID FK → profiles(id)
actionTEXT
target_typeTEXT
target_idUUID
metadataJSONB
created_atTIMESTAMPTZ

RLS: admin read + insert only. Service role writes.

reports

Admin moderation queue (distinct from user_reports which is user-to-user flagging).

ColumnType
idUUID PK
reporter_idUUID FK → profiles(id)
target_typeTEXT
target_idUUID
reasonTEXT
statusTEXT CHECK: pending, reviewed, dismissed, actioned
reviewed_byUUID FK → profiles(id) — accountability column
created_atTIMESTAMPTZ

tag_affinity

Co-occurrence weight table for neural tag ordering. See context-map tag section.

ColumnTypeNotes
tag_a_idUUID FK → tags(id)
tag_b_idUUID FK → tags(id)
co_occurrencesINTEGERHow many users selected both
weightFLOATP(B|A) — conditional probability, range 0–1
computed_atTIMESTAMPTZ

RLS: public SELECT, service_role write. FORCE RLS ✅.

saved_events

ColumnType
user_idUUID FK → auth.users(id)§4.10 N/A: saved_events does not exist in live DB
event_idUUID FK → events(id)
created_atTIMESTAMPTZ

user_blocks / user_reports

  • user_blocks (blocker_id, blocked_id) — composite PK, user-to-user blocks
  • user_reports — user-submitted abuse reports (distinct from admin reports table)

Materialized Views

ViewMigrationPurpose
leaderboard_xp20260330000006Top 100 users by XP, refreshed hourly via pg_cron. RPC: get_leaderboard()

Migration History

Migrations in supabase/migrations/ ordered by timestamp:

FilePurpose
2026032400000120260324000013Original schema (profiles, events, venues, crews, tags, RLS)
20260330000001Notifications, badges, recommendations + RLS
20260330000002Full-text search indexes (pg_trgm)
20260330000003Connection depth + chat last_read_at
20260330000004Drop deprecated followers table
20260330000005Seed 20 badge definitions
20260330000006Leaderboard materialized view + RPC
20260414000001i18n translations JSONB on tags/badges/campaigns
20260420000001user_bans, admin_actions_log, events soft-delete, profiles ban columns
20260422060818reports table (admin moderation queue)
20260427000001tag_affinity table + FORCE RLS (neural tag ordering)
20260427000002Security & perf cleanup — FORCE RLS all tables, price_cents enforce, 6 new indexes
20260427000003Counter triggers — followers_count/following_count on followers, fixed friends_count INSERT gap, dropped posts_count, rebuilt public_profiles view, backfilled all counters
20260427000004connections FK — migrated user_id/friend_id FKs from auth.users to profiles(id)
20260427000005event_tags migration — ILIKE + alias map; 33 rows across 13 events. events.tags TEXT[] marked DEPRECATED
20260427000006app_config security keysmin_app_version, is_spoofing_prevention_active, app_hmac_key_version (Guide §7.1)
20260427000007tag affinity cron configcron_compute_tag_affinity row in app_config
20260427000008Drop legacy event columnsevents.price, events.currency, events.tags TEXT[]
20260427000009Security & index fixes (audit 2026-04-27) — notifications policy, FORCE RLS squads/crew, event_types admin-only, kpi_events/venue_tags use is_admin(), campaigns UPDATE policy, 2 performance indexes
20260427000010Social cleanup + event_series — drop followers/comments/reviews, drop followers_count/following_count from profiles, add event_series + event_ratings + series_id on events
20260427204655Drop legacy event columnsevents.price/currency/tags (ledger version; committed retroactively)
20260507112112Drop squads/squad_members — use crews only (ledger version; committed retroactively)
20260507131441Decouple event_likes — new event_likes table + events.like_count trigger, backfill from interested attendees (ledger version)
20260528000001waitlist — consumer beta pre-registration table (marketing site)
20260528000002partner_leads — B2B/pro partnership leads table (marketing site)

git↔DB reconciliation (2026-05-28)

Several migrations had been applied directly to production (dashboard/MCP) but never committed, and two local files carried mismatched 00000N timestamps. They are now committed under their exact ledger versions so supabase db push recognises them as applied. The event_areas_and_lineup_slots (20260502000001) ledger row was also inserted to match the existing tables.


Open Tech Debt

Items still requiring action from the Architecture Refactor Master Guide (2026-04-27):

ItemGuide refStatusNotes
events.tags TEXT[] column drop§4.5pending Flutter cutoverColumn deprecated, data migrated. Drop only after event_api_service.dart confirmed in production and events.tags reads reach zero
saved_events.user_id FK → profiles(id)§4.10N/Asaved_events table does not exist in live DB
event_tags write path in wizard§4.5 follow-updoneEventWizardNotifier._saveCurrentProgress() now syncs event_tags on every auto-save. createEvent() (direct API) still has a TODO for tag names → IDs lookup — low priority
HMAC signing — Edge Function verification§7.3pendingis_spoofing_prevention_active = false. Activate only after client version shipping X-Flow-Signature headers

| Event series Flutter integration | ✅ done 2026-04-28 | Event.seriesId field added. EventSeriesApiService created. See feat-event-series. | | event_ratings Flutter integration | ✅ done 2026-04-28 | EventApiService.rateEvent() + getUserRating(). Star-tap UI in event_details_screen.dart. | | Series discovery screen | ✅ done 2026-04-28 | SeriesDetailScreen at /series/:id. Series banner in event details. Series results in search “Tutto” tab. See feat-event-series. | | profiles counter columns: followers_count, following_count | ✅ Dropped 2026-04-27 | Removed in 000010 along with the followers table |

Items closed in the 2026-04-27 session:

ItemGuide refClosed by
FORCE RLS missing from all tables§2.120260427000002
Counter triggers (followers/following/friends)§4.120260427000003
posts_count drop + view rebuild§4.120260427000003
connections FKs → profiles(id)§4.420260427000004
events.tags data migrated to event_tags§4.520260427000005
squads / squad_members consolidation§4.3N/A — tables don’t exist
user_follows FK migration§4.10N/A — table doesn’t exist
app_config version & security keys§7.120260427000006
Flutter reads tags from event_tags join§4.5event_api_service.dart commit 54d205c
Flutter price_cents migration (§2.2)§2.2Flutter commit ca97c3eEvent.priceCents, isFree, displayPriceEuros getters; all 5 UI call-sites updated