AppForge: migrate off Supabase to external Postgres + Better Auth + local storage
- 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>
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
-- ============================================================
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user