Authentication — @super-line/plugin-auth
Proper authentication as a paired plugin: email/password sign-up + login, reusable access tokens, connection sessions, data-driven roles, API keys, and JWT — with all identity held in typed collections. It builds on super-line's connect-time authenticate model; you wire it in three places and the plugin owns the rest.
pnpm add @super-line/core @super-line/plugin-authNot sure you need the plugin over a hand-rolled authenticate? See Choose an auth strategy. New to it? Walk the Add auth + chat tutorial first.
Wire it in
1 · Contract
authContract() is the contract-time half — merged into your contract, it adds the guest role, the auth collections, and the signIn/signUp/signOut/whoami surface. Because it merges into the one contract, RowOf, client.collection, and per-role Requests all keep working end-to-end.
import { defineContract } from '@super-line/core'
import { authContract } from '@super-line/plugin-auth'
export const app = defineContract({
roles: { user: {}, admin: { /* … */ } }, // your app roles — do NOT declare `guest`, auth adds it
collections: { /* your app collections */ },
plugins: [authContract()],
})Auth owns identity: don't declare your own guest role or users collection (a collision throws at defineContract). Reference the directory from your rows with references: { authorId: 'users' }.
2 · Server
Hand the auth kit the same CollectionStore the server uses, then wire authenticate + identify at the top level and register the plugin. signIn/signUp/signOut/whoami are handled by the plugin — subtracted from your implement().
import { createSuperLineServer } from '@super-line/server'
import { auth } from '@super-line/plugin-auth/server'
const backend = sqliteCollections({ file: 'app.db', collections: app.collections })
const authKit = auth({ contract: app, collections: backend, defaultRoles: ['user'] })
createSuperLineServer(app, {
nodeKey: 'app-replica-1', // stable for this replica across restarts
collections: backend,
authenticate: authKit.authenticate, // verifies a credential and creates a connection session
identify: authKit.identify, // principal := userId, so every row policy keys on the logged-in user
plugins: [authKit.plugin], // handlers + open/deny-all row policies
})A stable nodeKey is required — the plugin keys per-node session reconciliation on it, and a changing value leaks prior-boot sessions.
3 · Client (React)
<SuperLineAuthProvider> owns the guest↔authed lifecycle and feeds the app's one shared React binding, so there is no bridge to write. Type the binding once by declaring Register — on @super-line/react, where the data hooks live:
// superline.d.ts — one declaration, and every module-level hook is typed by your contract
import type { app } from './contract'
declare module '@super-line/react' {
interface Register {
contract: typeof app
role: 'user'
}
}import { createSuperLineClient } from '@super-line/client'
import { webSocketClientTransport } from '@super-line/transport-websocket'
import { SuperLineAuthProvider } from '@super-line/plugin-auth/react'
const connect = ({ role, params }) =>
createSuperLineClient(app, { transport: webSocketClientTransport({ url }), role: role as 'user', params })
createRoot(el).render(
<SuperLineAuthProvider authedRole="user" connect={connect}>
<App />
</SuperLineAuthProvider>,
)import { useAuth } from '@super-line/plugin-auth/react'
import { useCollection } from '@super-line/react'
function App() {
const { state, signIn, signUp, signOut } = useAuth()
// Authed FIRST: a re-authentication keeps the current session live with `pending` set, so testing
// `pending` first would tear the app down mid-switch.
if (state.status === 'authed') return <Workspace me={state.userId} name={state.displayName} onSignOut={signOut} />
if (state.pending) return <Splash />
return <LoginForm onSignIn={signIn} onSignUp={signUp} />
}The data hooks come from @super-line/react and need no client passed in — useMaybeClient (null until authenticated), useCollection, useDoc, useEvent, useSubscription, useRequest, useEnv. Before there is a session they go idle: reads return empty, writes reject UNAUTHORIZED rather than silently succeeding. This module exports only what is auth's: the provider and useAuth.
Already own an authClient() instance (a script driving the same session, say)? Hand it over instead: <SuperLineAuthProvider client={auth}>. The provider never closes an instance it did not build. Nesting a second provider gives a second independent session — that is how one app runs two.
Not using React? authClient() from @super-line/plugin-auth/client is the same logic, framework-agnostic.
Switching accounts
reauthenticate() re-consults your credential source and replaces the session in place — no remount, no lost component state:
const auth = useAuth()
await switchUpstreamAccount(next)
await auth.reauthenticate()It never destroys a session it could not replace: the new connection is confirmed before the old one closes, so a failed mint leaves you signed in with state.error set. Only a null from the source drops you to guest.
How login works over the bus
super-line freezes a connection's role at connect, so there's no "log in and upgrade this socket." The client half hides the dance: signIn() connects as guest, mints an access token, then transparently reconnects as your authedRole carrying the token — and persists it across reloads.
Row security
The plugin ships policies for its own collections — users is a public directory (readable), while credentials/accessTokens/sessions/apiKeys are server-only (deny-all). Your collections key their policies on the principal, which is now the logged-in userId:
policies: {
notes: { read: (principal) => eq('ownerId', principal) }, // you only read your own
}See Policies for the full row-security model.
Go deeper
The plugin's full surface is split across focused guides:
- Sessions, roles & API keys — durable sessions, roles-as-data, and
slp_API keys. - JWT & sealed tokens — server-minted bearer assertions for stateless and cross-service connect, including the sealed-only client (
resolveToken/tokenParam). - Server-side hooks — before/after connection admission, provisioning policy, and audit.
- Provision an agent identity — run an AI agent as an API-key user, plus the
authKitmanagement surface and revocation. - Reset a password — the logged-out recovery flow via a host callback.
For the model — the connection lifecycle and why sealed tokens are server-minted — see the auth lifecycle.
Examples
examples/auth— a runnable CLI walkthrough of the whole flow.examples/collections-chat— a Slack-like app with real login, on top of collections.
Next: Sessions, roles & API keys · back to Choose an auth strategy.