Authentication
AppLooma RTC uses two credentials with two different lifetimes. A long-lived key pair authenticates your server to us. A short-lived token authenticates one participant, in one channel, for one session. Getting this split right is the whole of security on the platform.
The two-tier model#
| Credential | Who holds it | Lifetime | What it grants |
|---|---|---|---|
API key pair (ak_… / as_…) | Your server only | Until you rotate it | Full access to the project: issue any token, call every REST endpoint. |
| Token | One client, one participant | 6 hours | Join one named channel, with one identity and one role. |
| App ID | Anyone, including your client app | Permanent | Nothing on its own. It only identifies the project. |
Never embed the API secret in an app
Mobile binaries can be unzipped and JavaScript bundles can be read. Anyone who extracts your secret can issue unlimited tokens on your account, join any channel, and spend your quota. There is no client-side use for the secret — not in React Native, not in a Flutter app, not behind obfuscation. Tokens are issued server-side, always.
Authenticating your server#
Every server-to-server call carries the key pair in two headers.
x-applooma-key: ak_0f31c9a2b7d84e6f1c0a5b3e9d72f418x-applooma-secret: as_9c4b1e07af35d2680b7c1f4ae83d59c2a70e1b6f
A missing header, an unknown key, a wrong secret or a deactivated key pair all return 401. The secret is compared against a stored hash — we never hold the plaintext, which is why it cannot be shown to you again after creation.
Building a token endpoint#
Your token endpoint is the gate. It is the one place where you decide who may join, under what identity, and with which role. Authenticate your own user first, then derive every token field from your own records — never from the request body.
Authenticate your user
Use whatever session or bearer token your app already uses. If the request is not signed in, return 401 and stop.
Decide identity, role and channel yourself
Take the identity from your own user record. Take the role from your own permission check. Only the channel usually comes from the client, and even that should be checked against what the user is allowed to join.
Request the token and return it
Return token and wsUrl to the client. Nothing else from the response needs to leave your server.
app.post('/api/rtc-token', async (req, res) => {const user = await authenticate(req); // your authif (!user) return res.status(401).json({ error: 'Sign in first' });const channel = req.body.channel;if (!(await user.mayJoin(channel))) { // your rulesreturn res.status(403).json({ error: 'Not allowed in this channel' });}const { token, wsUrl } = await applooma.createToken({roomName: channel,identity: user.id, // from YOUR recordrole: user.isPresenter ? 'host' : 'audience', // from YOUR checkmetadata: { name: user.displayName }, // public to everyone});res.json({ token, wsUrl });});
What goes in a token#
| Field | Required | Rules |
|---|---|---|
roomName | Yes | The channel. Must match [a-zA-Z0-9_-]{1,64}, otherwise 400. |
identity | Yes | 1–128 characters. Must be unique in the channel: a second connection with the same identity replaces the first. |
role | No | One of host, cohost, audience. Defaults to host. Anything else is 400. |
metadata | No | A plain object, under 2048 bytes serialised. Arrays and non-objects are 400. |
Metadata is public inside the channel
Metadata travels inside the token and is broadcast to every other participant. Put display names and avatars there. Do not put email addresses, phone numbers, internal flags or anything you would not show to every person in the call.
The service writes the project and the role into the metadata itself after your fields, so a client cannot claim a different project or a higher role by passing those keys.
Default role is host
If you omit role, the token is issued as host — able to publish and to administer the channel. Pass the role explicitly on every call so a listener never gets host rights by accident.
Token lifetime#
Tokens are valid for 6 hours. The response includes expiresAt, the absolute expiry time.
- The expiry is checked when the participant joins. A session already connected is not cut off mid-call when its token passes 6 hours: the media server refreshes the credential in place, so there is no renew call to make from the client.
- An expired token cannot be used to join or rejoin. Fetch a new one from your endpoint and join again.
- Tokens are not refreshable and cannot be revoked individually. Keep them short-lived by fetching one per join, rather than caching one for the day.
async function joinWithFreshToken(channel) {const res = await fetch('/api/rtc-token', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ channel }),});const { token, wsUrl } = await res.json();await engine.joinChannel(token, wsUrl, { role: 'audience' });}// Do not cache the token across app launches — just call this again.
Rotating a key pair#
Rotate from the project's API keys page in the console. Rotation replaces the key and the secret on the same key-pair record, and the new secret is shown once.
Rotation takes effect immediately
The old key and secret stop working as soon as you rotate. Deploy the new values to your server's environment in the same change. If you need a gap-free switchover, create a second key pair on the project, move your servers to it, then deactivate the old one.
A key pair can also be deactivated without being rotated. A deactivated pair returns 401 on every call.
If a secret leaks
- Rotate the affected key pair immediately, or deactivate it if you have another pair in place.
- Deploy the new secret to every server that calls the API.
- Check usage in the console for minutes you did not expect. See Usage and quotas.
- Rotating does not end calls that are already connected. Existing tokens remain valid until they expire, up to 6 hours.
- Email support@applooma.dev with your App ID if you see traffic you cannot account for.
Common mistakes#
| Mistake | What you see | Fix |
|---|---|---|
| API secret shipped in the client app | Nothing, until someone else spends your quota | Move token issuance to your server. Rotate the exposed key pair. |
| Token endpoint with no authentication of your own | Anyone who can reach the URL can join any channel | Authenticate your user before issuing a token. |
Identity taken from req.body | Users impersonate each other and disconnect each other | Take the identity from your own session record. |
Role taken from req.body | An audience member asks for host and gets it | Derive the role from your own permission check. |
role omitted | Listeners can publish audio and video | Pass role: 'audience' explicitly. |
| One token cached and reused by many users | Participants kick each other out of the channel | One token per participant, with a unique identity. |
| Channel name with a space, colon or slash | 400 roomName must match [a-zA-Z0-9_-]{1,64} | Slugify the name before sending it. |
| Sensitive data in metadata | Every participant in the channel can read it | Keep metadata to display information only. |
| Token cached beyond 6 hours | Join fails after the token expires | Fetch a new token and rejoin. |