Authentication
Konet uses JWT tokens for all client connections. Tokens are validated with the KONET_JWT_SECRET environment variable.
Every key is a standard HS256 JWT signed with KONET_JWT_SECRET — there is no
custom prefix. The role is carried in the role claim.
Token Types
Anonymous Key (role: "anon")
- Generated by
konet keys generate(or set viaKONET_ANON_KEY) - Grants standard channel join and broadcast rights
- Safe to embed in client-side code
Service Key (role: "service")
- Grants admin-level access
- Required for Studio and the
/apiservice endpoints - Never expose in client bundles
JWT Claims
The server only requires a valid signature — every claim below is optional.
| Claim | Description |
|---|---|
role | anon or service (absent = treated as a plain client) |
sub | User ID, exposed server-side as socket.assigns.user_id |
channels | List of topics this token may join — see Scoping a token to specific channels |
exp | Expiration timestamp — validated if present |
Example Token (Node.js)
import jwt from 'jsonwebtoken'
const token = jwt.sign(
{ sub: 'user_123', role: 'anon' },
process.env.KONET_JWT_SECRET,
{ expiresIn: '1h' }
)Connection
Pass the token when opening the socket:
const client = createClient('ws://localhost:4000/socket', { token })On the server, socket.assigns.user_id and socket.assigns.role are populated from validated claims.
Scoping a Token to Specific Channels
By default, any valid token (including the shared anon_key) can join any room:* channel — fine for public rooms like a shared lobby or a broadcast feed.
For private, per-user channels (a 1:1 chat, a single device's location feed, an AI agent's private session), mint a token from your own backend with a channels claim listing exactly the topics it's allowed to join:
import jwt from 'jsonwebtoken'
const token = jwt.sign(
{ sub: 'user_42', channels: ['room:chat-42-99'] },
process.env.KONET_JWT_SECRET,
{ expiresIn: '1h' }
)A client connecting with this token can only join room:chat-42-99 — attempting to join any other room returns { reason: "unauthorized" } on join, and the attempt is logged in the Studio's log stream.
Entries may end in * to grant a whole namespace with one token instead of minting one per room:
{ sub: 'user_42', channels: ['room:user-42:*'] } // room:user-42:inbox, room:user-42:location, ...Tokens without a channels claim keep today's open behavior, so nothing changes for existing anon/service keys.