-- 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