Channels and grants
The three private channel levels, and the short-lived grant a member obtains to join them.
- Availability: Planned
- Evidence: Read from source
- Reference
Channels
| Channel | Name | Carries |
|---|---|---|
| Job | job:<job> |
Everything about one job: states, queue position, turns, stages, packages, modules, cache hits, retries, logs, usage |
| Application | app:<application> |
Job state changes, pinned graphs, persisted inventories, releases, domains, notices and application changes |
| Organization | org:<organization> |
Plan changes, credit, usage and application changes |
A name matches ^(org|app|job):[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$. Each event belongs to exactly one channel, the narrowest that applies: the job when there is one, otherwise the application, otherwise the organization. A summary that also appears on a wider channel is a separate event there, with its own id and sequence.
Obtain a grant
| Operation | Request | Capability | Retry |
|---|---|---|---|
realtime.grant |
POST /v1/realtime/grants |
events.subscribe |
natural |
The body lists 1 to 20 distinct channels:
{ "channels": ["app:app_Shop0001x", "job:job_Prep0001x"] }// Ask for a short-lived grant to join private channels. The service checks your
// access to the resource behind every channel; the name alone proves nothing.
const api = process.env.CDN_API_ORIGIN;
const token = process.env.CDN_TOKEN;
const response = await fetch(new URL('/v1/realtime/grants', api), {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ channels: [`app:${process.env.CDN_APPLICATION}`, `job:${process.env.CDN_JOB}`] })
});
const grant = await response.json();
if (!response.ok) {
console.log(`${response.status} ${grant.error.code} — ${grant.error.message}`);
process.exit(1);
}
console.log(`granted: ${grant.channels.join(', ')}`);
console.log(`denied or unknown: ${(grant.denied ?? []).join(', ') || 'none'}`);
console.log(`expires: ${grant.expires} — renew before then`);
console.log(`transport: ${grant.transport.kind}, event name ${grant.transport.event}`);| Member of the grant | Meaning |
|---|---|
token |
Short-lived permission to join exactly the listed channels. It is scoped to the caller and is never a service key. |
channels |
The channels granted |
denied |
Requested channels the caller may not join. They are indistinguishable from channels that do not exist. |
expires |
When the grant ends. Renew before then. |
transport.kind |
supabase-broadcast, or local for local development |
transport.url, transport.key |
Where to connect, and the publishable client key of the transport when it needs one. Never a service-role key. |
transport.event |
beyond-cdn-events/1: the broadcast event name that carries a microbatch. The same channel can also carry the broadcast event beyond-cdn-pointer/1, a pointer. |
When no requested channel is visible to the caller the answer is 404 NOT_FOUND. Issuing a grant has no lasting effect, so a retry simply issues another one. Too many requests answer 429 RATE_LIMITED; honor Retry-After.
What a browser never receives
Job status is published only by authorized services. Browsers receive a caller-scoped grant and, if the transport needs one, a publishable key. Service-role keys, registry credentials and other tenants' data never reach a client, and never appear in events or logs.