clavitor/clavitor.ai/admin/schema.sql

446 lines
18 KiB
SQL

-- Clavitor Corporate DB Schema
-- 95% Paddle format + 5% our extensions
-- ============================================
-- PADDLE PRODUCT CATALOG
-- ============================================
CREATE TABLE products (
id TEXT PRIMARY KEY, -- pro_xxx (Paddle ID)
name TEXT NOT NULL, -- "Personal", "Family", "Pro"
description TEXT,
tax_category TEXT, -- Paddle tax category
image_url TEXT,
custom_data TEXT, -- JSON for our use
status TEXT NOT NULL DEFAULT 'active', -- active, archived
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE TABLE prices (
id TEXT PRIMARY KEY, -- pri_xxx (Paddle ID)
product_id TEXT NOT NULL REFERENCES products(id),
description TEXT, -- Internal notes
name TEXT, -- Shown to customers: "Yearly USD"
billing_cycle TEXT, -- JSON: {"interval": "year", "frequency": 1}
trial_period TEXT, -- JSON or null: {"interval": "day", "frequency": 7}
tax_mode TEXT DEFAULT 'account_setting',
unit_price TEXT NOT NULL, -- JSON: {"amount": "1200", "currency_code": "USD"}
unit_price_overrides TEXT, -- JSON array for country-specific pricing
quantity TEXT, -- JSON: {"minimum": 1, "maximum": null}
status TEXT NOT NULL DEFAULT 'active',
custom_data TEXT, -- JSON: our plan identifier (Personal/Pro/etc)
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_prices_product ON prices(product_id);
CREATE TABLE discounts (
id TEXT PRIMARY KEY, -- dsc_xxx (Paddle ID)
status TEXT NOT NULL DEFAULT 'active',
description TEXT NOT NULL, -- "Johan MSP - 30% margin"
enabled_for_checkout BOOLEAN DEFAULT 0, -- We apply manually, not customer-facing
code TEXT, -- Null for MSP margins
type TEXT NOT NULL, -- percentage, flat, flat_per_seat
mode TEXT DEFAULT 'standard',
amount TEXT NOT NULL, -- "30" for 30%
currency_code TEXT, -- Only for flat types
recur BOOLEAN DEFAULT 1, -- True for subscriptions
maximum_recurring_intervals INTEGER,
usage_limit INTEGER,
restrict_to TEXT, -- JSON array of product/price IDs
expires_at INTEGER,
times_used INTEGER DEFAULT 0,
discount_group_id TEXT, -- dsg_xxx for grouping
custom_data TEXT, -- JSON: {"msp_id": "xxx", "tier": "standard"}
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- ============================================
-- PADDLE CUSTOMERS + OUR HIERARCHY
-- ============================================
CREATE TABLE customers (
id TEXT PRIMARY KEY, -- ctm_xxx (Paddle ID)
-- PADDLE FIELDS (95%)
email TEXT NOT NULL,
name TEXT,
locale TEXT DEFAULT 'en',
status TEXT NOT NULL DEFAULT 'active', -- active, archived
marketing_consent BOOLEAN DEFAULT 0,
import_meta TEXT, -- JSON if imported
-- OUR EXTENSIONS (5%)
parent_id TEXT REFERENCES customers(id), -- Hierarchy: MSP → Customer → Dept
level INTEGER NOT NULL DEFAULT 1, -- 1=MSP, 2=EndCustomer, 3=Dept
internal_notes TEXT,
custom_data TEXT, -- JSON: {"source": "web", "segment": "startup"}
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_customers_parent ON customers(parent_id);
CREATE INDEX idx_customers_level ON customers(level);
CREATE TABLE addresses (
id TEXT PRIMARY KEY, -- add_xxx (Paddle ID)
customer_id TEXT NOT NULL REFERENCES customers(id),
description TEXT,
city TEXT,
region TEXT,
country_code TEXT NOT NULL, -- ISO 3166-1 alpha-2
zip_code TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_addresses_customer ON addresses(customer_id);
CREATE TABLE businesses (
id TEXT PRIMARY KEY, -- biz_xxx (Paddle ID)
customer_id TEXT NOT NULL REFERENCES customers(id),
name TEXT NOT NULL, -- Company name
company_number TEXT,
tax_identifier TEXT, -- VAT number
contacts TEXT, -- JSON: [{"name": "", "email": ""}]
status TEXT NOT NULL DEFAULT 'active',
custom_data TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_businesses_customer ON businesses(customer_id);
-- ============================================
-- PADDLE BILLING + OUR VAULT LINK
-- ============================================
CREATE TABLE subscriptions (
id TEXT PRIMARY KEY, -- sub_xxx (Paddle ID)
-- PADDLE REFERENCES
customer_id TEXT NOT NULL REFERENCES customers(id),
address_id TEXT NOT NULL REFERENCES addresses(id),
business_id TEXT REFERENCES businesses(id), -- Optional for B2B
-- PADDLE BILLING
currency_code TEXT NOT NULL,
status TEXT NOT NULL, -- active, past_due, paused, canceled
collection_mode TEXT NOT NULL DEFAULT 'automatic', -- automatic, manual
billing_details TEXT, -- JSON: payment_terms, etc.
discount TEXT, -- JSON: {id, starts_at, ends_at, type}
-- PADDLE TIMESTAMPS
started_at INTEGER,
first_billed_at INTEGER,
next_billed_at INTEGER,
paused_at INTEGER,
canceled_at INTEGER,
-- OUR EXTENSION (5%): Link to vault
vault_l0 TEXT, -- Which vault this subscription pays for
-- PADDLE METADATA
billing_cycle TEXT, -- JSON: {interval, frequency}
current_billing_period TEXT, -- JSON: {starts_at, ends_at}
scheduled_change TEXT, -- JSON: {action, effective_at, resume_at}
items TEXT NOT NULL, -- JSON array of subscription items
management_urls TEXT, -- JSON: {update_payment_method, cancel}
custom_data TEXT,
import_meta TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER,
consent_requirements TEXT -- JSON array
);
CREATE INDEX idx_subscriptions_customer ON subscriptions(customer_id);
CREATE INDEX idx_subscriptions_vault ON subscriptions(vault_l0);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE TABLE subscription_items (
id TEXT PRIMARY KEY, -- Paddle item ID
subscription_id TEXT NOT NULL REFERENCES subscriptions(id),
price_id TEXT NOT NULL, -- pri_xxx
product_id TEXT NOT NULL, -- pro_xxx
quantity INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'active',
recurring BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER,
previously_billed_at INTEGER,
next_billed_at INTEGER,
trial_dates TEXT, -- JSON: {starts_at, ends_at}
price_snapshot TEXT, -- JSON: price entity at time of add
product_snapshot TEXT -- JSON: product entity at time of add
);
CREATE INDEX idx_items_subscription ON subscription_items(subscription_id);
CREATE TABLE transactions (
id TEXT PRIMARY KEY, -- txn_xxx (Paddle ID)
subscription_id TEXT REFERENCES subscriptions(id),
customer_id TEXT NOT NULL REFERENCES customers(id),
address_id TEXT NOT NULL REFERENCES addresses(id),
business_id TEXT REFERENCES businesses(id),
invoice_id TEXT, -- Deprecated, but Paddle includes it
invoice_number TEXT,
discount_id TEXT REFERENCES discounts(id),
-- TRANSACTION STATUS
status TEXT NOT NULL, -- ready, billed, completed, past_due, canceled
origin TEXT, -- checkout, api, subscription_renewal, etc.
-- FINANCIALS (from Paddle 'details' object)
currency_code TEXT NOT NULL,
details TEXT NOT NULL, -- JSON: totals, tax_rates_used, line_items
payments TEXT, -- JSON array of payment attempts
-- ADJUSTED TOTALS (post-adjustments)
adjusted_totals TEXT,
payout_totals TEXT,
adjusted_payout_totals TEXT,
-- INVOICE
billing_details TEXT, -- JSON: enable_checkout, payment_terms, etc.
additional_information TEXT, -- Notes on invoice
purchase_order_number TEXT,
-- CHECKOUT (if applicable)
checkout_url TEXT,
-- TIMESTAMPS
billed_at INTEGER,
completed_at INTEGER,
revised_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER,
-- OUR EXTENSION: Track if we processed this
processed_at INTEGER,
processing_error TEXT
);
CREATE INDEX idx_transactions_subscription ON transactions(subscription_id);
CREATE INDEX idx_transactions_customer ON transactions(customer_id);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_billed ON transactions(billed_at);
CREATE TABLE adjustments (
id TEXT PRIMARY KEY, -- adj_xxx (Paddle ID)
transaction_id TEXT NOT NULL REFERENCES transactions(id),
subscription_id TEXT REFERENCES subscriptions(id),
customer_id TEXT NOT NULL REFERENCES customers(id),
action TEXT NOT NULL, -- chargeback, credit, refund
type TEXT, -- partial, full, tax, proration
amount TEXT NOT NULL, -- "1200" cents
currency_code TEXT NOT NULL,
totals TEXT NOT NULL, -- JSON: adjusted totals
payout_totals TEXT,
reason TEXT,
credit_applied_to TEXT, -- For credit adjustments
created_at INTEGER NOT NULL
);
-- ============================================
-- OUR VAULT-SPECIFIC DATA (Beyond Paddle)
-- ============================================
CREATE TABLE vaults (
l0 TEXT PRIMARY KEY, -- First 4 bytes of L3, hex
customer_id TEXT NOT NULL REFERENCES customers(id),
subscription_id TEXT REFERENCES subscriptions(id),
status TEXT NOT NULL DEFAULT 'active', -- active, suspended, archived
created_at INTEGER NOT NULL,
last_seen_at INTEGER,
-- For audit trail
created_by_human INTEGER -- Who created this vault (if tracked)
);
CREATE INDEX idx_vaults_customer ON vaults(customer_id);
-- WL3 Registry (for POP sync) - OUR DATA
CREATE TABLE credentials (
id INTEGER PRIMARY KEY, -- Our 8-byte int64
customer_id TEXT NOT NULL REFERENCES customers(id),
p0 TEXT NOT NULL, -- First 4 bytes of PRF (lookup key)
l0 TEXT NOT NULL REFERENCES vaults(l0),
credential_id BLOB NOT NULL, -- WebAuthn credential ID
wl3_path TEXT NOT NULL, -- Path in storage: a1/a1b2c3d4/20260405_abc.cla
enrolled_by INTEGER, -- Who authorized this device
created_at INTEGER NOT NULL,
last_used_at INTEGER
);
CREATE INDEX idx_credentials_customer ON credentials(customer_id);
CREATE INDEX idx_credentials_l0 ON credentials(l0);
CREATE INDEX idx_credentials_p0 ON credentials(p0);
CREATE INDEX idx_credentials_created ON credentials(created_at);
-- Our RBAC (not in Paddle - our internal access control)
CREATE TABLE roles (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- SuperAdmin, MSPAdmin, TenantAdmin, VaultUser
permissions TEXT NOT NULL, -- JSON
description TEXT,
created_at INTEGER NOT NULL
);
-- Pre-populate
INSERT INTO roles (id, name, permissions, description, created_at) VALUES
(1, 'SuperAdmin', '{"all": true}', 'Full system access', strftime('%s', 'now')),
(2, 'MSPAdmin', '{"customer_admin": true, "billing_view": true}', 'MSP-level admin', strftime('%s', 'now')),
(3, 'TenantAdmin', '{"vault_create": true, "agent_create": true, "device_enroll": true}', 'Customer admin', strftime('%s', 'now')),
(4, 'VaultUser', '{"vault_read": true, "vault_write": true}', 'Regular user', strftime('%s', 'now')),
(5, 'BillingAdmin', '{"billing_view": true, "invoices": true}', 'Can view/pay invoices', strftime('%s', 'now')),
(6, 'ReadOnly', '{"vault_read": true}', 'View only', strftime('%s', 'now'));
-- Our internal users (humans in our system)
CREATE TABLE humans (
id INTEGER PRIMARY KEY, -- Our 8-byte int64
customer_id TEXT REFERENCES customers(id), -- Can be null for our internal staff
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- RBAC: Who has what role for which customer/tenant
CREATE TABLE role_assignments (
id INTEGER PRIMARY KEY,
human_id INTEGER NOT NULL REFERENCES humans(id),
customer_id TEXT NOT NULL REFERENCES customers(id), -- The tenant they have access to
role_id INTEGER NOT NULL REFERENCES roles(id),
granted_by INTEGER REFERENCES humans(id),
created_at INTEGER NOT NULL,
UNIQUE(human_id, customer_id, role_id)
);
CREATE INDEX idx_role_assignments_human ON role_assignments(human_id);
CREATE INDEX idx_role_assignments_customer ON role_assignments(customer_id);
-- ============================================
-- POP SYNC STATE (Our distributed system)
-- ============================================
CREATE TABLE pop_sync_state (
pop_id TEXT PRIMARY KEY, -- e.g., "zurich-01", "singapore-02"
last_sync_at INTEGER NOT NULL,
last_sync_cursor INTEGER, -- Last credential.id synced
status TEXT NOT NULL DEFAULT 'active', -- active, paused, error
error_message TEXT,
updated_at INTEGER NOT NULL
);
-- ============================================
-- AUDIT LOG (Our record keeping)
-- ============================================
CREATE TABLE events (
id INTEGER PRIMARY KEY,
occurred_at INTEGER NOT NULL,
-- What happened
event_type TEXT NOT NULL, -- customer_created, subscription_started, payment_received, etc.
entity_type TEXT, -- customer, subscription, transaction, vault, etc.
entity_id TEXT, -- The Paddle or internal ID
-- Who did it / Who it affected
customer_id TEXT REFERENCES customers(id),
human_id INTEGER REFERENCES humans(id),
vault_l0 TEXT,
-- Details
amount_cents INTEGER,
currency_code TEXT,
details TEXT, -- JSON: full event data
-- Source
source TEXT DEFAULT 'api', -- api, webhook, system
ip_address TEXT,
user_agent TEXT
);
CREATE INDEX idx_events_time ON events(occurred_at);
CREATE INDEX idx_events_customer ON events(customer_id);
CREATE INDEX idx_events_type ON events(event_type);
-- ============================================
-- SYNC TRACKING FIELDS (for dual-write layer)
-- ============================================
-- Track when we last synced with Paddle and what event we processed
-- This helps detect: "We haven't heard from Paddle in X minutes, retry"
ALTER TABLE customers ADD COLUMN last_paddle_sync_at INTEGER; -- Timestamp of last successful sync
ALTER TABLE customers ADD COLUMN paddle_event_id TEXT; -- Last webhook event ID processed
ALTER TABLE customers ADD COLUMN pending_since INTEGER; -- When operation was queued for retry logic
ALTER TABLE subscriptions ADD COLUMN last_paddle_sync_at INTEGER;
ALTER TABLE subscriptions ADD COLUMN paddle_event_id TEXT;
ALTER TABLE subscriptions ADD COLUMN pending_since INTEGER;
ALTER TABLE discounts ADD COLUMN last_paddle_sync_at INTEGER;
ALTER TABLE discounts ADD COLUMN paddle_event_id TEXT;
ALTER TABLE discounts ADD COLUMN pending_since INTEGER;
-- Index for finding stale pending syncs (retry after 5 minutes)
CREATE INDEX idx_subscriptions_pending_sync
ON subscriptions(sync_status, pending_since)
WHERE sync_status = 'pending';
CREATE INDEX idx_customers_pending_sync
ON customers(sync_status, pending_since)
WHERE sync_status = 'pending';
-- Index for finding records that haven't synced in a while
CREATE INDEX idx_subscriptions_stale_sync
ON subscriptions(last_paddle_sync_at)
WHERE last_paddle_sync_at IS NULL OR last_paddle_sync_at < strftime('%s', 'now', '-1 hour');
-- ============================================
-- WL3 STORAGE (for POP distribution)
-- ============================================
-- TODO: Replace with WORM filesystem (256 shards)
-- Path format: {shard}/{p0}/{timestamp}_{random}.cla
CREATE TABLE wl3_storage (
path TEXT PRIMARY KEY,
content BLOB NOT NULL, -- The encrypted wrapped L3
p0 TEXT NOT NULL, -- For quick lookup
customer_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
sync_count INTEGER DEFAULT 0, -- How many POPs have pulled this
last_sync_at INTEGER
);
CREATE INDEX idx_wl3_p0 ON wl3_storage(p0);
CREATE INDEX idx_wl3_customer ON wl3_storage(customer_id);
CREATE INDEX idx_wl3_created ON wl3_storage(created_at);
-- TODO: SUPER DUPER SECURE POP AUTHENTICATION
-- Requirements:
-- 1. mTLS - POP presents certificate signed by our CA
-- 2. Signed requests - each request signed with POP's private key
-- 3. Replay protection - nonce + timestamp validation
-- 4. IP allowlist - only known POP IPs
-- 5. Rate limiting per POP ID
-- 6. Request signing with HMAC
-- 7. Audit logging of ALL POP access
-- 8. Short-lived tokens (5 min) with refresh