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 name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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="icon" type="image/svg+xml" href="../public/favicon.svg">
|
||||
<link rel="icon" type="image/png" href="../public/favicon.png">
|
||||
@@ -165,7 +165,7 @@
|
||||
v2.0.4
|
||||
</div>
|
||||
<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">
|
||||
<a href="#quick-start" class="btn btn-primary">Quick Start →</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>
|
||||
<p class="text-muted mb-4">Check the documentation sections above or reach out to the development team.</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2924,7 +2924,7 @@ npm run build
|
||||
</main>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -3,9 +3,8 @@
|
||||
"version": "2.0.2",
|
||||
"private": true,
|
||||
"description": "AppForge is a comprehensive platform for building and managing applications.",
|
||||
"homepage": "https://appforge.wrapcoders.com",
|
||||
"license": "ISC",
|
||||
"author": "WRAPCODERS",
|
||||
"author": "AppForge",
|
||||
"type": "module",
|
||||
"main": "eslint.config.js",
|
||||
"directories": {
|
||||
@@ -16,6 +15,7 @@
|
||||
"dev:web": "vite",
|
||||
"dev:server": "tsx watch server/src/index.ts",
|
||||
"db:push": "tsx server/src/scripts/db-push.ts",
|
||||
"start": "tsx server/src/index.ts",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
|
||||
+16
-1
@@ -1,7 +1,9 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { toNodeHandler } from 'better-auth/node';
|
||||
import { env } from './env.js';
|
||||
import { env, ROOT_DIR } from './env.js';
|
||||
import { auth } from './auth.js';
|
||||
import { withSession } from './middleware/auth.js';
|
||||
import { ensureBuckets } from './lib/storage.js';
|
||||
@@ -64,5 +66,18 @@ export function createApp() {
|
||||
// Fallback for unknown API routes
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import './env.js';
|
||||
import { env } from './env.js';
|
||||
import { createApp } from './app.js';
|
||||
import { pool } from './db.js';
|
||||
import { ensureSchema } from './lib/schema.js';
|
||||
|
||||
async function main() {
|
||||
// verify DB connectivity early
|
||||
@@ -12,6 +13,15 @@ async function main() {
|
||||
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();
|
||||
app.listen(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>
|
||||
|
||||
{/* 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">
|
||||
© {new Date().getFullYear()} {settings.app_name}. All rights reserved.
|
||||
</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>
|
||||
</footer>
|
||||
|
||||
@@ -40,8 +40,8 @@ interface SystemSettings {
|
||||
}
|
||||
|
||||
const DEFAULTS: SystemSettings = {
|
||||
app_name: "My App",
|
||||
app_tagline: "Convert websites to native apps",
|
||||
app_name: "AppForge",
|
||||
app_tagline: "Convert any website into a native mobile app",
|
||||
maintenance_mode: false,
|
||||
demo_mode: false,
|
||||
default_signup_credits: 5,
|
||||
|
||||
@@ -259,7 +259,7 @@ const Settings = () => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
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);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
Reference in New Issue
Block a user