The 10 Commandments - Homelab Control Plane¶
These rules are non-negotiable and apply to all nodes in the homelab.
1. Database/Config Table is the Source of Truth¶
The Rule: - All configuration originates from a database table (PostgreSQL, SQLite, etc.) - There is ONE authoritative location for each type of configuration - Everything else is derived/generated from this source
Why: - Single version of truth prevents conflicts - Can query to understand system state - Changes are logged in database history - Constraints can prevent invalid states
Example (CharlieHub):
✅ CORRECT:
domains table in PostgreSQL → Domain Manager API → generated routes.yml
❌ WRONG:
Manual YAML file → Traefik
(What if someone edits the YAML directly?)
2. All Changes Go Through the API¶
The Rule: - Do not modify the source database directly - Always use the API layer (validation, constraints, audit) - The API is the only sanctioned modification path
Why: - API enforces validation rules - API can log changes and enforce audit trails - Database constraints are checked - Business logic lives in one place
Example (CharlieHub):
✅ CORRECT:
curl -X POST http://localhost:8001/api/domains \
-H "X-API-Key: $KEY" \
-d '{"domain": "api.example.com", "backend_host": "...", ...}'
❌ WRONG:
docker exec postgres psql -c "INSERT INTO domains ..."
(Bypasses validation, constraints may fail silently, no audit)
3. NEVER Edit Generated/Output Files Directly¶
The Rule: - Output files are outputs, not inputs - They are regenerated from source config - Direct edits will be overwritten
Why: - Changes get lost on next generation - Creates confusion about where config lives - Prevents understanding the true state
Example (CharlieHub):
❌ WRONG:
# Don't do this - it will be overwritten
vim /opt/charliehub/traefik/config/generated/routes.yml
✅ CORRECT:
# Update the database
curl -X PUT /api/domains/42 ...
# Routes are regenerated automatically
4. NEVER Create Manual Configuration Files¶
The Rule: - Do not create /config/temp-route.yml - Do not add "emergency" routes and promise to clean up later - Do not create workaround config files
Why: - Temporary becomes permanent - Creates shadow configurations - Hard to track what's actually running - Next person doesn't know it's temporary
Example (CharlieHub):
❌ WRONG:
# Don't create this
sudo vim /traefik/config/core/temporary-route.yml
# (It won't be tracked, will be forgotten, will become permanent)
✅ CORRECT:
# Use the database with status='disabled' for temporary needs
curl -X PUT /api/domains/42 -d '{"status": "disabled"}'
# To re-enable: status='active'
5. NEVER Use Docker Labels for Configuration¶
The Rule: - Do not add Traefik labels to docker-compose.yml - Do not try to configure via container environment variables - Do not add provider labels to services
Why: - The Docker provider may be disabled - Labels create another source of truth - Hard to see all configuration in one place
Example (CharlieHub):
❌ WRONG:
# This won't work - Docker provider is disabled
services:
my-service:
labels:
traefik.http.routers.service.rule: "Host(`api.example.com`)"
✅ CORRECT:
curl -X POST /api/domains \
-d '{"domain": "api.example.com", "backend_host": "my-service", ...}'
6. NEVER Bypass the Validation Layer¶
The Rule: - Constraints exist for a reason - If the API rejects something, don't work around it - Invalid states should be impossible
Why: - Constraints prevent data corruption - Constraints maintain invariants (business rules) - Bypassing one breaks the whole system
Example (CharlieHub):
❌ WRONG:
# API rejects this because TCP routes can't use CORS
curl -X POST /api/domains \
-d '{"protocol": "tcp", "cors_enabled": true}'
# Response: 422 Unprocessable Entity
# Don't try to insert directly into database
docker exec postgres psql -c "INSERT INTO domains (protocol='tcp', cors_enabled=true)"
# (Database will reject it with CHECK constraint violation)
✅ CORRECT:
# Either remove cors_enabled OR change to protocol='http'
curl -X POST /api/domains \
-d '{"protocol": "tcp", "cors_enabled": false}'
7. Extend the API, Don't Bypass It¶
The Rule: - If the API can't express what you need, extend the API - Never bypass the API with manual edits or direct SQL - Extensions should follow established patterns
Why: - Extensions are reusable (other people benefit) - Extensions are auditable (git history) - Extensions are tested (other people found bugs) - Bypasses compound into technical debt
Example (CharlieHub TCP Routing):
When MQTT routing was needed:
❌ WRONG (Bypass):
Create /config/core/01-mqtt.yml manually
Hard to update, hard to track, hard to audit
✅ CORRECT (Extend):
1. Extend database schema: Add protocol field
2. Extend models: Add protocol + tcp_entrypoint fields
3. Extend generator: Handle TCP routers
4. Use API: POST /api/domains with protocol='tcp'
Result: Reusable pattern for SSH, PostgreSQL, Redis, etc.
8. Output Files Are Read-Only Artifacts¶
The Rule: - Treat generated files as output, not input - Do not commit generated files with manual edits - Changes should be made at the source, not the output
Why: - Source is the source of truth - Output regenerates from source - Manual edits to output are lost
Example (CharlieHub):
routes.yml is generated from:
PostgreSQL domains table
↓
Domain Manager API
↓
traefik_generator.py
↓
/traefik/config/generated/routes.yml (READ-ONLY)
❌ Don't edit routes.yml directly
✅ Update the database, regenerate
9. No Shadow Configurations¶
The Rule: - All configuration lives in one place - No temporary routes - No workarounds that become permanent - No emergency fixes that never get cleaned up
Why: - Shadow configs become permanent - Hard to know what's actually running - Creates inconsistency and confusion
Example (CharlieHub):
❌ WRONG:
# "Temporary" solution
sudo vim /traefik/config/core/emergency-route.yml
# Promises: "I'll clean this up later"
# Reality: It's been there for 6 months
✅ CORRECT:
# If something needs temporary disable
curl -X PUT /api/domains/42 -d '{"status": "disabled"}'
# If something is experimental
curl -X POST /api/domains -d '{"status": "draft", ...}'
# Everything is tracked, nothing is hidden
10. When Unsure: STOP and Propose an Extension¶
The Rule: - If you're about to bypass the system, STOP - Propose an extension instead - Work through the proper channels
Why: - Bypasses compound exponentially (precedent) - Extensions compound into capability - System gets stronger, not weaker
Example Decision Tree:
Want to make a change?
↓
Can the API express it?
├─ YES → Use the API ✅
└─ NO → Go to next step
↓
Propose extending the API
├─ Add new field to database ✅
├─ Update models.py ✅
├─ Update generator ✅
└─ Now use the API ✅
❌ DON'T: Edit YAML, bypass API, direct SQL
Summary¶
| Rule | What This Prevents |
|---|---|
| #1: Single source | Multiple conflicting config sources |
| #2: API only | Bypassed validation, missing audit |
| #3: Don't edit output | Lost changes, confusion about state |
| #4: No manual files | Temporary becomes permanent |
| #5: No Docker labels | Disabled provider, scattered config |
| #6: Don't bypass validation | Invalid states, data corruption |
| #7: Extend, don't bypass | Technical debt, reusable patterns |
| #8: Output is read-only | Overwritten edits, confusion |
| #9: No shadow config | Hidden state, inconsistency |
| #10: Propose extensions | Exponential technical debt |
Questions?¶
If you need to make a change: 1. Check the documentation for your node type 2. Use the API if it supports your change 3. If not, propose an extension 4. Never bypass the system with manual edits