This page covers using Livqeno from a browser: what your frontend may hold, and how to tell Livqeno which websites are allowed to connect.
Where this fits: your token endpoint works and your app joins from
localhost. Read this before you deploy your site to a real domain, because
that's when Allowed Origins start to matter.
The short version:
- Your API key never goes to the browser. Your backend makes short-lived grants; the page gets those.
- Localhost just works, on any port, with no setup.
- Production sites are listed per project, under Project Settings → Security → Allowed Origins.
- CORS is only a browser feature. Livqeno keeps projects apart on the server, not with CORS.
1. What CORS is (and isn't)
An origin is a website's address: scheme, host, and port, like
https://app.example.com. CORS (Cross-Origin Resource Sharing) is a
browser rule about origins: a browser lets any site send a request to
api.livqeno.com, but only lets the site read the reply if the reply allows
it.
Two things to know:
- CORS doesn't check who you are. A request with a valid token is allowed
because of the token, whether it comes from a browser,
curl, or a server. - CORS doesn't apply to WebSockets. So Livqeno's RTC and chat servers check
the
Originheader themselves. See section 6.
2. The recommended flow
Your backend holds the API key. The browser holds a grant: the token plus the connection details Livqeno chose for this session.
On your backend, once:
import { Livqeno } from '@livqeno/server';
const livqeno = new Livqeno({
apiKey: process.env.LIVQENO_API_KEY!, // server-side only, always
baseUrl: process.env.LIVQENO_API_URL!, // https://api.livqeno.com
});
app.post('/api/rtc/token', async (req, res) => {
// Identity comes from *your* session. Never from the request body, or any
// caller can claim to be anyone.
const grant = await livqeno.tokens.create({ room: roomId, identity: req.user.id });
res.json(grant);
});In your browser app, pass the grant along unchanged:
import { createRTCClient } from '@livqeno/rtc';
const grant = await fetch('/api/rtc/token', { method: 'POST' }).then((r) => r.json());
const client = createRTCClient(grant); // token, endpoint, iceServers
const room = await client.join(); // the token names its own roomNothing in this frontend code is secret. You don't type any Livqeno address:
endpoint, iceServers, and telemetryUrl all come inside the grant. So the
same code works locally and in production.
3. Localhost development
It already works. http://localhost:3000, :5173, :8080, 127.0.0.1,
[::1]: any loopback address, any port, http or https. Nothing to register.
It is not a wildcard. https://localhost.evil.example is a real domain
someone else owns, so it's rejected. Only true loopback addresses count.
Once you've shipped, you can turn this off with allowLocalhostOrigins: false
or the dashboard toggle. Then only your listed production origins work.
Why you might: with localhost allowed, a page running on anyone's own machine can use a token for your project. Turning it off closes that door.
4. Production origins
The threat: someone copies a valid token from your site and uses it on their own website. When your origins are listed, Livqeno refuses connections from any other site.
Add them per project in the dashboard:
Project Settings → Security → Allowed Originshttps://app.example.com
https://example.comEach entry is a plain origin: scheme, host, and a port if it isn't the default. Bad entries are rejected when you save, and the error names them.
| Entry | |
|---|---|
https://app.example.com | ✅ |
http://localhost:3000 | ✅ (also covered by section 3) |
https://app.example.com/dashboard | ❌ a path is not part of an origin |
https://*.example.com | ❌ wildcards are not supported |
app.example.com | ❌ no scheme |
Why no wildcards? https://*.example.com would also allow any future
subdomain, including one an attacker takes over. List the ones you use.
Matching is exact (after cleanup):
https://APP.example.com/andhttps://app.example.comare the same.https://example.comdoes not coverhttps://app.example.com.
When a change takes effect
- New connections: right away, across all Livqeno servers. Removing an origin blocks it immediately.
- Open connections: not affected. Origin is checked only when a connection
starts. Pages already connected keep working but can't reconnect. To cut
them off now, close the room (
DELETE /v1/rooms/{roomId}).
5. Why the API key must stay on the server
An API key lasts until revoked and works across your whole project. It can make tokens for any room and any user, read diagnostics, and manage rooms. A grant works for one user, one room or conversation, and expires in an hour or less.
@livqeno/server is a backend-only package. If you're importing it in a
component, you want the token flow in section 2 instead.
6. HTTP CORS vs WebSocket Origin validation
Livqeno checks origins differently for HTTP and for WebSockets.
HTTP (/v1/telemetry/* and /v1/chat/*):
- These routes send open CORS headers, so the browser never blocks an SDK call with an unclear "blocked by CORS policy" error.
- After checking the token, Livqeno checks the page's origin for that project.
- If it's not allowed, you get a
403that names the origin and the setting to change.
Why after? A browser's preflight (OPTIONS) request has no token, so at that
point Livqeno can't know which project is asking.
WebSockets (/v1/chat/ws and /v1/rtc):
- There is no browser check, so the server checks
Originitself, right after checking the token. - A rejected connection closes with
ORIGIN_NOT_ALLOWED. - The SDKs report this as a permission error and don't retry. Reconnecting can't fix a dashboard setting.
No Origin header at all? Allowed on both. Browsers always send one, so
a missing header means a server is calling. It still needs a valid token.
The token is the real protection; the origin check is an extra layer.
7. RTC grant architecture
const grant = await livqeno.tokens.create({ room: roomId, identity: session.userId });
// { token, endpoint, iceServers, telemetryUrl, roomName, expiresAt, ... }Send it to the frontend whole. The frontend never needs LIVQENO_API_KEY, an
SFU address, STUN/TURN URLs, or TURN credentials. The TURN credentials are
short-lived and arrive in iceServers.
8. Chat grant architecture
const grant = await livqeno.chat.createToken({
userId: session.userId,
conversations: [conversationId],
});
// { token, chatUrl, apiUrl, scopes, conversations, expiresAt, ... }const chat = createChatClient(grant); // token + chatUrl + apiUrl
await chat.connect();The grant includes chatUrl and apiUrl, so the browser never has to build
a WebSocket address.
On every connection, the chat server checks:
- The token's signature and expiry.
- That the token hasn't been revoked.
- The project it belongs to.
- The user it names. A client can only act as itself; the request body can't
change
senderId. - The conversations it's allowed in.
- The project's allowed origins.
9. Live Streaming authentication
Hosts and viewers get credentials from different methods. The role depends on which method your backend calls, never on a field in a request.
The threat: if the app could ask for a role, any viewer could ask to be the host and take over the stream.
const stream = await livqeno.live.create({ title: 'Launch day', hostIdentity: session.userId });
// Publish access. Call it only after your own authorization check.
const host = await livqeno.live.addHost(stream.id, { identity: session.userId, role: 'HOST' });
// Always subscribe-only on RTC, always MEMBER on chat. No way to widen it.
const viewer = await livqeno.live.createViewerToken(stream.id, viewerId);Both return { streamId, identity, role, rtc, chat?, chatRootMessageId }.
rtc is a normal RTC grant and chat a normal chat grant, so everything above
applies. (livqeno.live and livqeno.liveStreams are the same object; use
either name.) A viewer token can
never publish, and neither token can reach a stream in another project.
Troubleshooting
Origin errors usually appear the first time you deploy to a real domain:
| Symptom | Cause |
|---|---|
403 Origin … is not allowed for this project | The page's origin is not on the list. Add it under Project Settings → Security → Allowed Origins. |
ORIGIN_NOT_ALLOWED on connect | Same, on a WebSocket. Retrying will not help. |
TOKEN_REVOKED on connect | This token was revoked (DELETE /v1/rooms/{roomId}/rtc-tokens/{tokenId}). Mint a new one; retrying cannot help. See RTC authentication. |
blocked by CORS policy on your own API | The failing request is to your backend, not to Livqeno. Configure CORS there. |
| Telemetry POST failing but calls working | Telemetry is best-effort and never blocks RTC. Pass telemetry: false to silence it. |
Related
- Build for production: the rest of the pre-launch steps.
- Security: the bigger picture
- API keys: creating and rotating them
- Access tokens: what's in a grant and how long it lasts