Raxx · internal docs

internal · gated ↑ index

ADR 0004 — raxx-console Stack: Flask + Jinja2 + HTMX + Tailwind CDN

Status: Accepted Date: 2026-04-22 Deciders: product owner (user), software-architect Scope: raxx-console — the operator admin console at console.raxx.app

Context

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.

Decision

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.

Consequences

Positive

Negative / risks

Alternatives Considered

FastAPI + React (same pattern as Antlers, new codebase)

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.

FastAPI + Jinja2 + HTMX

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.

Django + Django admin

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.

Streamlit / Gradio (data-app frameworks)

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.

Full SPA with a separate Flask API (raxx-console-api + raxx-console-ui)

Rejected. Two repos, two deploy pipelines, two sets of logs. Doubles operational burden with no benefit at this scale.

Compliance Checklist

Revisit When