Files
devplacepy/quality_software.md
T
retoorandClaude Sonnet 5.5 f55c7ee5f4
DevPlace CI / test (push) Failing after 31m42s
Add community node protocol spec, Agent Skills, and make the failing tests hermetic
Community nodes (Phase 1 of plan.md): the protocol specification in
services/nodes/spec.py as the single source of truth, GET /nodes/spec.json,
a generated protocol docs page, the Community nodes API docs group, and the
implementation plan plus the residential proxy research. The no-open-proxy rule
is enforced in the spec (open_proxy cannot be set).

Agent Skills: register the Skills API docs group, cascade skills on account
deletion, add the nested CLAUDE.md, README and routes docs, and cover the
parser, the fetcher (end to end against a mock origin), the data layer and the
routes with 73 unit and 16 api tests.

Tests made hermetic with the repo's existing patterns instead of ambient
environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the
workspace tests install FakeBackend via runtime.set_backend, and the skills
install tests use run_async. DockerCliBackend now returns 127 (command not
found) when the CLI is missing instead of raising FileNotFoundError, so a host
without docker answers with a clean error rather than a 500. The Providers tab
empty state no longer names the Model routes tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 07:13:25 +02:00

26 KiB

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<ts>/...), 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 <time data-dt> and reformatted to viewer timezone client-side, one format for users (DevPlace: DD/MM/YYYY).
  • Modals, lightbox, toasts, floating windows, scroll memory: one implementation each, reused by class name. No inline JS.

8. Background jobs and services (one pattern)

  • One generic fire-and-forget queue: submit(fn, *args). When the consumer runs, it defers; when it does not (tests, full queue), it runs inline so behavior stays deterministic.
  • One base service class: run loop, supervised start/stop, status, logs, settings. One manager registers all services.
  • One async-job pattern for slow work (archives, audits, reports): enqueue (POST .../run), poll (GET .../{uid}), read report (GET .../{uid}/report), expire and prune by CLI.
  • Multi-worker safety: schema work behind one init lock, background services elected by one service lock so exactly one worker owns them. Never assume single-process in job code.

Hosting example: provisioning, backups, SSL renewal, and usage metering are all job services with run/poll/report plus cli <domain> prune retention commands.

9. Testing: three tiers, one tree, no skips

  • Tiers by fixture, not by folder debate:
    • unit: pure in-process, no HTTP, no browser.
    • api: HTTP integration against a live test server + temp DB.
    • e2e: Playwright browser, real user flows.
  • Test server runs on its own port with its own temp DB and data dir, background services disabled. One serial process, no parallelism, so results are stable.
  • Every run reports every failure (-rf), triage cheapest first: unit+api (~minutes), then browser tier, then --last-failed loop until empty.
  • Required browser habits: explicit domcontentloaded on navigations, visible-state waits over sleeps, scoped selectors, global settings restored in try/finally.
  • Gate before suite: clean app import, per-language syntax check, linter on touched files (catches missing imports that compile checks miss), no forbidden characters or patterns.

Quality bar: the full suite (all three tiers, every test) passes before a change is done. A red test covering your change is the requirement, not a stale artifact. Never narrow a run to make it pass.

10. Extra verification for money, state, and races

Example-based tests are blind to wrong formulas at untried inputs, long-sequence invariant breaks, and races. For spendable resources (credits, coins, quotas), bounded state machines (tiers, levels), or read-then-write mutations reachable from two requests, add four disposable layers against a temp DB (not committed tests unless asked):

  1. Property checks over the full input domain (monotonicity, bounds, non-negativity) across hundreds of values.
  2. Stateful fuzzing: randomized action sequences across many entities, invariants re-checked after every action.
  3. Real-process concurrency: separate OS processes (not threads), fresh production-like rows (never pre-zero NULL columns), exact success counts and exact final totals. Fix races with one atomic conditional UPDATE ... WHERE <precondition> checked by real rowcount at the chokepoint.
  4. Static analysis on every touched file (pyflakes/ruff), which catches runtime NameError paths the other layers may miss.

Hosting example: apply this to credit spend, plan upgrades, affiliate payouts, and concurrent provisioning of the last IP or stock-limited node.

11. Operations: deploy, verify, prune

  • One-command environments: install, dev (reload, one worker), prod (multi-worker, no template reload, real backlog), test*, coverage, load test. Every Python target uses the project venv.
  • No bytecode in repo: PYTHONDONTWRITEBYTECODE=1 for all targets, clean removes venv and stray artifacts.
  • CI runs the full suite serially on every push/PR, publishes coverage and failure screenshots. CI green before merge.
  • Production DB is never touched without explicit written confirmation plus a two-factor token; disposable scripts always target a scratch DB via env override.
  • Retention is a feature: every expirable domain gets prune (expired only) and clear (everything) CLI commands, wired to the same GC the server uses.
  • Backups are first-class: targets, worker, schedules, offload, download restricted to the primary admin, prune missing files.

12. Security, moderation, and trust checklist

  • Passwords hashed with a slow KDF (PBKDF2-SHA256), session tokens random 256-bit, per-process TTL cache keyed by token.
  • Ownership checked server-side on every mutation; deletes confirm explicitly and audit; destructive automation actions require a two-step confirm flag declared in their schema.
  • Every user-visible content table is reportable by construction: registry entry, object URL resolver, report button in the action bar. Private-only tables are listed as unreportable with a reason, enforced by a test that fails on unclassified tables.
  • Outbound HTTP goes through one stealth/guarded client (consistent TLS + headers + proxy + SSRF guard), never bare clients per call.
  • Audit log is append-only, never raises into the caller, with one catalogue of event keys. Admin-only read.
  • Privacy docs and store declarations update in the same change as any new personal-data collection or third-party transmission.

Hosting example: tickets, reviews, and public server comments are reportable; invoices and API tokens are private-by-design with written reasons; provisioning calls go through the one guarded client with proxy support.

13. Docs, SEO, and API surface

  • Every public/auth endpoint gets prose docs plus an interactive API entry (params, auth, sample response). Public pages get SEO context and JSON-LD; sitemap and robots are served, not static.
  • One docs registry (DOCS_PAGES) owns prose pages; API docs own endpoint entries. New route without docs is incomplete.
  • README stays product-facing (routes, config, env vars, features). Nested CLAUDE.md files hold subsystem mechanics next to the code so the always-loaded root doc stays small.
  • Version static assets, canonical URLs per listing, breadcrumbs, related content, and one default OG image.

14. Development flow (the daily loop)

Every feature follows the same eight steps in data-flow order, and a failed step blocks the rest (never skip ahead):

  1. Understand. Read the router, template, matching tests, and the subsystem doc. Trace input model to router to data helper to response (HTML and JSON). Find a relatable existing example and match it structurally and visually; if none exists, stop and agree on the new pattern before building.
  2. Data layer. Query/batch helpers first (never inline N+1), input model, JSON response schema (every exposed key declared or it is silently dropped), idempotent schema/index changes.
  3. Server layer. Handler with the right guard (public/member/admin), specific paths before catch-alls, HTML+JSON via one responder.
  4. View layer. Extend base, reuse partials and globals, page CSS/JS in the standard blocks, one ES class per file.
  5. Safety layer. Reportable/private classification, ownership checks, confirm gates, audit events, privacy-doc update if new personal data or third-party transmission appears.
  6. Agent + docs layer. Tool action per user-askable operation, endpoint doc entry, prose page if user-facing, SEO context and sitemap entry if indexable. This step is the most-forgotten; do not skip it.
  7. Document. Product README for user-visible change, subsystem doc for new mechanics or pitfalls, root doc only for new global rules.
  8. Validate. Clean import, per-language syntax check, linter on touched files, full three-tier suite green (see section 9), plus the money/state/race procedure where it applies.

Failure triage is cheapest-first: fast tiers (unit+api, minutes) before browser tier, then re-run only failures (--last-failed) until empty. One pass reports every failure; fix the list, not one bug per pass.

Session rule: commit, deploy, verify in the same session, in that order. A change that is not running in production is not done. After deploys, verify live; green locally means nothing until prod confirms it.

15. Workflows and automation

  • One command surface. Every repeated action is a named target or CLI command (dev, prod, test, test-fast, test-failed, coverage, load test, prune, backups). No tribal shell one-liners; if it is run twice, it gets a name.
  • DTAP pipeline. Development (reload, one worker) to Test (CI serial suite + coverage + failure screenshots, green before merge) to production (multi-worker, no template reload, real backlog).
  • Scheduled hygiene inside the system, not in heads: retention, cleanup, quota resets, and health checks run on loops or CLI commands with dry-run defaults and narrow scopes for destructive ops. Destructive operations are dry-run first, explicitly confirmed, date-guarded or allowlisted.
  • Load testing is a target, not an event: scripted Locust run against a scratch DB with fixed users, spawn rate, and run time, producing an HTML report.
  • Production guardrails are enforced, not remembered: a hook denies commands reaching the live DB without explicit written confirmation plus a second approval; disposable scripts always target scratch databases via env override.

Hosting example: provision, suspend, backup-run, ssl-renew, usage-meter, invoice-prune are all named commands with --dry-run; nightly loops handle expiry, renewal, and status checks with human-readable pressure states.

16. Agents: in-product assistant, services, and coding help

DevPlace quality comes partly from treating agents as first-class consumers of the same routes humans use:

  • In-product assistant (DevPlace: Devii). A conversational operator over the real API: per-owner sessions and channels (web, terminal, chat bridge), a tool catalog where each tool mirrors one route with matching auth, confirmation gates on destructive tools (first call refused, agent shows exact target, second call passes confirm=true), per-user cost attribution and quotas through one AI gateway, and usage/ledger pages per user. New feature rule: if a user could ask the assistant to do it, it gets a tool.
  • Gateway pattern. Every AI/model call flows through one internal gateway (usage ledger, provider/model routing, per-call cost headers), so spend is attributable and every consumer (correction, modifier, grading, chat) shares quotas and audit.
  • Background service agents. Crawlers, importers, reconcilers, and persona bots run as supervised services under one manager with start/stop/status/logs and admin pages (DevPlace: news importer, bot fleet with live monitor, container reconciler, audit writer). Exactly one worker owns them via a service lock.
  • Coding-agent support. Nested subsystem docs next to the code so an agent loads only what it touches; strict conventions (no second implementation, named helpers, closed token/breakpoint/z-index sets) so generated code matches; tool catalog and docs registry so the agent can act and document through the same contracts.
  • Human authority. Agents propose and execute reversible work; destructive, cross-user, or policy-crossing actions need explicit human confirmation, seniority checks where roles exist, and an audit trail. When a floor is hit (retention minimum, protected path), the agent holds and pages a human instead of deleting.

Hosting example: a support assistant with tools like server_reboot (public read tools open, mutating tools auth-gated, destructive tools confirm-gated), all spend metered per customer through your gateway, provisioning and renewal running as supervised services.

17. Documenting: what lives where, and when

Documentation is layered by audience and distance from the code, and every layer updates in the same change as the code it describes:

  • Source code: no comments, no docstrings. Code is self-documenting through names, small helpers, and one obvious way per task. A decision worth recording goes in the change description or the subsystem doc, never as deliberation text in the source.
  • Subsystem docs (DevPlace: nested CLAUDE.md next to the code). Each directory that owns a mechanic holds its own doc: helpers, pitfalls, cross-layer wiring, worked examples. An agent or human loads only the docs for the area being touched, so the always- loaded cost stays flat as the project grows.
  • Root doc (DevPlace: CLAUDE.md, ARCHITECTURE.md). Global rules, commands, and architecture only. A per-feature detail never lands here; a new global rule always does. The architecture atlas is measured from the running app and schema (counts, diagrams), not written from memory, and disagreements between docs and code are recorded explicitly.
  • Product README. User-visible surface: routes, config, env vars, features, quick start. Updated when behavior a user can see changes.
  • User docs site (DevPlace: /docs with a DOCS_PAGES registry). Prose pages per audience tier plus an interactive API reference where every endpoint entry carries params, auth, and a sample response. A new public or auth endpoint without a doc entry is incomplete by definition.
  • Privacy and store declarations. Any new personal-data category or third-party transmission updates the privacy doc and platform declarations in the same change, never as a follow-up.
  • Event and reason registries. Audit event keys, report reasons, and similar closed sets live in one catalogue each, so every client and test renders the same list.

Rule: docs state what the system does today, with gaps listed next to the code that will close them. No fiction, no roadmap prose.

Hosting example: billing/CLAUDE.md documents proration and retry rules next to the billing code; the root doc only says billing exists and which commands run it; /docs gains a prose page plus one API entry per new endpoint in the same change.

18. Roles, permissions, and ownership

One small role set, enforced server-side at every surface (web UI, REST API, and agent tools alike):

  • Roles (DevPlace: guest, member, admin, primary admin). Stored as exact strings, tested through one is_admin() helper, never hand-rolled compares. Any write to a role invalidates the cached role lookups.
  • Guests read public pages; members write their own content; admins moderate and operate. Public reads resolve the viewer but never require login; every write names its guard explicitly.
  • Ownership first: edits are owner-only, deletes are owner-or-admin, checked by one helper comparing resource owner to actor. Admins can remove abuse but cannot rewrite someone else's words.
  • Seniority among admins: a junior admin (later registration) cannot mutate a more senior admin's account or content. Every per-user admin mutation is gated by one seniority predicate, denials are audited, and the gate is server-side so it also covers agent tools. One primary admin (the first admin account) is the single exception with full visibility and full control, including private-by-admin content and restricted downloads.
  • Visibility predicates are single functions, never inline checks: e.g. can_view(project, user) decides private-content access in one place (owner sees own, admins see members' private rows, but an admin's own private rows stay visible only to that admin except for the primary admin). Container and workspace isolation get their own stricter predicates on the same pattern.
  • Machines authenticate as users: every account holds an API key, plus issuable scoped tokens; the same guards apply regardless of credential type, so the REST API cannot do what the UI forbids.
  • Agents act as the asking user: tool auth mirrors route guards, destructive tools add confirmation gates, and every admin tool passes through the same seniority and audit path as the web UI.

Rule: if a permission check appears twice in different words, one of them is a bug. Hoist it into the single predicate and call it from both places.

Hosting example: guest (pricing, status), customer (own servers and invoices), support (view customers' servers, restart but not delete; junior support cannot touch senior staff accounts), owner (full access plus backup downloads and billing refunds). One can_view_server / can_manage_server pair gates panel, API, and assistant tools together.

19. Replication checklist for your hosting site

Use this order; it is the data-flow order DevPlace follows:

  1. Scaffold: app factory, config with one data root, shared templates, response helper, auth choke point, middleware stack.
  2. Data: users, sessions, settings, then one domain (products). Add soft-delete columns, batch helpers, idempotent init.
  3. Server: public reads first (HTML+JSON via one handler), then member writes with guards, then admin views.
  4. View: base template, tokens, partials, one JS utility set. Server-render listings and detail pages.
  5. Safety: reportable/private classification, ownership checks, confirm gates, audit events.
  6. Automation + docs: tool action per user-askable operation, API doc entry per endpoint, SEO context per public page.
  7. Verification: import gate, lint touched files, full three-tier suite green, plus the four-layer money/state/race procedure where it applies.
  8. Ops: venv targets, CI serial suite, prune/clear CLIs, backups, production-DB guard, deploy then verify live.

20. What to copy literally vs adapt

Copy literally: single data root, soft-delete columns and filters, batch loaders, one response helper, one auth resolver, test tiers and temp-DB server fixture, prune/clear per domain, audit-never- raises, versioned static URLs.

Adapt to hosting: table list (customers, plans, servers, invoices, tickets, status incidents), provisioning job services, payment and registrar integrations behind the one outbound client, usage metering and quota rules, status page and notifications.

If you keep the constraints and the verification loop, the domain can change completely and the quality transfers with it.

Doc history

Version Date Change
1.0.0 2026-10-11 Created: philosophy, stack, layout, data, pipeline, frontend, jobs, testing, verification, ops, security, docs, checklist.
1.1.0 2026-10-11 Added development flow, workflows/automation, and agents sections.
1.2.0 2026-10-11 Added documenting and roles/permissions sections.
1.2.1 2026-10-11 Added doc version header and history per quality_versioning.md.