ADR-0046 — Support Portal: FreeScout API Token in Infisical (not SSM)
(Renumbered from 0044 → 0046 to avoid collision with PR #996's ADR-0044 which claimed the slot first.)
Status: Accepted
Date: 2026-05-03 UTC
Deciders: software-architect
Scope: Secret store selection for the FreeScout API token used by Raptor's support portal endpoints
Design doc: docs/architecture/support-raxx-app.md
Refs: Epic #651 · ADR-0002 (no stored credentials)
Context
Raptor's /api/v1/support/* endpoints proxy FreeScout using a FreeScout API token. This token is a server-side secret: it must never touch the browser bundle, CF Pages env vars accessible to client code, or any observable response. It must be rotatable without a code redeploy.
The constraint document asks us to choose between AWS SSM Parameter Store and Infisical and justify the decision. The project memory file feedback_aws_workloads_use_ssm_not_vault.md indicates SSM is preferred for AWS-hosted workloads. Raptor runs on Heroku, not AWS.
Decision
Use Infisical for the FreeScout API token and webhook secret.
The two secrets are:
- /raxx/prod/freescout-api-token → Heroku config var FREESCOUT_API_TOKEN
- /raxx/prod/freescout-support-webhook-secret → Heroku config var FREESCOUT_SUPPORT_WEBHOOK_SECRET
Both are surfaced to Raptor as Heroku config vars at dyno boot. Infisical's Heroku integration (or CI-driven Heroku config:set) syncs them. Rotation: update the value in Infisical, re-sync to Heroku config, Heroku restarts the dyno. No code redeploy required.
Consequences
Positive
- Consistent with all other Raptor secrets — the project already uses Infisical for Heroku-hosted services. Adding SSM would require an IAM user, an AWS SDK dependency in Raptor, and cross-provider secret management overhead.
- Rotation path is established: Infisical → Heroku config var → dyno restart (sub-30-second).
- Auditable: Infisical logs every secret access and rotation event.
- The Infisical path prefix
/raxx/prod/is consistent with existing secrets (/raxx/prod/session-jwt-secret, etc.).
Negative / Risks
- Vault folder must exist before writing. Per
feedback_vault_folder_must_exist.md, Infisical returns 404 if/raxx/prod/does not exist as a folder. Operator must verify the path exists before the S1 sub-card sets the secret. - Heroku config:set echoes secrets. Per
feedback_heroku_config_set_echoes_secrets.md, anyheroku config:setcommand must append>/dev/null 2>&1to suppress stdout. This is a CI/deploy script concern, not a Raptor concern — documented in the S9 operational sub-card.
Neutral
- SSM would be appropriate if Raptor migrated to an AWS-hosted runtime (ECS, Lambda). At that point, the SSM preference from
feedback_aws_workloads_use_ssm_not_vault.mdwould apply and the secret path would change. Infisical can co-exist with SSM; this is not a locked-in decision.
Alternatives Considered
Alternative A: AWS SSM Parameter Store
Rejected for Heroku-hosted Raptor. SSM requires either (a) AWS SDK in Raptor with an IAM user and access keys — adding two new secrets to manage a secret, or (b) a Lambda/ECS sidecar to sync SSM → Heroku env vars. Either path adds more complexity than Infisical's existing Heroku integration provides. The feedback_aws_workloads_use_ssm_not_vault.md preference applies to AWS-native workloads; Raptor is not one.
Alternative B: Heroku config var only (no external vault)
Rejected. A secret set only in Heroku config has no audit trail, no rotation automation, and no DPA-ready access log. Per platform invariant, secrets must be in a managed secret store with rotation and audit capability.
Alternative C: CF Pages environment variables (client-accessible)
Rejected outright. The FreeScout API token in CF Pages env vars could be read by any CF Pages build step or — worse — accidentally bundled into the static output. The architecture invariant is absolute: the FreeScout API token is a server-side secret only. It lives in Raptor (Heroku), never in CF Pages.