Architecture
Version 1 · Initial architecture page: the module registry pattern, URL mounting, and the template/static namespacing constraint — verified directly against qwebhub/modules.py, urls.py, core/views.py, core/templates/hub/index.html, qwebhub/config.py
Historical versionQWebHub Architecture
QWebHub is a shared Django hub that mounts several otherwise-independent Django projects (QDBAdmin, QProcess, QDG KB Viewer) under one server, each under its own URL prefix, plus a landing page listing them.
The module registry (the core pattern)
Everything routes through one file: qwebhub/modules.py. A Module is a
frozen dataclass:
@dataclass(frozen=True)
class Module:
key: str
url_prefix: str
type: str # "django" | "asgi" | "proxy"
project_subdir: str = ""
urlconf: str = ""
app: str = ""
db_alias: str = "qdb"
asgi_factory: str = "" # future
proxy_target: str = "" # future
enabled: bool = True
notes: str = ""
MODULES is a plain list of these. Today it has 3 entries, all type="django":
qdbadmin (→ AdminManagement/QDBAdmin), qprocess (→ QProcess), qdgkb
(→ QDG KB Viewer, the read-only Knowledge Base viewer, db_alias="qdgwiki").
asgi/proxy types are already scaffolded in the dataclass for future
non-Django modules but nothing uses them yet.
Adding a new linked project is one Module(...) entry appended to this
list — nothing else needs to change. This was confirmed by direct code
reading (see ClickUp task 86d40jy36), tracing through every layer below.
How one registry entry becomes a working link
qwebhub/urls.pyloopsenabled_django_modules()and appendspath(m.url_prefix, include(m.urlconf))for each — the root URLconf is entirely generated from the registry, never hand-edited per module.core/views.py'sindexview reads the sameenabled_django_modules()list and passes{key, url_prefix, notes}per module to the template. A separatehealthzview returns{"status": "ok", "modules": [list of keys]}as JSON — a lightweight liveness/inventory check.core/templates/hub/index.htmlloops over that list and renders one card per module (key, notes, link to/{{ url_prefix }}) — nothing about any specific project is hardcoded in the template.
Each Module.project_root resolves project_subdir against PROJECTS_ROOT
(from QWEBHUB_PROJECTS_ROOT env var, or auto-detected as the folder
containing QWebHub itself) — so the same registry works unmodified on a dev
laptop and on the server, as long as sibling project folders exist at the
same relative layout.
Constraint that must never be reintroduced: template/static namespacing
Django's template loader is one flat search list across every configured
directory. Each module's own top-level templates (e.g. base.html) must
live under a subfolder namespaced to that module (templates/core/base.html
for QDBAdmin, templates/racing/base.html for QProcess, core/templates/hub/base.html
for the hub itself) — never a bare templates/base.html. A shared,
un-namespaced file would silently shadow another module's layout with no
error, just a broken-looking page.
This was a real regression, fixed 2026-08-01 across all three modules, and
is documented as a hard rule in qwebhub/config.py's
module_template_dirs(). The same namespacing applies to static assets
(module_static_dirs()) — modules namespace assets below static/<module-key>/
for the same collision-safety reason.
Related pages
- Database Configuration — how
db.jsonresolution and theqdb/qdgwikialiases work. - Odin Deployment — how changes actually reach the server.
- QWebHub Q&A — credential rotation procedures.