ADR-0045 — Support Portal Topology: CF Pages + Raptor Proxy (Option A)
Status: Accepted
Date: 2026-05-03 UTC
Deciders: software-architect
Scope: Hosting topology for support.raxx.app — which of three architectural options best meets the privacy, brand, and infra constraints
Design doc: docs/architecture/support-raxx-app.md
Refs: Epic #651 · ADR-0028 (status page hosting) · ADR-0002 (no stored credentials)
Context
Three topology options exist for the customer-facing support portal:
Option A: New CF Pages project (raxx-support), static React SPA, calls Raptor API endpoints that proxy FreeScout. No direct customer → FreeScout traffic.
Option B: Public Antlers route inside the existing raxx.app app (e.g., raxx.app/support) that uses the same backend and auth.
Option C: FreeScout's own customer-facing portal UI, re-skinned via the Customization & Rebranding module.
The constraints to evaluate against:
1. No FreeScout/Tagras name ever visible to customers.
2. Privacy boundary enforced server-side (not in the browser).
3. No new Heroku app — Raptor endpoints on existing raxx-api-prod.
4. Confidence Engine visual identity (full design control).
5. Passkey-only authentication (platform invariant).
6. support.raxx.app subdomain (specified in epic #651).
7. CF Access gate absent — this is a public-access surface.
Decision
Option A: New CF Pages project raxx-support, calling Raptor API proxy endpoints.
This mirrors the proven status.raxx.app pattern (ADR-0028) and satisfies all six constraints above. The CF Pages project is a new deployment (raxx-support) distinct from raxx-status and raxx-app. Raptor endpoints are added to the existing raxx-api-prod dyno — no new Heroku app.
Consequences
Positive
- Full design control. The React SPA owns 100% of the visual layer — Confidence Engine tokens, Bandz-in-scenario states, no vendor artifacts.
- Privacy boundary is architectural, not configurable. The Raptor proxy layer enforces the customer-email ownership check server-side. There is no FreeScout public portal to misconfigure.
- FreeScout API key never reaches the browser. The SPA talks only to Raptor. FreeScout's URL and any vendor-identifying strings stay server-side.
- Passkey auth is native. The SPA uses the same WebAuthn flow (#110) as the rest of Antlers. No parallel auth system.
- Pattern reuse. CF Pages project creation, DNS, and CI/CD wiring follow the
raxx-statusplaybook exactly. The S9 sub-card operator knows this pattern. - Blast-radius isolation.
support.raxx.appruns as a separate CF Pages project. A Antlers (raxx.app) deploy cannot affect the support portal and vice versa.
Negative / Risks
- Cross-domain auth complexity.
support.raxx.appis a different origin fromapi.raxx.app. The design resolves this by using JWT Bearer tokens insessionStoragerather than the session cookie — a well-understood pattern but slightly different from the main Antlers cookie flow. - Separate CF Pages project to maintain. One more deployment pipeline. At current team size this is low overhead; the
raxx-statuspattern is already maintained. - Requires new Raptor endpoints. Option A adds ~6 new routes to Raptor. This is a design requirement, not a defect — but it means the backend work (S1–S4) must land before the frontend (S5–S7) is testable end-to-end.
Neutral
- Option A does not preclude eventually embedding the support portal inside Antlers (
raxx.app/support) — the Raptor API contract is the same regardless of which origin the SPA is served from. Migration from separate origin to embedded route is a frontend-only change.
Alternatives Considered
Option B: Public Antlers route at raxx.app/support
Rejected for this design run, not permanently.
Option B would share the Antlers codebase and avoid the cross-domain auth complexity. However, it conflicts with the support.raxx.app subdomain requirement in epic #651. It also couples the support portal release cycle to the main Antlers app — a support portal regression could require an Antlers hotfix. Option B remains a valid long-term consolidation path if the team wants to reduce surface count post-launch.
Option C: FreeScout native customer portal, re-skinned
Rejected.
FreeScout's customer portal has a fixed URL structure under tickets.raxx.app (the same host as the operator UI, which is CF Access gated). Making it customer-accessible without CF Access would expose the operator UI host to the public network. Routing customers to a different subdomain (support.raxx.app) would require reverse-proxy rewriting of FreeScout's HTML — fragile and version-sensitive. The Customization & Rebranding module allows CSS and logo overrides but not full design control; the result would be FreeScout's layout with Raxx colors, not Confidence Engine. Passkey authentication cannot be integrated into FreeScout's auth flow without a custom FreeScout plugin (PHP, non-trivial). Option C fails constraints 1, 4, and 5 without significant custom FreeScout development that would need to be maintained across FreeScout upgrades.