aethex-forge/apply_missing_migrations.sql
MrPiglr 25d584fd46
feat: Complete database migration and developer platform
- Applied all 31 pending Supabase migrations successfully
- Fixed 100+ policy/trigger/index duplication errors for shared database
- Resolved foundation_contributions schema mismatch (added user_id, contribution_type, resource_id, points columns)
- Added DROP IF EXISTS statements for all policies, triggers, and indexes
- Wrapped storage.objects operations in permission-safe DO blocks

Developer Platform (10 Phases Complete):
- API key management dashboard with RLS and SHA-256 hashing
- Complete API documentation (8 endpoint categories)
- 9 template starters + 9 marketplace products + 12 code examples
- Quick start guide and SDK distribution
- Testing framework and QA checklist

Database Schema Now Includes:
- Ethos: Artist/guild tracking, verification, tracks, storage
- GameForge: Games, assets, monetization
- Foundation: Courses, mentorship, resources, contributions
- Nexus: Creator marketplace, portfolios, contracts, escrow
- Corp Hub: Invoices, contracts, team management, projects
- Developer: API keys, usage logs, profiles

Platform Status: Production Ready 
2026-01-10 02:05:15 +00:00

3615 lines
169 KiB
PL/PgSQL

-- Migration: Add tier and badges system for AI persona access
-- Run this migration in your Supabase SQL Editor
-- 1. Create subscription tier enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'subscription_tier_enum') THEN
CREATE TYPE subscription_tier_enum AS ENUM ('free', 'pro', 'council');
END IF;
END $$;
-- 2. Add tier and Stripe columns to user_profiles
ALTER TABLE user_profiles
ADD COLUMN IF NOT EXISTS tier subscription_tier_enum DEFAULT 'free',
ADD COLUMN IF NOT EXISTS stripe_customer_id TEXT,
ADD COLUMN IF NOT EXISTS stripe_subscription_id TEXT;
-- 3. Create badges table
CREATE TABLE IF NOT EXISTS badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT,
icon TEXT,
unlock_criteria TEXT,
unlocks_persona TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 4. Create user_badges junction table
CREATE TABLE IF NOT EXISTS user_badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
badge_id UUID NOT NULL REFERENCES badges(id) ON DELETE CASCADE,
earned_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, badge_id)
);
-- 5. Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON user_badges(user_id);
CREATE INDEX IF NOT EXISTS idx_user_badges_badge_id ON user_badges(badge_id);
CREATE INDEX IF NOT EXISTS idx_badges_slug ON badges(slug);
CREATE INDEX IF NOT EXISTS idx_user_profiles_tier ON user_profiles(tier);
CREATE INDEX IF NOT EXISTS idx_user_profiles_stripe_customer ON user_profiles(stripe_customer_id);
-- 6. Enable RLS on new tables
ALTER TABLE badges ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_badges ENABLE ROW LEVEL SECURITY;
-- 7. RLS Policies for badges (read-only for authenticated users)
DROP POLICY IF EXISTS "Badges are viewable by everyone" ON badges;
CREATE POLICY "Badges are viewable by everyone"
ON badges FOR SELECT
USING (true);
-- 8. RLS Policies for user_badges
DROP POLICY IF EXISTS "Users can view their own badges" ON user_badges;
CREATE POLICY "Users can view their own badges"
ON user_badges FOR SELECT
USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "Users can view others badges" ON user_badges;
CREATE POLICY "Users can view others badges"
ON user_badges FOR SELECT
USING (true);
-- 9. Seed initial badges that unlock AI personas
INSERT INTO badges (name, slug, description, icon, unlock_criteria, unlocks_persona) VALUES
('Forge Apprentice', 'forge_apprentice', 'Complete 3 game design reviews with Forge Master', 'hammer', 'Complete 3 game design reviews', 'forge_master'),
('SBS Scholar', 'sbs_scholar', 'Create 5 business profiles with SBS Architect', 'building', 'Create 5 business profiles', 'sbs_architect'),
('Curriculum Creator', 'curriculum_creator', 'Generate 10 lesson plans with Curriculum Weaver', 'book', 'Generate 10 lesson plans', 'curriculum_weaver'),
('Data Pioneer', 'data_pioneer', 'Analyze 20 datasets with QuantumLeap', 'chart', 'Analyze 20 datasets', 'quantum_leap'),
('Synthwave Artist', 'synthwave_artist', 'Write 15 song lyrics with Vapor', 'wave', 'Write 15 song lyrics', 'vapor'),
('Pitch Survivor', 'pitch_survivor', 'Receive 10 critiques from Apex VC', 'money', 'Receive 10 critiques', 'apex'),
('Sound Designer', 'sound_designer', 'Generate 25 audio briefs with Ethos Producer', 'music', 'Generate 25 audio briefs', 'ethos_producer'),
('Lore Master', 'lore_master', 'Create 50 lore entries with AeThex Archivist', 'scroll', 'Create 50 lore entries', 'aethex_archivist')
ON CONFLICT (slug) DO NOTHING;
-- 10. Grant permissions
GRANT SELECT ON badges TO authenticated;
GRANT SELECT ON user_badges TO authenticated;
GRANT INSERT, DELETE ON user_badges TO authenticated;
-- ========================================
-- Next Migration
-- ========================================
-- Create fourthwall_products table
CREATE TABLE IF NOT EXISTS fourthwall_products (
id BIGSERIAL PRIMARY KEY,
fourthwall_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
image_url TEXT,
category TEXT,
synced_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create fourthwall_orders table
CREATE TABLE IF NOT EXISTS fourthwall_orders (
id BIGSERIAL PRIMARY KEY,
fourthwall_order_id TEXT UNIQUE NOT NULL,
customer_email TEXT NOT NULL,
items JSONB DEFAULT '[]'::jsonb,
total_amount DECIMAL(10, 2) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
paid_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create fourthwall_webhook_logs table
CREATE TABLE IF NOT EXISTS fourthwall_webhook_logs (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB,
received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_fourthwall_products_fourthwall_id ON fourthwall_products(fourthwall_id);
CREATE INDEX IF NOT EXISTS idx_fourthwall_products_category ON fourthwall_products(category);
CREATE INDEX IF NOT EXISTS idx_fourthwall_orders_fourthwall_order_id ON fourthwall_orders(fourthwall_order_id);
CREATE INDEX IF NOT EXISTS idx_fourthwall_orders_customer_email ON fourthwall_orders(customer_email);
CREATE INDEX IF NOT EXISTS idx_fourthwall_orders_status ON fourthwall_orders(status);
CREATE INDEX IF NOT EXISTS idx_fourthwall_webhook_logs_event_type ON fourthwall_webhook_logs(event_type);
CREATE INDEX IF NOT EXISTS idx_fourthwall_webhook_logs_received_at ON fourthwall_webhook_logs(received_at);
-- Enable RLS (Row Level Security)
ALTER TABLE fourthwall_products ENABLE ROW LEVEL SECURITY;
ALTER TABLE fourthwall_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE fourthwall_webhook_logs ENABLE ROW LEVEL SECURITY;
-- Create RLS policies - allow authenticated users to read, admins to manage
DROP POLICY IF EXISTS "Allow authenticated users to read fourthwall products" ON fourthwall_products;
CREATE POLICY "Allow authenticated users to read fourthwall products"
ON fourthwall_products
FOR SELECT
USING (true);
DROP POLICY IF EXISTS "Allow service role to manage fourthwall products" ON fourthwall_products;
CREATE POLICY "Allow service role to manage fourthwall products"
ON fourthwall_products
FOR ALL
USING (auth.role() = 'service_role');
DROP POLICY IF EXISTS "Allow service role to manage fourthwall orders" ON fourthwall_orders;
CREATE POLICY "Allow service role to manage fourthwall orders"
ON fourthwall_orders
FOR ALL
USING (auth.role() = 'service_role');
DROP POLICY IF EXISTS "Allow service role to manage webhook logs" ON fourthwall_webhook_logs;
CREATE POLICY "Allow service role to manage webhook logs"
ON fourthwall_webhook_logs
FOR ALL
USING (auth.role() = 'service_role');
-- ========================================
-- Next Migration
-- ========================================
-- Migration: Add wallet verification support
-- This adds a wallet_address field to user_profiles to support the Bridge UI
-- for Phase 2 (Unified Identity: .aethex TLD verification)
ALTER TABLE user_profiles
ADD COLUMN IF NOT EXISTS wallet_address VARCHAR(255) UNIQUE NULL DEFAULT NULL;
-- Create an index for faster wallet lookups during verification
CREATE INDEX IF NOT EXISTS idx_user_profiles_wallet_address
ON user_profiles(wallet_address)
WHERE wallet_address IS NOT NULL;
-- Add a comment explaining the field
COMMENT ON COLUMN user_profiles.wallet_address IS 'Connected wallet address (e.g., 0x123...). Used for Phase 2 verification and .aethex TLD checks.';
-- ========================================
-- Next Migration
-- ========================================
-- OAuth Federation: Link external OAuth providers to Foundation Passports
-- This allows users to login via GitHub, Discord, Google, Roblox, etc.
-- and all logins federate to a single Foundation Passport
CREATE TABLE IF NOT EXISTS public.provider_identities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Reference to the Foundation Passport (user_profiles.id)
user_id UUID NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
-- OAuth provider name (github, discord, google, roblox, ethereum, etc)
provider TEXT NOT NULL,
-- The unique ID from the OAuth provider
provider_user_id TEXT NOT NULL,
-- User's email from the provider (for identity verification)
provider_email TEXT,
-- Additional provider data (JSON: avatar, username, etc)
provider_data JSONB,
-- When this provider was linked
linked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Unique constraint: one provider ID per provider
UNIQUE(provider, provider_user_id),
-- Ensure one user doesn't have duplicate providers
UNIQUE(user_id, provider)
);
-- Indexes for fast OAuth callback lookups
DROP INDEX IF EXISTS public.idx_provider_identities_provider_user_id;
CREATE INDEX idx_provider_identities_provider_user_id
ON public.provider_identities(provider, provider_user_id);
DROP INDEX IF EXISTS public.idx_provider_identities_user_id;
CREATE INDEX idx_provider_identities_user_id
ON public.provider_identities(user_id);
-- Grant access
GRANT SELECT, INSERT, UPDATE, DELETE ON public.provider_identities TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.provider_identities TO service_role;
-- Enable RLS
ALTER TABLE public.provider_identities ENABLE ROW LEVEL SECURITY;
-- Users can only see their own provider identities
DROP POLICY IF EXISTS "Users can view own provider identities" ON public.provider_identities;
CREATE POLICY "Users can view own provider identities"
ON public.provider_identities FOR SELECT
USING (auth.uid() = user_id);
-- Users can only insert their own provider identities
DROP POLICY IF EXISTS "Users can insert own provider identities" ON public.provider_identities;
CREATE POLICY "Users can insert own provider identities"
ON public.provider_identities FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can only update their own provider identities
DROP POLICY IF EXISTS "Users can update own provider identities" ON public.provider_identities;
CREATE POLICY "Users can update own provider identities"
ON public.provider_identities FOR UPDATE
USING (auth.uid() = user_id);
-- Users can only delete their own provider identities
DROP POLICY IF EXISTS "Users can delete own provider identities" ON public.provider_identities;
CREATE POLICY "Users can delete own provider identities"
ON public.provider_identities FOR DELETE
USING (auth.uid() = user_id);
-- Service role can do anything for OAuth flows
DROP POLICY IF EXISTS "Service role can manage all provider identities" ON public.provider_identities;
CREATE POLICY "Service role can manage all provider identities"
ON public.provider_identities
FOR ALL
TO service_role
USING (true);
-- ========================================
-- Next Migration
-- ========================================
-- Add cache tracking columns to user_profiles table
-- This tracks when passport was synced from Foundation and when cache expires
-- These fields are CRITICAL for validating that local data is fresh from Foundation
ALTER TABLE user_profiles
ADD COLUMN IF NOT EXISTS foundation_synced_at TIMESTAMP DEFAULT NULL,
ADD COLUMN IF NOT EXISTS cache_valid_until TIMESTAMP DEFAULT NULL;
-- Create index for cache validation queries
CREATE INDEX IF NOT EXISTS idx_user_profiles_cache_valid
ON user_profiles (cache_valid_until DESC)
WHERE cache_valid_until IS NOT NULL;
-- Add comment explaining the cache architecture
COMMENT ON COLUMN user_profiles.foundation_synced_at IS
'Timestamp when this passport was last synced from aethex.foundation (SSOT).
This ensures we only serve passports that were explicitly synced from Foundation,
not locally created or modified.';
COMMENT ON COLUMN user_profiles.cache_valid_until IS
'Timestamp when this cached passport data becomes stale.
If current time > cache_valid_until, passport must be refreshed from Foundation.
Typical TTL is 24 hours.';
-- Create a validation function to prevent non-Foundation writes
CREATE OR REPLACE FUNCTION validate_passport_ownership()
RETURNS TRIGGER AS $$
BEGIN
-- Only allow updates to these fields (via Foundation sync or local metadata)
-- Reject any attempt to modify core passport fields outside of sync
IF TG_OP = 'UPDATE' THEN
-- If foundation_synced_at is not being set, this is a non-sync update
-- which should not be modifying passport fields
IF NEW.foundation_synced_at IS NULL AND OLD.foundation_synced_at IS NOT NULL THEN
RAISE EXCEPTION 'Cannot modify Foundation passport without sync from Foundation';
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to enforce passport immutability outside of sync
DROP TRIGGER IF EXISTS enforce_passport_ownership ON user_profiles;
CREATE TRIGGER enforce_passport_ownership
BEFORE INSERT OR UPDATE ON user_profiles
FOR EACH ROW
EXECUTE FUNCTION validate_passport_ownership();
-- Log message confirming migration
DO $$ BEGIN
RAISE NOTICE 'Passport cache tracking columns added.
Foundation is now SSOT, aethex.dev acts as read-only cache.
All passport mutations must originate from aethex.foundation.';
END $$;
-- ========================================
-- Next Migration
-- ========================================
-- Table for storing Discord webhook configurations for community posts
CREATE TABLE IF NOT EXISTS public.discord_post_webhooks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
guild_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
webhook_url TEXT NOT NULL,
webhook_id TEXT NOT NULL,
arm_affiliation TEXT NOT NULL,
auto_post BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, guild_id, channel_id, arm_affiliation)
);
-- Enable RLS
ALTER TABLE public.discord_post_webhooks ENABLE ROW LEVEL SECURITY;
-- Policies for discord_post_webhooks
DROP POLICY IF EXISTS "discord_webhooks_read_own" ON public.discord_post_webhooks;
CREATE POLICY "discord_webhooks_read_own" ON public.discord_post_webhooks
FOR SELECT TO authenticated USING (user_id = auth.uid());
DROP POLICY IF EXISTS "discord_webhooks_manage_own" ON public.discord_post_webhooks;
CREATE POLICY "discord_webhooks_manage_own" ON public.discord_post_webhooks
FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_discord_post_webhooks_user_id ON public.discord_post_webhooks(user_id);
CREATE INDEX IF NOT EXISTS idx_discord_post_webhooks_guild_id ON public.discord_post_webhooks(guild_id);
-- Grant service role access
GRANT SELECT, INSERT, UPDATE, DELETE ON public.discord_post_webhooks TO service_role;
-- ========================================
-- Next Migration
-- ========================================
-- Add likes_count and comments_count columns to community_posts if they don't exist
ALTER TABLE public.community_posts
ADD COLUMN IF NOT EXISTS likes_count INTEGER DEFAULT 0 NOT NULL,
ADD COLUMN IF NOT EXISTS comments_count INTEGER DEFAULT 0 NOT NULL;
-- Create function to update likes_count
CREATE OR REPLACE FUNCTION update_post_likes_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE public.community_posts
SET likes_count = (SELECT COUNT(*) FROM public.community_post_likes WHERE post_id = NEW.post_id)
WHERE id = NEW.post_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE public.community_posts
SET likes_count = (SELECT COUNT(*) FROM public.community_post_likes WHERE post_id = OLD.post_id)
WHERE id = OLD.post_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Create function to update comments_count
CREATE OR REPLACE FUNCTION update_post_comments_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE public.community_posts
SET comments_count = (SELECT COUNT(*) FROM public.community_comments WHERE post_id = NEW.post_id)
WHERE id = NEW.post_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE public.community_posts
SET comments_count = (SELECT COUNT(*) FROM public.community_comments WHERE post_id = OLD.post_id)
WHERE id = OLD.post_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Drop existing triggers if they exist
DROP TRIGGER IF EXISTS trigger_update_post_likes_count ON public.community_post_likes;
DROP TRIGGER IF EXISTS trigger_update_post_comments_count ON public.community_comments;
-- Create triggers for likes
CREATE TRIGGER trigger_update_post_likes_count
AFTER INSERT OR DELETE ON public.community_post_likes
FOR EACH ROW
EXECUTE FUNCTION update_post_likes_count();
-- Create triggers for comments
CREATE TRIGGER trigger_update_post_comments_count
AFTER INSERT OR DELETE ON public.community_comments
FOR EACH ROW
EXECUTE FUNCTION update_post_comments_count();
-- ========================================
-- Next Migration
-- ========================================
-- Add arm_affiliation column to community_posts
ALTER TABLE public.community_posts
ADD COLUMN IF NOT EXISTS arm_affiliation TEXT DEFAULT 'labs' NOT NULL;
-- Create index on arm_affiliation for faster filtering
CREATE INDEX IF NOT EXISTS idx_community_posts_arm_affiliation ON public.community_posts(arm_affiliation);
-- Drop table if it exists (from earlier migration)
DROP TABLE IF EXISTS public.user_followed_arms CASCADE;
-- Create user_followed_arms table to track which arms users follow
CREATE TABLE IF NOT EXISTS public.user_followed_arms (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
arm_id TEXT NOT NULL,
followed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, arm_id)
);
-- Create index on user_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_user_followed_arms_user_id ON public.user_followed_arms(user_id);
-- Create index on arm_id for faster filtering
CREATE INDEX IF NOT EXISTS idx_user_followed_arms_arm_id ON public.user_followed_arms(arm_id);
-- Enable RLS on user_followed_arms
ALTER TABLE public.user_followed_arms ENABLE ROW LEVEL SECURITY;
-- Policy: Users can read all followed arms data
DROP POLICY IF EXISTS "user_followed_arms_read" ON public.user_followed_arms;
CREATE POLICY "user_followed_arms_read" ON public.user_followed_arms
FOR SELECT TO authenticated USING (true);
-- Policy: Users can manage their own followed arms
DROP POLICY IF EXISTS "user_followed_arms_manage_self" ON public.user_followed_arms;
CREATE POLICY "user_followed_arms_manage_self" ON public.user_followed_arms
FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
-- Update community_posts table constraints and indexes
CREATE INDEX IF NOT EXISTS idx_community_posts_created_at ON public.community_posts(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_community_posts_author_id ON public.community_posts(author_id);
-- Add grant for service role (backend API access)
GRANT SELECT, INSERT, UPDATE, DELETE ON public.user_followed_arms TO service_role;
-- ========================================
-- Next Migration
-- ========================================
-- Add location column to staff_members table if it doesn't exist
ALTER TABLE IF EXISTS staff_members
ADD COLUMN IF NOT EXISTS location TEXT;
-- Also add to staff_contractors for consistency
ALTER TABLE IF EXISTS staff_contractors
ADD COLUMN IF NOT EXISTS location TEXT;
-- ========================================
-- Next Migration
-- ========================================
-- Create extension if needed
create extension if not exists "pgcrypto";
-- Ethos Tracks Table
-- Stores music, SFX, and audio assets uploaded by artists to the Ethos Guild
create table if not exists public.ethos_tracks (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
title text not null,
description text,
file_url text not null, -- Path to MP3/WAV in storage
duration_seconds int, -- Track length in seconds
genre text[], -- e.g., ['Synthwave', 'Orchestral', 'SFX']
license_type text not null default 'ecosystem' check (license_type in ('ecosystem', 'commercial_sample')),
-- 'ecosystem': Free license for non-commercial AeThex use
-- 'commercial_sample': Demo track (user must negotiate commercial licensing)
bpm int, -- Beats per minute (useful for synchronization)
is_published boolean not null default true,
download_count int not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists ethos_tracks_user_id_idx on public.ethos_tracks (user_id);
create index if not exists ethos_tracks_license_type_idx on public.ethos_tracks (license_type);
create index if not exists ethos_tracks_genre_gin on public.ethos_tracks using gin (genre);
create index if not exists ethos_tracks_created_at_idx on public.ethos_tracks (created_at desc);
-- Ethos Artist Profiles Table
-- Extends user_profiles with Ethos-specific skills, pricing, and portfolio info
create table if not exists public.ethos_artist_profiles (
user_id uuid primary key references public.user_profiles(id) on delete cascade,
skills text[] not null default '{}', -- e.g., ['Synthwave', 'SFX Design', 'Orchestral', 'Game Audio']
for_hire boolean not null default true, -- Whether artist accepts commissions
bio text, -- Artist bio/statement
portfolio_url text, -- External portfolio link
sample_price_track numeric(10, 2), -- e.g., 500.00 for "Custom Track - $500"
sample_price_sfx numeric(10, 2), -- e.g., 150.00 for "SFX Pack - $150"
sample_price_score numeric(10, 2), -- e.g., 2000.00 for "Full Score - $2000"
turnaround_days int, -- Estimated delivery time in days
verified boolean not null default false, -- Verified Ethos artist
total_downloads int not null default 0, -- Total downloads across all tracks
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists ethos_artist_profiles_for_hire_idx on public.ethos_artist_profiles (for_hire);
create index if not exists ethos_artist_profiles_verified_idx on public.ethos_artist_profiles (verified);
create index if not exists ethos_artist_profiles_skills_gin on public.ethos_artist_profiles using gin (skills);
-- Ethos Guild Membership Table (optional - tracks who's "part of" the guild)
create table if not exists public.ethos_guild_members (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
role text not null default 'member' check (role in ('member', 'curator', 'admin')),
-- member: regular artist
-- curator: can feature/recommend tracks
-- admin: manages the guild (hiring, moderation, etc.)
joined_at timestamptz not null default now(),
bio text -- Member's artist bio
);
create index if not exists ethos_guild_members_user_id_idx on public.ethos_guild_members (user_id);
create index if not exists ethos_guild_members_role_idx on public.ethos_guild_members (role);
create unique index if not exists ethos_guild_members_user_id_unique on public.ethos_guild_members (user_id);
-- Licensing Agreements Table (for tracking commercial contracts)
create table if not exists public.ethos_licensing_agreements (
id uuid primary key default gen_random_uuid(),
track_id uuid not null references public.ethos_tracks(id) on delete cascade,
licensee_id uuid not null references public.user_profiles(id) on delete cascade,
-- licensee_id: The person/org licensing the track (e.g., CORP consulting client)
license_type text not null check (license_type in ('commercial_one_time', 'commercial_exclusive', 'broadcast')),
agreement_url text, -- Link to signed contract or legal document
approved boolean not null default false,
created_at timestamptz not null default now(),
expires_at timestamptz
);
create index if not exists ethos_licensing_agreements_track_id_idx on public.ethos_licensing_agreements (track_id);
create index if not exists ethos_licensing_agreements_licensee_id_idx on public.ethos_licensing_agreements (licensee_id);
-- Enable RLS
alter table public.ethos_tracks enable row level security;
alter table public.ethos_artist_profiles enable row level security;
alter table public.ethos_guild_members enable row level security;
alter table public.ethos_licensing_agreements enable row level security;
-- RLS Policies: ethos_tracks
drop policy if exists "Ethos tracks are readable by all authenticated users" on public.ethos_tracks;
create policy "Ethos tracks are readable by all authenticated users" on public.ethos_tracks
for select using (auth.role() = 'authenticated');
drop policy if exists "Users can insert their own tracks" on public.ethos_tracks;
create policy "Users can insert their own tracks" on public.ethos_tracks
for insert with check (auth.uid() = user_id);
drop policy if exists "Users can update their own tracks" on public.ethos_tracks;
create policy "Users can update their own tracks" on public.ethos_tracks
for update using (auth.uid() = user_id);
drop policy if exists "Users can delete their own tracks" on public.ethos_tracks;
create policy "Users can delete their own tracks" on public.ethos_tracks
for delete using (auth.uid() = user_id);
-- RLS Policies: ethos_artist_profiles
drop policy if exists "Ethos artist profiles are readable by all authenticated users" on public.ethos_artist_profiles;
create policy "Ethos artist profiles are readable by all authenticated users" on public.ethos_artist_profiles
for select using (auth.role() = 'authenticated');
drop policy if exists "Users can insert their own artist profile" on public.ethos_artist_profiles;
create policy "Users can insert their own artist profile" on public.ethos_artist_profiles
for insert with check (auth.uid() = user_id);
drop policy if exists "Users can update their own artist profile" on public.ethos_artist_profiles;
create policy "Users can update their own artist profile" on public.ethos_artist_profiles
for update using (auth.uid() = user_id);
-- RLS Policies: ethos_guild_members
drop policy if exists "Guild membership is readable by all authenticated users" on public.ethos_guild_members;
create policy "Guild membership is readable by all authenticated users" on public.ethos_guild_members
for select using (auth.role() = 'authenticated');
drop policy if exists "Admins can manage guild members" on public.ethos_guild_members;
create policy "Admins can manage guild members" on public.ethos_guild_members
for all using (
exists(
select 1 from public.ethos_guild_members
where user_id = auth.uid() and role = 'admin'
)
);
drop policy if exists "Users can see their own membership" on public.ethos_guild_members;
create policy "Users can see their own membership" on public.ethos_guild_members
for select using (auth.uid() = user_id or auth.role() = 'authenticated');
-- RLS Policies: ethos_licensing_agreements
drop policy if exists "Licensing agreements readable by involved parties" on public.ethos_licensing_agreements;
create policy "Licensing agreements readable by involved parties" on public.ethos_licensing_agreements
for select using (
auth.uid() in (
select user_id from public.ethos_tracks where id = track_id
union
select licensee_id
)
or exists(
select 1 from public.ethos_guild_members
where user_id = auth.uid() and role = 'admin'
)
);
drop policy if exists "Track owners can approve agreements" on public.ethos_licensing_agreements;
create policy "Track owners can approve agreements" on public.ethos_licensing_agreements
for update using (
auth.uid() in (
select user_id from public.ethos_tracks where id = track_id
)
);
-- Triggers to maintain updated_at
create or replace function public.set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists ethos_tracks_set_updated_at on public.ethos_tracks;
create trigger ethos_tracks_set_updated_at
before update on public.ethos_tracks
for each row execute function public.set_updated_at();
drop trigger if exists ethos_artist_profiles_set_updated_at on public.ethos_artist_profiles;
create trigger ethos_artist_profiles_set_updated_at
before update on public.ethos_artist_profiles
for each row execute function public.set_updated_at();
drop trigger if exists ethos_guild_members_set_updated_at on public.ethos_guild_members;
create trigger ethos_guild_members_set_updated_at
before update on public.ethos_guild_members
for each row execute function public.set_updated_at();
-- Comments for documentation
comment on table public.ethos_tracks is 'Music, SFX, and audio tracks uploaded by Ethos Guild artists';
comment on table public.ethos_artist_profiles is 'Extended profiles for Ethos Guild artists with skills, pricing, and portfolio info';
comment on table public.ethos_guild_members is 'Membership tracking for the Ethos Guild community';
comment on table public.ethos_licensing_agreements is 'Commercial licensing agreements for track usage';
-- ========================================
-- Next Migration
-- ========================================
-- Ethos Artist Verification Requests Table
-- Tracks pending artist verification submissions
create table if not exists public.ethos_verification_requests (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
artist_profile_id uuid not null references public.ethos_artist_profiles(user_id) on delete cascade,
status text not null default 'pending' check (status in ('pending', 'approved', 'rejected')),
-- pending: awaiting review
-- approved: artist verified
-- rejected: application rejected
submitted_at timestamptz not null default now(),
reviewed_at timestamptz,
reviewed_by uuid references public.user_profiles(id), -- Admin who reviewed
rejection_reason text, -- Why was this rejected
submission_notes text, -- Artist's application notes
portfolio_links text[], -- Links to artist's portfolio/samples
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists ethos_verification_requests_user_id_idx on public.ethos_verification_requests (user_id);
create index if not exists ethos_verification_requests_status_idx on public.ethos_verification_requests (status);
create index if not exists ethos_verification_requests_submitted_at_idx on public.ethos_verification_requests (submitted_at desc);
create unique index if not exists ethos_verification_requests_user_id_unique on public.ethos_verification_requests (user_id);
-- Ethos Artist Verification Audit Log
-- Tracks all verification decisions for compliance
create table if not exists public.ethos_verification_audit_log (
id uuid primary key default gen_random_uuid(),
request_id uuid not null references public.ethos_verification_requests(id) on delete cascade,
action text not null check (action in ('submitted', 'approved', 'rejected', 'resubmitted')),
actor_id uuid references public.user_profiles(id), -- Who performed this action
notes text, -- Additional context
created_at timestamptz not null default now()
);
create index if not exists ethos_verification_audit_log_request_id_idx on public.ethos_verification_audit_log (request_id);
create index if not exists ethos_verification_audit_log_actor_id_idx on public.ethos_verification_audit_log (actor_id);
create index if not exists ethos_verification_audit_log_action_idx on public.ethos_verification_audit_log (action);
-- Enable RLS
alter table public.ethos_verification_requests enable row level security;
alter table public.ethos_verification_audit_log enable row level security;
-- RLS Policies: ethos_verification_requests
drop policy if exists "Artists can view their own verification request" on public.ethos_verification_requests;
create policy "Artists can view their own verification request" on public.ethos_verification_requests
for select using (auth.uid() = user_id);
drop policy if exists "Admins can view all verification requests" on public.ethos_verification_requests;
create policy "Admins can view all verification requests" on public.ethos_verification_requests
for select using (
exists(
select 1 from public.user_profiles
where id = auth.uid() and user_type = 'staff'
)
);
drop policy if exists "Artists can submit verification request" on public.ethos_verification_requests;
create policy "Artists can submit verification request" on public.ethos_verification_requests
for insert with check (auth.uid() = user_id);
drop policy if exists "Admins can update verification status" on public.ethos_verification_requests;
create policy "Admins can update verification status" on public.ethos_verification_requests
for update using (
exists(
select 1 from public.user_profiles
where id = auth.uid() and user_type = 'staff'
)
);
-- RLS Policies: ethos_verification_audit_log
drop policy if exists "Admins can view audit log" on public.ethos_verification_audit_log;
create policy "Admins can view audit log" on public.ethos_verification_audit_log
for select using (
exists(
select 1 from public.user_profiles
where id = auth.uid() and user_type = 'staff'
)
);
drop policy if exists "System can write audit logs" on public.ethos_verification_audit_log;
create policy "System can write audit logs" on public.ethos_verification_audit_log
for insert with check (true);
-- Triggers to maintain updated_at
create or replace function public.set_verification_request_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists ethos_verification_requests_set_updated_at on public.ethos_verification_requests;
create trigger ethos_verification_requests_set_updated_at
before update on public.ethos_verification_requests
for each row execute function public.set_verification_request_updated_at();
-- Comments for documentation
comment on table public.ethos_verification_requests is 'Tracks artist verification submissions and decisions for manual admin review';
comment on table public.ethos_verification_audit_log is 'Audit trail for all verification actions and decisions';
-- ========================================
-- Next Migration
-- ========================================
-- Create storage bucket for Ethos tracks if it doesn't exist
-- Note: This SQL migration cannot create buckets directly via SQL
-- The bucket must be created via the Supabase Dashboard or API:
--
-- 1. Go to Supabase Dashboard > Storage
-- 2. Click "New bucket"
-- 3. Name: "ethos-tracks"
-- 4. Make it PUBLIC
-- 5. Set up these RLS policies (see below)
-- After bucket is created, apply these RLS policies in SQL:
-- Enable RLS on storage objects (wrapped in error handling for permissions)
DO $$ BEGIN
drop policy if exists "Allow authenticated users to upload tracks" on storage.objects;
create policy "Allow authenticated users to upload tracks"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'ethos-tracks'
and (storage.foldername(name))[1] = auth.uid()::text
);
EXCEPTION WHEN insufficient_privilege THEN
RAISE NOTICE 'Skipping ethos-tracks upload policy - insufficient permissions. Apply manually via Dashboard.';
END $$;
DO $$ BEGIN
drop policy if exists "Allow public read access to ethos tracks" on storage.objects;
create policy "Allow public read access to ethos tracks"
on storage.objects
for select
to public
using (bucket_id = 'ethos-tracks');
EXCEPTION WHEN insufficient_privilege THEN
RAISE NOTICE 'Skipping ethos-tracks read policy - insufficient permissions. Apply manually via Dashboard.';
END $$;
DO $$ BEGIN
drop policy if exists "Allow users to delete their own tracks" on storage.objects;
create policy "Allow users to delete their own tracks"
on storage.objects
for delete
to authenticated
using (
bucket_id = 'ethos-tracks'
and (storage.foldername(name))[1] = auth.uid()::text
);
EXCEPTION WHEN insufficient_privilege THEN
RAISE NOTICE 'Skipping ethos-tracks delete policy - insufficient permissions. Apply manually via Dashboard.';
END $$;
-- Create index for better performance (skip if permissions issue)
DO $$ BEGIN
create index if not exists idx_storage_bucket_name on storage.objects(bucket_id);
EXCEPTION WHEN insufficient_privilege THEN NULL; END $$;
DO $$ BEGIN
create index if not exists idx_storage_name on storage.objects(name);
EXCEPTION WHEN insufficient_privilege THEN NULL; END $$;
-- ========================================
-- Next Migration
-- ========================================
-- Add service pricing and licensing fields to ethos_artist_profiles
-- Add new columns for service pricing
ALTER TABLE public.ethos_artist_profiles
ADD COLUMN IF NOT EXISTS price_list jsonb DEFAULT '{
"track_custom": null,
"sfx_pack": null,
"full_score": null,
"day_rate": null,
"contact_for_quote": false
}'::jsonb;
-- Add ecosystem license acceptance tracking
ALTER TABLE public.ethos_artist_profiles
ADD COLUMN IF NOT EXISTS ecosystem_license_accepted boolean NOT NULL DEFAULT false;
ALTER TABLE public.ethos_artist_profiles
ADD COLUMN IF NOT EXISTS ecosystem_license_accepted_at timestamptz;
-- Create index for faster queries on for_hire status
CREATE INDEX IF NOT EXISTS idx_ethos_artist_for_hire ON public.ethos_artist_profiles(for_hire)
WHERE for_hire = true;
-- Create index for ecosystem license acceptance tracking
CREATE INDEX IF NOT EXISTS idx_ethos_artist_license_accepted ON public.ethos_artist_profiles(ecosystem_license_accepted)
WHERE ecosystem_license_accepted = true;
-- Add table to track ecosystem license agreements per artist per track
CREATE TABLE IF NOT EXISTS public.ethos_ecosystem_licenses (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
track_id uuid NOT NULL REFERENCES public.ethos_tracks(id) ON DELETE CASCADE,
artist_id uuid NOT NULL REFERENCES public.user_profiles(id) ON DELETE CASCADE,
accepted_at timestamptz NOT NULL DEFAULT now(),
agreement_version text NOT NULL DEFAULT '1.0', -- Track KND-008 version
agreement_text_hash text, -- Hash of agreement text for audit
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_ethos_ecosystem_licenses_artist_id ON public.ethos_ecosystem_licenses(artist_id);
CREATE INDEX IF NOT EXISTS idx_ethos_ecosystem_licenses_track_id ON public.ethos_ecosystem_licenses(track_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_ethos_ecosystem_licenses_unique ON public.ethos_ecosystem_licenses(track_id, artist_id);
-- Enable RLS on ecosystem licenses table
ALTER TABLE public.ethos_ecosystem_licenses ENABLE ROW LEVEL SECURITY;
-- RLS Policies: ethos_ecosystem_licenses
DROP POLICY IF EXISTS "Artists can view their own ecosystem licenses" ON public.ethos_ecosystem_licenses;
CREATE POLICY "Artists can view their own ecosystem licenses" ON public.ethos_ecosystem_licenses
FOR SELECT USING (auth.uid() = artist_id);
DROP POLICY IF EXISTS "Admins can view all ecosystem licenses" ON public.ethos_ecosystem_licenses;
CREATE POLICY "Admins can view all ecosystem licenses" ON public.ethos_ecosystem_licenses
FOR SELECT USING (
EXISTS(
SELECT 1 FROM public.user_profiles
WHERE id = auth.uid() AND user_type = 'staff'
)
);
DROP POLICY IF EXISTS "Artists can create ecosystem license records" ON public.ethos_ecosystem_licenses;
CREATE POLICY "Artists can create ecosystem license records" ON public.ethos_ecosystem_licenses
FOR INSERT WITH CHECK (auth.uid() = artist_id);
-- Add comments for documentation
COMMENT ON COLUMN public.ethos_artist_profiles.price_list IS 'JSON object with pricing: {track_custom: 500, sfx_pack: 150, full_score: 2000, day_rate: 1500, contact_for_quote: false}';
COMMENT ON COLUMN public.ethos_artist_profiles.ecosystem_license_accepted IS 'Whether artist has accepted the KND-008 Ecosystem License agreement';
COMMENT ON COLUMN public.ethos_artist_profiles.ecosystem_license_accepted_at IS 'Timestamp when artist accepted the Ecosystem License';
COMMENT ON TABLE public.ethos_ecosystem_licenses IS 'Tracks individual ecosystem license acceptances per track for audit and compliance';
-- ========================================
-- Next Migration
-- ========================================
-- Ethos Service Requests Table
-- Tracks service commission requests from clients to artists
create table if not exists public.ethos_service_requests (
id uuid primary key default gen_random_uuid(),
artist_id uuid not null references public.user_profiles(id) on delete cascade,
requester_id uuid not null references public.user_profiles(id) on delete cascade,
service_type text not null check (service_type in ('track_custom', 'sfx_pack', 'full_score', 'day_rate', 'contact_for_quote')),
-- track_custom: Custom music track
-- sfx_pack: Sound effects package
-- full_score: Full game score/composition
-- day_rate: Hourly consulting rate
-- contact_for_quote: Custom quote request
description text not null,
budget numeric, -- Optional budget in USD
deadline timestamptz, -- Optional deadline
status text not null default 'pending' check (status in ('pending', 'accepted', 'declined', 'in_progress', 'completed', 'cancelled')),
notes text, -- Artist notes on the request
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- Create indexes for performance
create index if not exists ethos_service_requests_artist_id_idx on public.ethos_service_requests (artist_id);
create index if not exists ethos_service_requests_requester_id_idx on public.ethos_service_requests (requester_id);
create index if not exists ethos_service_requests_status_idx on public.ethos_service_requests (status);
create index if not exists ethos_service_requests_created_at_idx on public.ethos_service_requests (created_at desc);
-- Enable RLS
alter table public.ethos_service_requests enable row level security;
-- RLS Policies: ethos_service_requests
drop policy if exists "Artists can view their service requests" on public.ethos_service_requests;
create policy "Artists can view their service requests"
on public.ethos_service_requests
for select using (auth.uid() = artist_id);
drop policy if exists "Requesters can view their service requests" on public.ethos_service_requests;
create policy "Requesters can view their service requests"
on public.ethos_service_requests
for select using (auth.uid() = requester_id);
drop policy if exists "Authenticated users can create service requests" on public.ethos_service_requests;
create policy "Authenticated users can create service requests"
on public.ethos_service_requests
for insert with check (auth.uid() = requester_id);
drop policy if exists "Artists can update their service requests" on public.ethos_service_requests;
create policy "Artists can update their service requests"
on public.ethos_service_requests
for update using (auth.uid() = artist_id);
-- Trigger to maintain updated_at
drop trigger if exists ethos_service_requests_set_updated_at on public.ethos_service_requests;
create trigger ethos_service_requests_set_updated_at
before update on public.ethos_service_requests
for each row execute function public.set_updated_at();
-- Comments for documentation
comment on table public.ethos_service_requests is 'Service commission requests from clients to Ethos Guild artists';
comment on column public.ethos_service_requests.status is 'Status of the service request: pending (awaiting response), accepted (artist accepted), declined (artist declined), in_progress (work started), completed (work finished), cancelled (client cancelled)';
comment on column public.ethos_service_requests.budget is 'Optional budget amount in USD for the requested service';
comment on column public.ethos_service_requests.deadline is 'Optional deadline for the service completion';
-- ========================================
-- Next Migration
-- ========================================
-- GameForge Studio Management System
-- Complete project lifecycle tracking for the GameForge game development studio
-- GameForge Projects Table
-- Tracks all game projects in development across the studio
create table if not exists public.gameforge_projects (
id uuid primary key default gen_random_uuid(),
name text not null unique,
description text,
status text not null default 'planning' check (status in ('planning', 'in_development', 'qa', 'released', 'hiatus', 'cancelled')),
lead_id uuid not null references public.user_profiles(id) on delete set null,
platform text not null check (platform in ('Unity', 'Unreal', 'Godot', 'Custom', 'WebGL')),
genre text[] not null default '{}', -- e.g., ['Action', 'RPG', 'Puzzle']
target_release_date timestamptz,
actual_release_date timestamptz,
budget numeric(12, 2), -- Project budget in USD
current_spend numeric(12, 2) not null default 0, -- Actual spending to date
team_size int default 0,
repository_url text, -- GitHub/GitLab repo link
documentation_url text, -- Design docs, wiki, etc.
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists gameforge_projects_status_idx on public.gameforge_projects (status);
create index if not exists gameforge_projects_lead_id_idx on public.gameforge_projects (lead_id);
create index if not exists gameforge_projects_created_at_idx on public.gameforge_projects (created_at desc);
create index if not exists gameforge_projects_platform_idx on public.gameforge_projects (platform);
-- GameForge Team Members Table
-- Studio employees and contractors assigned to projects
create table if not exists public.gameforge_team_members (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
role text not null check (role in ('engineer', 'designer', 'artist', 'producer', 'qa', 'sound_designer', 'writer', 'manager')),
position text, -- e.g., "Lead Programmer", "Character Artist"
contract_type text not null default 'employee' check (contract_type in ('employee', 'contractor', 'consultant', 'intern')),
hourly_rate numeric(8, 2), -- Contract rate (if applicable)
project_ids uuid[] not null default '{}', -- Projects they work on
skills text[] default '{}', -- e.g., ['C#', 'Unreal', 'Blueprints']
bio text,
joined_date timestamptz not null default now(),
left_date timestamptz, -- When they left the studio (null if still active)
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists gameforge_team_members_user_id_idx on public.gameforge_team_members (user_id);
create index if not exists gameforge_team_members_role_idx on public.gameforge_team_members (role);
create index if not exists gameforge_team_members_is_active_idx on public.gameforge_team_members (is_active);
create unique index if not exists gameforge_team_members_user_id_unique on public.gameforge_team_members (user_id);
-- GameForge Builds Table
-- Track game builds, releases, and versions
create table if not exists public.gameforge_builds (
id uuid primary key default gen_random_uuid(),
project_id uuid not null references public.gameforge_projects(id) on delete cascade,
version text not null, -- e.g., "1.0.0", "0.5.0-alpha"
build_type text not null check (build_type in ('alpha', 'beta', 'release_candidate', 'final')),
release_date timestamptz not null default now(),
download_url text,
changelog text, -- Release notes and what changed
file_size bigint, -- Size in bytes
target_platforms text[] not null default '{}', -- Windows, Mac, Linux, WebGL, iOS, Android
download_count int not null default 0,
created_by uuid references public.user_profiles(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists gameforge_builds_project_id_idx on public.gameforge_builds (project_id);
create index if not exists gameforge_builds_release_date_idx on public.gameforge_builds (release_date desc);
create index if not exists gameforge_builds_version_idx on public.gameforge_builds (version);
create unique index if not exists gameforge_builds_project_version_unique on public.gameforge_builds (project_id, version);
-- GameForge Metrics Table
-- Track monthly/sprint metrics: velocity, shipping speed, team productivity
create table if not exists public.gameforge_metrics (
id uuid primary key default gen_random_uuid(),
project_id uuid not null references public.gameforge_projects(id) on delete cascade,
metric_date timestamptz not null default now(), -- When this metric period ended
metric_type text not null check (metric_type in ('monthly', 'sprint', 'milestone')),
-- Productivity metrics
velocity int, -- Story points or tasks completed in period
hours_logged int, -- Total team hours
team_size_avg int, -- Average team size during period
-- Quality metrics
bugs_found int default 0,
bugs_fixed int default 0,
build_count int default 0,
-- Shipping metrics
days_from_planned_to_release int, -- How many days late/early (shipping velocity)
on_schedule boolean, -- Whether release hit target date
-- Financial metrics
budget_allocated numeric(12, 2),
budget_spent numeric(12, 2),
created_at timestamptz not null default now()
);
create index if not exists gameforge_metrics_project_id_idx on public.gameforge_metrics (project_id);
create index if not exists gameforge_metrics_metric_date_idx on public.gameforge_metrics (metric_date desc);
create index if not exists gameforge_metrics_metric_type_idx on public.gameforge_metrics (metric_type);
-- Enable RLS
alter table public.gameforge_projects enable row level security;
alter table public.gameforge_team_members enable row level security;
alter table public.gameforge_builds enable row level security;
alter table public.gameforge_metrics enable row level security;
-- RLS Policies: gameforge_projects
drop policy if exists "Projects are readable by all authenticated users" on public.gameforge_projects;
create policy "Projects are readable by all authenticated users" on public.gameforge_projects
for select using (auth.role() = 'authenticated');
drop policy if exists "Studio leads can create projects" on public.gameforge_projects;
create policy "Studio leads can create projects" on public.gameforge_projects
for insert with check (auth.uid() = lead_id);
drop policy if exists "Project leads can update their projects" on public.gameforge_projects;
create policy "Project leads can update their projects" on public.gameforge_projects
for update using (auth.uid() = lead_id);
-- RLS Policies: gameforge_team_members
drop policy if exists "Team members are readable by all authenticated users" on public.gameforge_team_members;
create policy "Team members are readable by all authenticated users" on public.gameforge_team_members
for select using (auth.role() = 'authenticated');
drop policy if exists "Team members can view their own record" on public.gameforge_team_members;
create policy "Team members can view their own record" on public.gameforge_team_members
for select using (auth.uid() = user_id);
drop policy if exists "Users can insert their own team member record" on public.gameforge_team_members;
create policy "Users can insert their own team member record" on public.gameforge_team_members
for insert with check (auth.uid() = user_id);
drop policy if exists "Users can update their own team member record" on public.gameforge_team_members;
create policy "Users can update their own team member record" on public.gameforge_team_members
for update using (auth.uid() = user_id);
-- RLS Policies: gameforge_builds
drop policy if exists "Builds are readable by all authenticated users" on public.gameforge_builds;
create policy "Builds are readable by all authenticated users" on public.gameforge_builds
for select using (auth.role() = 'authenticated');
drop policy if exists "Project leads can create builds" on public.gameforge_builds;
create policy "Project leads can create builds" on public.gameforge_builds
for insert with check (
exists(
select 1 from public.gameforge_projects
where id = project_id and lead_id = auth.uid()
)
);
drop policy if exists "Project leads can update builds" on public.gameforge_builds;
create policy "Project leads can update builds" on public.gameforge_builds
for update using (
exists(
select 1 from public.gameforge_projects
where id = project_id and lead_id = auth.uid()
)
);
-- RLS Policies: gameforge_metrics
drop policy if exists "Metrics are readable by all authenticated users" on public.gameforge_metrics;
create policy "Metrics are readable by all authenticated users" on public.gameforge_metrics
for select using (auth.role() = 'authenticated');
drop policy if exists "Project leads can insert metrics" on public.gameforge_metrics;
create policy "Project leads can insert metrics" on public.gameforge_metrics
for insert with check (
exists(
select 1 from public.gameforge_projects
where id = project_id and lead_id = auth.uid()
)
);
-- Triggers to maintain updated_at
create or replace function public.set_gameforge_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists gameforge_projects_set_updated_at on public.gameforge_projects;
create trigger gameforge_projects_set_updated_at
before update on public.gameforge_projects
for each row execute function public.set_gameforge_updated_at();
drop trigger if exists gameforge_team_members_set_updated_at on public.gameforge_team_members;
create trigger gameforge_team_members_set_updated_at
before update on public.gameforge_team_members
for each row execute function public.set_gameforge_updated_at();
drop trigger if exists gameforge_builds_set_updated_at on public.gameforge_builds;
create trigger gameforge_builds_set_updated_at
before update on public.gameforge_builds
for each row execute function public.set_gameforge_updated_at();
-- Comments for documentation
comment on table public.gameforge_projects is 'GameForge studio game projects with lifecycle tracking and team management';
comment on table public.gameforge_team_members is 'GameForge studio team members including engineers, designers, artists, producers, QA';
comment on table public.gameforge_builds is 'Game builds, releases, and versions for each GameForge project';
comment on table public.gameforge_metrics is 'Monthly/sprint metrics for shipping velocity, productivity, quality, and budget tracking';
comment on column public.gameforge_projects.status is 'Project lifecycle: planning → in_development → qa → released (or cancelled/hiatus)';
comment on column public.gameforge_metrics.days_from_planned_to_release is 'Positive = late, Negative = early, Zero = on-time (key shipping velocity metric)';
-- ========================================
-- Next Migration
-- ========================================
-- Foundation: Non-profit Education & Community Platform
-- Includes: Courses, Curriculum, Progress Tracking, Achievements, Mentorship
create extension if not exists "pgcrypto";
-- ============================================================================
-- COURSES & CURRICULUM
-- ============================================================================
create table if not exists public.foundation_courses (
id uuid primary key default gen_random_uuid(),
slug text unique not null,
title text not null,
description text,
category text not null, -- 'getting-started', 'intermediate', 'advanced', 'specialization'
difficulty text not null default 'beginner' check (difficulty in ('beginner', 'intermediate', 'advanced')),
instructor_id uuid not null references public.user_profiles(id) on delete cascade,
cover_image_url text,
estimated_hours int, -- estimated time to complete
is_published boolean not null default false,
order_index int, -- for curriculum ordering
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_courses_published_idx on public.foundation_courses (is_published);
create index if not exists foundation_courses_category_idx on public.foundation_courses (category);
create index if not exists foundation_courses_slug_idx on public.foundation_courses (slug);
-- Course Modules (chapters/sections)
create table if not exists public.foundation_course_modules (
id uuid primary key default gen_random_uuid(),
course_id uuid not null references public.foundation_courses(id) on delete cascade,
title text not null,
description text,
content text, -- markdown or HTML
video_url text, -- optional embedded video
order_index int not null,
is_published boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_course_modules_course_idx on public.foundation_course_modules (course_id);
-- Course Lessons (within modules)
create table if not exists public.foundation_course_lessons (
id uuid primary key default gen_random_uuid(),
module_id uuid not null references public.foundation_course_modules(id) on delete cascade,
course_id uuid not null references public.foundation_courses(id) on delete cascade,
title text not null,
content text not null, -- markdown
video_url text,
reading_time_minutes int,
order_index int not null,
is_published boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_course_lessons_module_idx on public.foundation_course_lessons (module_id);
-- User Enrollments & Progress
create table if not exists public.foundation_enrollments (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
course_id uuid not null references public.foundation_courses(id) on delete cascade,
progress_percent int not null default 0,
status text not null default 'in_progress' check (status in ('in_progress', 'completed', 'paused')),
completed_at timestamptz,
enrolled_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique(user_id, course_id)
);
create index if not exists foundation_enrollments_user_idx on public.foundation_enrollments (user_id);
create index if not exists foundation_enrollments_course_idx on public.foundation_enrollments (course_id);
create index if not exists foundation_enrollments_status_idx on public.foundation_enrollments (status);
-- Lesson Completion Tracking
create table if not exists public.foundation_lesson_progress (
user_id uuid not null references public.user_profiles(id) on delete cascade,
lesson_id uuid not null references public.foundation_course_lessons(id) on delete cascade,
completed boolean not null default false,
completed_at timestamptz,
created_at timestamptz not null default now(),
primary key (user_id, lesson_id)
);
-- ============================================================================
-- ACHIEVEMENTS & BADGES
-- ============================================================================
create table if not exists public.foundation_achievements (
id uuid primary key default gen_random_uuid(),
slug text unique not null,
name text not null,
description text,
icon_url text,
badge_color text, -- hex color for badge
requirement_type text not null check (requirement_type in ('course_completion', 'milestone', 'contribution', 'mentorship')),
requirement_data jsonb, -- e.g., {"course_id": "...", "count": 1}
tier int default 1, -- 1 (bronze), 2 (silver), 3 (gold), 4 (platinum)
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_achievements_requirement_idx on public.foundation_achievements (requirement_type);
-- User Achievements (earned badges)
create table if not exists public.foundation_user_achievements (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
achievement_id uuid not null references public.foundation_achievements(id) on delete cascade,
earned_at timestamptz not null default now(),
unique(user_id, achievement_id)
);
create index if not exists foundation_user_achievements_user_idx on public.foundation_user_achievements (user_id);
create index if not exists foundation_user_achievements_earned_idx on public.foundation_user_achievements (earned_at);
-- ============================================================================
-- MENTORSHIP
-- ============================================================================
-- Mentor Profiles (extends user_profiles)
create table if not exists public.foundation_mentors (
user_id uuid primary key references public.user_profiles(id) on delete cascade,
bio text,
expertise text[] not null default '{}', -- e.g., ['Web3', 'Game Dev', 'AI/ML']
available boolean not null default false,
max_mentees int default 3,
current_mentees int not null default 0,
approval_status text not null default 'pending' check (approval_status in ('pending', 'approved', 'rejected')),
approved_by uuid references public.user_profiles(id) on delete set null,
approved_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_mentors_available_idx on public.foundation_mentors (available);
create index if not exists foundation_mentors_approval_idx on public.foundation_mentors (approval_status);
create index if not exists foundation_mentors_expertise_gin on public.foundation_mentors using gin (expertise);
-- Mentorship Requests & Sessions
create table if not exists public.foundation_mentorship_requests (
id uuid primary key default gen_random_uuid(),
mentor_id uuid not null references public.user_profiles(id) on delete cascade,
mentee_id uuid not null references public.user_profiles(id) on delete cascade,
message text,
expertise_area text, -- which area they want help with
status text not null default 'pending' check (status in ('pending', 'accepted', 'rejected', 'cancelled')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create unique index if not exists foundation_mentorship_requests_pending_unique
on public.foundation_mentorship_requests (mentor_id, mentee_id)
where status = 'pending';
create index if not exists foundation_mentorship_requests_mentor_idx on public.foundation_mentorship_requests (mentor_id);
create index if not exists foundation_mentorship_requests_mentee_idx on public.foundation_mentorship_requests (mentee_id);
create index if not exists foundation_mentorship_requests_status_idx on public.foundation_mentorship_requests (status);
-- Mentorship Sessions
create table if not exists public.foundation_mentorship_sessions (
id uuid primary key default gen_random_uuid(),
mentor_id uuid not null references public.user_profiles(id) on delete cascade,
mentee_id uuid not null references public.user_profiles(id) on delete cascade,
scheduled_at timestamptz not null,
duration_minutes int not null default 60,
topic text,
notes text, -- notes from the session
completed boolean not null default false,
completed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists foundation_mentorship_sessions_mentor_idx on public.foundation_mentorship_sessions (mentor_id);
create index if not exists foundation_mentorship_sessions_mentee_idx on public.foundation_mentorship_sessions (mentee_id);
create index if not exists foundation_mentorship_sessions_scheduled_idx on public.foundation_mentorship_sessions (scheduled_at);
-- ============================================================================
-- CONTRIBUTIONS & COMMUNITY
-- ============================================================================
create table if not exists public.foundation_contributions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
contribution_type text not null, -- 'course_creation', 'lesson_review', 'mentorship', 'community_support'
resource_id uuid, -- e.g., course_id, lesson_id
points int not null default 0, -- contribution points toward achievements
created_at timestamptz not null default now()
);
create index if not exists foundation_contributions_user_idx on public.foundation_contributions (user_id);
create index if not exists foundation_contributions_type_idx on public.foundation_contributions (contribution_type);
-- ============================================================================
-- RLS POLICIES
-- ============================================================================
-- Enable RLS only on tables that exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_courses') THEN
ALTER TABLE public.foundation_courses ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_course_modules') THEN
ALTER TABLE public.foundation_course_modules ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_course_lessons') THEN
ALTER TABLE public.foundation_course_lessons ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_enrollments') THEN
ALTER TABLE public.foundation_enrollments ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_lesson_progress') THEN
ALTER TABLE public.foundation_lesson_progress ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_achievements') THEN
ALTER TABLE public.foundation_achievements ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_user_achievements') THEN
ALTER TABLE public.foundation_user_achievements ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_mentors') THEN
ALTER TABLE public.foundation_mentors ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_mentorship_requests') THEN
ALTER TABLE public.foundation_mentorship_requests ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_mentorship_sessions') THEN
ALTER TABLE public.foundation_mentorship_sessions ENABLE ROW LEVEL SECURITY;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'foundation_contributions') THEN
ALTER TABLE public.foundation_contributions ENABLE ROW LEVEL SECURITY;
END IF;
END $$;
-- Courses: Published courses readable by all, all ops by instructor/admin
drop policy if exists "Published courses readable by all" on public.foundation_courses;
create policy "Published courses readable by all" on public.foundation_courses
for select using (is_published = true or auth.uid() = instructor_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
drop policy if exists "Instructors manage own courses" on public.foundation_courses;
create policy "Instructors manage own courses" on public.foundation_courses
for all using (auth.uid() = instructor_id) with check (auth.uid() = instructor_id);
-- Course modules: same as courses (published visible, instructor/admin manage)
drop policy if exists "Published modules readable by all" on public.foundation_course_modules;
create policy "Published modules readable by all" on public.foundation_course_modules
for select using (
is_published = true or
exists(select 1 from public.foundation_courses where id = course_id and instructor_id = auth.uid()) or
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
drop policy if exists "Instructors manage course modules" on public.foundation_course_modules;
create policy "Instructors manage course modules" on public.foundation_course_modules
for all using (exists(select 1 from public.foundation_courses where id = course_id and instructor_id = auth.uid()));
-- Lessons: same pattern
drop policy if exists "Published lessons readable by all" on public.foundation_course_lessons;
create policy "Published lessons readable by all" on public.foundation_course_lessons
for select using (
is_published = true or
exists(select 1 from public.foundation_courses where id = course_id and instructor_id = auth.uid()) or
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
drop policy if exists "Instructors manage course lessons" on public.foundation_course_lessons;
create policy "Instructors manage course lessons" on public.foundation_course_lessons
for all using (exists(select 1 from public.foundation_courses where id = course_id and instructor_id = auth.uid()));
-- Enrollments: users see own, instructors see their course enrollments
drop policy if exists "Users see own enrollments" on public.foundation_enrollments;
create policy "Users see own enrollments" on public.foundation_enrollments
for select using (auth.uid() = user_id or
exists(select 1 from public.foundation_courses where id = course_id and instructor_id = auth.uid()));
drop policy if exists "Users manage own enrollments" on public.foundation_enrollments;
create policy "Users manage own enrollments" on public.foundation_enrollments
for insert with check (auth.uid() = user_id);
drop policy if exists "Users update own enrollments" on public.foundation_enrollments;
create policy "Users update own enrollments" on public.foundation_enrollments
for update using (auth.uid() = user_id);
-- Lesson progress: users see own
drop policy if exists "Users see own lesson progress" on public.foundation_lesson_progress;
create policy "Users see own lesson progress" on public.foundation_lesson_progress
for select using (auth.uid() = user_id);
drop policy if exists "Users update own lesson progress" on public.foundation_lesson_progress;
create policy "Users update own lesson progress" on public.foundation_lesson_progress
for insert with check (auth.uid() = user_id);
drop policy if exists "Users update own lesson completion" on public.foundation_lesson_progress;
create policy "Users update own lesson completion" on public.foundation_lesson_progress
for update using (auth.uid() = user_id);
-- Achievements: all readable, admin/system manages
drop policy if exists "Achievements readable by all" on public.foundation_achievements;
create policy "Achievements readable by all" on public.foundation_achievements
for select using (true);
-- User achievements: users see own, admin manages
drop policy if exists "Users see own achievements" on public.foundation_user_achievements;
create policy "Users see own achievements" on public.foundation_user_achievements
for select using (auth.uid() = user_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
-- Mentors: approved mentors visible, mentors manage own
drop policy if exists "Approved mentors visible to all" on public.foundation_mentors;
create policy "Approved mentors visible to all" on public.foundation_mentors
for select using (approval_status = 'approved' or auth.uid() = user_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
drop policy if exists "Users manage own mentor profile" on public.foundation_mentors;
create policy "Users manage own mentor profile" on public.foundation_mentors
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
-- Mentorship requests: involved parties can see
drop policy if exists "Mentorship requests visible to involved" on public.foundation_mentorship_requests;
create policy "Mentorship requests visible to involved" on public.foundation_mentorship_requests
for select using (auth.uid() = mentor_id or auth.uid() = mentee_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
drop policy if exists "Mentees request mentorship" on public.foundation_mentorship_requests;
create policy "Mentees request mentorship" on public.foundation_mentorship_requests
for insert with check (auth.uid() = mentee_id);
drop policy if exists "Mentors respond to requests" on public.foundation_mentorship_requests;
create policy "Mentors respond to requests" on public.foundation_mentorship_requests
for update using (auth.uid() = mentor_id);
-- Mentorship sessions: involved parties can see/manage
drop policy if exists "Sessions visible to involved" on public.foundation_mentorship_sessions;
create policy "Sessions visible to involved" on public.foundation_mentorship_sessions
for select using (auth.uid() = mentor_id or auth.uid() = mentee_id);
drop policy if exists "Mentorship sessions insert" on public.foundation_mentorship_sessions;
create policy "Mentorship sessions insert" on public.foundation_mentorship_sessions
for insert with check (auth.uid() = mentor_id or auth.uid() = mentee_id);
drop policy if exists "Mentorship sessions update" on public.foundation_mentorship_sessions;
create policy "Mentorship sessions update" on public.foundation_mentorship_sessions
for update using (auth.uid() = mentor_id or auth.uid() = mentee_id);
-- Contributions: users see own, admin sees all
drop policy if exists "Contributions visible to user and admin" on public.foundation_contributions;
create policy "Contributions visible to user and admin" on public.foundation_contributions
for select using (auth.uid() = user_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
drop policy if exists "System logs contributions" on public.foundation_contributions;
create policy "System logs contributions" on public.foundation_contributions
for insert with check (true);
-- ============================================================================
-- TRIGGERS
-- ============================================================================
create or replace function public.set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists foundation_courses_set_updated_at on public.foundation_courses;
create trigger foundation_courses_set_updated_at before update on public.foundation_courses for each row execute function public.set_updated_at();
drop trigger if exists foundation_course_modules_set_updated_at on public.foundation_course_modules;
create trigger foundation_course_modules_set_updated_at before update on public.foundation_course_modules for each row execute function public.set_updated_at();
drop trigger if exists foundation_course_lessons_set_updated_at on public.foundation_course_lessons;
create trigger foundation_course_lessons_set_updated_at before update on public.foundation_course_lessons for each row execute function public.set_updated_at();
drop trigger if exists foundation_enrollments_set_updated_at on public.foundation_enrollments;
create trigger foundation_enrollments_set_updated_at before update on public.foundation_enrollments for each row execute function public.set_updated_at();
drop trigger if exists foundation_mentors_set_updated_at on public.foundation_mentors;
create trigger foundation_mentors_set_updated_at before update on public.foundation_mentors for each row execute function public.set_updated_at();
drop trigger if exists foundation_mentorship_requests_set_updated_at on public.foundation_mentorship_requests;
create trigger foundation_mentorship_requests_set_updated_at before update on public.foundation_mentorship_requests for each row execute function public.set_updated_at();
drop trigger if exists foundation_mentorship_sessions_set_updated_at on public.foundation_mentorship_sessions;
create trigger foundation_mentorship_sessions_set_updated_at before update on public.foundation_mentorship_sessions for each row execute function public.set_updated_at();
-- ============================================================================
-- COMMENTS
-- ============================================================================
comment on table public.foundation_courses is 'Foundation curriculum courses - free, public, educational';
comment on table public.foundation_course_modules is 'Course modules/chapters';
comment on table public.foundation_course_lessons is 'Individual lessons within modules';
comment on table public.foundation_enrollments is 'User course enrollments and progress tracking';
comment on table public.foundation_lesson_progress is 'Granular lesson completion tracking';
comment on table public.foundation_achievements is 'Achievement/badge definitions for community members';
comment on table public.foundation_user_achievements is 'User-earned achievements (many-to-many)';
comment on table public.foundation_mentors is 'Mentor profiles with approval status and expertise';
comment on table public.foundation_mentorship_requests is 'Mentorship requests from mentees to mentors';
comment on table public.foundation_mentorship_sessions is 'Scheduled mentorship sessions between mentor and mentee';
comment on table public.foundation_contributions is 'Community contributions (course creation, mentorship, etc) for gamification';
-- ========================================
-- Next Migration
-- ========================================
-- Nexus: Talent Marketplace
-- Commercial bridge between Foundation (community) and Corp (clients)
-- Includes: Creator Profiles, Opportunities, Applications, Messaging, Payments/Commissions
create extension if not exists "pgcrypto";
-- ============================================================================
-- CREATOR PROFILES & PORTFOLIO
-- ============================================================================
create table if not exists public.nexus_creator_profiles (
user_id uuid primary key references public.user_profiles(id) on delete cascade,
headline text, -- e.g., "Game Developer | Unreal Engine Specialist"
bio text,
profile_image_url text,
skills text[] not null default '{}', -- e.g., ['Unreal Engine', 'C++', 'Game Design']
experience_level text not null default 'intermediate' check (experience_level in ('beginner', 'intermediate', 'advanced', 'expert')),
hourly_rate numeric(10, 2),
portfolio_url text,
availability_status text not null default 'available' check (availability_status in ('available', 'busy', 'unavailable')),
availability_hours_per_week int,
verified boolean not null default false,
total_earnings numeric(12, 2) not null default 0,
rating numeric(3, 2), -- average rating
review_count int not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_creator_profiles_verified_idx on public.nexus_creator_profiles (verified);
create index if not exists nexus_creator_profiles_availability_idx on public.nexus_creator_profiles (availability_status);
create index if not exists nexus_creator_profiles_skills_gin on public.nexus_creator_profiles using gin (skills);
create index if not exists nexus_creator_profiles_rating_idx on public.nexus_creator_profiles (rating desc);
-- Creator Portfolio Projects
create table if not exists public.nexus_portfolio_items (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.user_profiles(id) on delete cascade,
title text not null,
description text,
project_url text,
image_url text,
skills_used text[] not null default '{}',
featured boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_portfolio_items_user_idx on public.nexus_portfolio_items (user_id);
create index if not exists nexus_portfolio_items_featured_idx on public.nexus_portfolio_items (featured);
-- Creator Endorsements (peer-to-peer skill validation)
create table if not exists public.nexus_skill_endorsements (
id uuid primary key default gen_random_uuid(),
creator_id uuid not null references public.user_profiles(id) on delete cascade,
endorsed_by uuid not null references public.user_profiles(id) on delete cascade,
skill text not null,
created_at timestamptz not null default now(),
unique(creator_id, endorsed_by, skill)
);
create index if not exists nexus_skill_endorsements_creator_idx on public.nexus_skill_endorsements (creator_id);
create index if not exists nexus_skill_endorsements_endorsed_by_idx on public.nexus_skill_endorsements (endorsed_by);
-- ============================================================================
-- OPPORTUNITIES (JOBS/COLLABS)
-- ============================================================================
create table if not exists public.nexus_opportunities (
id uuid primary key default gen_random_uuid(),
posted_by uuid not null references public.user_profiles(id) on delete cascade,
title text not null,
description text not null,
category text not null, -- 'development', 'design', 'audio', 'marketing', etc.
required_skills text[] not null default '{}',
budget_type text not null check (budget_type in ('hourly', 'fixed', 'range')),
budget_min numeric(12, 2),
budget_max numeric(12, 2),
timeline_type text not null default 'flexible' check (timeline_type in ('urgent', 'short-term', 'long-term', 'ongoing', 'flexible')),
duration_weeks int,
location_requirement text default 'remote' check (location_requirement in ('remote', 'onsite', 'hybrid')),
required_experience text default 'any' check (required_experience in ('any', 'beginner', 'intermediate', 'advanced', 'expert')),
company_name text,
status text not null default 'open' check (status in ('open', 'in_progress', 'filled', 'closed', 'cancelled')),
application_count int not null default 0,
selected_creator_id uuid references public.user_profiles(id) on delete set null,
views int not null default 0,
is_featured boolean not null default false,
published_at timestamptz not null default now(),
closed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_opportunities_posted_by_idx on public.nexus_opportunities (posted_by);
create index if not exists nexus_opportunities_status_idx on public.nexus_opportunities (status);
create index if not exists nexus_opportunities_category_idx on public.nexus_opportunities (category);
create index if not exists nexus_opportunities_skills_gin on public.nexus_opportunities using gin (required_skills);
create index if not exists nexus_opportunities_featured_idx on public.nexus_opportunities (is_featured);
create index if not exists nexus_opportunities_created_idx on public.nexus_opportunities (created_at desc);
-- ============================================================================
-- APPLICATIONS & MATCHING
-- ============================================================================
create table if not exists public.nexus_applications (
id uuid primary key default gen_random_uuid(),
opportunity_id uuid not null references public.nexus_opportunities(id) on delete cascade,
creator_id uuid not null references public.user_profiles(id) on delete cascade,
status text not null default 'submitted' check (status in ('submitted', 'reviewing', 'accepted', 'rejected', 'hired', 'archived')),
cover_letter text,
proposed_rate numeric(12, 2),
proposal text, -- detailed proposal/pitch
application_questions jsonb, -- answers to custom questions if any
viewed_at timestamptz,
responded_at timestamptz,
response_message text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique(opportunity_id, creator_id)
);
create index if not exists nexus_applications_opportunity_idx on public.nexus_applications (opportunity_id);
create index if not exists nexus_applications_creator_idx on public.nexus_applications (creator_id);
create index if not exists nexus_applications_status_idx on public.nexus_applications (status);
create index if not exists nexus_applications_created_idx on public.nexus_applications (created_at desc);
-- Application Reviews/Ratings
create table if not exists public.nexus_reviews (
id uuid primary key default gen_random_uuid(),
application_id uuid not null references public.nexus_applications(id) on delete cascade,
opportunity_id uuid not null references public.nexus_opportunities(id) on delete cascade,
reviewer_id uuid not null references public.user_profiles(id) on delete cascade,
creator_id uuid not null references public.user_profiles(id) on delete cascade,
rating int not null check (rating between 1 and 5),
review_text text,
created_at timestamptz not null default now(),
unique(application_id, reviewer_id)
);
create index if not exists nexus_reviews_creator_idx on public.nexus_reviews (creator_id);
create index if not exists nexus_reviews_reviewer_idx on public.nexus_reviews (reviewer_id);
-- ============================================================================
-- CONTRACTS & ORDERS
-- ============================================================================
create table if not exists public.nexus_contracts (
id uuid primary key default gen_random_uuid(),
opportunity_id uuid references public.nexus_opportunities(id) on delete set null,
creator_id uuid not null references public.user_profiles(id) on delete cascade,
client_id uuid not null references public.user_profiles(id) on delete cascade,
title text not null,
description text,
contract_type text not null check (contract_type in ('one-time', 'retainer', 'hourly')),
total_amount numeric(12, 2) not null,
aethex_commission_percent numeric(5, 2) not null default 20,
aethex_commission_amount numeric(12, 2) not null default 0,
creator_payout_amount numeric(12, 2) not null default 0,
status text not null default 'pending' check (status in ('pending', 'active', 'completed', 'disputed', 'cancelled')),
start_date timestamptz,
end_date timestamptz,
milestone_count int default 1,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_contracts_creator_idx on public.nexus_contracts (creator_id);
create index if not exists nexus_contracts_client_idx on public.nexus_contracts (client_id);
create index if not exists nexus_contracts_status_idx on public.nexus_contracts (status);
-- Milestones (for progressive payments)
create table if not exists public.nexus_milestones (
id uuid primary key default gen_random_uuid(),
contract_id uuid not null references public.nexus_contracts(id) on delete cascade,
milestone_number int not null,
description text,
amount numeric(12, 2) not null,
due_date timestamptz,
status text not null default 'pending' check (status in ('pending', 'submitted', 'approved', 'paid', 'rejected')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique(contract_id, milestone_number)
);
-- Payments & Commission Tracking
create table if not exists public.nexus_payments (
id uuid primary key default gen_random_uuid(),
contract_id uuid not null references public.nexus_contracts(id) on delete cascade,
milestone_id uuid references public.nexus_milestones(id) on delete set null,
amount numeric(12, 2) not null,
creator_payout numeric(12, 2) not null,
aethex_commission numeric(12, 2) not null,
payment_method text not null default 'stripe', -- stripe, bank_transfer, paypal
payment_status text not null default 'pending' check (payment_status in ('pending', 'processing', 'completed', 'failed', 'refunded')),
payment_date timestamptz,
payout_date timestamptz,
stripe_payment_intent_id text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_payments_contract_idx on public.nexus_payments (contract_id);
create index if not exists nexus_payments_status_idx on public.nexus_payments (payment_status);
-- Commission Ledger (financial tracking)
create table if not exists public.nexus_commission_ledger (
id uuid primary key default gen_random_uuid(),
payment_id uuid references public.nexus_payments(id) on delete set null,
period_start date,
period_end date,
total_volume numeric(12, 2) not null,
total_commission numeric(12, 2) not null,
creator_payouts numeric(12, 2) not null,
aethex_revenue numeric(12, 2) not null,
status text not null default 'pending' check (status in ('pending', 'settled', 'disputed')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- ============================================================================
-- MESSAGING & COLLABORATION
-- ============================================================================
create table if not exists public.nexus_messages (
id uuid primary key default gen_random_uuid(),
conversation_id uuid,
sender_id uuid not null references public.user_profiles(id) on delete cascade,
recipient_id uuid not null references public.user_profiles(id) on delete cascade,
opportunity_id uuid references public.nexus_opportunities(id) on delete set null,
contract_id uuid references public.nexus_contracts(id) on delete set null,
message_text text not null,
is_read boolean not null default false,
read_at timestamptz,
created_at timestamptz not null default now()
);
create index if not exists nexus_messages_sender_idx on public.nexus_messages (sender_id);
create index if not exists nexus_messages_recipient_idx on public.nexus_messages (recipient_id);
create index if not exists nexus_messages_opportunity_idx on public.nexus_messages (opportunity_id);
create index if not exists nexus_messages_created_idx on public.nexus_messages (created_at desc);
-- Conversations (threads)
create table if not exists public.nexus_conversations (
id uuid primary key default gen_random_uuid(),
participant_1 uuid not null references public.user_profiles(id) on delete cascade,
participant_2 uuid not null references public.user_profiles(id) on delete cascade,
opportunity_id uuid references public.nexus_opportunities(id) on delete set null,
contract_id uuid references public.nexus_contracts(id) on delete set null,
subject text,
last_message_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique(participant_1, participant_2, opportunity_id)
);
create index if not exists nexus_conversations_participants_idx on public.nexus_conversations (participant_1, participant_2);
-- ============================================================================
-- DISPUTES & RESOLUTION
-- ============================================================================
create table if not exists public.nexus_disputes (
id uuid primary key default gen_random_uuid(),
contract_id uuid not null references public.nexus_contracts(id) on delete cascade,
reported_by uuid not null references public.user_profiles(id) on delete cascade,
reason text not null,
description text,
evidence_urls text[] default '{}',
status text not null default 'open' check (status in ('open', 'reviewing', 'resolved', 'escalated')),
resolution_notes text,
resolved_by uuid references public.user_profiles(id) on delete set null,
resolved_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_disputes_contract_idx on public.nexus_disputes (contract_id);
create index if not exists nexus_disputes_status_idx on public.nexus_disputes (status);
-- ============================================================================
-- RLS POLICIES
-- ============================================================================
alter table public.nexus_creator_profiles enable row level security;
alter table public.nexus_portfolio_items enable row level security;
alter table public.nexus_skill_endorsements enable row level security;
alter table public.nexus_opportunities enable row level security;
alter table public.nexus_applications enable row level security;
alter table public.nexus_reviews enable row level security;
alter table public.nexus_contracts enable row level security;
alter table public.nexus_milestones enable row level security;
alter table public.nexus_payments enable row level security;
alter table public.nexus_commission_ledger enable row level security;
alter table public.nexus_messages enable row level security;
alter table public.nexus_conversations enable row level security;
alter table public.nexus_disputes enable row level security;
-- Creator Profiles: verified visible, own editable
drop policy if exists "Verified creator profiles visible to all" on public.nexus_creator_profiles;
create policy "Verified creator profiles visible to all" on public.nexus_creator_profiles
for select using (verified = true or auth.uid() = user_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
drop policy if exists "Users manage own creator profile" on public.nexus_creator_profiles;
create policy "Users manage own creator profile" on public.nexus_creator_profiles
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
-- Portfolio: public for verified creators
drop policy if exists "Portfolio items visible when creator verified" on public.nexus_portfolio_items;
create policy "Portfolio items visible when creator verified" on public.nexus_portfolio_items
for select using (
exists(select 1 from public.nexus_creator_profiles where user_id = user_id and verified = true) or
auth.uid() = user_id
);
drop policy if exists "Users manage own portfolio" on public.nexus_portfolio_items;
create policy "Users manage own portfolio" on public.nexus_portfolio_items
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
-- Endorsements: all visible
drop policy if exists "Endorsements readable by all authenticated" on public.nexus_skill_endorsements;
create policy "Endorsements readable by all authenticated" on public.nexus_skill_endorsements
for select using (auth.role() = 'authenticated');
drop policy if exists "Users can endorse skills" on public.nexus_skill_endorsements;
create policy "Users can endorse skills" on public.nexus_skill_endorsements
for insert with check (auth.uid() = endorsed_by);
-- Opportunities: open ones visible, own/applied visible to creator
drop policy if exists "Open opportunities visible to all" on public.nexus_opportunities;
create policy "Open opportunities visible to all" on public.nexus_opportunities
for select using (status = 'open' or auth.uid() = posted_by or
exists(select 1 from public.nexus_applications where opportunity_id = id and creator_id = auth.uid()));
drop policy if exists "Clients post opportunities" on public.nexus_opportunities;
create policy "Clients post opportunities" on public.nexus_opportunities
for insert with check (auth.uid() = posted_by);
drop policy if exists "Clients manage own opportunities" on public.nexus_opportunities;
create policy "Clients manage own opportunities" on public.nexus_opportunities
for update using (auth.uid() = posted_by);
-- Applications: involved parties see
drop policy if exists "Applications visible to applicant and poster" on public.nexus_applications;
create policy "Applications visible to applicant and poster" on public.nexus_applications
for select using (auth.uid() = creator_id or auth.uid() in (select posted_by from public.nexus_opportunities where id = opportunity_id));
drop policy if exists "Creators submit applications" on public.nexus_applications;
create policy "Creators submit applications" on public.nexus_applications
for insert with check (auth.uid() = creator_id);
drop policy if exists "Applicants/posters update applications" on public.nexus_applications;
create policy "Applicants/posters update applications" on public.nexus_applications
for update using (auth.uid() = creator_id or auth.uid() in (select posted_by from public.nexus_opportunities where id = opportunity_id));
-- Reviews: visible to parties, admin
drop policy if exists "Reviews visible to involved" on public.nexus_reviews;
create policy "Reviews visible to involved" on public.nexus_reviews
for select using (auth.uid() = creator_id or auth.uid() = reviewer_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
-- Contracts: involved parties only
drop policy if exists "Contracts visible to parties" on public.nexus_contracts;
create policy "Contracts visible to parties" on public.nexus_contracts
for select using (auth.uid() = creator_id or auth.uid() = client_id or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
-- Messages: involved parties
drop policy if exists "Messages visible to parties" on public.nexus_messages;
create policy "Messages visible to parties" on public.nexus_messages
for select using (auth.uid() = sender_id or auth.uid() = recipient_id);
drop policy if exists "Users send messages" on public.nexus_messages;
create policy "Users send messages" on public.nexus_messages
for insert with check (auth.uid() = sender_id);
-- Conversations: participants
drop policy if exists "Conversations visible to participants" on public.nexus_conversations;
create policy "Conversations visible to participants" on public.nexus_conversations
for select using (auth.uid() in (participant_1, participant_2));
-- Disputes: involved parties
drop policy if exists "Disputes visible to involved" on public.nexus_disputes;
create policy "Disputes visible to involved" on public.nexus_disputes
for select using (auth.uid() in (select creator_id from public.nexus_contracts where id = contract_id union select client_id from public.nexus_contracts where id = contract_id) or exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
-- ============================================================================
-- TRIGGERS
-- ============================================================================
create or replace function public.set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists nexus_creator_profiles_set_updated_at on public.nexus_creator_profiles;
create trigger nexus_creator_profiles_set_updated_at before update on public.nexus_creator_profiles for each row execute function public.set_updated_at();
drop trigger if exists nexus_portfolio_items_set_updated_at on public.nexus_portfolio_items;
create trigger nexus_portfolio_items_set_updated_at before update on public.nexus_portfolio_items for each row execute function public.set_updated_at();
drop trigger if exists nexus_opportunities_set_updated_at on public.nexus_opportunities;
create trigger nexus_opportunities_set_updated_at before update on public.nexus_opportunities for each row execute function public.set_updated_at();
drop trigger if exists nexus_applications_set_updated_at on public.nexus_applications;
create trigger nexus_applications_set_updated_at before update on public.nexus_applications for each row execute function public.set_updated_at();
drop trigger if exists nexus_contracts_set_updated_at on public.nexus_contracts;
create trigger nexus_contracts_set_updated_at before update on public.nexus_contracts for each row execute function public.set_updated_at();
drop trigger if exists nexus_milestones_set_updated_at on public.nexus_milestones;
create trigger nexus_milestones_set_updated_at before update on public.nexus_milestones for each row execute function public.set_updated_at();
drop trigger if exists nexus_payments_set_updated_at on public.nexus_payments;
create trigger nexus_payments_set_updated_at before update on public.nexus_payments for each row execute function public.set_updated_at();
drop trigger if exists nexus_commission_ledger_set_updated_at on public.nexus_commission_ledger;
create trigger nexus_commission_ledger_set_updated_at before update on public.nexus_commission_ledger for each row execute function public.set_updated_at();
drop trigger if exists nexus_conversations_set_updated_at on public.nexus_conversations;
create trigger nexus_conversations_set_updated_at before update on public.nexus_conversations for each row execute function public.set_updated_at();
drop trigger if exists nexus_disputes_set_updated_at on public.nexus_disputes;
create trigger nexus_disputes_set_updated_at before update on public.nexus_disputes for each row execute function public.set_updated_at();
-- ============================================================================
-- COMMENTS
-- ============================================================================
comment on table public.nexus_creator_profiles is 'Creator profiles with skills, rates, portfolio';
comment on table public.nexus_portfolio_items is 'Creator portfolio/project showcase';
comment on table public.nexus_skill_endorsements is 'Peer endorsements for skill validation';
comment on table public.nexus_opportunities is 'Job/collaboration postings by clients';
comment on table public.nexus_applications is 'Creator applications to opportunities';
comment on table public.nexus_reviews is 'Reviews/ratings for completed work';
comment on table public.nexus_contracts is 'Signed contracts with AeThex commission split';
comment on table public.nexus_milestones is 'Contract milestones for progressive payments';
comment on table public.nexus_payments is 'Payment transactions and commission tracking';
comment on table public.nexus_commission_ledger is 'Financial ledger for AeThex revenue tracking';
comment on table public.nexus_messages is 'Marketplace messaging between parties';
comment on table public.nexus_conversations is 'Message conversation threads';
comment on table public.nexus_disputes is 'Dispute resolution for contracts';
-- ========================================
-- Next Migration
-- ========================================
-- Add Spotify portfolio URL to aethex_creators
-- This field allows ALL creators (regardless of type) to link their Spotify profile
-- for social proof and portfolio display on their public profiles (/passport/:username, /creators/:username)
-- V1: Simple URL field. V2: Will integrate Spotify API for metadata/embed
ALTER TABLE public.aethex_creators
ADD COLUMN IF NOT EXISTS spotify_profile_url text;
-- Add comment for documentation
COMMENT ON COLUMN public.aethex_creators.spotify_profile_url IS 'Spotify artist profile URL for universal portfolio/social proof. Supports all creator types. V1: URL link only. V2: Will support web player embed.';
-- ========================================
-- Next Migration
-- ========================================
-- Add rating column to ethos_tracks table
-- Allows artists and users to rate tracks on a scale of 1-5
ALTER TABLE public.ethos_tracks
ADD COLUMN IF NOT EXISTS rating numeric(2, 1) DEFAULT 5.0;
-- Add price column for commercial tracks
ALTER TABLE public.ethos_tracks
ADD COLUMN IF NOT EXISTS price numeric(10, 2);
-- Create index on rating for efficient sorting
CREATE INDEX IF NOT EXISTS idx_ethos_tracks_rating ON public.ethos_tracks(rating DESC);
-- Add comment
COMMENT ON COLUMN public.ethos_tracks.rating IS 'Track rating from 1.0 to 5.0 based on user reviews. Defaults to 5.0 for new tracks.';
COMMENT ON COLUMN public.ethos_tracks.price IS 'Price in USD for commercial licensing of the track. NULL if not for sale.';
-- ========================================
-- Next Migration
-- ========================================
-- Expand user_profiles table with additional profile fields
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS twitter_url TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS linkedin_url TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS github_url TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS portfolio_url TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS youtube_url TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS twitch_url TEXT;
-- Skills and expertise
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS skills_detailed JSONB DEFAULT '[]'::jsonb;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS experience_level TEXT DEFAULT 'intermediate'::text;
-- Professional information
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS bio_detailed TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS hourly_rate DECIMAL(10, 2);
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS availability_status TEXT DEFAULT 'available'::text;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS timezone TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS location TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS languages JSONB DEFAULT '[]'::jsonb;
-- Arm affiliations (which arms user is part of)
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS arm_affiliations JSONB DEFAULT '[]'::jsonb;
-- Work experience
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS work_experience JSONB DEFAULT '[]'::jsonb;
-- Verification badges
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS verified_badges JSONB DEFAULT '[]'::jsonb;
-- Nexus profile data
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS nexus_profile_complete BOOLEAN DEFAULT false;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS nexus_headline TEXT;
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS nexus_categories JSONB DEFAULT '[]'::jsonb;
-- Portfolio items
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS portfolio_items JSONB DEFAULT '[]'::jsonb;
-- Create indexes for searchability
CREATE INDEX IF NOT EXISTS idx_user_profiles_skills ON user_profiles USING GIN(skills_detailed);
CREATE INDEX IF NOT EXISTS idx_user_profiles_arms ON user_profiles USING GIN(arm_affiliations);
CREATE INDEX IF NOT EXISTS idx_user_profiles_availability ON user_profiles(availability_status);
CREATE INDEX IF NOT EXISTS idx_user_profiles_nexus_complete ON user_profiles(nexus_profile_complete);
-- Create arm_affiliations table for tracking which activities count toward each arm
CREATE TABLE IF NOT EXISTS user_arm_affiliations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES user_profiles(id) ON DELETE CASCADE,
arm TEXT NOT NULL CHECK (arm IN ('foundation', 'gameforge', 'labs', 'corp', 'devlink')),
affiliation_type TEXT NOT NULL CHECK (affiliation_type IN ('courses', 'projects', 'research', 'opportunities', 'manual')),
affiliation_data JSONB DEFAULT '{}'::jsonb,
confirmed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT now(),
UNIQUE(user_id, arm, affiliation_type)
);
CREATE INDEX IF NOT EXISTS idx_user_arm_affiliations_user ON user_arm_affiliations(user_id);
CREATE INDEX IF NOT EXISTS idx_user_arm_affiliations_arm ON user_arm_affiliations(arm);
CREATE INDEX IF NOT EXISTS idx_user_arm_affiliations_confirmed ON user_arm_affiliations(confirmed);
-- Enable RLS
ALTER TABLE user_arm_affiliations ENABLE ROW LEVEL SECURITY;
-- RLS policies for user_arm_affiliations
DROP POLICY IF EXISTS "users_can_view_own_affiliations" ON user_arm_affiliations;
CREATE POLICY "users_can_view_own_affiliations" ON user_arm_affiliations
FOR SELECT USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "users_can_manage_own_affiliations" ON user_arm_affiliations;
CREATE POLICY "users_can_manage_own_affiliations" ON user_arm_affiliations
FOR INSERT WITH CHECK (auth.uid() = user_id);
DROP POLICY IF EXISTS "users_can_update_own_affiliations" ON user_arm_affiliations;
CREATE POLICY "users_can_update_own_affiliations" ON user_arm_affiliations
FOR UPDATE USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "authenticated_can_view_public_affiliations" ON user_arm_affiliations;
CREATE POLICY "authenticated_can_view_public_affiliations" ON user_arm_affiliations
FOR SELECT TO authenticated USING (confirmed = true);
-- ========================================
-- Next Migration
-- ========================================
-- CORP: Enterprise Client Portal Schema
-- For invoicing, contracts, team management, and reporting
-- ============================================================================
-- INVOICES & BILLING
-- ============================================================================
create table if not exists public.corp_invoices (
id uuid primary key default gen_random_uuid(),
client_company_id uuid not null references public.user_profiles(id) on delete cascade,
invoice_number text not null unique,
project_id uuid,
description text,
issue_date date not null default now(),
due_date date not null,
amount_due numeric(12, 2) not null,
amount_paid numeric(12, 2) not null default 0,
status text not null default 'draft' check (status in ('draft', 'sent', 'viewed', 'paid', 'overdue', 'cancelled')),
currency text not null default 'USD',
notes text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists corp_invoices_client_idx on public.corp_invoices (client_company_id);
create index if not exists corp_invoices_status_idx on public.corp_invoices (status);
create index if not exists corp_invoices_due_date_idx on public.corp_invoices (due_date);
create index if not exists corp_invoices_number_idx on public.corp_invoices (invoice_number);
-- Invoice Line Items
create table if not exists public.corp_invoice_items (
id uuid primary key default gen_random_uuid(),
invoice_id uuid not null references public.corp_invoices(id) on delete cascade,
description text not null,
quantity numeric(10, 2) not null default 1,
unit_price numeric(12, 2) not null,
amount numeric(12, 2) not null,
category text, -- 'service', 'product', 'license', etc.
created_at timestamptz not null default now()
);
create index if not exists corp_invoice_items_invoice_idx on public.corp_invoice_items (invoice_id);
-- Payments received on invoices
create table if not exists public.corp_invoice_payments (
id uuid primary key default gen_random_uuid(),
invoice_id uuid not null references public.corp_invoices(id) on delete cascade,
amount_paid numeric(12, 2) not null,
payment_date date not null default now(),
payment_method text not null default 'bank_transfer', -- 'stripe', 'bank_transfer', 'check', etc.
reference_number text,
notes text,
created_at timestamptz not null default now()
);
create index if not exists corp_invoice_payments_invoice_idx on public.corp_invoice_payments (invoice_id);
-- ============================================================================
-- CONTRACTS & AGREEMENTS
-- ============================================================================
create table if not exists public.corp_contracts (
id uuid primary key default gen_random_uuid(),
client_company_id uuid not null references public.user_profiles(id) on delete cascade,
vendor_id uuid not null references public.user_profiles(id) on delete cascade,
contract_name text not null,
contract_type text not null check (contract_type in ('service', 'retainer', 'license', 'nda', 'other')),
description text,
start_date date,
end_date date,
contract_value numeric(12, 2),
status text not null default 'draft' check (status in ('draft', 'pending_approval', 'signed', 'active', 'completed', 'terminated', 'archived')),
document_url text, -- URL to signed document
signed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists corp_contracts_client_idx on public.corp_contracts (client_company_id);
create index if not exists corp_contracts_vendor_idx on public.corp_contracts (vendor_id);
create index if not exists corp_contracts_status_idx on public.corp_contracts (status);
-- Contract Milestones
create table if not exists public.corp_contract_milestones (
id uuid primary key default gen_random_uuid(),
contract_id uuid not null references public.corp_contracts(id) on delete cascade,
milestone_name text not null,
description text,
due_date date,
deliverables text,
status text not null default 'pending' check (status in ('pending', 'in_progress', 'completed', 'blocked')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists corp_contract_milestones_contract_idx on public.corp_contract_milestones (contract_id);
-- ============================================================================
-- TEAM COLLABORATION
-- ============================================================================
create table if not exists public.corp_team_members (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references public.user_profiles(id) on delete cascade,
user_id uuid not null references public.user_profiles(id) on delete cascade,
role text not null check (role in ('owner', 'admin', 'member', 'viewer')),
email text not null,
full_name text,
job_title text,
status text not null default 'active' check (status in ('active', 'inactive', 'pending_invite')),
invited_at timestamptz,
joined_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique(company_id, user_id)
);
create index if not exists corp_team_members_company_idx on public.corp_team_members (company_id);
create index if not exists corp_team_members_user_idx on public.corp_team_members (user_id);
-- Activity Log (for audit trail)
create table if not exists public.corp_activity_log (
id uuid primary key default gen_random_uuid(),
company_id uuid not null references public.user_profiles(id) on delete cascade,
actor_id uuid not null references public.user_profiles(id) on delete cascade,
action text not null, -- 'created_invoice', 'sent_contract', 'paid_invoice', etc.
resource_type text, -- 'invoice', 'contract', 'team_member'
resource_id uuid,
metadata jsonb,
ip_address text,
user_agent text,
created_at timestamptz not null default now()
);
create index if not exists corp_activity_log_company_idx on public.corp_activity_log (company_id);
create index if not exists corp_activity_log_actor_idx on public.corp_activity_log (actor_id);
create index if not exists corp_activity_log_created_idx on public.corp_activity_log (created_at desc);
-- ============================================================================
-- PROJECTS & TRACKING
-- ============================================================================
create table if not exists public.corp_projects (
id uuid primary key default gen_random_uuid(),
client_company_id uuid not null references public.user_profiles(id) on delete cascade,
project_name text not null,
description text,
status text not null default 'active' check (status in ('planning', 'active', 'paused', 'completed', 'archived')),
budget numeric(12, 2),
spent numeric(12, 2) default 0,
start_date date,
end_date date,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists corp_projects_client_idx on public.corp_projects (client_company_id);
create index if not exists corp_projects_status_idx on public.corp_projects (status);
-- ============================================================================
-- ANALYTICS & REPORTING
-- ============================================================================
create table if not exists public.corp_financial_summary (
id uuid primary key default gen_random_uuid(),
company_id uuid not null unique references public.user_profiles(id) on delete cascade,
period_start date not null,
period_end date not null,
total_invoiced numeric(12, 2) default 0,
total_paid numeric(12, 2) default 0,
total_overdue numeric(12, 2) default 0,
active_contracts int default 0,
completed_contracts int default 0,
total_contract_value numeric(12, 2) default 0,
average_payment_days int,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists corp_financial_summary_company_idx on public.corp_financial_summary (company_id);
-- ============================================================================
-- RLS POLICIES
-- ============================================================================
alter table public.corp_invoices enable row level security;
alter table public.corp_invoice_items enable row level security;
alter table public.corp_invoice_payments enable row level security;
alter table public.corp_contracts enable row level security;
alter table public.corp_contract_milestones enable row level security;
alter table public.corp_team_members enable row level security;
alter table public.corp_activity_log enable row level security;
alter table public.corp_projects enable row level security;
alter table public.corp_financial_summary enable row level security;
-- Invoices: client and team members can view
drop policy if exists "Invoices visible to client and team" on public.corp_invoices;
create policy "Invoices visible to client and team" on public.corp_invoices
for select using (
auth.uid() = client_company_id or
exists(select 1 from public.corp_team_members where company_id = client_company_id and user_id = auth.uid())
);
drop policy if exists "Client creates invoices" on public.corp_invoices;
create policy "Client creates invoices" on public.corp_invoices
for insert with check (auth.uid() = client_company_id);
drop policy if exists "Client manages invoices" on public.corp_invoices;
create policy "Client manages invoices" on public.corp_invoices
for update using (auth.uid() = client_company_id);
-- Contracts: parties involved can view
drop policy if exists "Contracts visible to involved parties" on public.corp_contracts;
create policy "Contracts visible to involved parties" on public.corp_contracts
for select using (
auth.uid() = client_company_id or auth.uid() = vendor_id or
exists(select 1 from public.corp_team_members where company_id = client_company_id and user_id = auth.uid())
);
-- Team: company members can view
drop policy if exists "Team members visible to company" on public.corp_team_members;
create policy "Team members visible to company" on public.corp_team_members
for select using (
auth.uid() = company_id or
exists(select 1 from public.corp_team_members where company_id = company_id and user_id = auth.uid())
);
-- Activity: company members can view
drop policy if exists "Activity visible to company" on public.corp_activity_log;
create policy "Activity visible to company" on public.corp_activity_log
for select using (
exists(select 1 from public.corp_team_members where company_id = company_id and user_id = auth.uid())
);
-- ============================================================================
-- TRIGGERS
-- ============================================================================
create or replace function public.set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists corp_invoices_set_updated_at on public.corp_invoices;
create trigger corp_invoices_set_updated_at before update on public.corp_invoices for each row execute function public.set_updated_at();
drop trigger if exists corp_contracts_set_updated_at on public.corp_contracts;
create trigger corp_contracts_set_updated_at before update on public.corp_contracts for each row execute function public.set_updated_at();
drop trigger if exists corp_contract_milestones_set_updated_at on public.corp_contract_milestones;
create trigger corp_contract_milestones_set_updated_at before update on public.corp_contract_milestones for each row execute function public.set_updated_at();
drop trigger if exists corp_team_members_set_updated_at on public.corp_team_members;
create trigger corp_team_members_set_updated_at before update on public.corp_team_members for each row execute function public.set_updated_at();
drop trigger if exists corp_projects_set_updated_at on public.corp_projects;
create trigger corp_projects_set_updated_at before update on public.corp_projects for each row execute function public.set_updated_at();
drop trigger if exists corp_financial_summary_set_updated_at on public.corp_financial_summary;
create trigger corp_financial_summary_set_updated_at before update on public.corp_financial_summary for each row execute function public.set_updated_at();
-- ============================================================================
-- COMMENTS
-- ============================================================================
comment on table public.corp_invoices is 'Invoices issued by the company to clients';
comment on table public.corp_invoice_items is 'Line items on invoices';
comment on table public.corp_invoice_payments is 'Payments received on invoices';
comment on table public.corp_contracts is 'Contracts with vendors and clients';
comment on table public.corp_team_members is 'Team members with access to the hub';
comment on table public.corp_activity_log is 'Audit trail of all activities';
comment on table public.corp_projects is 'Client projects for tracking work';
comment on table public.corp_financial_summary is 'Financial summary and metrics';
-- ========================================
-- Next Migration
-- ========================================
-- Add onboarded column to track if user has completed onboarding
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS onboarded BOOLEAN DEFAULT false;
-- Create index for filtering onboarded users
CREATE INDEX IF NOT EXISTS idx_user_profiles_onboarded ON user_profiles(onboarded);
-- ========================================
-- Next Migration
-- ========================================
-- Add Stripe payment fields to nexus_contracts
ALTER TABLE public.nexus_contracts ADD COLUMN IF NOT EXISTS stripe_payment_intent_id text;
-- Add index for quick lookup
CREATE INDEX IF NOT EXISTS nexus_contracts_stripe_payment_intent_idx ON public.nexus_contracts (stripe_payment_intent_id);
-- Add Stripe charge fields to nexus_payments
ALTER TABLE public.nexus_payments ADD COLUMN IF NOT EXISTS stripe_charge_id text;
-- Add index for quick lookup
CREATE INDEX IF NOT EXISTS nexus_payments_stripe_charge_idx ON public.nexus_payments (stripe_charge_id);
-- Add Stripe Connect fields to nexus_creator_profiles
ALTER TABLE public.nexus_creator_profiles ADD COLUMN IF NOT EXISTS stripe_connect_account_id text;
ALTER TABLE public.nexus_creator_profiles ADD COLUMN IF NOT EXISTS stripe_account_verified boolean default false;
-- Add indexes
CREATE INDEX IF NOT EXISTS nexus_creator_profiles_stripe_account_idx ON public.nexus_creator_profiles (stripe_connect_account_id);
-- Add comments
COMMENT ON COLUMN public.nexus_contracts.stripe_payment_intent_id IS 'Stripe PaymentIntent ID for tracking contract payments';
COMMENT ON COLUMN public.nexus_payments.stripe_charge_id IS 'Stripe Charge ID for refund tracking';
COMMENT ON COLUMN public.nexus_creator_profiles.stripe_connect_account_id IS 'Stripe Connect Express account ID for creator payouts';
COMMENT ON COLUMN public.nexus_creator_profiles.stripe_account_verified IS 'Whether Stripe Connect account is verified and ready for payouts';
-- ========================================
-- Next Migration
-- ========================================
-- Add 'staff' value to user_type_enum if not exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname = 'user_type_enum' AND e.enumlabel = 'staff'
) THEN
ALTER TYPE user_type_enum ADD VALUE 'staff';
END IF;
END$$;
-- ========================================
-- Next Migration
-- ========================================
-- Community post likes and comments
begin;
-- likes table for community_posts
create table if not exists public.community_post_likes (
post_id uuid not null references public.community_posts(id) on delete cascade,
user_id uuid not null references public.user_profiles(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (post_id, user_id)
);
-- comments table for community_posts
create table if not exists public.community_comments (
id uuid primary key default gen_random_uuid(),
post_id uuid not null references public.community_posts(id) on delete cascade,
user_id uuid not null references public.user_profiles(id) on delete cascade,
content text not null,
created_at timestamptz not null default now()
);
alter table public.community_post_likes enable row level security;
alter table public.community_comments enable row level security;
-- policies: users can read all published post likes/comments
DO $$ BEGIN
CREATE POLICY community_post_likes_read ON public.community_post_likes
FOR SELECT TO authenticated USING (true);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE POLICY community_comments_read ON public.community_comments
FOR SELECT TO authenticated USING (true);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- users manage their own likes/comments
DO $$ BEGIN
CREATE POLICY community_post_likes_manage_self ON public.community_post_likes
FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE POLICY community_comments_manage_self ON public.community_comments
FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
commit;
-- ========================================
-- Next Migration
-- ========================================
create extension if not exists pgcrypto;
-- Team memberships (avoids conflict with existing team_members table)
create table if not exists public.team_memberships (
team_id uuid not null references public.teams(id) on delete cascade,
user_id uuid not null references public.user_profiles(id) on delete cascade,
role text not null default 'member',
status text not null default 'active',
created_at timestamptz not null default now(),
primary key (team_id, user_id)
);
alter table public.team_memberships enable row level security;
do $$ begin
create policy team_memberships_read on public.team_memberships for select to authenticated using (user_id = auth.uid() or exists(select 1 from public.team_memberships m where m.team_id = team_id and m.user_id = auth.uid()));
exception when duplicate_object then null; end $$;
do $$ begin
create policy team_memberships_manage_self on public.team_memberships for all to authenticated using (user_id = auth.uid());
exception when duplicate_object then null; end $$;
-- Update teams policy to use team_memberships
do $$ begin
create policy teams_read_membership on public.teams for select to authenticated using (visibility = 'public' or owner_id = auth.uid() or exists(select 1 from public.team_memberships m where m.team_id = id and m.user_id = auth.uid()));
exception when duplicate_object then null; end $$;
-- ========================================
-- Next Migration
-- ========================================
-- Fix RLS recursion on team_memberships and define safe, non-recursive policies
begin;
-- Ensure RLS is enabled
alter table public.team_memberships enable row level security;
-- Drop problematic/overly broad policies if they exist
drop policy if exists team_memberships_read on public.team_memberships;
drop policy if exists team_memberships_manage_self on public.team_memberships;
-- Allow users to read only their own membership rows
drop policy if exists team_memberships_select_own on public.team_memberships;
create policy team_memberships_select_own on public.team_memberships
for select
to authenticated
using (user_id = auth.uid());
-- Allow users to create membership rows only for themselves
drop policy if exists team_memberships_insert_self on public.team_memberships;
create policy team_memberships_insert_self on public.team_memberships
for insert
to authenticated
with check (user_id = auth.uid());
-- Allow users to update only their own membership rows
drop policy if exists team_memberships_update_self on public.team_memberships;
create policy team_memberships_update_self on public.team_memberships
for update
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
-- Allow users to delete only their own membership rows
drop policy if exists team_memberships_delete_self on public.team_memberships;
create policy team_memberships_delete_self on public.team_memberships
for delete
to authenticated
using (user_id = auth.uid());
-- Drop legacy teams_read policy that referenced public.team_members (recursive)
drop policy if exists teams_read on public.teams;
commit;
-- ========================================
-- Next Migration
-- ========================================
create extension if not exists "pgcrypto";
-- Mentors registry
create table if not exists public.mentors (
user_id uuid primary key references public.user_profiles(id) on delete cascade,
bio text,
expertise text[] not null default '{}',
available boolean not null default true,
hourly_rate numeric(10,2),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists mentors_available_idx on public.mentors (available);
create index if not exists mentors_expertise_gin on public.mentors using gin (expertise);
-- Mentorship requests
create table if not exists public.mentorship_requests (
id uuid primary key default gen_random_uuid(),
mentor_id uuid not null references public.user_profiles(id) on delete cascade,
mentee_id uuid not null references public.user_profiles(id) on delete cascade,
message text,
status text not null default 'pending' check (status in ('pending','accepted','rejected','cancelled')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists mentorship_requests_mentor_idx on public.mentorship_requests (mentor_id);
create index if not exists mentorship_requests_mentee_idx on public.mentorship_requests (mentee_id);
create index if not exists mentorship_requests_status_idx on public.mentorship_requests (status);
-- Prevent duplicate pending requests between same pair
create unique index if not exists mentorship_requests_unique_pending on public.mentorship_requests (mentor_id, mentee_id) where status = 'pending';
-- Simple trigger to maintain updated_at
create or replace function public.set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
drop trigger if exists mentors_set_updated_at on public.mentors;
create trigger mentors_set_updated_at
before update on public.mentors
for each row execute function public.set_updated_at();
drop trigger if exists mentorship_requests_set_updated_at on public.mentorship_requests;
create trigger mentorship_requests_set_updated_at
before update on public.mentorship_requests
for each row execute function public.set_updated_at();
-- ========================================
-- Next Migration
-- ========================================
-- Social + Invites + Reputation/Loyalty/XP schema additions
-- Add missing columns to user_profiles
ALTER TABLE IF EXISTS public.user_profiles
ADD COLUMN IF NOT EXISTS loyalty_points integer DEFAULT 0,
ADD COLUMN IF NOT EXISTS reputation_score integer DEFAULT 0;
-- Invites table
CREATE TABLE IF NOT EXISTS public.invites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inviter_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
invitee_email text NOT NULL,
token text UNIQUE NOT NULL,
status text NOT NULL DEFAULT 'pending',
accepted_by uuid NULL REFERENCES auth.users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
accepted_at timestamptz NULL,
message text NULL
);
-- Connections (undirected; store both directions for simpler queries)
CREATE TABLE IF NOT EXISTS public.user_connections (
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
connection_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, connection_id)
);
-- Endorsements (skill-based reputation signals)
CREATE TABLE IF NOT EXISTS public.endorsements (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
endorser_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
endorsed_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
skill text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Reward event ledger (audit for xp/loyalty/reputation changes)
CREATE TABLE IF NOT EXISTS public.reward_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
type text NOT NULL, -- e.g. 'invite_sent', 'invite_accepted', 'post_created'
points_kind text NOT NULL DEFAULT 'xp', -- 'xp' | 'loyalty' | 'reputation'
amount integer NOT NULL DEFAULT 0,
metadata jsonb NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- RLS (service role bypasses; keep strict by default)
ALTER TABLE public.invites ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_connections ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.endorsements ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.reward_events ENABLE ROW LEVEL SECURITY;
-- Minimal readable policies for authenticated users (optional reads)
DO $$ BEGIN
CREATE POLICY invites_read_own ON public.invites
FOR SELECT TO authenticated
USING (inviter_id = auth.uid() OR accepted_by = auth.uid());
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE POLICY connections_read_own ON public.user_connections
FOR SELECT TO authenticated
USING (user_id = auth.uid() OR connection_id = auth.uid());
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE POLICY endorsements_read_public ON public.endorsements
FOR SELECT TO authenticated USING (true);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE POLICY reward_events_read_own ON public.reward_events
FOR SELECT TO authenticated
USING (user_id = auth.uid());
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ========================================
-- Next Migration
-- ========================================
-- Storage policies for post_media uploads
begin;
-- Ensure RLS is enabled on storage.objects (wrapped for permissions)
DO $$ BEGIN
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
EXCEPTION WHEN OTHERS THEN NULL; END $$;
-- Allow public read for objects in post_media bucket (because bucket is public)
DO $$ BEGIN
DROP POLICY IF EXISTS post_media_public_read ON storage.objects;
CREATE POLICY post_media_public_read ON storage.objects
FOR SELECT TO public
USING (bucket_id = 'post_media');
EXCEPTION WHEN OTHERS THEN NULL; END $$;
-- Allow authenticated users to upload to post_media bucket
DO $$ BEGIN
DROP POLICY IF EXISTS post_media_auth_insert ON storage.objects;
CREATE POLICY post_media_auth_insert ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'post_media');
EXCEPTION WHEN OTHERS THEN NULL; END $$;
commit;
-- ========================================
-- Next Migration
-- ========================================
-- NEXUS Core: Universal Data Layer
-- Single Source of Truth for talent/contract metadata
-- Supports AZ Tax Commission reporting, time logs, and compliance tracking
create extension if not exists "pgcrypto";
-- ============================================================================
-- TALENT PROFILES (Legal/Tax Layer)
-- ============================================================================
create table if not exists public.nexus_talent_profiles (
id uuid primary key default gen_random_uuid(),
user_id uuid not null unique references public.user_profiles(id) on delete cascade,
legal_first_name text,
legal_last_name text,
legal_name_encrypted bytea, -- pgcrypto encrypted full legal name
tax_id_encrypted bytea, -- SSN/EIN encrypted
tax_id_last_four text, -- last 4 digits for display
tax_classification text check (tax_classification in ('w2_employee', '1099_contractor', 'corp_entity', 'foreign')),
residency_state text, -- US state code (e.g., 'AZ', 'CA')
residency_country text not null default 'US',
address_line1_encrypted bytea,
address_city text,
address_state text,
address_zip text,
compliance_status text not null default 'pending' check (compliance_status in ('pending', 'verified', 'expired', 'rejected', 'review_needed')),
compliance_verified_at timestamptz,
compliance_expires_at timestamptz,
az_eligible boolean not null default false, -- Eligible for AZ Tax Credit
w9_submitted boolean not null default false,
w9_submitted_at timestamptz,
bank_account_connected boolean not null default false,
stripe_connect_account_id text,
payout_method text default 'stripe' check (payout_method in ('stripe', 'ach', 'check', 'paypal')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_talent_profiles_user_idx on public.nexus_talent_profiles (user_id);
create index if not exists nexus_talent_profiles_compliance_idx on public.nexus_talent_profiles (compliance_status);
create index if not exists nexus_talent_profiles_az_eligible_idx on public.nexus_talent_profiles (az_eligible);
create index if not exists nexus_talent_profiles_state_idx on public.nexus_talent_profiles (residency_state);
-- ============================================================================
-- TIME LOGS (Hour Tracking with AZ Compliance)
-- ============================================================================
create table if not exists public.nexus_time_logs (
id uuid primary key default gen_random_uuid(),
talent_profile_id uuid not null references public.nexus_talent_profiles(id) on delete cascade,
contract_id uuid references public.nexus_contracts(id) on delete set null,
milestone_id uuid references public.nexus_milestones(id) on delete set null,
log_date date not null,
start_time timestamptz not null,
end_time timestamptz not null,
hours_worked numeric(5, 2) not null,
description text,
task_category text, -- 'development', 'design', 'review', 'meeting', etc.
location_type text not null default 'remote' check (location_type in ('remote', 'onsite', 'hybrid')),
location_state text, -- State where work was performed (critical for AZ)
location_city text,
location_latitude numeric(10, 7),
location_longitude numeric(10, 7),
location_verified boolean not null default false,
az_eligible_hours numeric(5, 2) default 0, -- Hours qualifying for AZ Tax Credit
billable boolean not null default true,
billed boolean not null default false,
billed_at timestamptz,
invoice_id uuid references public.corp_invoices(id) on delete set null,
submission_status text not null default 'draft' check (submission_status in ('draft', 'submitted', 'approved', 'rejected', 'exported')),
submitted_at timestamptz,
approved_at timestamptz,
approved_by uuid references public.user_profiles(id) on delete set null,
tax_period text, -- e.g., '2025-Q1', '2025-12'
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_time_logs_talent_idx on public.nexus_time_logs (talent_profile_id);
create index if not exists nexus_time_logs_contract_idx on public.nexus_time_logs (contract_id);
create index if not exists nexus_time_logs_date_idx on public.nexus_time_logs (log_date desc);
create index if not exists nexus_time_logs_status_idx on public.nexus_time_logs (submission_status);
create index if not exists nexus_time_logs_state_idx on public.nexus_time_logs (location_state);
create index if not exists nexus_time_logs_az_idx on public.nexus_time_logs (az_eligible_hours) where az_eligible_hours > 0;
create index if not exists nexus_time_logs_period_idx on public.nexus_time_logs (tax_period);
-- ============================================================================
-- TIME LOG AUDITS (Review & AZ Submission Tracking)
-- ============================================================================
create table if not exists public.nexus_time_log_audits (
id uuid primary key default gen_random_uuid(),
time_log_id uuid not null references public.nexus_time_logs(id) on delete cascade,
reviewer_id uuid references public.user_profiles(id) on delete set null,
audit_type text not null check (audit_type in ('review', 'approval', 'rejection', 'az_submission', 'correction', 'dispute')),
decision text check (decision in ('approved', 'rejected', 'needs_correction', 'submitted', 'acknowledged')),
notes text,
corrections_made jsonb, -- { field: { old: value, new: value } }
az_submission_id text, -- ID from AZ Tax Commission API
az_submission_status text check (az_submission_status in ('pending', 'accepted', 'rejected', 'error')),
az_submission_response jsonb,
ip_address text,
user_agent text,
created_at timestamptz not null default now()
);
create index if not exists nexus_time_log_audits_log_idx on public.nexus_time_log_audits (time_log_id);
create index if not exists nexus_time_log_audits_reviewer_idx on public.nexus_time_log_audits (reviewer_id);
create index if not exists nexus_time_log_audits_type_idx on public.nexus_time_log_audits (audit_type);
create index if not exists nexus_time_log_audits_az_idx on public.nexus_time_log_audits (az_submission_id) where az_submission_id is not null;
-- ============================================================================
-- COMPLIANCE EVENTS (Cross-Entity Audit Trail)
-- ============================================================================
create table if not exists public.nexus_compliance_events (
id uuid primary key default gen_random_uuid(),
entity_type text not null, -- 'talent', 'client', 'contract', 'time_log', 'payout'
entity_id uuid not null,
event_type text not null, -- 'created', 'verified', 'exported', 'access_logged', 'financial_update', etc.
event_category text not null check (event_category in ('compliance', 'financial', 'access', 'data_change', 'tax_reporting', 'legal')),
actor_id uuid references public.user_profiles(id) on delete set null,
actor_role text, -- 'talent', 'client', 'admin', 'system', 'api'
realm_context text, -- 'nexus', 'corp', 'foundation', 'studio'
description text,
payload jsonb, -- Full event data
sensitive_data_accessed boolean not null default false,
financial_amount numeric(12, 2),
legal_entity text, -- 'for_profit', 'non_profit'
cross_entity_access boolean not null default false, -- True if Foundation accessed Corp data
ip_address text,
user_agent text,
created_at timestamptz not null default now()
);
create index if not exists nexus_compliance_events_entity_idx on public.nexus_compliance_events (entity_type, entity_id);
create index if not exists nexus_compliance_events_type_idx on public.nexus_compliance_events (event_type);
create index if not exists nexus_compliance_events_category_idx on public.nexus_compliance_events (event_category);
create index if not exists nexus_compliance_events_actor_idx on public.nexus_compliance_events (actor_id);
create index if not exists nexus_compliance_events_realm_idx on public.nexus_compliance_events (realm_context);
create index if not exists nexus_compliance_events_cross_entity_idx on public.nexus_compliance_events (cross_entity_access) where cross_entity_access = true;
create index if not exists nexus_compliance_events_created_idx on public.nexus_compliance_events (created_at desc);
-- ============================================================================
-- ESCROW LEDGER (Financial Tracking)
-- ============================================================================
create table if not exists public.nexus_escrow_ledger (
id uuid primary key default gen_random_uuid(),
contract_id uuid not null references public.nexus_contracts(id) on delete cascade,
client_id uuid not null references public.user_profiles(id) on delete cascade,
creator_id uuid not null references public.user_profiles(id) on delete cascade,
escrow_balance numeric(12, 2) not null default 0,
funds_deposited numeric(12, 2) not null default 0,
funds_released numeric(12, 2) not null default 0,
funds_refunded numeric(12, 2) not null default 0,
aethex_fees numeric(12, 2) not null default 0,
stripe_customer_id text,
stripe_escrow_intent_id text,
status text not null default 'unfunded' check (status in ('unfunded', 'funded', 'partially_funded', 'released', 'disputed', 'refunded')),
funded_at timestamptz,
released_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_escrow_ledger_contract_idx on public.nexus_escrow_ledger (contract_id);
create index if not exists nexus_escrow_ledger_client_idx on public.nexus_escrow_ledger (client_id);
create index if not exists nexus_escrow_ledger_creator_idx on public.nexus_escrow_ledger (creator_id);
create index if not exists nexus_escrow_ledger_status_idx on public.nexus_escrow_ledger (status);
-- ============================================================================
-- PAYOUT RECORDS (Separate from payments for tax tracking)
-- ============================================================================
create table if not exists public.nexus_payouts (
id uuid primary key default gen_random_uuid(),
talent_profile_id uuid not null references public.nexus_talent_profiles(id) on delete cascade,
contract_id uuid references public.nexus_contracts(id) on delete set null,
payment_id uuid references public.nexus_payments(id) on delete set null,
gross_amount numeric(12, 2) not null,
platform_fee numeric(12, 2) not null default 0,
processing_fee numeric(12, 2) not null default 0,
tax_withholding numeric(12, 2) not null default 0,
net_amount numeric(12, 2) not null,
payout_method text not null default 'stripe' check (payout_method in ('stripe', 'ach', 'check', 'paypal')),
stripe_payout_id text,
ach_trace_number text,
check_number text,
status text not null default 'pending' check (status in ('pending', 'processing', 'completed', 'failed', 'cancelled')),
scheduled_date date,
processed_at timestamptz,
failure_reason text,
tax_year int not null default extract(year from now()),
tax_form_type text, -- '1099-NEC', 'W-2', etc.
tax_form_generated boolean not null default false,
tax_form_file_id text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists nexus_payouts_talent_idx on public.nexus_payouts (talent_profile_id);
create index if not exists nexus_payouts_contract_idx on public.nexus_payouts (contract_id);
create index if not exists nexus_payouts_status_idx on public.nexus_payouts (status);
create index if not exists nexus_payouts_tax_year_idx on public.nexus_payouts (tax_year);
create index if not exists nexus_payouts_scheduled_idx on public.nexus_payouts (scheduled_date);
-- ============================================================================
-- FOUNDATION GIG RADAR VIEW (Read-Only Projection)
-- ============================================================================
create or replace view public.foundation_gig_radar as
select
o.id as opportunity_id,
o.title,
o.category,
o.required_skills,
o.timeline_type,
o.location_requirement,
o.required_experience,
o.status,
o.published_at,
case
when o.status = 'open' then 'available'
when o.status = 'in_progress' then 'in_progress'
else 'filled'
end as availability_status,
(select count(*) from public.nexus_applications a where a.opportunity_id = o.id) as applicant_count,
case when o.budget_type = 'hourly' then 'hourly' else 'project' end as compensation_type
from public.nexus_opportunities o
where o.status in ('open', 'in_progress')
order by o.published_at desc;
comment on view public.foundation_gig_radar is 'Read-only view for Foundation Gig Radar - no financial data exposed';
-- ============================================================================
-- RLS POLICIES
-- ============================================================================
alter table public.nexus_talent_profiles enable row level security;
alter table public.nexus_time_logs enable row level security;
alter table public.nexus_time_log_audits enable row level security;
alter table public.nexus_compliance_events enable row level security;
alter table public.nexus_escrow_ledger enable row level security;
alter table public.nexus_payouts enable row level security;
-- Talent Profiles: own profile only (sensitive data)
create policy "Users view own talent profile" on public.nexus_talent_profiles
for select using (auth.uid() = user_id);
create policy "Users manage own talent profile" on public.nexus_talent_profiles
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "Admins view all talent profiles" on public.nexus_talent_profiles
for select using (exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
-- Time Logs: talent and contract parties
create policy "Talent views own time logs" on public.nexus_time_logs
for select using (
auth.uid() in (select user_id from public.nexus_talent_profiles where id = talent_profile_id)
);
create policy "Contract clients view time logs" on public.nexus_time_logs
for select using (
contract_id is not null and
auth.uid() in (select client_id from public.nexus_contracts where id = contract_id)
);
create policy "Talent manages own time logs" on public.nexus_time_logs
for all using (
auth.uid() in (select user_id from public.nexus_talent_profiles where id = talent_profile_id)
) with check (
auth.uid() in (select user_id from public.nexus_talent_profiles where id = talent_profile_id)
);
-- Time Log Audits: reviewers and talent
create policy "Time log audit visibility" on public.nexus_time_log_audits
for select using (
auth.uid() = reviewer_id or
auth.uid() in (select tp.user_id from public.nexus_talent_profiles tp join public.nexus_time_logs tl on tp.id = tl.talent_profile_id where tl.id = time_log_id)
);
-- Compliance Events: admins only (sensitive audit data)
create policy "Compliance events admin only" on public.nexus_compliance_events
for select using (exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin'));
create policy "System inserts compliance events" on public.nexus_compliance_events
for insert with check (true); -- Service role only in practice
-- Escrow Ledger: contract parties
create policy "Escrow visible to contract parties" on public.nexus_escrow_ledger
for select using (auth.uid() = client_id or auth.uid() = creator_id);
-- Payouts: talent only
create policy "Payouts visible to talent" on public.nexus_payouts
for select using (
auth.uid() in (select user_id from public.nexus_talent_profiles where id = talent_profile_id)
);
-- ============================================================================
-- TRIGGERS
-- ============================================================================
drop trigger if exists nexus_talent_profiles_set_updated_at on public.nexus_talent_profiles;
create trigger nexus_talent_profiles_set_updated_at before update on public.nexus_talent_profiles for each row execute function public.set_updated_at();
drop trigger if exists nexus_time_logs_set_updated_at on public.nexus_time_logs;
create trigger nexus_time_logs_set_updated_at before update on public.nexus_time_logs for each row execute function public.set_updated_at();
drop trigger if exists nexus_escrow_ledger_set_updated_at on public.nexus_escrow_ledger;
create trigger nexus_escrow_ledger_set_updated_at before update on public.nexus_escrow_ledger for each row execute function public.set_updated_at();
drop trigger if exists nexus_payouts_set_updated_at on public.nexus_payouts;
create trigger nexus_payouts_set_updated_at before update on public.nexus_payouts for each row execute function public.set_updated_at();
-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
-- Calculate AZ-eligible hours for a time period
create or replace function public.calculate_az_eligible_hours(
p_talent_id uuid,
p_start_date date,
p_end_date date
) returns numeric as $$
select coalesce(sum(az_eligible_hours), 0)
from public.nexus_time_logs
where talent_profile_id = p_talent_id
and log_date between p_start_date and p_end_date
and location_state = 'AZ'
and submission_status = 'approved';
$$ language sql stable;
-- Get talent compliance summary
create or replace function public.get_talent_compliance_summary(p_user_id uuid)
returns jsonb as $$
select jsonb_build_object(
'profile_complete', (tp.legal_first_name is not null and tp.tax_id_encrypted is not null),
'compliance_status', tp.compliance_status,
'az_eligible', tp.az_eligible,
'w9_submitted', tp.w9_submitted,
'bank_connected', tp.bank_account_connected,
'pending_time_logs', (select count(*) from public.nexus_time_logs where talent_profile_id = tp.id and submission_status = 'submitted'),
'total_hours_this_month', (select coalesce(sum(hours_worked), 0) from public.nexus_time_logs where talent_profile_id = tp.id and log_date >= date_trunc('month', now())),
'az_hours_this_month', (select coalesce(sum(az_eligible_hours), 0) from public.nexus_time_logs where talent_profile_id = tp.id and log_date >= date_trunc('month', now()) and location_state = 'AZ')
)
from public.nexus_talent_profiles tp
where tp.user_id = p_user_id;
$$ language sql stable;
-- ============================================================================
-- COMMENTS
-- ============================================================================
comment on table public.nexus_talent_profiles is 'Talent legal/tax profiles with encrypted PII for compliance';
comment on table public.nexus_time_logs is 'Hour tracking with location for AZ Tax Credit eligibility';
comment on table public.nexus_time_log_audits is 'Audit trail for time log reviews and AZ submissions';
comment on table public.nexus_compliance_events is 'Cross-entity compliance event log for legal separation';
comment on table public.nexus_escrow_ledger is 'Escrow account tracking per contract';
comment on table public.nexus_payouts is 'Payout records with tax form tracking';
comment on function public.calculate_az_eligible_hours is 'Calculate AZ Tax Credit eligible hours for a talent in a date range';
comment on function public.get_talent_compliance_summary is 'Get compliance status summary for a talent';
-- ========================================
-- Next Migration
-- ========================================
-- NEXUS Core: Strengthened RLS Policies for Legal Entity Separation
-- This migration updates RLS policies to enforce:
-- 1. Client/Admin only access to escrow (no creators)
-- 2. Admin access to all sensitive tables
-- 3. Proper INSERT/UPDATE/DELETE policies
-- ============================================================================
-- DROP EXISTING POLICIES (will recreate with stronger rules)
-- ============================================================================
drop policy if exists "Escrow visible to contract parties" on public.nexus_escrow_ledger;
drop policy if exists "Payouts visible to talent" on public.nexus_payouts;
drop policy if exists "Compliance events admin only" on public.nexus_compliance_events;
drop policy if exists "System inserts compliance events" on public.nexus_compliance_events;
drop policy if exists "Time log audit visibility" on public.nexus_time_log_audits;
-- ============================================================================
-- NEXUS ESCROW LEDGER - Client/Admin Only (Legal Entity Separation)
-- Creators should NOT see escrow details - they see contract/payment status instead
-- ============================================================================
-- Clients can view their own escrow records
create policy "Clients view own escrow" on public.nexus_escrow_ledger
for select using (auth.uid() = client_id);
-- Admins can view all escrow records (for management/reporting)
create policy "Admins view all escrow" on public.nexus_escrow_ledger
for select using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- Only clients can insert escrow records (via API with proper validation)
create policy "Clients create escrow" on public.nexus_escrow_ledger
for insert with check (auth.uid() = client_id);
-- Clients can update their own escrow (funding operations)
create policy "Clients update own escrow" on public.nexus_escrow_ledger
for update using (auth.uid() = client_id) with check (auth.uid() = client_id);
-- Admins can update any escrow (for disputes/releases)
create policy "Admins update escrow" on public.nexus_escrow_ledger
for update using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- ============================================================================
-- NEXUS PAYOUTS - Talent + Admin Access
-- Talent sees their own payouts, Admins manage all
-- ============================================================================
-- Talent can view their own payouts
create policy "Talent views own payouts" on public.nexus_payouts
for select using (
auth.uid() in (select user_id from public.nexus_talent_profiles where id = talent_profile_id)
);
-- Admins can view all payouts
create policy "Admins view all payouts" on public.nexus_payouts
for select using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- Only admins can insert/update payouts (payroll processing)
create policy "Admins manage payouts" on public.nexus_payouts
for all using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- ============================================================================
-- NEXUS COMPLIANCE EVENTS - Admin Only + Service Insert
-- Sensitive audit trail - admin read, system write
-- ============================================================================
-- Admins can view all compliance events
create policy "Admins view compliance events" on public.nexus_compliance_events
for select using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- Only admins can insert compliance events (via adminClient in API)
-- Non-admin users cannot create compliance log entries directly
create policy "Admins insert compliance events" on public.nexus_compliance_events
for insert with check (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- ============================================================================
-- NEXUS TIME LOG AUDITS - Enhanced Access Control
-- ============================================================================
-- Talent can view audits for their own time logs
create policy "Talent views own time log audits" on public.nexus_time_log_audits
for select using (
auth.uid() in (
select tp.user_id
from public.nexus_talent_profiles tp
join public.nexus_time_logs tl on tp.id = tl.talent_profile_id
where tl.id = time_log_id
)
);
-- Reviewers can view audits they created
create policy "Reviewers view own audits" on public.nexus_time_log_audits
for select using (auth.uid() = reviewer_id);
-- Clients can view audits for time logs on their contracts
create policy "Clients view contract time log audits" on public.nexus_time_log_audits
for select using (
exists(
select 1 from public.nexus_time_logs tl
join public.nexus_contracts c on tl.contract_id = c.id
where tl.id = time_log_id and c.client_id = auth.uid()
)
);
-- Admins can view all audits
create policy "Admins view all time log audits" on public.nexus_time_log_audits
for select using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- Talent can insert audits for their own time logs (submission)
create policy "Talent inserts own time log audits" on public.nexus_time_log_audits
for insert with check (
exists(
select 1 from public.nexus_time_logs tl
join public.nexus_talent_profiles tp on tl.talent_profile_id = tp.id
where tl.id = time_log_id and tp.user_id = auth.uid()
)
);
-- Clients can insert audits for time logs on their contracts (approval/rejection)
create policy "Clients insert contract time log audits" on public.nexus_time_log_audits
for insert with check (
exists(
select 1 from public.nexus_time_logs tl
join public.nexus_contracts c on tl.contract_id = c.id
where tl.id = time_log_id and c.client_id = auth.uid()
)
);
-- Admins can insert any audits
create policy "Admins insert time log audits" on public.nexus_time_log_audits
for insert with check (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- ============================================================================
-- NEXUS TIME LOGS - Add Admin Access
-- ============================================================================
-- Admins can view all time logs (for approval/reporting)
create policy "Admins view all time logs" on public.nexus_time_logs
for select using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- Admins can update any time log (for approval workflow)
create policy "Admins update time logs" on public.nexus_time_logs
for update using (
exists(select 1 from public.user_profiles where id = auth.uid() and user_type = 'admin')
);
-- ============================================================================
-- FOUNDATION GIG RADAR - Verify Read-Only Access
-- No financial data exposed - safe for Foundation users
-- ============================================================================
-- Grant select on gig radar view (if not already granted)
grant select on public.foundation_gig_radar to authenticated;
-- ============================================================================
-- COMMENTS
-- ============================================================================
comment on policy "Clients view own escrow" on public.nexus_escrow_ledger is 'Clients can only view escrow records where they are the client';
comment on policy "Admins view all escrow" on public.nexus_escrow_ledger is 'Admins have full visibility for management';
comment on policy "Talent views own payouts" on public.nexus_payouts is 'Talent sees their own payout history';
comment on policy "Admins manage payouts" on public.nexus_payouts is 'Only admins can create/modify payouts (payroll)';
comment on policy "Admins view compliance events" on public.nexus_compliance_events is 'Compliance events are admin-only for audit purposes';
-- ========================================
-- Next Migration
-- ========================================
-- Developer API Keys System
-- Manages API keys for developers using the AeThex platform
-- API Keys table
CREATE TABLE IF NOT EXISTS developer_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE NOT NULL,
-- Key identification
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- First 8 chars for display (e.g., "aethex_sk_12345678")
key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hash of full key
-- Permissions
scopes TEXT[] DEFAULT ARRAY['read']::TEXT[], -- ['read', 'write', 'admin']
-- Usage tracking
last_used_at TIMESTAMPTZ,
usage_count INTEGER DEFAULT 0,
-- Rate limiting
rate_limit_per_minute INTEGER DEFAULT 60,
rate_limit_per_day INTEGER DEFAULT 10000,
-- Status
is_active BOOLEAN DEFAULT true,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ, -- NULL = no expiration
-- Audit
created_by_ip TEXT,
last_used_ip TEXT
);
-- Indexes
CREATE INDEX idx_developer_api_keys_user_id ON developer_api_keys(user_id);
CREATE INDEX idx_developer_api_keys_key_hash ON developer_api_keys(key_hash);
CREATE INDEX idx_developer_api_keys_active ON developer_api_keys(is_active) WHERE is_active = true;
-- API usage logs (for analytics)
CREATE TABLE IF NOT EXISTS api_usage_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
api_key_id UUID REFERENCES developer_api_keys(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES user_profiles(id) ON DELETE CASCADE NOT NULL,
-- Request details
endpoint TEXT NOT NULL,
method TEXT NOT NULL, -- GET, POST, etc.
status_code INTEGER NOT NULL,
-- Timing
response_time_ms INTEGER,
timestamp TIMESTAMPTZ DEFAULT NOW(),
-- IP and user agent
ip_address TEXT,
user_agent TEXT,
-- Error tracking
error_message TEXT
);
-- Indexes for analytics queries
CREATE INDEX idx_api_usage_logs_api_key_id ON api_usage_logs(api_key_id);
CREATE INDEX idx_api_usage_logs_user_id ON api_usage_logs(user_id);
CREATE INDEX idx_api_usage_logs_timestamp ON api_usage_logs(timestamp DESC);
CREATE INDEX idx_api_usage_logs_endpoint ON api_usage_logs(endpoint);
-- Rate limit tracking (in-memory cache in production, but DB fallback)
CREATE TABLE IF NOT EXISTS api_rate_limits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
api_key_id UUID REFERENCES developer_api_keys(id) ON DELETE CASCADE NOT NULL,
-- Time windows
minute_window TIMESTAMPTZ NOT NULL,
day_window DATE NOT NULL,
-- Counters
requests_this_minute INTEGER DEFAULT 0,
requests_this_day INTEGER DEFAULT 0,
-- Updated timestamp
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(api_key_id, minute_window, day_window)
);
CREATE INDEX idx_rate_limits_api_key ON api_rate_limits(api_key_id);
CREATE INDEX idx_rate_limits_windows ON api_rate_limits(minute_window, day_window);
-- Developer profiles (extended user data)
CREATE TABLE IF NOT EXISTS developer_profiles (
user_id UUID PRIMARY KEY REFERENCES user_profiles(id) ON DELETE CASCADE,
-- Developer info
company_name TEXT,
website_url TEXT,
github_username TEXT,
-- Verification
is_verified BOOLEAN DEFAULT false,
verified_at TIMESTAMPTZ,
-- Plan
plan_tier TEXT DEFAULT 'free', -- 'free', 'pro', 'enterprise'
plan_starts_at TIMESTAMPTZ DEFAULT NOW(),
plan_ends_at TIMESTAMPTZ,
-- Limits
max_api_keys INTEGER DEFAULT 3,
rate_limit_multiplier NUMERIC DEFAULT 1.0,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Cleanup function for old logs (keep last 90 days)
CREATE OR REPLACE FUNCTION cleanup_old_api_logs()
RETURNS void AS $$
BEGIN
-- Delete logs older than 90 days
DELETE FROM api_usage_logs
WHERE timestamp < NOW() - INTERVAL '90 days';
-- Delete old rate limit records
DELETE FROM api_rate_limits
WHERE minute_window < NOW() - INTERVAL '1 hour';
END;
$$ LANGUAGE plpgsql;
-- Function to get API key usage stats
CREATE OR REPLACE FUNCTION get_api_key_stats(key_id UUID)
RETURNS TABLE(
total_requests BIGINT,
requests_today BIGINT,
requests_this_week BIGINT,
avg_response_time_ms NUMERIC,
error_rate NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(*) as total_requests,
COUNT(*) FILTER (WHERE timestamp >= CURRENT_DATE) as requests_today,
COUNT(*) FILTER (WHERE timestamp >= CURRENT_DATE - INTERVAL '7 days') as requests_this_week,
AVG(response_time_ms) as avg_response_time_ms,
(COUNT(*) FILTER (WHERE status_code >= 400)::NUMERIC / NULLIF(COUNT(*), 0) * 100) as error_rate
FROM api_usage_logs
WHERE api_key_id = key_id;
END;
$$ LANGUAGE plpgsql;
-- Row Level Security (RLS)
ALTER TABLE developer_api_keys ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_usage_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE developer_profiles ENABLE ROW LEVEL SECURITY;
-- Users can only see their own API keys
CREATE POLICY api_keys_user_policy ON api_keys
FOR ALL USING (auth.uid() = user_id);
-- Users can only see their own usage logs
DROP POLICY IF EXISTS api_usage_logs_user_policy ON api_usage_logs;
CREATE POLICY api_usage_logs_user_policy ON api_usage_logs
FOR ALL USING (auth.uid() = user_id);
-- Users can only see their own developer profile
DROP POLICY IF EXISTS developer_profiles_user_policy ON developer_profiles;
CREATE POLICY developer_profiles_user_policy ON developer_profiles
FOR ALL USING (auth.uid() = user_id);
-- Trigger to update developer profile timestamp
CREATE OR REPLACE FUNCTION update_developer_profile_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS developer_profiles_updated_at ON developer_profiles;
CREATE TRIGGER developer_profiles_updated_at
BEFORE UPDATE ON developer_profiles
FOR EACH ROW
EXECUTE FUNCTION update_developer_profile_timestamp();
-- Comments
COMMENT ON TABLE developer_api_keys IS 'Stores API keys for developer access to AeThex platform';
COMMENT ON TABLE api_usage_logs IS 'Logs all API requests for analytics and debugging';
COMMENT ON TABLE developer_profiles IS 'Extended profile data for developers';
COMMENT ON COLUMN developer_api_keys.key_prefix IS 'First 8 characters of key for display purposes';
COMMENT ON COLUMN developer_api_keys.key_hash IS 'SHA-256 hash of the full API key for verification';
COMMENT ON COLUMN developer_api_keys.scopes IS 'Array of permission scopes: read, write, admin';
-- ========================================
-- Next Migration
-- ========================================