5.0 KiB
Oikos
Self-hosted family planner PWA. Node.js/Express, Vanilla JS (ES modules, no build step), SQLite/SQLCipher, Docker.
Quick Reference
npm start # Production server (PORT from .env, default 3000)
npm run dev # Development with --watch
npm test # All test suites (requires Node ≥22 for --experimental-sqlite)
docker compose up -d # Production deployment
Architecture
server/
index.js # Express entry, middleware chain, static serving
db.js # SQLite/SQLCipher connection, migrations
auth.js # Session auth middleware + login/logout/user-mgmt routes
routes/ # One file per module: tasks, shopping, meals, calendar, notes, contacts, budget, weather
services/ # google-calendar.js, apple-calendar.js, recurrence.js (RRULE)
public/
index.html # SPA shell — single entry point
router.js # History API router (~50 lines, no library)
api.js # Fetch wrapper: auth, CSRF, error handling
styles/ # tokens.css (design tokens), reset.css, layout.css, [module].css
components/ # Web Components: oikos-[module]-[name].js
pages/ # Page modules loaded by router
sw.js # Service worker (app-shell caching)
manifest.json
docs/
SPEC.md # Full product spec — module definitions, data model, design system
Request flow: Client → Express static (public/) or /api/v1/* → auth middleware (session check) → route handler → better-sqlite3 (sync) → JSON response.
No SPA framework. Client-side routing via History API. Pages are ES modules that export a render() function. Web Components for reusable UI. No React, Vue, Svelte, or build tooling.
Code Conventions
- ES modules everywhere (
type: "module"in package.json,import/exportin all JS) - Semicolons: yes
- Web Component prefix:
oikos-(notfb-), one component per file - All UI text in German. Dates:
DD.MM.YYYY. Times:HH:MM(24h) - API responses:
{ data: ... }on success,{ error: string, code: number }on failure - Every route handler:
try/catchwrapping, no unhandled promise rejections - No
eval(), noinnerHTMLwith user input — usetextContentor DOM API - No external frontend dependencies except Lucide Icons (SVG sprite, self-hosted — no CDN at runtime)
- Backend deps minimal: express, better-sqlite3, bcrypt, express-session, express-rate-limit, helmet, dotenv
- Header comment in every file: purpose, module, dependencies
Testing
Tests use Node.js built-in test runner with --experimental-sqlite for in-memory SQLite (no SQLCipher dep in tests). Each module gets a test file in tests/. Run: npm test. Add new tests: create tests/[module].test.js, it auto-discovers via glob pattern.
Security Model
- Auth: Session-based.
express-sessionwith SQLite store,httpOnly + secure + sameSite: strictcookies. 7-day TTL. No public registration — admin creates users. - CSRF: Double Submit Cookie pattern. Backend sets
csrfTokencookie; frontend sends it asX-CSRF-Tokenheader. Validate on all state-changing requests. - Rate limiting: 5 login attempts/min/IP, 15-min lockout via
express-rate-limit. - Passwords: bcrypt, cost factor 12.
- Headers:
helmet()defaults + strict CSP allowing only'self'.
Database
SQLite via better-sqlite3. Optional SQLCipher encryption (AES-256) — enabled when DB_ENCRYPTION_KEY is set in .env.
Migrations: server/db.js runs migrations sequentially on startup. Each migration is an idempotent SQL block in a migrations array. Add new tables/columns by appending to that array — never modify existing entries.
Schema conventions: Every table has id INTEGER PRIMARY KEY, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')). Foreign keys enforced via PRAGMA foreign_keys = ON.
Deployment
# Base: node:20-slim + SQLCipher build deps (libsqlcipher-dev)
# Volume: /app/data (SQLite DB file)
# Expose: 3000
Required env vars: SESSION_SECRET, DB_ENCRYPTION_KEY (optional), PORT (default 3000).
Optional: OPENWEATHERMAP_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET.
Runs behind Nginx reverse proxy with SSL. Example config in nginx.conf.example.
Bootstrap Sequence
When starting from scratch, follow this order:
npm init+ install deps +.env.example+.gitignore+Dockerfile+docker-compose.yml- Express server + SQLite connection + migration runner + auth system
- Frontend app shell: SPA router, nav, layout, CSS design tokens
- Modules one by one (see
docs/SPEC.mdfor detailed specs per module) - Cross-module integrations (meal→shopping, dashboard widgets)
- PWA (service worker, manifest, offline shell)
- Security hardening (CSRF, rate limiting, CSP, input validation)
Read docs/SPEC.md before implementing any module — it contains the data model, UI specs, and design system.