Skip to content

Resolve and verify roles

A connection's role is resolved once, when the connection is established, and fixed for its lifetime. It decides which surface and which ctx the connection gets. This page is the recipe — resolve a role in authenticate, give each role its own ctx, verify the client's claim, and lean on NOT_FOUND to enforce the boundary. For why the role is the security boundary and how server-authority makes it un-bypassable, see Server-authoritative.

This is the hand-rolled primitive

Most apps want @super-line/plugin-auth — email/password, sessions, roles-as-data, API keys, and JWT out of the box — which is built on this same authenticate seam. Reach for the hand-rolled recipe below when you're bringing your own identity store. See Choose an auth strategy.

Return { role, ctx } from authenticate

authenticate runs as the connection opens. It receives the handshake ({ transport, headers, query, peer?, raw }) — read query params via h.query.X and headers via h.headers, regardless of which transport carried the connection. Return { role, ctx }, or throw to reject (no connection is opened):

ts
import { webSocketServerTransport } from '@super-line/transport-websocket'

const srv = createSuperLineServer(api, {
  transports: [webSocketServerTransport({ server })],
  authenticate: async (h) => {
    const token = h.query.token
    const user = await verifyJwt(token)   // throw -> rejected
    return { role: 'user' as const, ctx: { user } }
  },
})

@super-line/transport-websocket provides the WebSocket transport. Other transports (HTTP/SSE, libp2p) all hand authenticate the same Handshake — see Choose a transport.

Return role as a literal ('user' as const) so it's inferred as a role key rather than widening to string.

Give each role its own ctx

Different roles usually carry different identity data. Return a discriminated { role, ctx } and each handler block sees the right ctx:

ts
authenticate: (h) => {
  const u = verify(h)
  return u.role === 'admin'
    ? { role: 'admin' as const, ctx: { adminId: u.id } }
    : { role: 'user' as const,  ctx: { userId: u.id } }
}

srv.implement({
  admin: { /* ctx is { adminId: string } */ },
  user:  { /* ctx is { userId: string } */ },
})

In a shared handler, ctx is the union of all roles' ctx — use common fields, or branch on conn.role.

Verify the claim

The client passes its role to createSuperLineClient; it's surfaced to authenticate on the handshake (h.query.role for the WS/HTTP transports) so authenticate can read it. It's a claim, not a fact — always verify it against the credential:

ts
authenticate: (h) => {
  const u = verify(tokenFrom(h))
  const claimed = h.query.role
  if (u.role !== claimed) throw new SuperLineError('FORBIDDEN', 'role not granted')
  return { role: u.role, ctx: { user: u } }
}

Rely on NOT_FOUND for enforcement

Dispatch resolves a handler by conn.role, so a request or subscribe outside shared ∪ roles[conn.role] resolves to nothing and is rejected with NOT_FOUND — even if a client hand-crafts the frame to bypass its typed surface. NOT_FOUND (rather than FORBIDDEN) is deliberate: it doesn't reveal that the method exists for some other role. You don't wire this up; it's a property of role-scoped dispatch.

Model AI agents as a role

Roles shine when a server serves both humans and AI agents. Give each its own verbs and topics:

ts
roles: {
  user:  { clientToServer: { say: {…} } },
  agent: {
    clientToServer: { reportResult: {…} },
    serverToClient: { taskAssigned: { payload: z.object({ taskId: z.string(), prompt: z.string() }), subscribe: true } },
  },
}
  • An agent client (role: 'agent') sees only the agent surface — it can reportResult and subscribe('taskAssigned'), but agent.say(...) won't compile.
  • A user can't call agent-only methods (compile error, and NOT_FOUND at runtime).
  • Each gets its own ctx ({ userId } vs { agentId, capabilities }).

The chat example shows a human and an AI agent sharing one room. For patterns building agent-facing surfaces, see AI agents.

Beyond hand-rolled auth

This page is the primitive. For a batteries-included identity system — email/password sign-up, server-issued sessions, data-driven roles, API keys, and JWT — reach for @super-line/plugin-auth, which builds on this same connect-time authenticate model.

Next: Add authentication (plugin) · back to Choose an auth strategy.

Released under the MIT License.