Concepts / Your users
Concepts

Your users

Your users don't sign up for Wakeshift. You have the Wakeshift account; they add your app to their agent and use it. This is how Stripe and Pusher work too.

Wakeshift keeps a workspace for each of your users, owned by your app and used only by it. Adding your app to an agent is the user's consent. There's no Wakeshift screen to click through.

In that workspace, Wakeshift handles delivery and acknowledgements, pacing, marking outside messages, stamping senders, and an audit log.

Who a subscriber is

Without sign-in, each conversation stands alone: Wakeshift identifies it from the tool call. If you know who the person is, give serveEvents your own ID for them as user. Their subscriptions then belong to them, personal events can reach them, and list_subscriptions shows them theirs:

TypeScript
wake.serveEvents(server, { user: (extra) => extra.authInfo?.extra?.userId as string | undefined })

Native subscriptions (ChatGPT) and personal events need a user.

Using your own sign-in

If your MCP server already signs people in, with OAuth or API tokens, there's nothing Wakeshift-specific to wire up: no token exchange and no callback. Authenticate as you do now, and pass your ID for the person as user.

extra.authInfo above is whatever your auth code put on the request. The MCP SDK's Streamable HTTP transport copies req.auth into each tool call's extra.authInfo, and userId is a field you choose to set:

TypeScript
// Your auth middleware, before the MCP transport handles the request:
app.use("/mcp", async (req, res, next) => {
  const token = (req.headers.authorization ?? "").replace(/^Bearer /, "")
  const person = await lookUpToken(token) // your own code
  if (!person) return res.status(401).end()
  req.auth = { token, clientId: person.clientId, scopes: [], extra: { userId: person.wakeshiftId } }
  next()
})

User IDs

user is your ID for the person. Wakeshift trims spaces at either end, then keeps it as is. The rules:

  • At most 200 characters, and it can't start with ~.
  • To use private u/<user>/… topics, the ID must also be a valid topic segment: lowercase a-z 0-9 . _ -, matched exactly.
  • Use an ID that stays the same and isn't personal information. Not an email address: map emails, mixed-case IDs and IDs like auth0|abc123 to an opaque one, such as a hash of your internal ID:
TypeScript
import { createHash } from "node:crypto"
const wakeshiftId = (internalId: string) => createHash("sha256").update("wakeshift:" + internalId).digest("hex").slice(0, 32)

Guest sign-in for MCP clients

MCP clients like Claude Code and Codex expect OAuth. If your app has no accounts of its own, GuestAuth runs a small OAuth server inside it that signs each client in as a guest, with nothing to click: adding your app is the consent. Each install of a harness is one guest, with its own workspace and an agent named after the harness (claude-code). It stores nothing: clients, codes and tokens are signed with a key derived from your app key.

TypeScript
import { createServer } from "node:http"
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
import { GuestAuth } from "@wakeshift/sdk"

const guests = new GuestAuth({ wake, mcpUrl: "https://holdem.example/mcp", proxyHops: 2 })

createServer(async (req, res) => {
  if (await guests.handle(req, res)) return // the OAuth endpoints and their metadata
  if (!guests.authenticate(req, res)) return // 401 with the header MCP clients sign in from
  const server = new McpServer({ name: "holdem", version: "1.0" })
  wake.serveEvents(server) // user: the guest, by default
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
  await server.connect(transport)
  await transport.handleRequest(req, res)
}).listen(8080)
  • serveEvents uses the guest as user by default, so a guest's subscriptions are its own; in your own tools, guestOf(extra) is the guest ({ user, workspace, agent, client, expires }). guest.user is guest:<id>, one per registered client, so it can't name u/<user>/… topics.
  • Registration gives each client a secret, and the token endpoint requires it with every code and refresh token, so knowing a client ID (it travels in URLs) isn't enough to become the guest. Codes need PKCE (S256) and last a minute; access tokens last an hour (accessTtl), refresh tokens 30 days (refreshTtl). Each refresh returns a new refresh token, and the old one keeps working until it expires: nothing is stored, so nothing records that it was used.
  • A guest your app hasn't acted for in over 30 days (longer than a refresh token lasts) is forgotten for you, as wake.forgetUser would: its workspace and everything in it are deleted, and your app's log says so. A guest that comes back after that signs in again as a new guest.
  • To cut a guest off, wake.forgetUser(guest.user) refuses all its refreshes from then on. revoked(guest, issuedAt) refuses the ones you decide to, such as every refresh token issued before a cutoff. Either takes effect within an hour, as the last access token expires.
  • Anyone who can reach your app can become a guest; that's the point. Registering and signing in are limited per address (below), and Wakeshift caps how many users an app can have: 200 for a test app, 100,000 for a live one. Past the cap, new guests are refused (access_denied).
  • Registering and token requests are limited per address: 20 and 60 per 10 minutes, per process, with an IPv6 /64 counting as one address. Behind proxies, set proxyHops to how many append to X-Forwarded-For (Google's load balancer: 2).
  • grants: true also gives each guest's agent a grant for your manifest's agent scope, as the Go SDK does, and stops refreshes once the grant is revoked. Events don't need it.

The Go and Python SDKs have the same guest sign-in. Our games, Secret Agent, Nightfall, Sketch Stadium and Agent Hold'em, use it.

Linking a person's Wakeshift account

A guest is one install of one harness. To know that two guests are the same person, for example to rank players across devices, have the person sign in with Wakeshift once. WakeshiftSignIn sends them to Wakeshift to sign in and pick their agent, then gives you their Wakeshift user ID, which stays the same across sign-ins, devices and agents:

TypeScript
import { WakeshiftSignIn } from "@wakeshift/sdk"

const login = new WakeshiftSignIn({ wake, redirectUri: "https://holdem.example/auth/wakeshift/callback" })

// A link the agent gives the person, e.g. https://holdem.example/link?guest=…:
async function startLink(guestUser: string, res: import("node:http").ServerResponse) {
  login.start(res, { link: guestUser, agentHint: "Claude Code" })
}

// The redirect URI:
async function callback(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) {
  const { signedIn, link } = await login.finish(req, res) // throws SignInError
  await saveLink(link, signedIn.user)
}

async function saveLink(guestUser: string, wakeshiftUser: string) {
  // Your code: record that this guest is this person.
}
  • It checks the state against a signed, HttpOnly cookie, and that the response came from your Wakeshift (iss), then exchanges the code with PKCE. link is carried through signed, not encrypted: don't put a secret in it.
  • List the redirect URI in your manifest's agent.redirect_uris (wake.publishManifest). The person sees your agent scope on Wakeshift's consent screen.
  • It needs a live key: a test app can't sign Wakeshift users in.
  • signedIn.refreshToken lets you check later that the person hasn't revoked your app: wake.refresh throws invalid_grant once they have.

Test keys

The keys you make in the console (your app, then Keys) are test keys: wst_test_<app>_<seed>.

  • A key is your app's private key. It never goes over the wire: the SDK signs a short-lived assertion with it for each request. Wakeshift stores only the public key.
  • An app can have up to 10 named keys at once, say one for production and one for CI. Each works on its own. The console shows a key once, when it's made.
  • Revoke stops one key at once and leaves the others working. Rotate replaces a key with a new one of the same name; the old one stops at once. For a change with no gap, make a new key, deploy it, then revoke the old one.
  • Delete (in Settings) revokes every key and ends everything your app subscribed. Within 30 days, Wakeshift deletes everything it keeps for your app's users, guests included: their workspaces, agents, conversations, subscriptions, events and deliveries, and the grants people gave your app. You can read your app's log for 30 days after deleting it; then it's deleted too.
  • A test key works only in the workspaces Wakeshift keeps for your app's users. It can never reach a workspace someone signs in to.

Live keys aren't issued. Moving to one needs no change to your code.

When someone loses access

Wakeshift checks access only when a conversation subscribes: your authorize runs then, and not again. When someone leaves an org or loses access to a repo, end their subscriptions yourself:

TypeScript
// One subscription, by the ID a subscribe gave you (or list_subscriptions shows).
await ciFinished.end(user.id, id)

forgetUser ends all of a user's subscriptions at once. For topics, endSubscription and unsubscribeUser do the same.

What Wakeshift stores about your users

In the workspace it keeps for each user:

  • the user ID you pass, exactly as you send it;
  • their conversations' sessions: the agent's client name, and the conversation's ID where the agent sends one (a Codex thread ID, for example);
  • their subscriptions, and what each agent needs in order to be woken;
  • the occurrences you emitted (and events you published) to them, with their text and payload, and the record of each delivery;
  • an audit log of what happened in the workspace.

It doesn't get your users' names, emails or tokens unless you put them in a user ID, a topic or an event. Don't.

Forgetting a user

When a person deletes their account, call forgetUser:

TypeScript
await wake.forgetUser(user.id)

It ends their subscriptions, closes their agents' streams, and deletes the workspace Wakeshift keeps for them, with everything in it listed above. It returns true if there was something to delete; calling it again returns false. If the same ID comes back later, it starts with an empty workspace.

Apps on Wakeshift accounts

An app can instead have its users sign in with Wakeshift, so it works in their own workspace, under the consent they give on Wakeshift's screen. That's for apps that act on the person's own things; Agent Chat works this way. Its events work the same, with three differences:

  • A subscription is allowed only for an agent the person has approved your app for, and it's made in their own workspace.
  • The person sees each subscription in their Wakeshift console, and can revoke it there. Revoking your app ends all its subscriptions at once.
  • A personal event goes to the person's agent that subscribed: Wakeshift takes it from that agent's sign-in, never from the arguments.

Your MCP server checks each request's token with wake.verifyAccessToken, exchanges it for the agent with wake.exchange, and tells serveEvents who that is:

TypeScript
const wake = new Wakeshift({ key: process.env.WAKESHIFT_KEY!, core: process.env.WAKESHIFT_CORE, mcpUrl: "https://chat.example.com/mcp" })

// Serve wake.protectedResource() at /.well-known/oauth-protected-resource, check each
// request's bearer token with wake.verifyAccessToken, and answer one without a valid
// token with 401 and WWW-Authenticate: wake.challenge().
wake.serveEvents(server, {
  agent: (extra) => (extra.authInfo ? wake.exchange(extra.authInfo.token) : undefined),
})
  • wake.exchange(token) returns the Agent: agent.user() is the subscriber its subscriptions and personal events go to (<workspace>/<agent>); pass that as to to send a personal event to the agent.
  • Your own tools pass it too: event.subscribe(extra, args, { server, agent }).
  • Return nothing, or throw, from agent for a caller that isn't signed in: it gets no subscription.

The same calls are wake.Verify, wake.Exchange and EventsOptions.Agent in Go, and verify_access_token, exchange and serve_events(server, agent=) in Python. Sign in with Wakeshift needs a live key, so test apps can't use it.