Files
zeekay d0736c16fd feat: zoo.computer — decentralized AI supercomputing landing page
Forked from hanzo.computer, fully rebranded for Zoo Labs Foundation.
Vite + React + Tailwind. Deploys to GitHub Pages.
2026-03-31 13:25:51 -07:00

326 lines
13 KiB
PL/PgSQL

-- 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();