- New Express + TypeScript backend (server/) with pg, Better Auth, local file storage - De-Supabased Postgres schema (server/db) and TS reimplementations of DB functions - Frontend data layer rewired to REST (rest-client + backend-client compat shim) - Removed all Supabase references (code, config, deps, docs) - New brand assets: gradient favicon/app icons + dark/white wordmark logos Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
414 lines
16 KiB
PL/PgSQL
414 lines
16 KiB
PL/PgSQL
-- ============================================================
|
|
-- 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;
|