Files
computer/supabase/schema.sql
T
zeekay c951278680 feat: Integrate Supabase backend for RFQ and Cluster requests
Add complete Supabase integration for data persistence and authentication:

Backend Setup:
- Created Supabase client configuration (src/lib/supabase.ts)
- Defined TypeScript interfaces for all database tables
- Implemented helper functions for RFQ and cluster request submission
- Added functions for customer data retrieval (quotes, orders, subscriptions)
- Added admin functions for managing all requests

Database Schema (supabase/schema.sql):
- 10 tables: users, rfqs, cluster_requests, quotes, orders, subscriptions,
  invoices, gpu_reservations, usage_records, admin_actions
- Row Level Security (RLS) policies for customer/admin access control
- Indexes for query performance
- Triggers for automatic timestamp updates
- Complete foreign key relationships

Security Features:
- RLS policies: Customers can only view their own data
- Admins can view and manage all data
- Anonymous users can submit RFQs and cluster requests
- Automatic timestamp management
- Secure credential storage

Form Integration:
- Request Quote page now submits to Supabase rfqs table
- Clusters page now submits to Supabase cluster_requests table
- Both forms include proper error handling and success messages
- Duration converted to months for database storage

Configuration:
- .env.example with Supabase URL and API key placeholders
- supabase/README.md with comprehensive setup instructions
- Project ID: fefkbezinxawrutwjdkq
- Region: us-east-1

Helper Functions:
- submitRFQ() - Submit request for quote
- submitClusterRequest() - Submit cluster inquiry
- getUserRFQs/Quotes/Orders/Subscriptions() - Customer data retrieval
- getAllRFQs/ClusterRequests() - Admin data retrieval
- updateRFQStatus/ClusterRequestStatus() - Admin workflow management

Next Steps:
1. Run schema.sql in Supabase SQL Editor
2. Configure environment variables in .env.local
3. Test RFQ and cluster request submissions
4. Build customer dashboard to view submitted requests
5. Build admin dashboard to manage requests and send quotes
2025-10-31 11:14:49 -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();