Backend API reference
Stateless token + room-discovery service for Petal. It exists for one reason: mint scoped LiveKit JWTs server-side so the LiveKit API secret never ships inside a client (the P0 blocker, internal/ISSUES.md). It also provides cross-machine room discovery.
Minimal-backend design (product directive): the only infrastructure is
LiveKit + this lean Vercel function — no database. Rooms live entirely on
LiveKit; discovery reads LiveKit’s active-room list and the human room name rides
in LiveKit room metadata. The only env vars are the three LiveKit creds.
Favorites/recents and logs/stats stay local on the client.
Endpoints
Section titled “Endpoints”| Method | Path | Body | Returns |
|---|---|---|---|
| POST | /api/token |
{ room, identity, displayName? } |
{ url, token, room, displayName? } |
| GET | /api/rooms |
— | { rooms: [{ id, name, open, occupancy }] } |
| POST | /api/rooms |
{ name, open?, room? } |
{ room } (generates or stamps a credential) |
| POST | /api/admin |
{ action: "kick" | "close", room, identity? } |
{ ok, action, room } |
| GET | /api/updater (alias /latest.json) |
— | the Tauri updater manifest, verbatim from Blob |
| GET | /api/download |
— | 302 to the current signed DMG’s Blob URL |
| GET | / |
— | 302 redirect to the marketing site (petal.live) |
room in the token request is the internal credential
room-<32 lowercase hex chars>, derived client-side from the user-facing
abc-defg-hjk access code. Bare human names like eng-sync, old public
<label>-<32hex> credentials, and display labels are rejected and do not mint
tokens. The returned room is the LiveKit room name (petal-room-<credential>),
and displayName is the human room name read from LiveKit metadata when it is
available.
Public room discovery never returns the credential, derived LiveKit room name,
or participant identities; its id is an
opaque stable display/matching id and occupancy is the live participant count.
The create response returns the short access code to the creator. Native clients
that already generated a credential pass it as room so the backend stamps that
LiveKit room’s metadata instead of creating a separate generated room. For those
native credential stamps, open is only an initial value; if server metadata
already exists, the backend preserves its open flag and refreshes only the
display label.
Public token requests always mint visible participant tokens with the fixed
Petal participant profile: publish, subscribe, data publish, and own-metadata
updates enabled; hidden is always false. Caller-supplied grant fields are
ignored for compatibility and must not be used for hidden gallery/bridge
participants. Hidden subscribe-only tokens require a trusted server-owned path
bound to an already-visible participant.
Token identity must be a generated participant id, not a human name. Accepted
forms are the native UUID participant id, the web harness web-<uuid> id, and
the legacy p-...-... generated fallback. Human-readable names belong in
displayName; the backend rejects values such as alice or Jane Doe as
LiveKit identities so callers cannot trivially mint a token whose authenticated
subject is a teammate’s name.
/api/admin is the server-side revocation primitive for operators and future
trusted app flows. It requires Authorization: Bearer <PETAL_ADMIN_TOKEN> and
never accepts a room credential alone as admin authority. action: "kick"
removes one LiveKit participant from the derived room; action: "close" deletes
the LiveKit room. This is not user authentication: invite credentials are still
bearer capabilities until Petal grows a real account/device-attestation layer.
Browser-Origin And Burst Limits
Section titled “Browser-Origin And Burst Limits”The backend accepts native/server requests without an Origin header. Browser
callers must come from the configured allowlist:
- default:
https://app.petal.live,https://meet.petal.live - local development:
http://localhost:<port>andhttp://127.0.0.1:<port> - override: comma-separated
PETAL_ALLOWED_ORIGINS
Disallowed browser origins are rejected before endpoint handlers run. /api/token
and /api/rooms also have best-effort warm-instance token buckets keyed by
forwarded client IP. This is not a global abuse-prevention service across all
Vercel instances, but it closes the unbounded same-instance polling/minting path
and keeps room/identity data out of limiter logs.
⚠️ Lockstep contract
Section titled “⚠️ Lockstep contract”lib/slug.ts MUST stay byte-for-byte behaviorally identical to:
apps/desktop/src-tauri/src/rooms.rs—slugify/livekit_room_name_forshared/logic/meetingCode.ts—slugify/livekitRoomName
If they diverge, the same invite credential maps to different LiveKit rooms on
different clients and people never meet. test/local.ts asserts the exact values
the native Rust unit tests use. Change one, change all three.
Separately, normalizeAccessCode/credentialForAccessCode (the access-code ->
credential half of lib/slug.ts, used for parsing join links) are also
duplicated in web-harness/api/_lib/slug.ts — that project’s own join-link
interstitial (web-harness/api/j.ts, served at meet.petal.live) needs them but
can’t import across Vercel project roots. Keep those two functions in sync too.
Auto-update & distribution
Section titled “Auto-update & distribution”Distribution is private — no public GitHub releases repo. The release CI (built separately) signs + notarizes the app and uploads artifacts to Vercel Blob at fixed, stable pathnames (no random suffix, so this backend can always find “the current one” without any database):
| Blob pathname | What it is |
|---|---|
latest.json |
the Tauri updater manifest, produced by CI |
Petal_<version>_universal.dmg |
the human-facing download |
Petal_universal.app.tar.gz |
the updater artifact tauri-plugin-updater fetches |
Petal_universal.app.tar.gz.sig |
its minisign signature (referenced from inside latest.json) |
This backend only reads from Blob (lib/blob.ts, via list) — CI does
all the writing, this service never uploads anything. Three endpoints serve
that content:
GET /api/updater(also reachable at/latest.jsonvia avercel.jsonrewrite) — fetcheslatest.jsonfrom Blob and returns it verbatim. This is whattauri.conf.json’splugins.updater.endpointsshould point at. Returns204 No Contentif no release has been published yet so the updater treats it as “no update available” instead of logging a launch-time endpoint error.GET /api/download— looks up the currentPetal_<version>_universal.dmgblob (by prefixPetal_/ suffix_universal.dmg— there’s exactly one live at a time since CI always uploads to the same stable pathname pattern) and 302-redirects to its public Blob URL. This is what a website “Download for macOS” button hits.GET /— this project is a pure API host (app.petal.live); it just 302-redirects to the marketing site athttps://petal.live/.
New required env var: BLOB_READ_WRITE_TOKEN (alongside the 3 existing
LIVEKIT_* ones). Despite the name, this backend
only ever calls list with it — Vercel Blob’s list API needs a token
even for read-only access; the actual blob URLs it returns are public CDN
URLs that don’t need the token to fetch. Get it from the Vercel project’s
Storage → Blob tab (same project, or a linked Blob store) and set it as a
Project env var like the LiveKit ones.
Optional admin env var: PETAL_ADMIN_TOKEN enables /api/admin. Without it,
admin-control requests return 503 and no participant kick or room close is
available through the backend.
Local dev + test
Section titled “Local dev + test”Needs a local SFU: livekit-server --dev (listens on :7880, key/secret
devkey/secret).
npm installnpm run typechecknpm run test:local # runs against livekit-server :7880 (real create/list/delete)test/local.ts verifies slug lockstep, JWT grants, a real RoomServiceClient
round-trip against the running server, and rooms-directory idempotency.
Deploy (Vercel)
Section titled “Deploy (Vercel)”- New Vercel project, root directory =
backend/. - Set Project env vars (server-side, never exposed to clients):
LIVEKIT_URL,LIVEKIT_API_KEY,LIVEKIT_API_SECRET(from the LiveKit Cloud project) +BLOB_READ_WRITE_TOKEN(from the project’s Blob store — see “Auto-update & distribution” above). Still no database to provision. - The deployed base URL (
https://app.petal.live) is what the client bakes in as its token endpoint (internal/ISSUES.md) and its updater endpoint (/api/updater, alias/latest.jsonserved from Vercel Blob). The marketing site (https://petal.live) is a separate repo/project (petal-website); the browser SPA and join links live athttps://meet.petal.live(web-harness).
Client config
Section titled “Client config”Desktop clients read PETAL_BACKEND_URL and call this service for
POST /api/token and GET /api/rooms; browser harness builds can set
VITE_PETAL_BACKEND_URL to the same deployed base URL. LiveKit API secrets stay
in this backend’s environment only. Local probe binaries may still use
LIVEKIT_* directly for transport diagnostics, but the app join/gallery/rooms
paths do not.