maravilla-media-rooms
Maravilla media rooms
Server-managed video/audio rooms backed by LiveKit. The Maravilla runtime exposes a thin platform.media surface that lets your server create rooms, mint participant tokens, and tell clients where to connect — clients then use the LiveKit JS SDK with the issued token to join the room.
platform.media is optional. If the project hasn't been configured with a media provider, getPlatform() returns media: undefined. Always check before using:
import { platform } from '~/lib/platform.server';
if (!platform.media) {
throw new Error('Media not configured for this project');
}
mediaUrl() separately returns string | null — null means "media is supported but no LiveKit URL is configured yet." Treat both as feature gates, not errors.
Surface
MediaService, MediaRoomInfo, MediaRoomInfoSettings, MediaParticipantInfo, MediaTokenResult are exported from @maravilla-labs/platform — import the types and let the IDE / tsc resolve. Method list:
| Method | Returns |
|---|---|
createRoom(roomId, settings?) |
MediaRoomInfo — idempotent; returns the existing room if roomId already exists |
deleteRoom(roomId) |
void |
listRooms() |
MediaRoomInfo[] |
generateToken(roomId, participant) |
MediaTokenResult — { token, url } |
mediaUrl() |
string | null — null means "no LiveKit URL configured" |
Notes worth knowing without opening the file:
MediaParticipantInfo.identityMUST be unique within a room. A second connect with the same identity boots the prior session — useuser_idfor single-tab, append a nonce (${user_id}#${tabId}) for multi-tab.canPublish/canSubscribe/canPublishDataall default totrue. Set explicitly if you need a listener-only role.MediaRoomInfoSettings.emptyTimeoutSecsis server-side; 60–300 is the usual range. Short = clean rooms, breaks "BRB" UX.MediaTokenResult.tokenis a JWT with an embedded TTL (~6h server default). Re-issue on rejoin; don't cache past the page lifetime.
Pattern: idempotent room creation + per-user token
A canonical "huddle" flow: every authenticated user can join a shared room scoped to a parent resource (huddle:<groupId>). The server provisions the room on first join (idempotent), then mints a token bound to the user's identity.
// app/routes/api.huddles.$groupId.join.ts (or +server.ts / api route)
import { platform } from '~/lib/platform.server';
import { requireUser } from '~/lib/auth.server';
export async function action({ request, params }: ActionArgs) {
const session = await requireUser(request); // 3-step auth contract — see [auth](../maravilla-auth/SKILL.md)
if (!platform.media) throw new Response('Media disabled', { status: 503 });
const roomId = `huddle:${params.groupId}`;
// Idempotent: createRoom returns the existing room if it exists.
await platform.media.createRoom(roomId, {
maxParticipants: 12,
emptyTimeoutSecs: 60, // tear down 60s after last participant leaves
});
const { token, url } = await platform.media.generateToken(roomId, {
identity: session.id, // collisions are not allowed; user_id is canonical
name: session.email.split('@')[0],
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
return { token, url, roomId };
}
Browser side, with the LiveKit SDK:
import { Room, RoomEvent } from 'livekit-client';
const { token, url } = await fetch(`/api/huddles/${groupId}/join`, { method: 'POST' }).then((r) => r.json());
const room = new Room({ adaptiveStream: true, dynacast: true });
await room.connect(url, token);
room.on(RoomEvent.ParticipantConnected, (p) => console.log('joined', p.identity));
await room.localParticipant.setCameraEnabled(true);
await room.localParticipant.setMicrophoneEnabled(true);
Presence: combine with realtime
platform.media only handles the call itself — it doesn't tell other parts of your app who's currently in a huddle. For presence (e.g. "5 people in this huddle right now"), use realtime channels:
// On join: publish presence on a realtime channel scoped to the same room.
await platform.realtime.presence.join(`huddle:${groupId}`, session.id, {
display_name: session.email.split('@')[0],
});
Then any +page.server.ts load can presence.members(channel) to render the live roster, and an onChannel({ channel: 'huddle:*', type: 'presence' }) handler can mirror it into KV for cross-tab readers (see events).
Token lifecycle
- Tokens are JWTs with embedded grants and a TTL (server default ~6h). Re-issue when a user rejoins; don't cache them client-side past the page lifetime.
- The same
identityconnecting twice will boot the previous connection. If you need multi-tab participation, append a tab-local nonce:${session.id}#${tabId}. - Revoking access =
deleteRoom(roomId). There's no per-user kick API in v1 — re-create with a tighter ACL or filter at your app layer.
Footguns
- Don't mint tokens in the browser. The token grants room access — minting it client-side leaks the LiveKit API key. Always go through a server route.
platform.mediais optional. Code defensively (if (!platform.media)) — your app must work in dev where LiveKit isn't wired up.identityis the join key. Re-using an identity disconnects the prior session. Useuser_iddirectly for "one tab per user" UX, or append a nonce for multi-tab.emptyTimeoutSecsis server-side. A short timeout cleans up zombie rooms but breaks "be right back" UX. 60–300 is the usual range.
See also
- maravilla-realtime — for presence rosters that mirror the call
- maravilla-auth —
setCurrentUsermust run before any protected/api/huddles/...route - LiveKit JS SDK docs — https://docs.livekit.io/client-sdk-js/
- Live Maravilla reference — https://www.maravilla.cloud/llms-full.txt
More from maravilla-labs/maravilla-cli
maravilla-auth
Maravilla Cloud authentication. Use whenever wiring login/register/session, OAuth callbacks, resource policies, or hitting `platform.auth.*` APIs. Critical: the 3-step request-scoped contract (validate → setCurrentUser → can) — skipping any step silently breaks Layer-2 policies and owner-scoped reads return empty with no error.
12maravilla-events
Maravilla Cloud event handlers — files in `events/*.ts` auto-discovered by the framework adapter. Use to react to data changes (`onKvChange`, `onDb`), auth lifecycle (`onAuth`), schedule (`onSchedule`), queue messages (`onQueue`), realtime publishes (`onChannel`), deploy phases (`onDeploy`), object storage (`onStorage`), or arbitrary REN events (`defineEvent`). Run inside the Maravilla runtime with full platform access via `ctx`.
12maravilla-workflows
Maravilla Cloud durable workflows — replay-based, multi-step processes that survive restarts. Use whenever you need sleeps spanning minutes/hours/days, multi-step pipelines where each step's output feeds the next, waiting for external events, or strict step-history audit. `defineWorkflow` from `@maravilla-labs/functions/workflows/runtime` with `step.run`, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, `step.invoke`.
12maravilla-media-transforms
Async media + document derivations via `platform.media.transforms` and the declarative `transforms` block in `maravilla.config.ts`. Media: transcode video, thumbnail extraction, image resize/variants, OCR. Documents (.docx/.odt/.pptx/.xlsx/...): convert to PDF, render page thumbnails, generic format conversion, Markdown extraction (RAG-ready), single-file HTML with inlined images, image-replacement templating ({{TAG}} swap + named-object swap), QR-code injection. Use when ingesting user uploads that need normalised renditions, generating contracts/invoices from templates, or extracting structured content for LLMs. Critical: derived keys are content-addressed — `keyFor(srcKey, spec)` is known up front, before the worker starts, so clients can render placeholder UI without round-trips. Declarative config is the default; imperative `transforms.*` calls are for one-offs.
12maravilla-db
Maravilla Cloud document database — MongoDB-style queries, secondary indexes, and vector search. Use for structured app data, multi-field queries, sorting, semantic search via `findSimilar` / hybrid `find` with `options.vector`. Exposed as `platform.env.DB`. Vector indexes support int8/bit quantization, matryoshka, and multi-vector (ColBERT) out of the box.
11maravilla-config
The `maravilla.config.ts` declarative project file. Use whenever creating or modifying auth resources, groups, relations, registration fields, OAuth providers, password/session policy, branding, database indexes, or media transforms. Reconciled into delivery on every deploy — partial adoption is supported (omit a section to leave it untouched).
11