Security
AppFeedback is pre-launch. The list on the left is what the running system already implements, including backend-only paths. The list on the right is required before paid launch and is not in place yet.
Implemented now
- Members can record private feedback at /inbox/new using a session and trusted Origin. Manual items have no reporter identity.
- Magic-link sessions in HTTP-only cookies, with CSRF and origin checks left on in tests. Sign-in creates a personal workspace.
- SQL tenant and project lookups join on workspace membership; a foreign project returns not found.
- When an SDK key is issued, only its hash is stored and it is scoped to a project. Operators can create a project, copy the key once, and rotate it from Projects & SDK.
- Product policy is private-first: user-authored content stays private until a person reviews an external effect.
- Project POST is same-origin and session-authenticated. The signed-in empty state includes the project form and reveals the key once.
- Preview POST /api/v1/feedback authenticates an active hashed project SDK key. Anonymous reporter identity is a client-generated af_reporter_ token stored only as SHA-256. Missing, invalid, and revoked keys share one 401.
- Preview GET /api/v1/feedback/mine uses the same hashed SDK key and reporter token. It returns that reporter’s own submissions for the authorized project, including merged source rows, without following merge targets or exposing staging flags, notes, or other reporters. Missing reporter on mine, updates, and ack is 401. Unknown well-formed reporter sees an empty page.
- Session-authenticated, same-origin release draft and publish. Publish is transactional and then immutable. Merge of a shipped family returns 409. Publishing does not change item status or visibility and does not publish a public board.
- Preview GET /api/v1/feedback/updates and POST /api/v1/feedback/updates/ack authenticate the hashed SDK key and reporter token. Missing reporter is 401. Unknown well-formed reporter sees an empty page. Those routes are not in the Preview mine rate-limit bucket.
- Preview per-project and per-IP rate limits apply to POST /api/v1/feedback, GET /api/v1/feedback/mine, GET /api/v1/feedback/board, and PUT/DELETE vote. Over budget returns 429 RATE_LIMITED with Retry-After. Client IP is stored only as a SHA-256 hash in the limit table.
- Dashboard feedback reads require a session cookie. Status changes and internal notes also require a trusted Origin. They are not the Preview ingest credential.
- Membership-scoped inbox list and detail. A foreign project or item returns 404, not 403.
- Session-authenticated, same-origin mutations for status, internal notes, private staging, reversible merge, and membership-scoped assign or unassign from the inspector, signed-in /inbox rows, and signed-in /board cards. Leftover (left-workspace) assignees cannot be re-selected. staged is operator intent only and is not externally readable. Public board JSON has no assignee.
- Preview GET /api/v1/feedback/board lists the Preview public board: only items whose that-platform visibility is public. staged remains not externally readable. Hidden, inbox, rejected, and merged sources 404 the same way as missing ids.
- Preview signed reporter JWTs are verified ES256 with per-project public keys registered on Projects & SDK via optional X-AppFeedback-Reporter-JWT. Raw external ids are keyed-hashed (HMAC-SHA256 with server pepper); the JWT sub is not stored. The opaque af_reporter_ header is still required; a bare client-supplied user id is not enough to read mine.
- Votes are one per reporter token hash per canonical item. Vote missing-reporter is 401. GET board without a reporter does not insert a reporter.
- Session-authenticated /board shows the same published set. Publish requires confirmPublic: true after staging. Signed-in /inbox rows and /board cards can show and change a workspace-member assignee.
- Membership-scoped workspace JSON export from /settings/data. A foreign workspace id is 404. The file omits credential hashes.
- A workspace owner can delete the workspace from /settings/data without a support mailbox. Foreign workspace id is 404. The transaction revokes SDK keys, deletes dashboard sessions, and removes tenant rows. A redacted deletion event remains (workspace id, actor user id, project and item counts). Hosted backup purge is not claimed.
- The workspace owner invites members from /team by email. Accept requires a dashboard session whose email matches the invite, plus a hashed expiring token. Membership is re-joined on every private request; a foreign workspace is 404. Leftover assignees cannot be re-selected. The optional af_workspace cookie is HTTP-only and ignored unless the user is a member of that workspace.
- Stripe webhook signatures are verified on the raw request body. Event ids are unique. Dashboard Checkout and Customer Portal are owner-only with Origin and session checks. A foreign workspace id is 404.
- Paying status (whether a reporter currently pays for the customer's app) is written only by trusted server code and stored as paying_at on the reporter. Public HTTP has none of it. It is not AppFeedback workspace Free/Indie billing.
- Store provider secrets are AES-256-GCM sealed per credential row. Wrapping keys live in process environment, not Postgres. Ciphertext moved to another row will not open.
- Paying-supporter counts on dashboard /board Public cards and the inspector are membership-scoped. Preview GET /api/v1/feedback/board does not include paidVotes.
Required before paid launch
- Infrastructure encryption and operational confirmation. This page does not claim encryption at rest is implemented.
- Automated backup restore testing.
- Production-grade abuse controls on public HTTP APIs (WAF, durable distributed limits, operational alerts). Preview PostgreSQL per-project and per-IP limits are not that.
- Reviewed external effects for store replies.
- AI boundaries: customer content treated as untrusted, no silent mutations, and no model access until those controls exist.
SDK keys are public credentials for a project, not dashboard passwords. MCP tokens and media handling are not implemented. Do not send production customer content until production-grade abuse controls exist.