# Recipe: the default weeny stack (Node + SQLite) Use this unless the app clearly needs something heavier. One Node/Express process, SQLite built into Node 22 (zero deps, no database server to run), scheduled work inside the same process, and whatever frontend fits — plain static files or a bundled (Vite/React) build: a weeny box builds comfortably, and push re-runs the build automatically. When NOT to use this — pick by app, not habit: - A full framework app (Next.js etc.) → https://app.weeny.cloud/recipes/nextjs.txt — and pair it with node:sqlite exactly as below; the database advice here still applies. - Software that truly requires Postgres (an off-the-shelf service that demands it, several apps sharing one DB) → /recipes/postgres.txt. A typical weeny app does NOT need Postgres — SQLite handles its traffic with room to spare and is one less service to run. - A teeny (1 GB) box → /recipes/teeny.txt — server-side builds crawl there; go no-build or build locally and push the output. --- Layout (build this locally) --- myapp/ package.json # {"type":"module"}, deps: express — and NO "build" script (that's the point) server.mjs public/ # index.html, style.css, app.js — served as-is, no bundler .gitignore # one line: data/ # IMPORTANT: the DB lives in data/ ON THE SERVER. push respects # .gitignore, so a re-push can never overwrite live data. --- server.mjs (the whole pattern) --- import express from 'express' import { DatabaseSync } from 'node:sqlite' // built into Node 22+ — not an npm package import { mkdirSync } from 'node:fs' mkdirSync('data', { recursive: true }) const db = new DatabaseSync('data/app.db') db.exec('PRAGMA journal_mode = WAL') // lets a worker/CLI read while the app writes db.exec(`CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY, text TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP)`) const app = express() app.use(express.json()) app.use(express.static('public')) app.get('/api/notes', (req, res) => res.json(db.prepare('SELECT * FROM notes ORDER BY id DESC').all())) app.post('/api/notes', (req, res) => { const info = db.prepare('INSERT INTO notes (text) VALUES (?)').run(req.body.text) res.json({ id: info.lastInsertRowid }) }) // Repeatable tasks: schedule them here, in-process — no crontab, no extra app. setInterval(() => { try { db.prepare("DELETE FROM notes WHERE created_at < datetime('now','-30 days')").run() } catch (e) { console.error('cleanup failed:', e) } // a failed job must not crash the server }, 60 * 60 * 1000) app.listen(3000, '127.0.0.1', () => console.log('up on 3000')) node:sqlite is synchronous — prepare(...).run/get/all, no await. The startup "SQLite is an experimental feature" warning is harmless. (better-sqlite3 also installs fine here and has the same API shape, but the built-in means zero native deps.) --- Frontend: pick what fits --- - Simple UI (forms, dashboards, small tools): plain HTML/CSS/JS in public/, calling your /api routes with fetch. Zero build → every push is live in seconds. For multi-file JS use