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:
co-authored by
Claude Opus 4.8
parent
1a8b8ee5a0
commit
fd72540b6a
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
server/storage
|
||||||
|
*.log
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
android
|
||||||
|
ios
|
||||||
+26
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user