# Quality Software Playbook: How to Build a Site as Good as DevPlace This is a generic, reusable method distilled from how DevPlace was built. It is written so you can apply it to a completely different site, for example a hosting company (accounts, products, orders, provisioning, support tickets, status pages), and get the same stability and quality. It is not about copying features. It is about copying structure, constraints, and verification habits. ## 1. Core philosophy 1. Boring technology, smart edges. One backend language, one database file, server-rendered HTML, plain JavaScript. Put complexity where you can see it (request handlers, templates), keep storage dumb. 2. Fewer moving parts. Every dependency is a way to fail. Prefer the standard library, one process type, one data root. 3. Server-rendered first. The page works without JavaScript. JavaScript only upgrades live behavior (polling, websockets, optimistic UI). 4. One data source, many faces. Every domain object is served as HTML for browsers, JSON for API clients, an agent/tool action for automation, and a documented endpoint. Design the data once, then expose all four. 5. Exact, minimal diffs. Edit existing files, reuse existing helpers, match existing visual structure. Zero tolerance for a second way of doing something that already has one way. 6. Measure, do not theorize. Performance claims come from runs on real machines. If there is no measurement, say so. 7. Loud skips beat silent corruption. Reject unverifiable input with a clear reason and a counter. Never store what you cannot vouch for. Hosting example: a VPS order is one row in one table. The same handler renders the order page (HTML), returns the order (JSON), exposes a `order_create` automation action, and is documented in your API docs. ## 2. Reference stack (proven shape) You can swap brands, but keep the shape: - Backend: one typed web framework (DevPlace: Python + FastAPI). - Templates: server-side templates with one shared instance (DevPlace: Jinja2, single `templates` object imported everywhere). - Frontend: no framework, no build step. Plain ES modules, one class per file, small shared utilities (`Http`, `Poller`, modal, toast). - Database: one embedded relational file with WAL mode for small to medium scale (DevPlace: SQLite via `dataset`, WAL, busy timeout, mmap). Synchronous local calls are fine when they are sub-millisecond; do not make the data layer async for fashion. - Auth: session cookie for browsers plus API key and Basic for machines, resolved in one `get_current_user` choke point. No JWT unless you need third-party verification. - Static assets: content-addressed or boot-versioned URLs served immutable (`/static/v/...`), one helper builds every URL. - Background work: one in-process queue with inline fallback so tests stay deterministic and production stays fast. Hosting example: keep customers, services, invoices, tickets in one SQLite/Postgres schema behind one data module. Serve the control panel as HTML, the reseller API as JSON from the same handlers. ## 3. Repository layout that scales Mirror URLs with code paths, one segment per directory: - `routers/` mirrors the URL tree. Flat file for one resource (`feed.py`), package directory for sub-resources (`projects/`). Each leaf owns an `APIRouter`, the package `__init__.py` aggregates. - `templates/` mirrors pages, extends one `base.html`. Page CSS in `extra_head`, page JS in `extra_js`. Shared partials for repeated UI (avatar link, user link, pagination, report button). - `static/js/` mirrors behavior, one class per file, instantiated as `app`. Shared utilities live once and are reused, never forked. - `static/css/` uses design tokens, no literal colors in page files, one closed breakpoint set, one z-index scale. - `services/` holds background services behind one base class and one manager (start/stop/status/logs). Each service owns its tables and its admin controls. - `tests/` mirrors the same tree by path: `tests/api/...` for HTTP, `tests/e2e/...` for browser, `tests/unit/...` for pure logic. Rule: if you cannot guess the file from the URL, the layout is wrong. ## 4. Single data root and single config module All runtime artifacts live under one root (`DATA_DIR`), every path derived from it in one config module, created at boot: - Database file, uploads, attachments, generated archives, keys, locks, backups, service state. - Stored DB values are relative (`directory` + `stored_name`), so a layout move never needs a DB rewrite. - One `DATA_PATHS` registry lists every path; one `ensure_data_dirs()` creates them before anything writes. Hosting example: `data/` holds `app.db`, `uploads/`, `invoices/`, `backups/`, `locks/`, `keys/`. Point `DATA_DIR` at a volume in prod. ## 5. Request pipeline (same for every route) 1. Single app factory: mount `/static`, register routers, install middleware in fixed order (timing outermost, then security headers, rate limit, maintenance gate, error handlers). 2. One auth resolution: cookie, API key header, Bearer, Basic, all in `get_current_user`. Guards: public read (`get_current_user`), member write (`require_user`), admin (`require_admin`). 3. One response helper (`respond()`): same context renders HTML for browsers and JSON for `Accept: application/json` via a response schema. A context key missing from the schema is dropped from JSON by design, so schemas are explicit contracts. 4. One validation style: Pydantic form models bound from forms, one global validation-error handler (re-render or redirect). 5. One timing mechanism: outermost middleware stamps start time and sets `X-Response-Time`. Never re-time per route. 6. Rate limit only mutations (`POST/PUT/PATCH/DELETE`), keyed by real IP, tunable from settings. Reads stay fast and exempt. ## 6. Data conventions that prevent entire bug classes - Primary keys are opaque string IDs (`uid`), slugs accept slug or bare ID via one resolver. Slugs embed the random tail of the ID, never a timestamp prefix (prefix sharding funnels writes). - Soft delete everywhere user content is removable: `deleted_at` + `deleted_by`, live rows are `deleted_at IS NULL`. Every insert writes both columns, every list filters them. Only garbage collection hard-deletes. One admin Trash restores/purges by event. - Ownership rule: edits are owner-only, deletes are owner-or-admin, checked by one helper. Admin seniority: junior admins cannot act on senior admins. - No N+1: batch helpers (`get_users_by_uids`, counts by ID lists, paginators) are the only way to load related rows in lists. - Idempotent schema init: ensure full column set before indexes, because auto-schema tools create partial tables from first insert. - Toggles revive, never duplicate: look up ignoring deleted flag, stamp on off, clear on on, under one shared stamp for cascades. Hosting example: canceling a server is a soft delete with one stamp covering service, DNS rows, and scheduled jobs, restorable from Trash until GC purges it. ## 7. Frontend rules that keep UI consistent - One base template owns nav, head, modals, toasts, timing badge. Nav active state is one global, never inline path checks. - Never hardcode `/static/...`. One helper builds versioned URLs in templates and one in JS. - Server renders content that exists at request time (SEO, no-JS, speed). Client pipeline is only for live content (chat bubbles, streamed results), always sanitized fail-closed. - Dates stored as UTC ISO, rendered as `