Skip to content

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