Status: PM scope document, 2026-04-30 UTC. Initiated by Kristerpher 2026-04-30. This document captures the product intent, architecture constraints, open decisions, and implementation card breakdown for the public customer support portal.
support.raxx.app is the customer-facing support portal for Raxx. Customers sign in with their existing Raxx passkey, see and interact with their own tickets only, and experience a clean interface that carries Raxx visual identity. FreeScout runs as a private backend; Raptor sits between FreeScout and the customer browser, enforcing a strict per-customer privacy boundary. This is distinct from tickets.raxx.app (the FreeScout admin UI, operator-only).
Kristerpher's direct framing (2026-04-30):
"support should be a front end workflow that allows someone to interface with their tickets. FreeScout becomes the API backend at that point. I think it's fine to be public."
customer browser
└── support.raxx.app (CF Pages, static React SPA — "raxx-support" project)
│ (HTTPS, Cloudflare-proxied)
└── api.raxx.app/api/support/* (Raptor — customer-scoped FreeScout proxy)
│ (server-to-server, FreeScout service token)
└── tickets.raxx.app (FreeScout REST API — private)
raxx-support (new; mirrors raxx-status pattern from #581)support.raxx.app unauthenticated → full-page sign-in gate{ customer_id, customer_email, exp }/api/support/* call requires this JWT as a Bearer tokenjwt.customer_email on every single-ticket endpoint| Rule | Implementation |
|---|---|
| Customer sees only their own tickets | Raptor filters GET /api/support/tickets by requester.email == jwt.customer_email |
| Wrong-customer ticket access returns 404 | Never 403 — do not confirm ticket existence to another customer |
| Internal notes stripped | Thread responses with type: note removed server-side before response leaves Raptor |
| Operator fields never exposed | assignee, tags, component_tag, incident_severity, FreeScout internal IDs, _internal_* custom fields stripped from all responses |
| FreeScout API key stays server-side | Loaded from Infisical at Raptor startup; zero client exposure |
| No "FreeScout" branding | Zero occurrences of "FreeScout" in any customer-facing surface (HTML, JS bundle, error messages) |
| Route | FreeScout mapping | Notes |
|---|---|---|
GET /api/support/tickets |
GET /api/conversations?customer[email]=X |
Filter server-side; paginated |
GET /api/support/tickets/:id |
GET /api/conversations/:id/threads |
Strip type: note; 404 if wrong customer |
POST /api/support/tickets/:id/reply |
POST /api/conversations/:id/threads |
type: message, fromCustomer: true |
POST /api/support/tickets |
POST /api/conversations |
Routes to support@raxx.app mailbox |
PUT /api/support/tickets/:id/resolve |
PUT /api/conversations/:id { status: resolved } |
Requester ownership check required |
| Story | Phase |
|---|---|
| Customer signs in with Raxx passkey | 1 |
| Customer views list of their tickets | 1 |
| Customer reads full ticket thread | 1 |
| Customer posts a reply on a ticket | 1 |
| Customer creates a new ticket | 1 |
| Customer receives email notification when operator replies | 1 |
| Customer marks a ticket resolved | 1 |
| Customer opts out of email notifications | 2+ |
| Customer views inline attachments in thread | 2+ |
| Real-time in-portal reply notifications | 2+ |
tickets.raxx.app)Confidence Engine palette (Direction C, locked 2026-04-25):
- Ink: #0f172a
- Moss: #4a7c59
- Snow/Cream: #f8f5f0
- Amber (WAITING status badge): #d97706
Copy voice: "Raxx Support" everywhere — never "Help Desk", "FreeScout", "Tickets system", or any broker name. Error messages in Raxx voice. Loading states in Raxx voice.
Status vocabulary (customer-visible only):
- OPEN — moss badge
- WAITING FOR YOUR REPLY — amber badge
- RESOLVED — muted/ink badge
UX designer is being dispatched in parallel for mockups. Card 4 (frontend) should implement functional-first; card 7 (mobile + polish) reconciles mockup divergences.
Question: Passkey-only at launch, or passkey + email-OTP fallback for first-time / legacy users?
Context: A customer who receives an operator reply email and clicks "View your ticket" must be able to authenticate to read the ticket. If they are on a device where they have not enrolled a passkey (e.g., a new phone, an email-only device), passkey-only fails them.
Options: - A. Passkey-only — cleanest; requires every customer to have enrolled a passkey first - B. Passkey + email-OTP fallback — covers the "I got an email, I want to read my ticket" case
Recommendation: Option B. The email link flow is a key activation moment; blocking it with "you need a passkey" frustrates customers at their most engaged moment.
Question: Can a customer email support@raxx.app to open a ticket (FreeScout native email intake), OR must ticket creation go through the portal UI only?
Options: - A. Both — email intake stays on + portal UI option. Recommended. - B. Portal-only — disables FreeScout email intake, enforces categorization. More friction for customers.
Recommendation: Option A. Email intake is FreeScout default behavior; disabling it is extra work and hurts customers who prefer email. The customer_raxx_id custom field (card 5) links email-originated tickets back to Raxx accounts retroactively when the customer logs into the portal.
Question: Do we expose the operator's incident_severity field, or normalize to simple labels?
Options: - A. Normalize to: OPEN / WAITING FOR YOUR REPLY / RESOLVED - B. Pass through FreeScout native statuses (active, pending, closed, spam)
Recommendation: Option A. FreeScout's native statuses ("pending", "spam") are operator concepts. Customers should see clean language.
Question: When a customer opens a new ticket, do we ask for a category?
Options: - A. Optional category dropdown: Account / Billing / Bug Report / Feature Request / Other - B. Free-form (subject + body only)
Recommendation: Option A, with the dropdown optional (not required). Operators benefit from routing; customers are not blocked by it.
Question: Do we use FreeScout's native email reply (SMTP via Postmark, already configured in terraform/freescout/) or build a Raptor webhook receiver for more template control?
Options: - A. FreeScout native SMTP — already wired; customize the FreeScout email template to Raxx branding - B. Raptor webhook receiver — more control over Postmark template; more infra work
Recommendation: Option A (scoped in card 6). Faster to ship; FreeScout template customization is well-documented. Migrate to Option B in Phase 2 if template limitations arise.
| # | Title | Size | Blocks |
|---|---|---|---|
| Card 1 | Architect: data model + API contract | S | Cards 3, 4 |
| Card 2 | Infra: DNS + CF Pages + deploy workflow | S | Card 4 |
| Card 3 | Backend: customer-scoped FreeScout proxy API | M | Card 4 (can mock) |
| Card 4 | Frontend: React support portal SPA | L | Cards 7, 8 |
| Card 5 | FreeScout: customer mailbox + custom field setup | S | Card 3, Card 6 |
| Card 6 | Email notifications: operator reply → customer email | S | Card 5 |
| Card 7 | Mobile + iOS Safari polish | S | Card 4 |
| Card 8 | Launch checklist: meta tags, lint, sitemap, robots | S | All cards |
Sprint 1 (in parallel): - Card 1 (architect ADR) + Card 2 (infra shell) + Card 5 (FreeScout mailbox config)
Sprint 2: - Card 3 (backend) + Card 4 (frontend, develops against mocks while Card 3 is in-flight)
Sprint 3: - Card 6 (email notifications) + Card 7 (mobile polish, requires ux-designer mockups)
Sprint 4: - Card 8 (launch checklist) — gate on Kristerpher's DECISION-1 through DECISION-5 answers
tickets.raxx.app (infra predecessor)docs/security/web-surface-posture.md — surface posture matrixterraform/freescout/ — live FreeScout infra