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
+16 -1
View File
@@ -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;
}
+10
View File
@@ -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}`);
+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();