-- ===================================================== -- BotKonzept - Database Schema for Customer Management -- ===================================================== -- This schema manages customers, instances, emails, and payments -- for the BotKonzept SaaS platform -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ===================================================== -- Table: customers -- ===================================================== -- Stores customer information and trial status CREATE TABLE IF NOT EXISTS customers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email VARCHAR(255) UNIQUE NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, company VARCHAR(255), phone VARCHAR(50), -- Status tracking status VARCHAR(50) DEFAULT 'trial' CHECK (status IN ('trial', 'active', 'cancelled', 'suspended', 'deleted')), -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), trial_end_date TIMESTAMPTZ, subscription_start_date TIMESTAMPTZ, subscription_end_date TIMESTAMPTZ, -- Marketing tracking utm_source VARCHAR(100), utm_medium VARCHAR(100), utm_campaign VARCHAR(100), referral_code VARCHAR(50), -- Metadata metadata JSONB DEFAULT '{}'::jsonb, -- Indexes CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') ); -- Create indexes for customers CREATE INDEX idx_customers_email ON customers(email); CREATE INDEX idx_customers_status ON customers(status); CREATE INDEX idx_customers_created_at ON customers(created_at); CREATE INDEX idx_customers_trial_end_date ON customers(trial_end_date); -- ===================================================== -- Table: instances -- ===================================================== -- Stores LXC instance information for each customer CREATE TABLE IF NOT EXISTS instances ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, -- Instance details ctid BIGINT NOT NULL UNIQUE, hostname VARCHAR(255) NOT NULL, ip VARCHAR(50) NOT NULL, fqdn VARCHAR(255) NOT NULL, vlan INTEGER, -- Status status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('creating', 'active', 'suspended', 'deleted', 'error')), -- Credentials (encrypted JSON) credentials JSONB NOT NULL, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ, trial_end_date TIMESTAMPTZ, -- Resource usage disk_usage_gb DECIMAL(10,2), memory_usage_mb INTEGER, cpu_usage_percent DECIMAL(5,2), -- Metadata metadata JSONB DEFAULT '{}'::jsonb ); -- Create indexes for instances CREATE INDEX idx_instances_customer_id ON instances(customer_id); CREATE INDEX idx_instances_ctid ON instances(ctid); CREATE INDEX idx_instances_status ON instances(status); CREATE INDEX idx_instances_hostname ON instances(hostname); -- ===================================================== -- Table: emails_sent -- ===================================================== -- Tracks all emails sent to customers CREATE TABLE IF NOT EXISTS emails_sent ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, -- Email details email_type VARCHAR(50) NOT NULL CHECK (email_type IN ( 'welcome', 'day3_upgrade', 'day5_reminder', 'day7_last_chance', 'day8_goodbye', 'payment_confirm', 'payment_failed', 'instance_created', 'instance_deleted', 'password_reset', 'newsletter' )), subject VARCHAR(255), recipient_email VARCHAR(255) NOT NULL, -- Status status VARCHAR(50) DEFAULT 'sent' CHECK (status IN ('sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')), -- Timestamps sent_at TIMESTAMPTZ DEFAULT NOW(), delivered_at TIMESTAMPTZ, opened_at TIMESTAMPTZ, clicked_at TIMESTAMPTZ, -- Metadata metadata JSONB DEFAULT '{}'::jsonb ); -- Create indexes for emails_sent CREATE INDEX idx_emails_customer_id ON emails_sent(customer_id); CREATE INDEX idx_emails_type ON emails_sent(email_type); CREATE INDEX idx_emails_sent_at ON emails_sent(sent_at); CREATE INDEX idx_emails_status ON emails_sent(status); -- ===================================================== -- Table: subscriptions -- ===================================================== -- Stores subscription and payment information CREATE TABLE IF NOT EXISTS subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, -- Plan details plan_name VARCHAR(50) NOT NULL CHECK (plan_name IN ('trial', 'starter', 'business', 'enterprise')), plan_price DECIMAL(10,2) NOT NULL, billing_cycle VARCHAR(20) DEFAULT 'monthly' CHECK (billing_cycle IN ('monthly', 'yearly')), -- Discount discount_percent DECIMAL(5,2) DEFAULT 0, discount_code VARCHAR(50), discount_end_date TIMESTAMPTZ, -- Status status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('active', 'cancelled', 'past_due', 'suspended')), -- Payment provider payment_provider VARCHAR(50) CHECK (payment_provider IN ('stripe', 'paypal', 'manual')), payment_provider_id VARCHAR(255), -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), current_period_start TIMESTAMPTZ, current_period_end TIMESTAMPTZ, cancelled_at TIMESTAMPTZ, -- Metadata metadata JSONB DEFAULT '{}'::jsonb ); -- Create indexes for subscriptions CREATE INDEX idx_subscriptions_customer_id ON subscriptions(customer_id); CREATE INDEX idx_subscriptions_status ON subscriptions(status); CREATE INDEX idx_subscriptions_plan_name ON subscriptions(plan_name); -- ===================================================== -- Table: payments -- ===================================================== -- Stores payment transaction history CREATE TABLE IF NOT EXISTS payments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL, -- Payment details amount DECIMAL(10,2) NOT NULL, currency VARCHAR(3) DEFAULT 'EUR', -- Status status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'succeeded', 'failed', 'refunded', 'cancelled')), -- Payment provider payment_provider VARCHAR(50) CHECK (payment_provider IN ('stripe', 'paypal', 'manual')), payment_provider_id VARCHAR(255), payment_method VARCHAR(50), -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), paid_at TIMESTAMPTZ, refunded_at TIMESTAMPTZ, -- Invoice invoice_number VARCHAR(50), invoice_url TEXT, -- Metadata metadata JSONB DEFAULT '{}'::jsonb ); -- Create indexes for payments CREATE INDEX idx_payments_customer_id ON payments(customer_id); CREATE INDEX idx_payments_subscription_id ON payments(subscription_id); CREATE INDEX idx_payments_status ON payments(status); CREATE INDEX idx_payments_created_at ON payments(created_at); -- ===================================================== -- Table: usage_stats -- ===================================================== -- Tracks usage statistics for each instance CREATE TABLE IF NOT EXISTS usage_stats ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), instance_id UUID NOT NULL REFERENCES instances(id) ON DELETE CASCADE, -- Usage metrics date DATE NOT NULL, messages_count INTEGER DEFAULT 0, documents_count INTEGER DEFAULT 0, api_calls_count INTEGER DEFAULT 0, storage_used_mb DECIMAL(10,2) DEFAULT 0, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), -- Unique constraint: one record per instance per day UNIQUE(instance_id, date) ); -- Create indexes for usage_stats CREATE INDEX idx_usage_instance_id ON usage_stats(instance_id); CREATE INDEX idx_usage_date ON usage_stats(date); -- ===================================================== -- Table: audit_log -- ===================================================== -- Audit trail for important actions CREATE TABLE IF NOT EXISTS audit_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), customer_id UUID REFERENCES customers(id) ON DELETE SET NULL, instance_id UUID REFERENCES instances(id) ON DELETE SET NULL, -- Action details action VARCHAR(100) NOT NULL, entity_type VARCHAR(50), entity_id UUID, -- User/system that performed the action performed_by VARCHAR(100), ip_address INET, user_agent TEXT, -- Changes old_values JSONB, new_values JSONB, -- Timestamp created_at TIMESTAMPTZ DEFAULT NOW(), -- Metadata metadata JSONB DEFAULT '{}'::jsonb ); -- Create indexes for audit_log CREATE INDEX idx_audit_customer_id ON audit_log(customer_id); CREATE INDEX idx_audit_instance_id ON audit_log(instance_id); CREATE INDEX idx_audit_action ON audit_log(action); CREATE INDEX idx_audit_created_at ON audit_log(created_at); -- ===================================================== -- Functions & Triggers -- ===================================================== -- Function to update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers for updated_at CREATE TRIGGER update_customers_updated_at BEFORE UPDATE ON customers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_instances_updated_at BEFORE UPDATE ON instances FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Function to calculate trial end date CREATE OR REPLACE FUNCTION set_trial_end_date() RETURNS TRIGGER AS $$ BEGIN IF NEW.trial_end_date IS NULL THEN NEW.trial_end_date = NEW.created_at + INTERVAL '7 days'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger for trial end date CREATE TRIGGER set_customer_trial_end_date BEFORE INSERT ON customers FOR EACH ROW EXECUTE FUNCTION set_trial_end_date(); -- ===================================================== -- Views -- ===================================================== -- View: Active trials expiring soon CREATE OR REPLACE VIEW trials_expiring_soon AS SELECT c.id, c.email, c.first_name, c.last_name, c.created_at, c.trial_end_date, EXTRACT(DAY FROM (c.trial_end_date - NOW())) as days_remaining, i.ctid, i.hostname, i.fqdn FROM customers c JOIN instances i ON c.id = i.customer_id WHERE c.status = 'trial' AND i.status = 'active' AND c.trial_end_date > NOW() AND c.trial_end_date <= NOW() + INTERVAL '3 days'; -- View: Customer overview with instance info CREATE OR REPLACE VIEW customer_overview AS SELECT c.id, c.email, c.first_name, c.last_name, c.company, c.status, c.created_at, c.trial_end_date, i.ctid, i.hostname, i.fqdn, i.ip, i.status as instance_status, s.plan_name, s.plan_price, s.status as subscription_status FROM customers c LEFT JOIN instances i ON c.id = i.customer_id AND i.status = 'active' LEFT JOIN subscriptions s ON c.id = s.customer_id AND s.status = 'active'; -- View: Revenue metrics CREATE OR REPLACE VIEW revenue_metrics AS SELECT DATE_TRUNC('month', paid_at) as month, COUNT(*) as payment_count, SUM(amount) as total_revenue, AVG(amount) as average_payment, COUNT(DISTINCT customer_id) as unique_customers FROM payments WHERE status = 'succeeded' AND paid_at IS NOT NULL GROUP BY DATE_TRUNC('month', paid_at) ORDER BY month DESC; -- ===================================================== -- Row Level Security (RLS) Policies -- ===================================================== -- Enable RLS on tables ALTER TABLE customers ENABLE ROW LEVEL SECURITY; ALTER TABLE instances ENABLE ROW LEVEL SECURITY; ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE payments ENABLE ROW LEVEL SECURITY; -- Policy: Customers can only see their own data CREATE POLICY customers_select_own ON customers FOR SELECT USING (auth.uid()::text = id::text); CREATE POLICY instances_select_own ON instances FOR SELECT USING (customer_id::text = auth.uid()::text); CREATE POLICY subscriptions_select_own ON subscriptions FOR SELECT USING (customer_id::text = auth.uid()::text); CREATE POLICY payments_select_own ON payments FOR SELECT USING (customer_id::text = auth.uid()::text); -- ===================================================== -- Sample Data (for testing) -- ===================================================== -- Insert sample customer (commented out for production) -- INSERT INTO customers (email, first_name, last_name, company, status) -- VALUES ('test@example.com', 'Max', 'Mustermann', 'Test GmbH', 'trial'); -- ===================================================== -- Grants -- ===================================================== -- Grant permissions to authenticated users GRANT SELECT, INSERT, UPDATE ON customers TO authenticated; GRANT SELECT ON instances TO authenticated; GRANT SELECT ON subscriptions TO authenticated; GRANT SELECT ON payments TO authenticated; GRANT SELECT ON usage_stats TO authenticated; -- Grant all permissions to service role (for n8n workflows) GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role; GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role; -- ===================================================== -- Comments -- ===================================================== COMMENT ON TABLE customers IS 'Stores customer information and trial status'; COMMENT ON TABLE instances IS 'Stores LXC instance information for each customer'; COMMENT ON TABLE emails_sent IS 'Tracks all emails sent to customers'; COMMENT ON TABLE subscriptions IS 'Stores subscription and payment information'; COMMENT ON TABLE payments IS 'Stores payment transaction history'; COMMENT ON TABLE usage_stats IS 'Tracks usage statistics for each instance'; COMMENT ON TABLE audit_log IS 'Audit trail for important actions'; -- ===================================================== -- End of Schema -- =====================================================