Send product feedback (bug report, idea, or general) to Solvintia
/api/feedback-submissionsReached 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.
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
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/feedback-submissions" \ -H "Content-Type: application/json" \ -d '{ "type": "bug", "message": "string" }'{ "ok": true, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"}Read the organization (API key) GET
The calling organization's own profile and settings. **Shared**, and this is the operation that makes the rest of the surface navigable: `businessType` here is how a program learns which engine it is talking to, and therefore which of the two sets of endpoints below apply to it. Guarding it to one vertical would make that undiscoverable. A **singleton**, so the envelope is `{ data: { … } }` with an object and there is no `nextCursor`, no `limit` and no `cursor`. One envelope, always `data`, so a caller unwraps every v1 response the same way. `timezone` is the org's own IANA zone, and it is what the wall-clock times on `GET /api/v1/service-periods` are expressed in **and what `startsAt`/`endsAt` on the two booking lists are expressed in too**. A program reading bookings needs this endpoint to interpret them correctly, so a key granted only `appointments:read` or `reservations:read` cannot resolve them; grant `organization:read` alongside. See the timestamps section of `docs/api-keys.md`. `currency` and the `deposit*` fields are what a program needs to quote a deposit without guessing. There is deliberately **no `organization:write` scope** to pair with this. Migration 0022 exists because a tenant could edit its own `slug`, `status` and `business_type` outside the app; handing that back to a long-lived program-held credential would undo it. `solvintia_client_id` is withheld: it links this org to a record in a different Solvintia product and means nothing here.
Attach a screenshot to a feedback submission you just sent POST
A 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.