Files
route-commerce/supabase/migrations/.archived/200_production_features.sql
T
openclaw 916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
feat(storage): MinIO object storage, Neon Auth, Supabase removal
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 12:23:37 -06:00

258 lines
10 KiB
PL/PgSQL

-- Production Database Migrations
-- Add comprehensive RLS policies, audit tables, referral system, and seed data
BEGIN;
-- =============================================================================
-- REFERRAL SYSTEM
-- =============================================================================
-- Referral codes table
CREATE TABLE IF NOT EXISTS referral_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
referrer_user_id UUID NOT NULL,
referral_code VARCHAR(50) UNIQUE NOT NULL,
referrer_email VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '1 year'),
max_uses INTEGER DEFAULT 1,
current_uses INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
reward_type VARCHAR(50) DEFAULT 'percentage',
reward_value DECIMAL(10,2) DEFAULT 20.00,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Referral redemptions table
CREATE TABLE IF NOT EXISTS referral_redemptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
referral_code_id UUID REFERENCES referral_codes(id) ON DELETE CASCADE,
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
referred_user_id UUID NOT NULL,
referred_email VARCHAR(255) NOT NULL,
redeemed_at TIMESTAMPTZ DEFAULT NOW(),
reward_type VARCHAR(50),
reward_value DECIMAL(10,2),
is_credit_applied BOOLEAN DEFAULT false,
signup_plan VARCHAR(50),
signup_value DECIMAL(10,2),
metadata JSONB DEFAULT '{}'::jsonb
);
-- =============================================================================
-- CHANGELOG SYSTEM
-- =============================================================================
CREATE TABLE IF NOT EXISTS changelogs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
version VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
content JSONB NOT NULL DEFAULT '[]'::jsonb,
released_at TIMESTAMPTZ DEFAULT NOW(),
is_published BOOLEAN DEFAULT false,
feature_type VARCHAR(50) DEFAULT 'general',
metadata JSONB DEFAULT '{}'::jsonb,
UNIQUE(brand_id, version)
);
CREATE TABLE IF NOT EXISTS changelog_reads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
changelog_id UUID REFERENCES changelogs(id) ON DELETE CASCADE,
read_at TIMESTAMPTZ DEFAULT NOW(),
dismissed BOOLEAN DEFAULT false,
UNIQUE(user_id, changelog_id)
);
-- =============================================================================
-- ONBOARDING PROGRESS
-- =============================================================================
CREATE TABLE IF NOT EXISTS onboarding_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
current_step VARCHAR(50) NOT NULL,
completed_steps JSONB DEFAULT '[]'::jsonb,
skipped_steps JSONB DEFAULT '[]'::jsonb,
data JSONB DEFAULT '{}'::jsonb,
started_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
is_completed BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}'::jsonb,
UNIQUE(brand_id, user_id)
);
-- =============================================================================
-- USER ACTIVITY / ANALYTICS
-- =============================================================================
CREATE TABLE IF NOT EXISTS user_activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(50),
resource_id UUID,
ip_address INET,
user_agent TEXT,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- If table pre-existed from 036 (without brand_id etc), add the columns now so policies below succeed
ALTER TABLE user_activity_logs
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS action VARCHAR(100),
ADD COLUMN IF NOT EXISTS resource_type VARCHAR(50),
ADD COLUMN IF NOT EXISTS resource_id UUID,
ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
-- =============================================================================
-- API KEYS FOR EXTERNAL INTEGRATIONS
-- =============================================================================
CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(255) NOT NULL,
key_prefix VARCHAR(20),
permissions JSONB DEFAULT '["read"]'::jsonb,
rate_limit INTEGER DEFAULT 1000,
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by UUID NOT NULL
);
-- =============================================================================
-- NOTIFICATION PREFERENCES
-- =============================================================================
CREATE TABLE IF NOT EXISTS notification_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
brand_id UUID REFERENCES brands(id) ON DELETE CASCADE,
email_orders BOOLEAN DEFAULT true,
email_marketing BOOLEAN DEFAULT false,
email_reports BOOLEAN DEFAULT true,
email_billing BOOLEAN DEFAULT true,
sms_orders BOOLEAN DEFAULT false,
sms_marketing BOOLEAN DEFAULT false,
push_orders BOOLEAN DEFAULT true,
push_marketing BOOLEAN DEFAULT false,
quiet_hours_start TIME,
quiet_hours_end TIME,
timezone VARCHAR(50) DEFAULT 'America/New_York',
metadata JSONB DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, brand_id)
);
-- =============================================================================
-- REFINED RLS POLICIES
-- =============================================================================
-- Enable RLS on new tables
ALTER TABLE referral_codes ENABLE ROW LEVEL SECURITY;
ALTER TABLE referral_redemptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE changelogs ENABLE ROW LEVEL SECURITY;
ALTER TABLE changelog_reads ENABLE ROW LEVEL SECURITY;
ALTER TABLE onboarding_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_activity_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
ALTER TABLE notification_preferences ENABLE ROW LEVEL SECURITY;
-- Referral codes policies
CREATE POLICY "Users can view their own referral codes"
ON referral_codes FOR SELECT
USING (referrer_user_id = current_setting('app.current_user_id', true)::uuid);
CREATE POLICY "Platform admins can manage all referral codes"
ON referral_codes FOR ALL
USING (current_setting('app.current_user_role', true) = 'platform_admin');
CREATE POLICY "Brand admins can manage their brand's referral codes"
ON referral_codes FOR ALL
USING (brand_id = current_setting('app.current_brand_id', true)::uuid);
-- Changelogs policies - public read for all authenticated
CREATE POLICY "Authenticated users can view published changelogs"
ON changelogs FOR SELECT
USING (
is_published = true
AND (
current_setting('app.current_user_role', true) = 'platform_admin'
OR brand_id = current_setting('app.current_brand_id', true)::uuid
)
);
CREATE POLICY "Brand admins can manage their changelogs"
ON changelogs FOR ALL
USING (brand_id = current_setting('app.current_brand_id', true)::uuid);
-- Onboarding progress policies
CREATE POLICY "Users can view their own onboarding progress"
ON onboarding_progress FOR SELECT
USING (user_id = current_setting('app.current_user_id', true)::uuid);
CREATE POLICY "Users can update their own onboarding progress"
ON onboarding_progress FOR UPDATE
USING (user_id = current_setting('app.current_user_id', true)::uuid);
CREATE POLICY "Brand admins can view onboarding progress for their brand"
ON onboarding_progress FOR SELECT
USING (brand_id = current_setting('app.current_brand_id', true)::uuid);
-- User activity logs - admins only
CREATE POLICY "Brand admins can view activity logs for their brand"
ON user_activity_logs FOR SELECT
USING (
brand_id = current_setting('app.current_brand_id', true)::uuid
OR current_setting('app.current_user_role', true) = 'platform_admin'
);
-- API keys policies
CREATE POLICY "Brand admins can manage their API keys"
ON api_keys FOR ALL
USING (brand_id = current_setting('app.current_brand_id', true)::uuid);
CREATE POLICY "Anyone can read active API key prefix (for verification)"
ON api_keys FOR SELECT
USING (is_active = true);
-- Notification preferences policies
CREATE POLICY "Users can manage their own notification preferences"
ON notification_preferences FOR ALL
USING (user_id = current_setting('app.current_user_id', true)::uuid);
CREATE POLICY "Brand admins can view preferences for their brand users"
ON notification_preferences FOR SELECT
USING (brand_id = current_setting('app.current_brand_id', true)::uuid);
-- =============================================================================
-- COMPREHENSIVE INDEXES
-- =============================================================================
CREATE INDEX IF NOT EXISTS idx_referral_codes_brand ON referral_codes(brand_id);
CREATE INDEX IF NOT EXISTS idx_referral_codes_referrer ON referral_codes(referrer_user_id);
CREATE INDEX IF NOT EXISTS idx_referral_codes_code ON referral_codes(referral_code);
CREATE INDEX IF NOT EXISTS idx_referral_redemptions_referrer ON referral_redemptions(referral_code_id);
CREATE INDEX IF NOT EXISTS idx_referral_redemptions_referred ON referral_redemptions(referred_user_id);
CREATE INDEX IF NOT EXISTS idx_changelogs_brand ON changelogs(brand_id);
CREATE INDEX IF NOT EXISTS idx_changelogs_published ON changelogs(is_published, released_at DESC);
CREATE INDEX IF NOT EXISTS idx_changelog_reads_user ON changelog_reads(user_id, changelog_id);
CREATE INDEX IF NOT EXISTS idx_onboarding_brand ON onboarding_progress(brand_id);
CREATE INDEX IF NOT EXISTS idx_onboarding_user ON onboarding_progress(user_id);
CREATE INDEX IF NOT EXISTS idx_user_activity_user ON user_activity_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_user_activity_brand ON user_activity_logs(brand_id);
CREATE INDEX IF NOT EXISTS idx_user_activity_created ON user_activity_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_api_keys_brand ON api_keys(brand_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
COMMIT;