-- Hanzo.Computer Database Schema for Supabase -- Run this in Supabase SQL Editor to create all tables -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Users table (extends Supabase auth.users) CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, company VARCHAR(255), stripe_customer_id VARCHAR(255), role VARCHAR(50) DEFAULT 'customer', -- 'customer', 'admin' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Request for Quotes (RFQs) CREATE TABLE IF NOT EXISTS public.rfqs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, company VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, phone VARCHAR(50), gpu_type VARCHAR(100) NOT NULL, -- 'b300', 'b200', 'h200', 'h100', 'custom' quantity INTEGER NOT NULL, duration_months INTEGER, -- null for one-time purchase use_case TEXT NOT NULL, budget_range VARCHAR(100), additional_requirements TEXT, status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'reviewing', 'quoted', 'accepted', 'rejected' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Cluster Requests CREATE TABLE IF NOT EXISTS public.cluster_requests ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, company VARCHAR(255) NOT NULL, cluster_requirements TEXT NOT NULL, number_of_gpus VARCHAR(50) NOT NULL, rental_duration VARCHAR(50) NOT NULL, project_description TEXT NOT NULL, hear_about_us VARCHAR(100) NOT NULL, status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'reviewing', 'quoted', 'accepted', 'rejected' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Quotes (sent by admin in response to RFQ or cluster request) CREATE TABLE IF NOT EXISTS public.quotes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), rfq_id UUID REFERENCES public.rfqs(id) ON DELETE SET NULL, cluster_request_id UUID REFERENCES public.cluster_requests(id) ON DELETE SET NULL, user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, quote_number VARCHAR(100) UNIQUE NOT NULL, -- 'Q-2025-001' items JSONB NOT NULL, -- [{name, description, quantity, unit_price, total}] subtotal DECIMAL(10,2) NOT NULL, tax DECIMAL(10,2) DEFAULT 0, total DECIMAL(10,2) NOT NULL, payment_terms VARCHAR(255), -- 'Monthly subscription', 'Annual prepaid', etc. valid_until DATE, notes TEXT, status VARCHAR(50) DEFAULT 'sent', -- 'sent', 'viewed', 'accepted', 'expired', 'rejected' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), accepted_at TIMESTAMP WITH TIME ZONE ); -- Orders (from accepted quotes or direct purchases) CREATE TABLE IF NOT EXISTS public.orders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), order_number VARCHAR(100) UNIQUE NOT NULL, -- 'ORD-2025-001' user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, quote_id UUID REFERENCES public.quotes(id) ON DELETE SET NULL, stripe_payment_intent_id VARCHAR(255), stripe_subscription_id VARCHAR(255), -- for recurring billing items JSONB NOT NULL, subtotal DECIMAL(10,2) NOT NULL, tax DECIMAL(10,2) DEFAULT 0, total DECIMAL(10,2) NOT NULL, status VARCHAR(50) DEFAULT 'pending', -- 'pending', 'paid', 'provisioning', 'active', 'cancelled' payment_method VARCHAR(50), -- 'stripe_card', 'stripe_ach', 'wire_transfer' billing_cycle VARCHAR(50), -- 'one_time', 'monthly', '6_month', 'annual' created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), paid_at TIMESTAMP WITH TIME ZONE, provisioned_at TIMESTAMP WITH TIME ZONE ); -- Subscriptions (for GPU leasing) CREATE TABLE IF NOT EXISTS public.subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL, stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL, stripe_product_id VARCHAR(255), stripe_price_id VARCHAR(255), gpu_type VARCHAR(100), quantity INTEGER, status VARCHAR(50) DEFAULT 'active', -- 'active', 'cancelled', 'past_due', 'paused' current_period_start TIMESTAMP WITH TIME ZONE, current_period_end TIMESTAMP WITH TIME ZONE, cancel_at_period_end BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), cancelled_at TIMESTAMP WITH TIME ZONE ); -- Invoices (generated by Stripe or manually) CREATE TABLE IF NOT EXISTS public.invoices ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), invoice_number VARCHAR(100) UNIQUE NOT NULL, -- 'INV-2025-001' user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL, subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE SET NULL, stripe_invoice_id VARCHAR(255), amount_due DECIMAL(10,2) NOT NULL, amount_paid DECIMAL(10,2) DEFAULT 0, status VARCHAR(50) DEFAULT 'draft', -- 'draft', 'open', 'paid', 'void', 'uncollectible' due_date DATE, paid_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- GPU Reservations (what customer actually gets) CREATE TABLE IF NOT EXISTS public.gpu_reservations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, order_id UUID REFERENCES public.orders(id) ON DELETE SET NULL, subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE SET NULL, gpu_type VARCHAR(100), quantity INTEGER, instance_ids JSONB, -- array of actual server/instance IDs access_credentials JSONB, -- encrypted connection details status VARCHAR(50) DEFAULT 'provisioning', -- 'provisioning', 'active', 'suspended', 'terminated' provisioned_at TIMESTAMP WITH TIME ZONE, expires_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Usage Tracking CREATE TABLE IF NOT EXISTS public.usage_records ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, gpu_reservation_id UUID REFERENCES public.gpu_reservations(id) ON DELETE CASCADE, hours_used DECIMAL(10,2), compute_units DECIMAL(10,2), recorded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Admin Actions Log CREATE TABLE IF NOT EXISTS public.admin_actions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), admin_id UUID REFERENCES public.users(id) ON DELETE SET NULL, action VARCHAR(100) NOT NULL, -- 'quote_sent', 'order_approved', 'subscription_cancelled', etc. entity_type VARCHAR(50), -- 'rfq', 'quote', 'order', 'subscription' entity_id UUID, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Create indexes for better query performance CREATE INDEX IF NOT EXISTS idx_rfqs_user_id ON public.rfqs(user_id); CREATE INDEX IF NOT EXISTS idx_rfqs_status ON public.rfqs(status); CREATE INDEX IF NOT EXISTS idx_rfqs_created_at ON public.rfqs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_cluster_requests_user_id ON public.cluster_requests(user_id); CREATE INDEX IF NOT EXISTS idx_cluster_requests_status ON public.cluster_requests(status); CREATE INDEX IF NOT EXISTS idx_cluster_requests_created_at ON public.cluster_requests(created_at DESC); CREATE INDEX IF NOT EXISTS idx_quotes_user_id ON public.quotes(user_id); CREATE INDEX IF NOT EXISTS idx_quotes_rfq_id ON public.quotes(rfq_id); CREATE INDEX IF NOT EXISTS idx_quotes_cluster_request_id ON public.quotes(cluster_request_id); CREATE INDEX IF NOT EXISTS idx_quotes_status ON public.quotes(status); CREATE INDEX IF NOT EXISTS idx_orders_user_id ON public.orders(user_id); CREATE INDEX IF NOT EXISTS idx_orders_status ON public.orders(status); CREATE INDEX IF NOT EXISTS idx_orders_created_at ON public.orders(created_at DESC); CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON public.subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON public.subscriptions(status); CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON public.subscriptions(stripe_subscription_id); CREATE INDEX IF NOT EXISTS idx_gpu_reservations_user_id ON public.gpu_reservations(user_id); CREATE INDEX IF NOT EXISTS idx_gpu_reservations_status ON public.gpu_reservations(status); -- Row Level Security (RLS) Policies -- Enable RLS on all tables ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; ALTER TABLE public.rfqs ENABLE ROW LEVEL SECURITY; ALTER TABLE public.cluster_requests ENABLE ROW LEVEL SECURITY; ALTER TABLE public.quotes ENABLE ROW LEVEL SECURITY; ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY; ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY; ALTER TABLE public.gpu_reservations ENABLE ROW LEVEL SECURITY; ALTER TABLE public.usage_records ENABLE ROW LEVEL SECURITY; ALTER TABLE public.admin_actions ENABLE ROW LEVEL SECURITY; -- Users: Users can read/update their own profile CREATE POLICY "Users can view own profile" ON public.users FOR SELECT USING (auth.uid() = id); CREATE POLICY "Users can update own profile" ON public.users FOR UPDATE USING (auth.uid() = id); -- RFQs: Users can create and view their own RFQs, admins can view all CREATE POLICY "Anyone can create RFQ" ON public.rfqs FOR INSERT WITH CHECK (true); CREATE POLICY "Users can view own RFQs" ON public.rfqs FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Cluster Requests: Same as RFQs CREATE POLICY "Anyone can create cluster request" ON public.cluster_requests FOR INSERT WITH CHECK (true); CREATE POLICY "Users can view own cluster requests" ON public.cluster_requests FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Quotes: Users can view their own quotes, admins can manage all CREATE POLICY "Users can view own quotes" ON public.quotes FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins can create quotes" ON public.quotes FOR INSERT WITH CHECK ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins can update quotes" ON public.quotes FOR UPDATE USING ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Orders: Users can view their own orders, admins can manage all CREATE POLICY "Users can view own orders" ON public.orders FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Users can create orders" ON public.orders FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Admins can update orders" ON public.orders FOR UPDATE USING ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Subscriptions: Users can view their own, admins can manage all CREATE POLICY "Users can view own subscriptions" ON public.subscriptions FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins can manage subscriptions" ON public.subscriptions FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- GPU Reservations: Users can view their own CREATE POLICY "Users can view own GPU reservations" ON public.gpu_reservations FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins can manage GPU reservations" ON public.gpu_reservations FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Usage Records: Users can view their own CREATE POLICY "Users can view own usage" ON public.usage_records FOR SELECT USING ( auth.uid() = user_id OR EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Admin Actions: Only admins can view CREATE POLICY "Admins can view admin actions" ON public.admin_actions FOR SELECT USING ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins can create admin actions" ON public.admin_actions FOR INSERT WITH CHECK ( EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role = 'admin') ); -- Functions for automatic timestamp updates CREATE OR REPLACE FUNCTION public.handle_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers for updated_at CREATE TRIGGER set_updated_at_users BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); CREATE TRIGGER set_updated_at_rfqs BEFORE UPDATE ON public.rfqs FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at(); CREATE TRIGGER set_updated_at_cluster_requests BEFORE UPDATE ON public.cluster_requests FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();