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:
Leon Serfaty
2026-06-28 04:50:29 -04:00
co-authored by Claude Opus 4.8
commit 1a8b8ee5a0
271 changed files with 63734 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
-- Better Auth core schema (v1.x). Identifiers are camelCase and quoted to
-- match Better Auth's default field->column mapping.
CREATE TABLE IF NOT EXISTS "user" (
"id" text PRIMARY KEY,
"name" text NOT NULL DEFAULT '',
"email" text NOT NULL UNIQUE,
"emailVerified" boolean NOT NULL DEFAULT false,
"image" text,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "session" (
"id" text PRIMARY KEY,
"expiresAt" timestamptz NOT NULL,
"token" text NOT NULL UNIQUE,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now(),
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "account" (
"id" text PRIMARY KEY,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamptz,
"refreshTokenExpiresAt" timestamptz,
"scope" text,
"password" text,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "verification" (
"id" text PRIMARY KEY,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" timestamptz NOT NULL,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_session_user ON "session" ("userId");
CREATE INDEX IF NOT EXISTS idx_account_user ON "account" ("userId");
CREATE INDEX IF NOT EXISTS idx_verification_identifier ON "verification" ("identifier");
+413
View File
@@ -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;