Raxx · internal docs

internal · gated

Support Portal (support.raxx.app) — Operations Runbook

Status: Pre-launch
Epic: #651
Architecture doc: docs/architecture/support-raxx-app.md
Flag: FLAG_SUPPORT_PORTAL_API (default off)


Required Heroku config vars

Set these before flipping FLAG_SUPPORT_PORTAL_API=1. All values must be silenced to avoid leaking credentials in Heroku's deploy output:

heroku config:set FREESCOUT_API_TOKEN=<value> >/dev/null 2>&1
heroku config:set FREESCOUT_BASE_URL=https://tickets.raxx.app >/dev/null 2>&1
heroku config:set FREESCOUT_SUPPORT_MAILBOX_ID=<value> >/dev/null 2>&1

SESSION_KEY is already required by #1028 (session auth middleware) and does not need to be set again. Verify it exists:

heroku config:get SESSION_KEY | wc -c   # should print > 1

Where to find each value

Config var Source
FREESCOUT_API_TOKEN Infisical /raxx/prod/freescout-api-token. See ADR-0046 for the secret path.
FREESCOUT_BASE_URL https://tickets.raxx.app (fixed for prod). Use http://localhost:9999 in local dev only — the service auto-stubs this in FLASK_ENV=testing.
FREESCOUT_SUPPORT_MAILBOX_ID Retrieve after #707 lands: GET https://tickets.raxx.app/api/mailboxes with Authorization: Bearer <FREESCOUT_API_TOKEN>. Use the id field of the "Support" mailbox.
SESSION_KEY Already set (see #1028).

Migrations

Three migrations must be applied before the flag is flipped on:

backend_v2/db/migrations/005_support_customer_map.sql
backend_v2/db/migrations/006_support_audit_log.sql
backend_v2/db/migrations/007_support_pending_submissions.sql

The migration runner applies them in filename order. Confirm they ran:

SELECT version, applied_at FROM schema_migrations
WHERE version LIKE '00%support%'
ORDER BY version;

Rollback

Flip the flag off immediately if issues are detected:

heroku config:set FLAG_SUPPORT_PORTAL_API=0 >/dev/null 2>&1

All /api/v1/support/* routes return 501 Not Implemented with {"error": "support_portal_api_disabled"} within one dyno restart. Email intake (Postmark → support mailbox) is unaffected — customers can still submit via email to support@raxx.app.

To fully roll back the schema (no customer data in these tables before GA):

DROP TABLE IF EXISTS support_pending_submissions;
DROP TABLE IF EXISTS support_audit_log;
DROP TABLE IF EXISTS support_customer_map;

GDPR / DSR procedure (Open Question OQ-3)

When a customer requests erasure:

  1. Raptor's GDPR DSR flow deletes the users row. The ON DELETE CASCADE constraint on support_customer_map and support_pending_submissions automatically removes all Raptor-side rows.

  2. Manual step (operator): FreeScout conversations that contain PII in the ticket body are NOT automatically deleted. The operator must: a. Open tickets.raxx.app as an admin. b. Search for all conversations with the customer's email. c. Delete or anonymize each conversation within the GDPR 30-day window.

  3. The support_audit_log rows for customer_id = <deleted_user_id> are purged by the nightly GDPR DSR job (same job that handles the main audit_log).

Note: OQ-3 from the design doc is formally documented here. Attorney guidance on whether Raptor-automated FreeScout deletion is required is pending. Until that decision is made, this manual step is the DSR procedure.


FreeScout webhook (support portal events)

A second FreeScout webhook endpoint is provisioned for support portal events:

This endpoint is separate from the status-page webhook at /api/webhooks/freescout. Configure it in FreeScout: Admin → Webhooks → Add webhook → URL: https://api.raxx.app/api/webhooks/freescout/support

2026-07-21 — registered as webhook ID 5 (events convo.customer.reply.created, convo.agent.reply.created — FreeScout has no conversation.replied event; see docs/ops/runbooks/freescout.md §"Failure mode K"). Synthetic HMAC-SHA256/hex signed requests verify correctly (200) on both staging and prod; a live E2E test (real FreeScout operator reply) confirmed the delivery reaches Raptor and is rejected 401 invalid signature — FreeScout's ApiWebhooks module signs with HMAC-SHA1/base64 using a secret derived from its own APP_KEY, which cannot be made to match Raptor's HMAC-SHA256/hex check against an arbitrary vault secret, regardless of what FREESCOUT_SUPPORT_WEBHOOK_SECRET is set to. This is a receiver-code bug, not a secret-provisioning gap — see Failure mode K for the required fix before this webhook can deliver real operator-reply-email notifications end-to-end.


Smoke test

After flipping the flag on:

# 1. Confirm the flag-off path returns 501 (before flip)
curl -s https://api.raxx.app/api/v1/support/tickets \
  -H "Authorization: Bearer test-token"
# Expected: {"error": "support_portal_api_disabled"} HTTP 501

# 2. Get a valid session token (passkey flow)
SESSION_TOKEN="<bearer-token-from-POST-/api/sessions>"

# 3. Verify health endpoint returns 200
curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  https://api.raxx.app/api/v1/support/health
# Expected: {"status": "ok"}

# 4. Verify session endpoint returns customer info
curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  https://api.raxx.app/api/v1/support/session
# Expected: {"customer_id": "...", "freescout_linked": true|false}

# 5. Verify ticket list returns empty array (not 404) for a new customer
curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  https://api.raxx.app/api/v1/support/tickets
# Expected: {"tickets": [], "total": 0}  (or real tickets if the customer has any)

Wiring the SPA to real endpoints (VITE_USE_MOCK_DATA)

The support portal SPA at support.raxx.app (frontend/support/) ships with a mock-data layer gated by the VITE_USE_MOCK_DATA environment variable set in the CF Pages build settings.

VITE_USE_MOCK_DATA FLAG_SUPPORT_PORTAL_API Result
true (default dev) any SPA uses mock fixtures — no Raptor calls
false false (default) SPA calls Raptor; all routes return 501
false true SPA calls Raptor live endpoints — real data

To wire the SPA to real data without a frontend deploy:

  1. Set VITE_USE_MOCK_DATA=false in the CF Pages build environment variable for the raxx-support project (CF dashboard → Pages → raxx-support → Settings → Environment variables → Production).
  2. Trigger a new CF Pages build to pick up the env var change.
  3. Flip the flag on Raptor: heroku config:set FLAG_SUPPORT_PORTAL_API=1 >/dev/null 2>&1

No additional configuration change is needed in the frontend code.

Kill-switch (mock data): Set VITE_USE_MOCK_DATA=true in CF Pages env and rebuild. This reverts the SPA to mock data without touching Raptor.


Monitoring