Status: Accepted
Date: 2026-04-22
Deciders: product owner (user), software-architect
Scope: raxx-console — the operator admin console at console.raxx.app
raxx-console is a separate Heroku app (see docs/architecture/console.md). It needs a UI. The question is what kind. The system is used exclusively by operators (internal staff, small headcount, maybe 5–10 admins at peak). The UI is functional-minimalist: tables, toggles, status badges. There is no public-facing surface, no marketing, no real-time feeds that require WebSocket push. The team already runs a Python Flask app (Raptor) and has validated the Heroku Python buildpack pattern as of PR #137.
Flask + Jinja2 server-rendered templates, HTMX for interactivity, Tailwind CSS loaded via CDN.
One Python process. No separate API layer. No frontend build step. No Node.js dependency in the raxx-console repo. HTML is generated server-side; HTMX swaps fragments via hx-get / hx-post without a full page reload. Tailwind is loaded from the CDN <script> tag for development and from the CDN <link> in production — acceptable for a low-traffic internal tool where caching the CDN URL is effective.
gunicorn process, one buildpack, no build pipeline. Deployments are identical to raxx-api-prod.HttpOnly + SameSite=Strict are the entire auth transport. This closes a class of XSS + token-theft attacks that SPA architectures must defend against explicitly.hx-* routes in Flask); a 1-page guide in console/docs/htmx-patterns.md is sufficient.<script> block. For v1 scope (tables, toggles, status badges) this is not an issue. If v2 needs real-time push, we add a text/event-stream SSE endpoint and a small <script> — still no SPA framework.| safe on user-supplied data. Code review enforces this; there is no user-supplied content to display in v1 (all data comes from Raptor's structured JSON).Rejected. Adds a Node.js/npm build pipeline to raxx-console. Requires a separate Heroku buildpack or multi-buildpack config. Requires CORS plumbing between a separate React SPA and the Flask backend. The SPA pattern introduces token storage decisions (localStorage is off the table; cookie-based SPAs are complex). React is overkill for tables and toggles used by 5 admins.
Considered. FastAPI's async support is attractive for concurrent Heroku Platform API + Cloudflare API + Sentry API calls on the dashboard. However, the asyncio + Jinja2 combination has more rough edges than Flask + Jinja2, and the concurrent calls are easily handled with concurrent.futures.ThreadPoolExecutor in Flask (as used in Raptor already). Not worth the migration from Flask expertise.
Rejected. Django admin gives a lot for free but imposes significant framework lock-in and a learning curve that exceeds the benefit for a bespoke 5-screen tool. Also ships password auth by default — we would spend effort removing it to meet invariant #2.
Rejected. These are notebook-adjacent tools, not production web apps. No production-grade session management, no RBAC primitives, no WebAuthn support. Not appropriate for a tool that gates user-account actions.
Rejected. Two repos, two deploy pipelines, two sets of logs. Doubles operational burden with no benefit at this scale.