Production deploy setup + finish AppForge rebrand

- Backend now serves the built frontend (dist) + SPA fallback for single-service deploy
- Schema applied idempotently on boot (ensureSchema) + working db:push script
- Add Dockerfile + .dockerignore + start script
- Footer credit removed; default app name -> AppForge; docs/package.json de-attributed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-06-29 02:40:49 -04:00
co-authored by Claude Opus 4.8
parent 1a8b8ee5a0
commit fd72540b6a
11 changed files with 116 additions and 14 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules
dist
.git
.env
.env.local
.env.*.local
server/storage
*.log
.vscode
.idea
android
ios
+26
View File
@@ -0,0 +1,26 @@
# AppForge — single-service production image
# Builds the Vite frontend and runs the Express backend, which serves both the
# API (/api), uploaded files (/storage), and the built frontend (SPA).
FROM node:20-bookworm-slim
WORKDIR /app
# System deps occasionally needed by build tooling
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies (full install — tsx/vite are needed for build & runtime)
COPY package.json package-lock.json ./
RUN npm ci
# Copy source and build the frontend
COPY . .
RUN npm run build
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
# The server applies the DB schema on boot (idempotent) and serves everything
CMD ["npm", "run", "start"]
+4 -4
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AppForge Documentation - Website to Native Mobile App Platform</title> <title>AppForge Documentation - Website to Native Mobile App Platform</title>
<meta name="description" content="Complete documentation for AppForge by WrapCoders — convert websites into native iOS and Android apps with built-in SaaS features, admin panel, and credit system."> <meta name="description" content="Complete documentation for AppForge — convert websites into native iOS and Android apps with built-in SaaS features, admin panel, and credit system.">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/svg+xml" href="../public/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../public/favicon.svg">
<link rel="icon" type="image/png" href="../public/favicon.png"> <link rel="icon" type="image/png" href="../public/favicon.png">
@@ -165,7 +165,7 @@
v2.0.4 v2.0.4
</div> </div>
<h1>AppForge Documentation</h1> <h1>AppForge Documentation</h1>
<p>The complete self-hosted SaaS platform for converting websites into native iOS & Android apps.<br>Built by <a href="https://wrapcoders.com" target="_blank" style="color: var(--accent); font-weight: 600;">WRAPCODERS</a> with React, TypeScript, Express, Postgres, and Capacitor.</p> <p>The complete self-hosted SaaS platform for converting websites into native iOS & Android apps.<br>Built with React, TypeScript, Express, Postgres, and Capacitor.</p>
<div class="flex gap-3 justify-center flex-wrap"> <div class="flex gap-3 justify-center flex-wrap">
<a href="#quick-start" class="btn btn-primary">Quick Start →</a> <a href="#quick-start" class="btn btn-primary">Quick Start →</a>
<a href="#api-reference" class="btn btn-outline">API Reference</a> <a href="#api-reference" class="btn btn-outline">API Reference</a>
@@ -2915,7 +2915,7 @@ npm run build
<h2 class="font-bold mb-4">Still Need Help?</h2> <h2 class="font-bold mb-4">Still Need Help?</h2>
<p class="text-muted mb-4">Check the documentation sections above or reach out to the development team.</p> <p class="text-muted mb-4">Check the documentation sections above or reach out to the development team.</p>
<div class="flex gap-3"> <div class="flex gap-3">
<a href="https://wrapcoders.com" class="btn btn-primary" target="_blank">Visit WrapCoders</a>
<a href="#getting-started" class="btn btn-outline">Re-read Setup Guide</a> <a href="#getting-started" class="btn btn-outline">Re-read Setup Guide</a>
</div> </div>
</div> </div>
@@ -2924,7 +2924,7 @@ npm run build
</main> </main>
<footer class="footer"> <footer class="footer">
<p>© 2026 AppForge by <a href="https://wrapcoders.com" target="_blank" style="color: var(--accent); font-weight: 600;">WRAPCODERS</a>. All rights reserved. · <a href="#top" style="color: var(--text-faint);">Back to top ↑</a></p> <p>© 2026 AppForge. All rights reserved. · <a href="#top" style="color: var(--text-faint);">Back to top ↑</a></p>
</footer> </footer>
</div> </div>
</div> </div>
+2 -2
View File
@@ -3,9 +3,8 @@
"version": "2.0.2", "version": "2.0.2",
"private": true, "private": true,
"description": "AppForge is a comprehensive platform for building and managing applications.", "description": "AppForge is a comprehensive platform for building and managing applications.",
"homepage": "https://appforge.wrapcoders.com",
"license": "ISC", "license": "ISC",
"author": "WRAPCODERS", "author": "AppForge",
"type": "module", "type": "module",
"main": "eslint.config.js", "main": "eslint.config.js",
"directories": { "directories": {
@@ -16,6 +15,7 @@
"dev:web": "vite", "dev:web": "vite",
"dev:server": "tsx watch server/src/index.ts", "dev:server": "tsx watch server/src/index.ts",
"db:push": "tsx server/src/scripts/db-push.ts", "db:push": "tsx server/src/scripts/db-push.ts",
"start": "tsx server/src/index.ts",
"build": "vite build", "build": "vite build",
"build:dev": "vite build --mode development", "build:dev": "vite build --mode development",
"lint": "eslint .", "lint": "eslint .",
+16 -1
View File
@@ -1,7 +1,9 @@
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import path from 'path';
import fs from 'fs';
import { toNodeHandler } from 'better-auth/node'; import { toNodeHandler } from 'better-auth/node';
import { env } from './env.js'; import { env, ROOT_DIR } from './env.js';
import { auth } from './auth.js'; import { auth } from './auth.js';
import { withSession } from './middleware/auth.js'; import { withSession } from './middleware/auth.js';
import { ensureBuckets } from './lib/storage.js'; import { ensureBuckets } from './lib/storage.js';
@@ -64,5 +66,18 @@ export function createApp() {
// Fallback for unknown API routes // Fallback for unknown API routes
app.use('/api', (_req, res) => res.status(404).json({ error: 'Not found' })); app.use('/api', (_req, res) => res.status(404).json({ error: 'Not found' }));
// ---- Serve the built frontend (production single-service deploy) ----
const distDir = path.join(ROOT_DIR, 'dist');
if (fs.existsSync(distDir)) {
app.use(express.static(distDir));
// SPA fallback: serve index.html for any non-API, non-storage route
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith(env.STORAGE_PUBLIC_PREFIX)) {
return next();
}
res.sendFile(path.join(distDir, 'index.html'));
});
}
return app; return app;
} }
+10
View File
@@ -2,6 +2,7 @@ import './env.js';
import { env } from './env.js'; import { env } from './env.js';
import { createApp } from './app.js'; import { createApp } from './app.js';
import { pool } from './db.js'; import { pool } from './db.js';
import { ensureSchema } from './lib/schema.js';
async function main() { async function main() {
// verify DB connectivity early // verify DB connectivity early
@@ -12,6 +13,15 @@ async function main() {
console.error('[server] FAILED to connect to Postgres:', (e as Error).message); console.error('[server] FAILED to connect to Postgres:', (e as Error).message);
} }
// Apply schema on boot (idempotent) unless explicitly disabled
if (process.env.RUN_DB_MIGRATIONS !== 'false') {
try {
await ensureSchema();
} catch (e) {
console.error('[server] schema apply failed:', (e as Error).message);
}
}
const app = createApp(); const app = createApp();
app.listen(env.PORT, () => { app.listen(env.PORT, () => {
console.log(`[server] AppForge API listening on http://localhost:${env.PORT}`); console.log(`[server] AppForge API listening on http://localhost:${env.PORT}`);
+24
View File
@@ -0,0 +1,24 @@
import fs from 'fs';
import path from 'path';
import { pool } from '../db.js';
import { ROOT_DIR } from '../env.js';
/**
* Applies the SQL schema files (idempotent — CREATE ... IF NOT EXISTS) to the
* configured database. Safe to run on every boot; a no-op once the schema
* exists. Set RUN_DB_MIGRATIONS=false to skip.
*/
export async function ensureSchema(): Promise<void> {
const dir = path.join(ROOT_DIR, 'server', 'db');
const files = ['auth-schema.sql', 'schema.sql'];
for (const file of files) {
const p = path.join(dir, file);
if (!fs.existsSync(p)) {
console.warn(`[schema] missing ${file}, skipping`);
continue;
}
const sql = fs.readFileSync(p, 'utf8');
await pool.query(sql);
console.log(`[schema] applied ${file}`);
}
}
+18
View File
@@ -0,0 +1,18 @@
import '../env.js';
import { ensureSchema } from '../lib/schema.js';
import { pool } from '../db.js';
/** Standalone schema apply (npm run db:push). */
async function main() {
try {
await ensureSchema();
console.log('[db:push] schema is up to date');
} catch (e) {
console.error('[db:push] failed:', (e as Error).message);
process.exitCode = 1;
} finally {
await pool.end();
}
}
main();
+1 -4
View File
@@ -67,13 +67,10 @@ const Footer = () => {
</div> </div>
{/* Bottom */} {/* Bottom */}
<div className="border-t border-border pt-8 flex flex-col md:flex-row items-center justify-between gap-4"> <div className="border-t border-border pt-8 flex items-center justify-center">
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
© {new Date().getFullYear()} {settings.app_name}. All rights reserved. © {new Date().getFullYear()} {settings.app_name}. All rights reserved.
</p> </p>
<p className="text-muted-foreground text-sm">
Made with by <a href="https://wrapcoders.com" target="_blank" rel="noopener noreferrer" className="font-semibold text-foreground hover:text-primary transition-colors">WRAPCODERS</a>
</p>
</div> </div>
</div> </div>
</footer> </footer>
+2 -2
View File
@@ -40,8 +40,8 @@ interface SystemSettings {
} }
const DEFAULTS: SystemSettings = { const DEFAULTS: SystemSettings = {
app_name: "My App", app_name: "AppForge",
app_tagline: "Convert websites to native apps", app_tagline: "Convert any website into a native mobile app",
maintenance_mode: false, maintenance_mode: false,
demo_mode: false, demo_mode: false,
default_signup_credits: 5, default_signup_credits: 5,
+1 -1
View File
@@ -259,7 +259,7 @@ const Settings = () => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `wrapcoders-data-export-${new Date().toISOString().split('T')[0]}.json`; a.download = `appforge-data-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);