Attach a screenshot to a feedback submission you just sent
/api/feedback-submissions/{id}/screenshotA second request, not folded into the JSON POST above: the same "create the entity, then a dedicated route for its image" shape every other upload in this app takes. Every uploaded byte is decoded and RE-ENCODED through sharp before it ever reaches storage, never stored verbatim: the whole point, since a screenshot is the one attachment surface in this app that could carry a hidden payload for something other than a browser to read later ("AI reading this on the other end", 2026-09-01). See the route's own header for the full four-part threat model (format sniffed against a hardcoded magic-number allowlist before sharp ever sees the bytes; EXIF/XMP/ICC and anything appended past the image's own end-of-file marker dropped by the re-encode; input pixel count capped before decode to refuse a decompression bomb; and an ownership + 10-minute recency check, since the caller's own JWT cannot even SELECT this table to guess at one).
One attachment per submission. A second attempt is a 409, not a silent replace.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Path Parameters
The book_feedback_submissions row id, from the JSON POST's own response.
Request Body
multipart/form-data
The screenshot. Sent as multipart/form-data under the field name file.
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/feedback-submissions/string/screenshot" \ -F file="string"{ "ok": true}Send product feedback (bug report, idea, or general) to Solvintia POST
Reached from NavUser's account menu, present for every signed-in dashboard login regardless of role or vertical: deliberately NOT the guest post-booking review at /api/feedback/{token} above, a different route on a different table for a different audience. Written through the caller's own RLS-scoped client, not service-role: book_feedback_submissions' `tenant_insert_own` policy (migration 0182) already enforces `company_id` and `user_id` both matching the JWT, so this route trusts the database rather than re-checking what it already checks. On success, also fires a best-effort alert email to `ALERT_EMAIL || NOTIFICATIONS_FROM_EMAIL` (the same operator address /api/cron/queue-health alerts to), synchronously rather than through book_job_queue: this is low-volume internal alerting, not a customer-facing send that needs retry resilience. A Resend failure here never turns a successful submission into an error response; the row is already saved. Private inbox by design: nothing this route returns, and nothing /superadmin/feedback shows, is ever surfaced back to the submitter beyond the 200 itself.
Create a client POST
A client with no booking behind it yet. Shares its find-or-create-by-email path with the two staff-side create routes, and reports which branch it took so the UI can say "you have merged into an existing record" instead of silently touching someone else's. With no email there is no dedup key at all, so a fresh row is always created. That is an accepted tradeoff, not a gap: `book_customers` allows a null email and the unique index is total.