Shared

Send product feedback (bug report, idea, or general) to Solvintia

post/api/feedback-submissions

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.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

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.