Database Schema
HiveForge uses PostgreSQL via Supabase with Row-Level Security (RLS) for multi-tenancy.
Core Tables
Users and Authentication
-- profiles: User profile information
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id),
email TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS Policy: Users can only read/update their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);Organizations (Tenants)
-- organizations: Multi-tenant workspaces
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
tier TEXT NOT NULL DEFAULT 'free', -- free, pro, enterprise
logo_url TEXT,
website TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Billing
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT,
subscription_status TEXT, -- active, canceled, past_due
-- Hard isolation (optional)
isolated_db_url TEXT, -- For hard-isolated tenants
CONSTRAINT valid_tier CHECK (tier IN ('free', 'pro', 'enterprise'))
);
-- RLS: Users see organizations they're members of
CREATE POLICY "Users can view their organizations"
ON organizations FOR SELECT
USING (
EXISTS (
SELECT 1 FROM organization_members
WHERE organization_id = organizations.id
AND user_id = auth.uid()
)
);Organization Membership
-- organization_members: User-to-organization relationships
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member', -- owner, admin, member, viewer
invited_by UUID REFERENCES profiles(id),
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(organization_id, user_id),
CONSTRAINT valid_role CHECK (role IN ('owner', 'admin', 'member', 'viewer'))
);
-- RLS: Users can view members of their organizations
CREATE POLICY "View organization members"
ON organization_members FOR SELECT
USING (
EXISTS (
SELECT 1 FROM organization_members AS om
WHERE om.organization_id = organization_members.organization_id
AND om.user_id = auth.uid()
)
);Invitations
-- organization_invitations: Pending invites
CREATE TABLE organization_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
invited_by UUID NOT NULL REFERENCES profiles(id),
token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
accepted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_role CHECK (role IN ('admin', 'member', 'viewer'))
);
-- Index for quick lookup
CREATE INDEX idx_invitations_token ON organization_invitations(token);
CREATE INDEX idx_invitations_email ON organization_invitations(email);RBAC System
Roles and Permissions
-- roles: Predefined roles
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL,
description TEXT,
is_system BOOLEAN DEFAULT FALSE, -- System roles cannot be deleted
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- permissions: Available permissions
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL, -- e.g., "organizations.update"
resource TEXT NOT NULL, -- e.g., "organizations"
action TEXT NOT NULL, -- e.g., "update"
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- role_permissions: Many-to-many
CREATE TABLE role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- user_permissions: User-specific overrides (optional)
CREATE TABLE user_permissions (
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
granted BOOLEAN NOT NULL DEFAULT TRUE,
PRIMARY KEY (user_id, organization_id, permission_id)
);Billing and Subscriptions
-- subscriptions: Subscription details
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
stripe_subscription_id TEXT UNIQUE NOT NULL,
stripe_customer_id TEXT NOT NULL,
stripe_price_id TEXT NOT NULL,
status TEXT NOT NULL, -- active, canceled, past_due, unpaid
current_period_start TIMESTAMPTZ NOT NULL,
current_period_end TIMESTAMPTZ NOT NULL,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- usage_records: Metered billing
CREATE TABLE usage_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
metric TEXT NOT NULL, -- api_calls, storage_gb, etc.
quantity BIGINT NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW(),
reported_to_stripe BOOLEAN DEFAULT FALSE,
stripe_usage_record_id TEXT
);
-- Index for aggregation queries
CREATE INDEX idx_usage_records_org_metric ON usage_records(organization_id, metric, timestamp);API Keys
-- api_keys: Organization API keys
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key_hash TEXT NOT NULL, -- Hashed API key
key_prefix TEXT NOT NULL, -- First few chars for identification
scopes TEXT[] DEFAULT ARRAY['read'], -- read, write, admin
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_by UUID NOT NULL REFERENCES profiles(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
-- Index for quick lookup
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE revoked_at IS NULL;
CREATE INDEX idx_api_keys_org ON api_keys(organization_id);Audit Logging
-- audit_logs: Track important events
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
user_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- create, update, delete, login, etc.
resource_type TEXT NOT NULL, -- organization, user, subscription, etc.
resource_id UUID,
metadata JSONB, -- Additional context
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for queries
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id, created_at DESC);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id, created_at DESC);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);Webhooks
-- webhooks: Organization webhook configurations
CREATE TABLE webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
url TEXT NOT NULL,
secret TEXT NOT NULL,
events TEXT[] NOT NULL, -- Array of event types to subscribe to
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- webhook_deliveries: Webhook delivery attempts
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
status_code INTEGER,
response_body TEXT,
attempt_count INTEGER DEFAULT 1,
next_retry_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for retry processing
CREATE INDEX idx_webhook_deliveries_retry ON webhook_deliveries(next_retry_at)
WHERE delivered_at IS NULL AND attempt_count < 5;Templates System
-- templates: App templates (DataForge, etc.)
CREATE TABLE templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
category TEXT, -- analytics, crm, project-management
icon_url TEXT,
config JSONB, -- Template-specific configuration
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- template_deployments: Deployed template instances
CREATE TABLE template_deployments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
template_id UUID NOT NULL REFERENCES templates(id),
name TEXT NOT NULL,
subdomain TEXT UNIQUE,
custom_domain TEXT UNIQUE,
config JSONB, -- Instance-specific configuration
status TEXT NOT NULL DEFAULT 'pending', -- pending, deploying, active, failed
deployment_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);Indexes and Performance
-- Common query patterns
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_profiles_email ON profiles(email);
CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_subscriptions_org ON subscriptions(organization_id);
CREATE INDEX idx_api_keys_org ON api_keys(organization_id);
-- Full-text search
CREATE INDEX idx_organizations_search ON organizations
USING gin(to_tsvector('english', name || ' ' || COALESCE(website, '')));RLS Policies
Helper Functions
-- Get user's role in organization
CREATE OR REPLACE FUNCTION get_user_role(org_id UUID, user_id UUID)
RETURNS TEXT AS $$
SELECT role FROM organization_members
WHERE organization_id = org_id AND user_id = user_id;
$$ LANGUAGE sql SECURITY DEFINER;
-- Check if user has permission
CREATE OR REPLACE FUNCTION has_permission(
org_id UUID,
user_id UUID,
permission_name TEXT
)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM organization_members om
JOIN role_permissions rp ON rp.role_id = om.role::uuid
JOIN permissions p ON p.id = rp.permission_id
WHERE om.organization_id = org_id
AND om.user_id = user_id
AND p.name = permission_name
);
$$ LANGUAGE sql SECURITY DEFINER;Policies by Table
-- Organizations
CREATE POLICY "Members can view organization"
ON organizations FOR SELECT
USING (id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
));
CREATE POLICY "Owners/Admins can update organization"
ON organizations FOR UPDATE
USING (
get_user_role(id, auth.uid()) IN ('owner', 'admin')
);
-- Organization Members
CREATE POLICY "Members can view other members"
ON organization_members FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
)
);
-- API Keys
CREATE POLICY "Members can view org API keys"
ON api_keys FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid()
)
);
CREATE POLICY "Admins can manage API keys"
ON api_keys FOR ALL
USING (
get_user_role(organization_id, auth.uid()) IN ('owner', 'admin')
);Triggers and Functions
-- Update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_organizations_updated_at
BEFORE UPDATE ON organizations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
-- Create profile on user signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();Migrations
Migrations are stored in supabase/migrations/ and applied in order:
supabase/migrations/
├── 20240101000000_initial_schema.sql
├── 20240102000000_add_organizations.sql
├── 20240103000000_add_rbac.sql
├── 20240104000000_add_billing.sql
├── 20240105000000_add_api_keys.sql
├── 20240106000000_add_webhooks.sql
└── 20240107000000_add_templates.sql