Flow Platform — Database Schema

Ultima revisione: 2026-04-20 — post migrazione Supabase-only

Questo documento riflette lo schema live in Supabase (project hshqhtnkcsbenxorvlmm), verificato via PostgREST API.


Database Architecture Overview

Flow usa un’architettura Supabase-only: un unico database Postgres contiene tutta la verità operativa del prodotto, protetto da RLS su ogni tabella.

TecnologiaRuolo
PostgreSQL (Supabase)Sorgente autoritativa per tutto: utenti, eventi, venue, chat, social graph, notifiche
PostGISQuery geospaziali (colonna geo_point)
pg_trgmFuzzy text search nativo
RLSAutorizzazione: nessun layer API intermedio deve riverificare

Architettura precedente (storica)

Fino al 2026-03-29 Flow usava un’architettura dual-database (Postgres + MongoDB + Redis). I microservizi Node.js nel repo legacy backend/ descrivevano quel sistema. La migrazione Supabase-only ha eliminato MongoDB e Redis; i dati operativi vivono tutti in Postgres, la cache è gestita da materialized views e connessioni Supabase persistenti. Vedi 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
posts_countINTEGERDenormalized — not auto-updated, always 0. Use JOIN.
followers_countINTEGERDenormalized — not auto-updated. Use JOIN.
following_countINTEGERDenormalized — not auto-updated. Use JOIN.
events_createdINTEGERDenormalized — not auto-updated. Use JOIN.
events_attendedINTEGERDenormalized — not auto-updated. Use JOIN.
friends_countINTEGERDenormalized — not auto-updated. Use JOIN.
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
priceNUMERIC
currencyTEXT
max_attendeesINTEGERMaximum capacity (was capacity in early code — use max_attendees)
attendee_countINTEGERDenormalized count
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
tagsTEXT[]Legacy array — prefer event_tags junction table
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 → auth.users(id)
connected_user_idUUIDFK → auth.users(id)
statusTEXTpending, accepted, blocked
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ

ℹ️ FKs reference auth.users instead of profiles. This is safe (auth.users.id === profiles.id always) but non-standard. A future migration should update the FK target to profiles.

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


followers

ColumnTypeNotes
idUUID PK
follower_idUUIDFK → profiles(id)
following_idUUIDFK → profiles(id)
created_atTIMESTAMPTZ

⚠️ There is also a user_follows table (created in a 2026 migration) with an identical structure — these two tables are duplicates. A product decision is needed: does the platform use a follower model or a mutual-friends model? See project_social_model memory for context. Until decided, both tables exist.


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

ColumnType
idUUID PK
nameTEXT UNIQUE
created_atTIMESTAMPTZ

Crews & Squads

⚠️ Two separate table families exist for roughly the same concept (small social groups attached to events). This is a known design issue. Standardization is deferred — see migration 008 SUMMARY comment.

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

squads

Similar to crews but used by a different feature path (squad_chat migration).

ColumnTypeNotes
idUUID PK
nameTEXT
creator_idUUIDFK → profiles(id)
event_idUUIDFK → events(id)
max_membersINTEGER
statusTEXT
invite_codeTEXT
created_atTIMESTAMPTZ

squad_members

ColumnType
idUUID PK
squad_idUUID FK → squads(id)
user_idUUID FK → profiles(id)
roleTEXT
joined_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.


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

comments

User comments (on events or posts). Low usage currently but non-empty — kept.

ColumnType
idUUID PK
user_idUUID FK → profiles(id)
target_idUUID
target_typeTEXT
contentTEXT
created_atTIMESTAMPTZ

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).

Always use this view for unauthenticated or public-facing profile reads instead of querying the profiles table directly.


Dropped Tables

The following tables were removed in migration 20260324000008_db_audit_fix:

TableReason
_deprecated_posts0 rows, no active RLS policies, replaced by application-level post system
friendshipsExact duplicate of connections, 0 rows, no references

Known Technical Debt

IssueStatusNotes
Denormalized counter columns in profiles always 0OpenNo triggers update posts_count, followers_count, etc. Compute via JOIN.
Duplicate social graph: followers + user_followsOpenProduct decision needed on follower vs. mutual-friend model
Duplicate group model: crews + squadsOpenEvolved separately, now both 0 rows. Consolidation deferred.
connections FK references auth.users not profilesLow prioritySame IDs in practice; changing requires constraint recreation
events.tags TEXT[] alongside event_tags junctionOpenMigrate app code to use junction table, then drop column
campaigns.creator_id nullableOpen23 system/seed campaigns have NULL. Enforce NOT NULL once data is populated.

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.


MongoDB Collections (per microservice)

Each backend microservice owns its own MongoDB database.

ServiceDatabaseKey Collections
user-serviceflow_usersusers, preferences
event-serviceflow_eventsevents, venues, categories
social-serviceflow_socialconnections, groups, posts
notification-serviceflow_notificationsnotifications, templates, user_preferences
realtime-serviceflow_realtimemessages, rooms, user_presence

MongoDB data overlaps with PostgreSQL (known dual-write issue). The canonical data source for mobile app and admin portal is PostgreSQL. MongoDB is authoritative only for operational/transactional flows handled by the microservices.