Four days. Four sessions. One massive feature surface (standalone database servers with 5 engines, admin UIs, domain management, backups, users, access control). And now, the consolidation round.
The Problem With Incremental Work
When you build a feature across multiple sessions, each session optimizes locally. Session 1 builds the core. Session 2 audits and fixes bugs. Session 3 adds the missing pieces (provisioning race fix, admin domain collision detection). Session 4 adds overview parity and admin UI security.
Each session is individually solid. But after 4 days: - A 3,194-line monolith handler file has grown organically - The scheduled backup executor has a bug that mirrors a fix already applied to the manual trigger path - Database server domains live in separate columns, invisible to the global domains page - The architecture has drifted from the original stack-app patterns
This is the "works individually, doesn't cohere" problem. And it's why we have a dedicated consolidation round.
What Round 4 Found
The Silent Backup Failure
The critical finding was in sh0-backup/src/scheduler.rs. The manual trigger_backup handler had been fixed on April 8th to try DatabaseServer::find_by_id before App::find_by_id in the volume backup branch. But the scheduled backup executor -- a completely separate code path -- still only looked up apps. Every scheduled Redis volume backup would silently fail.
This is a class of bug that only surfaces through cross-path auditing. The fix was applied on April 8th night, but only to one of two identical code paths. The audit prompt specifically called this out as Item 20, and verifying it was a 30-second read.
The Unified Domains Table
The biggest architectural change was making domains.app_id nullable and adding db_server_id. This was the single most impactful deferred item across all 4 sessions.
The migration strategy for SQLite (which doesn't support ALTER COLUMN ... DROP NOT NULL):
1. Rename existing table to _domains_old
2. Create new table with relaxed constraints + CHECK
3. Copy all existing rows
4. Drop old table
5. Backfill from existing database_servers.server_domain / admin_domain columns
The CHECK constraint ((app_id IS NOT NULL AND db_server_id IS NULL) OR (app_id IS NULL AND db_server_id IS NOT NULL)) enforces that exactly one owner is set.
The ripple effect touched 14 files across 6 crates. Every Domain { app_id: x, ... } construction site needed Some(x) and db_server_id: None. Every domain.app_id != app_id comparison needed .as_deref(). The DomainResponse type gained both optional fields. And the global domains endpoint switched from list_all_with_app_name to list_all_with_owner_name (a UNION query across apps and database_servers).
The Handler Refactor
A 3,194-line handler file is a code smell, but it's also a natural outcome of 4 days of incremental feature work. The split into 9 sub-modules was mechanical but important:
database_servers/
mod.rs (293 lines) -- shared helpers
crud.rs (586 lines) -- create/get/list/update/delete
lifecycle.rs (218 lines) -- start/stop/connection-info
databases.rs (198 lines) -- managed databases
users.rs (332 lines) -- database users + ACL
access.rs (589 lines) -- grants + external access
admin_ui.rs (673 lines) -- admin UI + admin domain
domains.rs (393 lines) -- server domain
logs.rs (68 lines) -- WebSocket log streamingThe key decision was pub(super) visibility for cross-module functions. assign_admin_domain lives in admin_ui.rs but is called from crud.rs (during server creation). Making it pub(super) keeps it internal to the module while allowing sibling access.
The Methodology in Action
The build-audit-audit-decide methodology is about convergence through diverse perspectives:
- Session 1 builds fast and optimistically
- Sessions 2-3 catch bugs and add missing pieces
- Session 4 (this one) steps back and asks: "does the whole thing cohere?"
The answer was: mostly, with two critical fixes (backup executor, domain unification) and one structural improvement (handler split). Each of these was explicitly called out in the audit prompt, which was itself the product of observations accumulated across 4 sessions.
What's Left
Round 4 explicitly deferred 10 items to Round 5, each with a specific rationale: - Visual restyle of Backups/Logs/Settings tabs (cosmetic, not functional) - a11y fixes (important but not blocking) - Insecure admin UI container detection banner (runtime Docker inspect needed) - Custom domain UI for db-servers (architecturally unblocked by this round)
The distinction matters: these are conscious deferrals with documented rationale, not forgotten items. The next session knows exactly where to start.
Takeaway
The consolidation round costs time upfront but prevents the slow accumulation of inconsistencies that makes software hard to maintain. After Round 4, the database server surface is: - Architecturally integrated (unified domains table) - Structurally organized (9 focused sub-modules) - Functionally complete for all audited paths - Ready for manual verification against a 25-point testing checklist
That's the difference between "it works" and "it's shippable."