1
0
forked from wrenn/wrenn
Co-authored-by: Tasnim Kabir Sadik <tksadik92@gmail.com>
Reviewed-on: wrenn/sandbox#8
This commit is contained in:
2026-04-09 19:24:49 +00:00
parent 32e5a5a715
commit d3e4812e46
199 changed files with 24552 additions and 2776 deletions

View File

@ -1,25 +1,237 @@
-- +goose Up
CREATE TABLE sandboxes (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL DEFAULT '',
host_id TEXT NOT NULL DEFAULT 'default',
template TEXT NOT NULL DEFAULT 'minimal',
status TEXT NOT NULL DEFAULT 'pending',
vcpus INTEGER NOT NULL DEFAULT 1,
memory_mb INTEGER NOT NULL DEFAULT 512,
timeout_sec INTEGER NOT NULL DEFAULT 0,
guest_ip TEXT NOT NULL DEFAULT '',
host_ip TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
last_active_at TIMESTAMPTZ,
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- teams
CREATE TABLE teams (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
is_byoc BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_teams_slug ON teams(slug);
-- users
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT,
name TEXT NOT NULL DEFAULT '',
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- users_teams (junction)
CREATE TABLE users_teams (
user_id UUID NOT NULL REFERENCES users(id),
team_id UUID NOT NULL REFERENCES teams(id),
is_default BOOLEAN NOT NULL DEFAULT FALSE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (team_id, user_id)
);
CREATE INDEX idx_users_teams_user ON users_teams(user_id);
-- team_api_keys
CREATE TABLE team_api_keys (
id UUID PRIMARY KEY,
team_id UUID NOT NULL REFERENCES teams(id),
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used TIMESTAMPTZ
);
CREATE INDEX idx_team_api_keys_team ON team_api_keys(team_id);
-- oauth_providers
CREATE TABLE oauth_providers (
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
email TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (provider, provider_id)
);
CREATE INDEX idx_oauth_providers_user ON oauth_providers(user_id);
-- admin_permissions
CREATE TABLE admin_permissions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
permission TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, permission)
);
CREATE INDEX idx_admin_permissions_user ON admin_permissions(user_id);
-- hosts
CREATE TABLE hosts (
id UUID PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'regular',
team_id UUID REFERENCES teams(id),
provider TEXT NOT NULL DEFAULT '',
availability_zone TEXT NOT NULL DEFAULT '',
arch TEXT NOT NULL DEFAULT '',
cpu_cores INTEGER NOT NULL DEFAULT 0,
memory_mb INTEGER NOT NULL DEFAULT 0,
disk_gb INTEGER NOT NULL DEFAULT 0,
address TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
last_heartbeat_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
cert_fingerprint TEXT NOT NULL DEFAULT '',
mtls_enabled BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX idx_hosts_type ON hosts(type);
CREATE INDEX idx_hosts_team ON hosts(team_id);
CREATE INDEX idx_hosts_status ON hosts(status);
-- host_tokens
CREATE TABLE host_tokens (
id UUID PRIMARY KEY,
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ
);
CREATE INDEX idx_host_tokens_host ON host_tokens(host_id);
-- host_tags
CREATE TABLE host_tags (
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (host_id, tag)
);
CREATE INDEX idx_host_tags_tag ON host_tags(tag);
-- host_refresh_tokens
CREATE TABLE host_refresh_tokens (
id UUID PRIMARY KEY,
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
CREATE INDEX idx_host_refresh_tokens_host ON host_refresh_tokens(host_id);
-- templates (TEXT primary key — not UUID)
CREATE TABLE templates (
name TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'base',
vcpus INTEGER NOT NULL DEFAULT 1,
memory_mb INTEGER NOT NULL DEFAULT 512,
size_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
team_id UUID NOT NULL
);
CREATE INDEX idx_templates_team ON templates(team_id);
-- sandboxes
CREATE TABLE sandboxes (
id UUID PRIMARY KEY,
team_id UUID NOT NULL REFERENCES teams(id),
host_id UUID NOT NULL,
template TEXT NOT NULL DEFAULT 'minimal',
status TEXT NOT NULL DEFAULT 'pending',
vcpus INTEGER NOT NULL DEFAULT 1,
memory_mb INTEGER NOT NULL DEFAULT 512,
timeout_sec INTEGER NOT NULL DEFAULT 300,
disk_size_mb INTEGER NOT NULL DEFAULT 5120,
guest_ip TEXT NOT NULL DEFAULT '',
host_ip TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
last_active_at TIMESTAMPTZ,
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sandboxes_status ON sandboxes(status);
CREATE INDEX idx_sandboxes_host_status ON sandboxes(host_id, status);
CREATE INDEX idx_sandboxes_team ON sandboxes(team_id);
-- audit_logs (id and team_id are UUID; actor_id and resource_id are TEXT for polymorphism)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
team_id UUID NOT NULL,
actor_type TEXT NOT NULL,
actor_id TEXT,
actor_name TEXT NOT NULL DEFAULT '',
resource_type TEXT NOT NULL,
resource_id TEXT,
action TEXT NOT NULL,
scope TEXT NOT NULL DEFAULT 'team',
status TEXT NOT NULL DEFAULT 'success',
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_team_time ON audit_logs(team_id, created_at DESC);
CREATE INDEX idx_audit_logs_team_resource ON audit_logs(team_id, resource_type, created_at DESC);
-- sandbox_metrics_snapshots
CREATE TABLE sandbox_metrics_snapshots (
id BIGSERIAL PRIMARY KEY,
team_id UUID NOT NULL,
sampled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
running_count INTEGER NOT NULL DEFAULT 0,
vcpus_reserved INTEGER NOT NULL DEFAULT 0,
memory_mb_reserved INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_metrics_snapshots_team_time ON sandbox_metrics_snapshots(team_id, sampled_at DESC);
-- sandbox_metric_points
CREATE TABLE sandbox_metric_points (
sandbox_id UUID NOT NULL,
tier TEXT NOT NULL CHECK (tier IN ('10m', '2h', '24h')),
ts BIGINT NOT NULL,
cpu_pct FLOAT8 NOT NULL DEFAULT 0,
mem_bytes BIGINT NOT NULL DEFAULT 0,
disk_bytes BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (sandbox_id, tier, ts)
);
CREATE INDEX idx_sandbox_metric_points_sandbox_tier ON sandbox_metric_points(sandbox_id, tier);
-- template_builds
CREATE TABLE template_builds (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
base_template TEXT NOT NULL,
recipe JSONB NOT NULL DEFAULT '[]',
healthcheck TEXT NOT NULL DEFAULT '',
vcpus INTEGER NOT NULL DEFAULT 1,
memory_mb INTEGER NOT NULL DEFAULT 512,
status TEXT NOT NULL DEFAULT 'pending',
current_step INTEGER NOT NULL DEFAULT 0,
total_steps INTEGER NOT NULL DEFAULT 0,
logs JSONB NOT NULL DEFAULT '[]',
error TEXT NOT NULL DEFAULT '',
sandbox_id UUID,
host_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
-- +goose Down
DROP TABLE sandboxes;
DROP TABLE IF EXISTS template_builds;
DROP TABLE IF EXISTS sandbox_metric_points;
DROP TABLE IF EXISTS sandbox_metrics_snapshots;
DROP TABLE IF EXISTS audit_logs;
DROP TABLE IF EXISTS sandboxes;
DROP TABLE IF EXISTS templates;
DROP TABLE IF EXISTS host_refresh_tokens;
DROP TABLE IF EXISTS host_tags;
DROP TABLE IF EXISTS host_tokens;
DROP TABLE IF EXISTS hosts;
DROP TABLE IF EXISTS admin_permissions;
DROP TABLE IF EXISTS oauth_providers;
DROP TABLE IF EXISTS team_api_keys;
DROP TABLE IF EXISTS users_teams;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS teams;

View File

@ -1,14 +0,0 @@
-- +goose Up
CREATE TABLE templates (
name TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'base', -- 'base' or 'snapshot'
vcpus INTEGER,
memory_mb INTEGER,
size_bytes BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- +goose Down
DROP TABLE templates;

View File

@ -1,46 +0,0 @@
-- +goose Up
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE teams (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE users_teams (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
is_default BOOLEAN NOT NULL DEFAULT TRUE,
role TEXT NOT NULL DEFAULT 'owner',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (team_id, user_id)
);
CREATE INDEX idx_users_teams_user ON users_teams(user_id);
CREATE TABLE team_api_keys (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT '',
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used TIMESTAMPTZ
);
CREATE INDEX idx_team_api_keys_team ON team_api_keys(team_id);
-- +goose Down
DROP TABLE team_api_keys;
DROP TABLE users_teams;
DROP TABLE teams;
DROP TABLE users;

View File

@ -1,31 +0,0 @@
-- +goose Up
ALTER TABLE sandboxes
ADD COLUMN team_id TEXT NOT NULL DEFAULT '';
UPDATE sandboxes SET team_id = owner_id WHERE owner_id != '';
ALTER TABLE sandboxes
DROP COLUMN owner_id;
ALTER TABLE templates
ADD COLUMN team_id TEXT NOT NULL DEFAULT '';
CREATE INDEX idx_sandboxes_team ON sandboxes(team_id);
CREATE INDEX idx_templates_team ON templates(team_id);
-- +goose Down
ALTER TABLE sandboxes
ADD COLUMN owner_id TEXT NOT NULL DEFAULT '';
UPDATE sandboxes SET owner_id = team_id WHERE team_id != '';
ALTER TABLE sandboxes
DROP COLUMN team_id;
ALTER TABLE templates
DROP COLUMN team_id;
DROP INDEX IF EXISTS idx_sandboxes_team;
DROP INDEX IF EXISTS idx_templates_team;

View File

@ -1,22 +0,0 @@
-- +goose Up
ALTER TABLE users
ALTER COLUMN password_hash DROP NOT NULL;
CREATE TABLE oauth_providers (
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (provider, provider_id)
);
CREATE INDEX idx_oauth_providers_user ON oauth_providers(user_id);
-- +goose Down
DROP TABLE oauth_providers;
UPDATE users SET password_hash = '' WHERE password_hash IS NULL;
ALTER TABLE users ALTER COLUMN password_hash SET NOT NULL;

View File

@ -1,21 +0,0 @@
-- +goose Up
ALTER TABLE users
ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;
CREATE TABLE admin_permissions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, permission)
);
CREATE INDEX idx_admin_permissions_user ON admin_permissions(user_id);
-- +goose Down
DROP TABLE admin_permissions;
ALTER TABLE users
DROP COLUMN is_admin;

View File

@ -1,9 +0,0 @@
-- +goose Up
ALTER TABLE teams
ADD COLUMN is_byoc BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose Down
ALTER TABLE teams
DROP COLUMN is_byoc;

View File

@ -1,47 +0,0 @@
-- +goose Up
CREATE TABLE hosts (
id TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'regular', -- 'regular' or 'byoc'
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
provider TEXT,
availability_zone TEXT,
arch TEXT,
cpu_cores INTEGER,
memory_mb INTEGER,
disk_gb INTEGER,
address TEXT, -- ip:port of host agent
status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'online', 'offline', 'draining'
last_heartbeat_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_by TEXT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE host_tokens (
id TEXT PRIMARY KEY,
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
created_by TEXT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ
);
CREATE TABLE host_tags (
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (host_id, tag)
);
CREATE INDEX idx_hosts_type ON hosts(type);
CREATE INDEX idx_hosts_team ON hosts(team_id);
CREATE INDEX idx_hosts_status ON hosts(status);
CREATE INDEX idx_host_tokens_host ON host_tokens(host_id);
CREATE INDEX idx_host_tags_tag ON host_tags(tag);
-- +goose Down
DROP TABLE host_tags;
DROP TABLE host_tokens;
DROP TABLE hosts;

View File

@ -1,11 +0,0 @@
-- +goose Up
ALTER TABLE hosts
ADD COLUMN cert_fingerprint TEXT,
ADD COLUMN mtls_enabled BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose Down
ALTER TABLE hosts
DROP COLUMN cert_fingerprint,
DROP COLUMN mtls_enabled;

View File

@ -0,0 +1,82 @@
-- +goose Up
-- 1. Add UUID id column to templates and make it the primary key.
ALTER TABLE templates ADD COLUMN id UUID DEFAULT gen_random_uuid();
UPDATE templates SET id = gen_random_uuid() WHERE id IS NULL;
ALTER TABLE templates ALTER COLUMN id SET NOT NULL;
ALTER TABLE templates DROP CONSTRAINT templates_pkey;
ALTER TABLE templates ADD PRIMARY KEY (id);
-- 2. Name becomes a display field with team-scoped uniqueness.
ALTER TABLE templates ADD CONSTRAINT uq_templates_team_name UNIQUE (team_id, name);
-- 3. Prevent team templates from using names that belong to global (platform) templates.
-- A team template insert/update with a name matching any platform template is rejected.
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION check_global_template_name_collision()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.team_id != '00000000-0000-0000-0000-000000000000' THEN
IF EXISTS (
SELECT 1 FROM templates
WHERE name = NEW.name
AND team_id = '00000000-0000-0000-0000-000000000000'
) THEN
RAISE EXCEPTION 'template name "%" is reserved by a global template', NEW.name
USING ERRCODE = 'unique_violation';
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
CREATE TRIGGER trg_check_global_template_name
BEFORE INSERT OR UPDATE ON templates
FOR EACH ROW
EXECUTE FUNCTION check_global_template_name_collision();
-- 4. Seed the built-in "minimal" template so it appears in all listings.
-- Both id and team_id are the all-zeros UUID (platform sentinel).
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id)
VALUES (
'00000000-0000-0000-0000-000000000000',
'minimal',
'base',
1,
512,
0,
'00000000-0000-0000-0000-000000000000'
) ON CONFLICT DO NOTHING;
-- 5. Add template UUID references to template_builds.
ALTER TABLE template_builds
ADD COLUMN template_id UUID,
ADD COLUMN team_id UUID;
-- 5. Add template UUID references to sandboxes.
ALTER TABLE sandboxes
ADD COLUMN template_id UUID,
ADD COLUMN template_team_id UUID;
-- +goose Down
ALTER TABLE sandboxes
DROP COLUMN IF EXISTS template_team_id,
DROP COLUMN IF EXISTS template_id;
ALTER TABLE template_builds
DROP COLUMN IF EXISTS team_id,
DROP COLUMN IF EXISTS template_id;
-- Remove the seeded minimal template.
DELETE FROM templates WHERE id = '00000000-0000-0000-0000-000000000000';
DROP TRIGGER IF EXISTS trg_check_global_template_name ON templates;
DROP FUNCTION IF EXISTS check_global_template_name_collision();
ALTER TABLE templates DROP CONSTRAINT IF EXISTS uq_templates_team_name;
ALTER TABLE templates DROP CONSTRAINT IF EXISTS templates_pkey;
ALTER TABLE templates ADD PRIMARY KEY (name);
ALTER TABLE templates DROP COLUMN IF EXISTS id;

View File

@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE hosts DROP COLUMN mtls_enabled;
ALTER TABLE hosts ADD COLUMN cert_expires_at TIMESTAMPTZ;
-- +goose Down
ALTER TABLE hosts DROP COLUMN cert_expires_at;
ALTER TABLE hosts ADD COLUMN mtls_enabled BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -0,0 +1,11 @@
-- +goose Up
-- Allow completed_at to be set when a build is cancelled.
-- (The UpdateBuildStatus query is updated in sqlc; no schema change needed for that.)
-- Add skip_pre_post flag: when true, the pre-build and post-build command phases
-- are skipped for this build.
ALTER TABLE template_builds ADD COLUMN skip_pre_post BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose Down
ALTER TABLE template_builds DROP COLUMN skip_pre_post;

View File

@ -0,0 +1,19 @@
-- +goose Up
CREATE TABLE channels (
id UUID PRIMARY KEY,
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
provider TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}',
event_types TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (team_id, name)
);
CREATE INDEX idx_channels_team ON channels(team_id);
-- +goose Down
DROP TABLE IF EXISTS channels;