-- ============================================================ -- AppForge — application schema (de-Supabased) -- Target: plain PostgreSQL. No RLS, no auth.* schema. -- User identity is owned by Better Auth ("user" table, text id). -- All user references are TEXT referencing "user"(id). -- This file is idempotent (safe to re-run). -- ============================================================ -- gen_random_uuid() is built into PG13+ core; pgcrypto kept for safety. CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ---------- Enums ---------- DO $$ BEGIN CREATE TYPE public.app_role AS ENUM ('admin', 'moderator', 'user'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE public.payment_method AS ENUM ('paypal', 'crypto', 'bank_transfer', 'stripe'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE public.subscription_tier AS ENUM ('free', 'pro', 'enterprise'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE public.transaction_status AS ENUM ('pending', 'completed', 'failed', 'refunded'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ---------- updated_at trigger fn ---------- CREATE OR REPLACE FUNCTION public.update_updated_at_column() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$; -- ============================================================ -- Tables -- ============================================================ CREATE TABLE IF NOT EXISTS public.profiles ( id text PRIMARY KEY, email text, display_name text, avatar_url text, company_name text, stripe_customer_id text UNIQUE, marketing_consent boolean DEFAULT false, marketing_consent_date timestamptz, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.user_roles ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, role public.app_role NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, CONSTRAINT user_roles_user_id_role_key UNIQUE (user_id, role) ); CREATE TABLE IF NOT EXISTS public.subscription_plans ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, tier public.subscription_tier NOT NULL UNIQUE, name text NOT NULL, description text, price_monthly numeric(10,2) DEFAULT 0 NOT NULL, price_yearly numeric(10,2) DEFAULT 0 NOT NULL, monthly_credits integer DEFAULT 0 NOT NULL, features jsonb DEFAULT '[]'::jsonb NOT NULL, is_active boolean DEFAULT true NOT NULL, stripe_price_id text, stripe_yearly_price_id text, paypal_plan_id text, paypal_yearly_plan_id text, paypal_product_id text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.user_credits ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL UNIQUE, monthly_credits integer DEFAULT 0 NOT NULL, bonus_credits integer DEFAULT 0 NOT NULL, credits_reset_at timestamptz, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.user_subscriptions ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL UNIQUE, plan_id uuid NOT NULL REFERENCES public.subscription_plans(id), status text DEFAULT 'active'::text NOT NULL, billing_cycle text DEFAULT 'monthly'::text NOT NULL, current_period_start timestamptz DEFAULT now() NOT NULL, current_period_end timestamptz NOT NULL, cancel_at_period_end boolean DEFAULT false NOT NULL, payment_method public.payment_method, external_subscription_id text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.app_projects ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, website_url text NOT NULL, app_name text NOT NULL, primary_color text DEFAULT '#22D3EE'::text, accent_color text DEFAULT '#A855F7'::text, navigation_style text DEFAULT 'bottom-nav'::text, features text[] DEFAULT '{}'::text[], app_category text, description text, icon_style text DEFAULT 'modern'::text, splash_screen_style text DEFAULT 'centered-logo'::text, build_status text DEFAULT 'draft'::text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, CONSTRAINT app_projects_user_id_website_url_key UNIQUE (user_id, website_url) ); CREATE TABLE IF NOT EXISTS public.app_builds ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, project_id uuid REFERENCES public.app_projects(id) ON DELETE SET NULL, website_url text NOT NULL, app_name text NOT NULL, package_name text NOT NULL, config jsonb DEFAULT '{}'::jsonb NOT NULL, status text DEFAULT 'pending'::text NOT NULL, progress integer DEFAULT 0 NOT NULL, download_url text, file_size_bytes bigint, error_message text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, CONSTRAINT app_builds_status_check CHECK (status = ANY (ARRAY['pending'::text,'building'::text,'complete'::text,'failed'::text])) ); CREATE TABLE IF NOT EXISTS public.app_templates ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, name text NOT NULL, description text, config jsonb NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.automation_configs ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, project_id uuid NOT NULL REFERENCES public.app_projects(id) ON DELETE CASCADE, user_id text NOT NULL, workflow_type text NOT NULL, is_enabled boolean DEFAULT true NOT NULL, config jsonb DEFAULT '{}'::jsonb NOT NULL, last_run_at timestamptz, next_run_at timestamptz, run_count integer DEFAULT 0 NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, CONSTRAINT automation_configs_project_id_workflow_type_key UNIQUE (project_id, workflow_type) ); CREATE TABLE IF NOT EXISTS public.automation_logs ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, automation_id uuid NOT NULL REFERENCES public.automation_configs(id) ON DELETE CASCADE, user_id text NOT NULL, status text DEFAULT 'pending'::text NOT NULL, message text, metadata jsonb DEFAULT '{}'::jsonb, started_at timestamptz DEFAULT now() NOT NULL, completed_at timestamptz, created_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.credit_packs ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, name text NOT NULL, credits integer NOT NULL, price numeric(10,2) NOT NULL, description text, is_active boolean DEFAULT true NOT NULL, stripe_price_id text, created_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.credit_usage_history ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, amount integer NOT NULL, action_type text NOT NULL, description text, metadata jsonb DEFAULT '{}'::jsonb, created_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.payment_transactions ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, amount numeric(10,2) NOT NULL, currency text DEFAULT 'USD'::text NOT NULL, payment_method public.payment_method NOT NULL, status public.transaction_status DEFAULT 'pending'::public.transaction_status NOT NULL, transaction_type text NOT NULL, reference_id text, external_transaction_id text, paypal_order_id text, paypal_subscription_id text, metadata jsonb DEFAULT '{}'::jsonb, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.bank_transfer_requests ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, transaction_id uuid REFERENCES public.payment_transactions(id), amount numeric(10,2) NOT NULL, currency text DEFAULT 'USD'::text NOT NULL, plan_id uuid REFERENCES public.subscription_plans(id), credit_pack_id uuid REFERENCES public.credit_packs(id), status text DEFAULT 'pending'::text NOT NULL, proof_of_payment_url text, admin_notes text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.chat_messages ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text NOT NULL, role text NOT NULL, content text NOT NULL, project_id uuid REFERENCES public.app_projects(id) ON DELETE CASCADE, created_at timestamptz DEFAULT now() NOT NULL, CONSTRAINT chat_messages_role_check CHECK (role = ANY (ARRAY['user'::text,'assistant'::text])) ); CREATE TABLE IF NOT EXISTS public.consent_records ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id text, email text, consent_type text NOT NULL, consented boolean DEFAULT false NOT NULL, ip_address text, user_agent text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.email_templates ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, name text NOT NULL UNIQUE, subject text NOT NULL, html_content text NOT NULL, text_content text, variables jsonb DEFAULT '[]'::jsonb, is_active boolean DEFAULT true NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.invoices ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, invoice_number text NOT NULL UNIQUE, user_id text NOT NULL, amount numeric NOT NULL, currency text DEFAULT 'USD'::text NOT NULL, status text DEFAULT 'draft'::text NOT NULL, due_date timestamptz, paid_at timestamptz, items jsonb DEFAULT '[]'::jsonb NOT NULL, notes text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.api_configurations ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, name text NOT NULL, provider text NOT NULL, api_key_masked text, is_active boolean DEFAULT false NOT NULL, rate_limit integer DEFAULT 1000, usage_count integer DEFAULT 0, last_used_at timestamptz, config jsonb DEFAULT '{}'::jsonb NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.plugins ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, name text NOT NULL, slug text NOT NULL UNIQUE, type text NOT NULL, description text, config jsonb DEFAULT '{}'::jsonb, is_active boolean DEFAULT false NOT NULL, version text DEFAULT '1.0.0'::text, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.system_settings ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, key text NOT NULL UNIQUE, value jsonb DEFAULT '{}'::jsonb NOT NULL, category text DEFAULT 'general'::text NOT NULL, description text, updated_at timestamptz DEFAULT now() NOT NULL, updated_by text ); CREATE TABLE IF NOT EXISTS public.settings_audit_log ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, setting_id uuid REFERENCES public.system_settings(id) ON DELETE SET NULL, setting_key text NOT NULL, old_value jsonb, new_value jsonb NOT NULL, changed_by text, changed_by_email text, change_type text DEFAULT 'update'::text NOT NULL, created_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.payment_gateway_configs ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, gateway text NOT NULL UNIQUE, is_enabled boolean DEFAULT false, is_test_mode boolean DEFAULT true, sandbox_config jsonb DEFAULT '{}'::jsonb, live_config jsonb DEFAULT '{}'::jsonb, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL ); CREATE TABLE IF NOT EXISTS public.webhook_event_logs ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, gateway text NOT NULL, event_type text NOT NULL, event_id text, status text NOT NULL DEFAULT 'received', payload jsonb DEFAULT '{}'::jsonb, response_status integer, error_message text, processing_time_ms integer, created_at timestamptz DEFAULT now() NOT NULL ); -- ---------- Indexes ---------- CREATE INDEX IF NOT EXISTS idx_automation_configs_project ON public.automation_configs (project_id); CREATE INDEX IF NOT EXISTS idx_automation_configs_type ON public.automation_configs (workflow_type); CREATE INDEX IF NOT EXISTS idx_automation_configs_user ON public.automation_configs (user_id); CREATE INDEX IF NOT EXISTS idx_automation_logs_automation ON public.automation_logs (automation_id); CREATE INDEX IF NOT EXISTS idx_automation_logs_status ON public.automation_logs (status); CREATE INDEX IF NOT EXISTS idx_chat_messages_user_project ON public.chat_messages (user_id, project_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_consent_records_email ON public.consent_records (email); CREATE INDEX IF NOT EXISTS idx_consent_records_user_id ON public.consent_records (user_id); CREATE INDEX IF NOT EXISTS idx_credit_usage_action ON public.credit_usage_history (action_type); CREATE INDEX IF NOT EXISTS idx_credit_usage_user_date ON public.credit_usage_history (user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_app_builds_user ON public.app_builds (user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_app_projects_user ON public.app_projects (user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_settings_audit_log_created_at ON public.settings_audit_log (created_at DESC); CREATE INDEX IF NOT EXISTS idx_settings_audit_log_setting_id ON public.settings_audit_log (setting_id); CREATE INDEX IF NOT EXISTS idx_webhook_event_logs_gateway ON public.webhook_event_logs (gateway); CREATE INDEX IF NOT EXISTS idx_webhook_event_logs_created_at ON public.webhook_event_logs (created_at DESC); CREATE INDEX IF NOT EXISTS idx_webhook_event_logs_status ON public.webhook_event_logs (status); -- ---------- updated_at triggers ---------- DO $$ DECLARE t text; BEGIN FOR t IN SELECT unnest(ARRAY[ 'api_configurations','app_builds','app_projects','app_templates','automation_configs', 'bank_transfer_requests','consent_records','email_templates','invoices','payment_transactions', 'plugins','profiles','subscription_plans','system_settings','user_credits','user_subscriptions', 'payment_gateway_configs' ]) LOOP EXECUTE format('DROP TRIGGER IF EXISTS update_%1$s_updated_at ON public.%1$s;', t); EXECUTE format('CREATE TRIGGER update_%1$s_updated_at BEFORE UPDATE ON public.%1$s FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();', t); END LOOP; END $$; -- ============================================================ -- Seed data -- ============================================================ INSERT INTO public.subscription_plans (tier, name, description, price_monthly, price_yearly, monthly_credits, features, is_active) VALUES ('free', 'Free', 'Get started building apps', 0, 0, 5, '["5 credits / month","Community support","Standard build queue"]'::jsonb, true), ('pro', 'Pro', 'For serious builders', 19, 190, 100, '["100 credits / month","Priority builds","Email support","Custom branding"]'::jsonb, true), ('enterprise', 'Enterprise', 'For teams and agencies', 99, 990, 1000, '["1000 credits / month","Fastest builds","Dedicated support","Team seats","White-label"]'::jsonb, true) ON CONFLICT (tier) DO NOTHING; INSERT INTO public.system_settings (key, value, category, description) VALUES ('credits_per_build', '1'::jsonb, 'builds', 'Number of credits consumed per app build'), ('default_signup_credits', '5'::jsonb, 'app', 'Number of free credits given to new users on signup') ON CONFLICT (key) DO NOTHING; INSERT INTO public.payment_gateway_configs (gateway, is_enabled, is_test_mode) VALUES ('paypal', false, true), ('stripe', false, true), ('coinbase', false, true), ('bank_transfer', false, false) ON CONFLICT (gateway) DO NOTHING;