Database Schema

RLS ENABLEDALL TABLES

Engine

Supabase (hosted PostgreSQL). All tables use Row Level Security (RLS). Server-side operations use the service role key which bypasses RLS. Client-side operations use the anon key with auth.uid() policies.

Tables

User profiles, auto-created on signup via trigger on auth.users.

ColumnTypeConstraintsDescription
idUUIDPK, FK -> auth.users(id) ON DELETE CASCADEUser ID (matches auth user)
created_atTIMESTAMPTZNOT NULL, DEFAULT now()Account creation time
updated_atTIMESTAMPTZNOT NULL, DEFAULT now()Last profile update (auto-trigger)
stripe_customer_idTEXTStripe customer ID (set on first checkout)
stripe_subscription_idTEXTStripe subscription ID
subscription_statusTEXTCHECK (free, trial, trialing, active, cancelled, expired, past_due), DEFAULT 'free'Current subscription tier
subscription_planTEXTPlan name from Stripe (e.g., price ID or nickname)
subscription_period_endTIMESTAMPTZCurrent billing period end date
trial_ends_atTIMESTAMPTZTrial expiration date
free_audits_remainingINTEGERDEFAULT 3Remaining free audits this month
prop_firmTEXTSelected prop firm name
firm_idTEXTFirm identifier
account_sizeINTEGERChallenge account size
challenge_phaseTEXTCurrent challenge phase
daily_loss_limit_pctDECIMALDaily loss limit percentage
daily_loss_typeTEXTType of daily loss calculation
max_drawdown_pctDECIMALMax drawdown percentage
max_drawdown_typeTEXTType of drawdown calculation
profit_target_pctDECIMALProfit target percentage
max_daily_tradesINTEGERMaximum daily trade count
ai_providerTEXTDEFAULT 'openai'AI provider preference
has_byok_keyBOOLEANDEFAULT falseWhether user has BYOK API key
current_streak_daysINTEGERDEFAULT 0Current consecutive audit days
longest_streak_daysINTEGERDEFAULT 0All-time longest streak
last_audit_dateDATEDate of most recent audit
onboarding_completeBOOLEANDEFAULT falseWhether onboarding wizard was completed
trading_rulesTEXTLegacy rules text field
telegram_chat_idTEXTUNIQUE (partial, WHERE NOT NULL)Linked Telegram chat ID
discord_user_idTEXTUNIQUE (partial, WHERE NOT NULL)Linked Discord user ID
whatsapp_phoneTEXTUNIQUE (partial, WHERE NOT NULL)Linked WhatsApp phone number
gmail_journal_addressTEXTLinked email for journal
journal_preferencesJSONBDEFAULT '{}'Journal config (summary time, digest day, etc.)
utm_source_firstTEXTFirst-touch UTM source
utm_medium_firstTEXTFirst-touch UTM medium
utm_campaign_firstTEXTFirst-touch UTM campaign
utm_source_lastTEXTLast-touch UTM source
utm_medium_lastTEXTLast-touch UTM medium
utm_campaign_lastTEXTLast-touch UTM campaign

Triggers:

  • on_auth_user_created -- Inserts a profile row when a new auth user is created
  • profiles_updated_at -- Auto-updates updated_at on every UPDATE

RLS: Users can SELECT and UPDATE their own row only. No INSERT policy (handled by trigger). No DELETE policy.


User-defined trading rules used for AI audit evaluation.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Rule ID
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADEOwner
rule_textTEXTNOT NULLRule description in plain English
categoryTEXTCHECK (entry, exit, risk, timing), DEFAULT 'entry'Rule category
is_activeBOOLEANDEFAULT trueWhether rule is currently active
sort_orderINTEGERDEFAULT 0Display order
versionINTEGERDEFAULT 1Rule version number
previous_version_idUUIDFK -> trading_rules(id) ON DELETE SET NULLPrevious version for history
quality_gradeTEXTQuality grade (A-F) from rule grading
win_rateDECIMALCompliance win rate (0-1)
avg_pnlDECIMALAverage P&L when rule is followed
sample_countINTEGERDEFAULT 0Number of verdicts used for grading
last_graded_atTIMESTAMPTZWhen grade was last calculated
created_atTIMESTAMPTZNOT NULL, DEFAULT now()
updated_atTIMESTAMPTZNOT NULL, DEFAULT now()Auto-updated

Triggers: trading_rules_updated_at -- Auto-updates updated_at.

RLS: Full CRUD for own rules (auth.uid() = user_id).


Individual trades with audit results and source tracking.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Trade ID
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADEOwner
daily_tracking_idUUIDFK -> daily_tracking(id) ON DELETE SET NULLAssociated daily tracking record
symbolTEXTTrading instrument (NQ, ES, EURUSD, etc.)
directionTEXT'long' or 'short'
entry_priceDECIMALEntry price
exit_priceDECIMALExit price
entry_timeTIMESTAMPTZEntry timestamp
exit_timeTIMESTAMPTZExit timestamp
position_sizeDECIMALPosition size (lots/contracts)
stop_lossDECIMALStop loss price
take_profitDECIMALTake profit price
pnlDECIMALRealized P&L
commissionsDECIMALCommission costs
screenshot_urlTEXTSupabase Storage URL for chart screenshot
screenshot_qualityTEXTScreenshot quality assessment
audit_statusTEXTCHECK (pending, audited, skipped), DEFAULT 'pending'Audit processing status
audit_resultJSONBFull AI audit result (verdicts, compliance score, cost)
audit_providerTEXTAI provider used for audit
compliance_scoreDECIMALOverall compliance score (0-100)
rules_followedINTEGERCount of rules followed
rules_violatedINTEGERCount of rules violated
rules_unclearINTEGERCount of unclear verdicts
has_overridesBOOLEANDEFAULT falseWhether any verdict was manually overridden
source_typeTEXTCHECK (csv_import, broker_sync, manual, journal), DEFAULT 'csv_import'How the trade was imported
source_connection_idUUIDFK -> broker_connections(id) ON DELETE SET NULLBroker connection that synced this trade
external_idTEXTExternal trade ID from broker (for dedup)
created_atTIMESTAMPTZNOT NULL, DEFAULT now()

Indexes:

  • idx_trades_user_created -- (user_id, created_at DESC) for chronological listing
  • idx_trades_external_dedup -- UNIQUE (user_id, source_connection_id, external_id) WHERE external_id IS NOT NULL for deduplication

RLS: Full CRUD for own trades (auth.uid() = user_id).


Per-rule verdict on each trade, created during AI audit.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Verdict ID
trade_idUUIDNOT NULL, FK -> trades(id) ON DELETE CASCADEAssociated trade
rule_idUUIDNOT NULL, FK -> trading_rules(id) ON DELETE CASCADEAssociated rule
verdictTEXTCHECK (followed, violated, unclear)AI determination
evidenceTEXTSpecific explanation with data points
severityTEXTDEFAULT 'minor'minor, major, or critical
ai_confidenceINTEGERCHECK (1-5)AI confidence in the verdict
is_overriddenBOOLEANDEFAULT falseWhether user manually overrode
original_verdictTEXTOriginal AI verdict before override
override_reasonTEXTUser's reason for overriding
created_atTIMESTAMPTZNOT NULL, DEFAULT now()

Indexes:

  • idx_trade_rule_verdicts_trade -- (trade_id) for joining with trades

RLS: Access controlled via trade ownership (EXISTS (SELECT 1 FROM trades WHERE trades.id = trade_id AND trades.user_id = auth.uid())).


Daily P&L, balance, and compliance tracking.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADEOwner
trade_dateDATENOT NULL, UNIQUE(user_id, trade_date)Calendar date
starting_balanceDECIMALDay's starting balance
ending_balanceDECIMALDay's ending balance
daily_pnlDECIMALTotal P&L for the day
max_equity_highDECIMALIntraday equity high
trades_takenINTEGERDEFAULT 0Number of trades
daily_loss_usedDECIMALDaily loss consumed
drawdown_usedDECIMALDrawdown consumed
compliance_scoreDECIMALAverage compliance score for the day
day_statusTEXTCHECK (trading, rest_day, gap), DEFAULT 'trading'Day classification

Indexes:

  • idx_daily_tracking_user_date -- (user_id, trade_date DESC) for chronological queries

RLS: Full CRUD for own tracking (auth.uid() = user_id).


Stores broker connection state, encrypted credentials, and sync configuration.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Connection ID
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADEOwner
broker_typeTEXTNOT NULL, CHECK (metatrader4, metatrader5, tradovate, ctrader, rithmic, dxtrade, match_trader, ninjatrader, tradingview)Broker platform
auth_typeTEXTNOT NULL, CHECK (oauth, credentials, api_key, file_upload)Authentication method
display_nameTEXTUser-chosen label
encrypted_credentialsTEXTAES-256-GCM encrypted credentials (base64)
statusTEXTNOT NULL, CHECK (pending, active, error, disconnected, token_expired), DEFAULT 'pending'Connection lifecycle state
error_messageTEXTLast error message
consecutive_errorsINTEGERDEFAULT 0Count of consecutive sync failures (resets on success)
last_sync_atTIMESTAMPTZTimestamp of last successful sync
sync_interval_minutesINTEGERDEFAULT 15Sync interval (used by cron)
trades_synced_totalINTEGERDEFAULT 0Total trades synced via this connection
created_atTIMESTAMPTZNOT NULL, DEFAULT now()
updated_atTIMESTAMPTZNOT NULL, DEFAULT now()Auto-updated

Unique constraint: (user_id, broker_type, display_name) -- prevents duplicate connections.

Indexes:

  • idx_broker_connections_user -- (user_id) for listing user's connections
  • idx_broker_connections_sync -- (status, last_sync_at) WHERE status = 'active' for cron sync queries

Triggers: broker_connections_updated_at -- Auto-updates updated_at.

RLS: Full CRUD for own connections (auth.uid() = user_id).


Per-sync audit trail, one row per sync attempt.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Log ID
connection_idUUIDNOT NULL, FK -> broker_connections(id) ON DELETE CASCADEAssociated connection
started_atTIMESTAMPTZNOT NULL, DEFAULT now()Sync start time
completed_atTIMESTAMPTZSync completion time
statusTEXTNOT NULL, CHECK (running, success, partial, error), DEFAULT 'running'Sync outcome
trades_fetchedINTEGERDEFAULT 0Total trades returned by adapter
trades_newINTEGERDEFAULT 0New trades inserted
trades_duplicateINTEGERDEFAULT 0Duplicate trades skipped
error_messageTEXTError details if failed
duration_msINTEGERSync duration in milliseconds

Indexes:

  • idx_sync_logs_connection -- (connection_id, started_at DESC) for chronological log retrieval

RLS: Read-only access via connection ownership.


Email captures from quiz, simulator, or waitlist.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
emailTEXTNOT NULL, UNIQUEEmail address
sourceTEXTDEFAULT 'quiz'Capture source (quiz, simulator, waitlist)
quiz_resultJSONBQuiz answers and recommendation
created_atTIMESTAMPTZDEFAULT now()
subscribedBOOLEANDEFAULT trueNewsletter subscription status

RLS: Enabled, no client policies (service role only).


Quiz answer tracking with attribution.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
created_atTIMESTAMPTZDEFAULT now()
marketTEXTNOT NULLfutures, forex, or both
styleTEXTNOT NULLscalp, daytrade, or swing
budgetTEXTNOT NULLunder100, 100-300, 300-500, 500plus
priorityTEXTNOT NULLcheapest, relaxed, split, payout
trailing_dd_okTEXTNOT NULLyes or no
recommended_firmTEXTNOT NULLTop recommendation
recommended_scoreINTEGERNOT NULLRecommendation score
all_scoresJSONBNOT NULLFull scoring breakdown
utm_sourceTEXTUTM attribution
utm_mediumTEXT
utm_campaignTEXT
referrerTEXTReferrer URL

RLS: Enabled, no client policies (service role only).


Unclaimed audit results for PLG funnel and rate limiting.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
ip_hashTEXTNOT NULLSHA-256 hashed IP for rate limiting
result_hashTEXTUNIQUE, NOT NULLUnique result identifier
firm_idTEXTNOT NULLSource identifier (firm name or 'strategy-analyzer')
audit_resultJSONBNOT NULLFull audit/analysis result
claimed_byUUIDUser who claimed this result after signup
created_atTIMESTAMPTZDEFAULT now()
expires_atTIMESTAMPTZNOT NULLAuto-cleanup date

Indexes:

  • idx_anon_audits_ip -- (ip_hash, created_at DESC) for rate limit queries
  • idx_anon_audits_hash -- (result_hash) for lookup by hash

RLS: Enabled, no client policies (service role only).


Referral tracking between users.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
referrer_idUUIDFK -> profiles(id) ON DELETE CASCADEUser who referred
referred_idUUIDFK -> profiles(id) ON DELETE CASCADE, UNIQUEUser who was referred
statusTEXTCHECK (pending, completed, rewarded), DEFAULT 'pending'Referral lifecycle
reward_appliedBOOLEANDEFAULT falseWhether reward was given
created_atTIMESTAMPTZDEFAULT now()

RLS: Users can SELECT where they are referrer or referred. Users can INSERT where referred_id = auth.uid().


System announcements (feature launches, warnings).

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
titleTEXTNOT NULLAnnouncement title
bodyTEXTNOT NULLAnnouncement content
typeTEXTCHECK (info, feature, warning), DEFAULT 'info'Category
activeBOOLEANDEFAULT trueWhether currently displayed
created_atTIMESTAMPTZDEFAULT now()

RLS: Anyone can SELECT active announcements.


Tracks which users dismissed which announcements.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDFK -> profiles(id) ON DELETE CASCADE
announcement_idUUIDFK -> announcements(id) ON DELETE CASCADE
dismissed_atTIMESTAMPTZDEFAULT now()

Unique constraint: (user_id, announcement_id).

RLS: Users manage their own dismissals.


Strategy analysis results from the screenshot analyzer.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDFK -> profiles(id) ON DELETE CASCADEOwner
screenshot_urlTEXTSource screenshot URL
user_descriptionTEXTUser's strategy description
extracted_rulesJSONBAI-extracted rules
overall_backtestabilityDECIMALBacktestability score
pine_scriptTEXTGenerated Pine Script v5 code
pine_script_versionINTEGERDEFAULT 1Pine Script version number
backtest_importedBOOLEANDEFAULT falseWhether backtest was imported
backtest_resultsJSONBImported backtest statistics
fitness_analysisJSONBStrategy fitness analysis
parent_analysis_idUUIDFK -> strategy_analyses(id)Parent for iteration chains
iteration_numberINTEGERDEFAULT 1Iteration count in optimization chain
created_atTIMESTAMPTZDEFAULT now()
updated_atTIMESTAMPTZDEFAULT now()

RLS: Users can SELECT, INSERT, UPDATE their own analyses.


Shareable audit result links with OG image data.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
result_hashTEXTUNIQUE, NOT NULLURL-safe hash for sharing
strategy_summaryTEXTStrategy description for display
pnl_percentDECIMALP&L percentage for OG image
statsJSONBFull statistics object
og_image_urlTEXTGenerated OG image URL
viewsINTEGERDEFAULT 0View counter
created_atTIMESTAMPTZDEFAULT now()

Indexes: idx_shared_results_hash -- (result_hash) for hash-based lookup.


Time-limited, single-use codes for connecting messaging platforms.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADECode owner
platformTEXTNOT NULL, CHECK (telegram, discord, whatsapp, email)Target platform
codeTEXTNOT NULL, UNIQUELink code (VGL-XXXX format)
usedBOOLEANDEFAULT falseWhether code has been consumed
created_atTIMESTAMPTZNOT NULL, DEFAULT now()
expires_atTIMESTAMPTZNOT NULL10-minute expiry

Indexes: idx_link_codes_lookup -- (code, used, expires_at) for fast validation.

RLS: Users can SELECT and INSERT their own codes.


State machine for multi-turn journal interactions per platform.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADE
platformTEXTNOT NULL, CHECK (telegram, discord, whatsapp, email)
platform_chat_idTEXTNOT NULLPlatform-specific chat identifier
stateTEXTNOT NULL, CHECK (idle, awaiting_confirmation, awaiting_screenshot, awaiting_details), DEFAULT 'idle'Conversation state
pending_tradeJSONBTrade data awaiting confirmation
contextJSONBDEFAULT '{}'Additional conversation context
updated_atTIMESTAMPTZNOT NULL, DEFAULT now()Auto-updated

Unique constraint: (user_id, platform) -- one conversation per platform per user.

RLS: Users manage their own conversations.


Audit log for journal message debugging.

ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()
user_idUUIDNOT NULL, FK -> profiles(id) ON DELETE CASCADE
platformTEXTNOT NULL, CHECK (telegram, discord, whatsapp, email)
directionTEXTNOT NULL, CHECK (inbound, outbound)Message direction
content_typeTEXTNOT NULL, CHECK (text, image, command, system), DEFAULT 'text'Message type
content_previewTEXTTruncated content for debugging
trade_idUUIDFK -> trades(id) ON DELETE SET NULLLinked trade if applicable
metadataJSONBDEFAULT '{}'Platform-specific metadata
created_atTIMESTAMPTZNOT NULL, DEFAULT now()

Indexes:

  • idx_journal_messages_user -- (user_id, created_at DESC) for message history
  • idx_journal_messages_trade -- (trade_id) WHERE trade_id IS NOT NULL

RLS: Users can SELECT their own messages.


Storage Buckets

Private bucket for trade chart screenshots.

SettingValue
PublicNo
Max file size5MB
Allowed MIME typesimage/png, image/jpeg, image/webp
Path convention{userId}/{tradeId}-{timestamp}.{ext}
Retention90 days (cleaned by cron)

RLS Policies:

  • Upload: bucket_id = 'screenshots' AND folder = auth.uid()
  • Read: Same ownership check
  • Delete: Same ownership check

Relationships

auth.users
  |-- profiles (1:1, auto-created via trigger)
        |-- trading_rules (1:many)
        |-- trades (1:many)
        |     |-- trade_rule_verdicts (1:many, joins to trading_rules)
        |-- daily_tracking (1:many, unique on user_id + trade_date)
        |-- broker_connections (1:many)
        |     |-- sync_logs (1:many)
        |     |-- trades.source_connection_id (1:many, backref)
        |-- referrals (as referrer or referred)
        |-- announcement_dismissals (1:many)
        |-- strategy_analyses (1:many, self-referencing parent chain)
        |-- platform_link_codes (1:many)
        |-- journal_conversations (1:many, unique on user_id + platform)
        |-- journal_messages (1:many)

Functions

Trigger function on auth.users INSERT. Creates a profiles row with the same UUID. SECURITY DEFINER.

Generic trigger function that sets NEW.updated_at = now(). Used by: profiles, trading_rules, broker_connections, journal_conversations.

RPC function to atomically increment free_audits_remaining. SECURITY DEFINER.

Migrations

15 migration files in supabase/migrations/, applied in timestamp order:

MigrationDescription
20260321094255_create_tradingaudit_tables.sqlInitial tables: email_leads, quiz_completions, anonymous_audits
20260322000000_fix_rls_security.sqlEnable RLS on initial tables
20260322010000_referrals.sqlReferrals, announcements, announcement_dismissals
20260322020000_storage_bucket.sqlScreenshots storage bucket with RLS
20260322030000_phase2_core_tables.sqlCore tables: profiles, trading_rules, daily_tracking, trades, trade_rule_verdicts + triggers + RLS
20260322040000_fix_profile_columns.sqlAdd onboarding_complete, trading_rules text, firm_id to profiles
20260322050000_increment_free_audits_rpc.sqlRPC function for atomic free audit increment
20260323000000_add_subscription_columns.sqlAdd subscription_plan, subscription_period_end, stripe_subscription_id; expand status CHECK
20260323022104_add_utm_columns_to_profiles.sqlAdd UTM attribution columns (first/last touch)
20260323100000_add_rule_quality_columns.sqlAdd quality_grade, win_rate, avg_pnl, sample_count, last_graded_at to trading_rules
20260323120000_add_strategy_analyses_table.sqlStrategy analyses table with RLS
20260323140000_add_shared_results_table.sqlShared results table with hash index
20260324000000_journal_platform_support.sqlPlatform IDs on profiles, link codes, conversations, messages tables; expand trades.source_type
20260325000000_broker_connections.sqlBroker connections, sync_logs; extend trades with source_type, source_connection_id, external_id + dedup index
20260325100000_broker_check_constraints.sqlAdd CHECK constraints on broker_type and auth_type enums
bash
# Apply locally (requires Supabase CLI and local Supabase running)
supabase db push

# Apply to remote (requires linked project)
supabase db push --linked

# Create a new migration
supabase migration new description_here