POST /api/feedback takes a feedback submission and fans it out to up to three sinks, each active only when configured: the website tenant database (permanent record), a Slack channel via incoming webhook (how the team sees submissions day to day), and email over SMTP (unused until an email decision exists). Any subset is a valid deployment phase; with none configured the endpoint answers 503 so a form wired up too early fails loudly instead of dropping submissions.
POST /api/feedback
Content-Type: application/json
{
"message": "required, 1 to 5000 characters",
"email": "optional, contact address of the submitter",
"page": "optional, path or name of the page the form was on",
"website": "honeypot: include the field in the form, keep it empty and hidden"
}
Same shape as the brochure site's newsletter endpoint, so form code can treat them alike:
{ "ok": true, "message": "Thanks for your feedback!" }
{ "ok": false, "message": "human-readable reason" } // 400, 500 or 503
A submission with a filled honeypot gets a fake 200 so bots learn nothing. A sink that fails is logged and the submission survives through any other; only all sinks failing is a 500.
| Variable | Sink | Meaning |
|---|---|---|
DATABASE_URL / WEBSITE_DB_* |
storage | The shared pool from src/lib/db; nothing feedback-specific, storage is on wherever the app has a database |
FEEDBACK_SLACK_WEBHOOK_URL |
slack | Slack incoming webhook; deployed it is mounted from Secret Manager by dfo-infra, whose placeholder value disabled (or anything that isn't a URL) means off |
SMTP_HOST / SMTP_PORT / SMTP_SECURE |
SMTP endpoint; SMTP_HOST unset turns email off |
|
SMTP_USER / SMTP_PASS |
optional SMTP auth | |
FEEDBACK_FROM / FEEDBACK_TO |
sender identity and destination inbox |
Rotating the Slack webhook (leak, channel change) is a Secret Manager version plus a redeploy, no code change; see the dfo-infra website stack README.
The feedback table is defined in db/schema.ts with a generated migration under db/migrations, applied like every other migration: just db-migrate locally, the migration Job on deploy (see docs/development.md, Database). Migration 0000's default privileges give the runtime role its DML, so the migration carries no grants of its own.
just db-up && just db-migrate # storage sink on via .env.local's DATABASE_URL
pnpm dev
curl -s -X POST localhost:3000/api/feedback \
-H 'Content-Type: application/json' \
-d '{"message":"hello from local","page":"/data"}'To exercise the Slack sink without posting to the real channel, point FEEDBACK_SLACK_WEBHOOK_URL at a request bin or any local HTTP server; any URL counts as configured.