A Resonate server exposed beyond a single trusted process needs two things the SDK does not provide for free: a way to reject unauthenticated clients, and a way to stop authenticated clients from reading or writing each other's promises. The Resonate constructor in @resonatehq/sdk takes a token option that attaches a Bearer JWT to every server request, and a prefix option that namespaces every generated promise ID — together they cover authentication and per-client authorization without any application-level code. This example repo demonstrates both modes against a resonate dev server started with --auth-publickey.
The shape of the solution
import {Resonate, type Context} from '@resonatehq/sdk'
function* helloAuth(ctx: Context, greeting: string) {
const result = yield* ctx.run((ctx: Context) => `${greeting} world!`)
return result
}
// A Resonate instance that connects to a server deployed at localhost:8001,
// Assumes the generated JWT is stored in the env variable MY_TOKEN
// and the token has a custom claim {"prefix": "worker-1"}
const resonateAuthenticated = new Resonate({url: "http://localhost:8001", token: process.env.MY_TOKEN, prefix: "worker-1"})
const workflow = resonateAuthenticated.register("workflow", helloAuth)
console.log(await workflow.run("workflow.id", "hello"))
console.log(await workflow.rpc("workflow.rpc.id", "hello rpc"))
resonateAuthenticated.stop()
// from example-token-auth-ts/prefix-authz/index.ts:1-17The workflow body is intentionally trivial — the load-bearing surface is the Resonate constructor. The token option carries an RS256-signed JWT; the prefix option scopes every promise this instance creates.
The durable primitives
new Resonate({ token })— attaches the JWT to every outgoing server request; unauthenticated clients are rejected before any promise is created.token-auth/index.ts:10,prefix-authz/index.ts:11.new Resonate({ prefix })— prepends<prefix>:to every workflow ID generated throughrun,rpc, and each tick ofschedule.prefix-authz/index.ts:11. The SDK assigns the prefix inresonate-sdk-ts/src/resonate.ts:133asthis.idPrefix = resolvedPrefix ? `${resolvedPrefix}:` : "".resonate.register("workflow", helloAuth)— registers the generator function so it can be invoked by name; not auth-specific, but required to call.run/.rpc.token-auth/index.ts:13,prefix-authz/index.ts:14.workflow.run(id, ...args)— durable execution of the registered workflow, routed through the server; runs in-process when this client claims the task. The final promise ID is<prefix>:<id>when a prefix is set, e.g.worker-1:workflow.id.prefix-authz/index.ts:15,prefix-authz/README.md:56-58.workflow.rpc(id, ...args)— Remote Function Invocation (RFI): durable execution routed to a registered worker. Same prefix rule applies.token-auth/index.ts:15,prefix-authz/index.ts:16.ctx.run(fn)— inside the workflow, an alias for the SDK'slfc(local function call) primitive, which records the step as a durable checkpoint.resonate-sdk-ts/src/context.ts:207-209, 276.
What the SDK handles vs. what you write
What the SDK handles: token attachment, env-var fallback, ID prefixing, and ResonateError translation.
- When
tokenis set on the constructor (orRESONATE_TOKENis set in the environment), theHttpNetworkadapter resolves it and attaches it to every request head as theauthfield — application code never touches the request envelope.resonate-sdk-ts/src/network/http.ts:65-66, 120-121. - When
prefixis set on the constructor (orRESONATE_PREFIXis set in the environment), theResonateinstance storesthis.idPrefix = "<prefix>:"and prepends it to every promise ID generated throughrun,rpc, and each tick ofschedule(the per-tick promise-ID template, not the schedule's own name).resonate-sdk-ts/src/resonate.ts:133, 322, 396, 481. The same prefix is also applied togetlookups.resonate-sdk-ts/src/resonate.ts:493. - When the server rejects a request, the SDK surfaces it as a
ResonateError—The request is unauthorizedfor a missing or bad signature,The request is forbiddenfor a valid token whose prefix claim does not authorize the requested promise ID.token-auth/index.ts:19,prefix-authz/index.ts:21.
What you write: the keys, the JWT, the server flag, and the choice of prefix.
- Generate an RSA keypair with
openssl genrsa/openssl rsa -pubout.README.md:32-39. - Mint a JWT. The payload differs by example:
'{"prefix":""}'fortoken-auth(empty claim grants access to all promises) and'{"prefix":"worker-1"}'forprefix-authz(claim scopes access to promise IDs starting withworker-1:). Omitting the claim makes the JWT invalid.token-auth/README.md:31-39, 71-75,prefix-authz/README.md:31-41,README.md:50-54. - Start the server with
resonate dev --auth-publickey public_key.pem.README.md:56-60. Without--auth-publickeythe server does not enforce JWTs. - Pick a prefix policy. Either set
prefixon theResonateinstance (every ID is implicitly namespaced —prefix-authz/index.ts:11) or write fully-qualified IDs at the call site (workflow.run("worker-1:workflow.id", ...)—prefix-authz/README.md:60-72).
Failure modes covered
- No token attached, server is enforcing auth. Construct a
Resonateinstance with notokenand noRESONATE_TOKENenv var: the first request fails and the worker crashes withResonateError: The request is unauthorized.token-auth/README.md:55-58,token-auth/index.ts:19. - Token present but signed by a different private key. The server rejects the signature on every request — same
unauthorizedoutcome. The bad token never gets to act on a promise.token-auth/README.md:55-58. - Token valid but missing the
prefixclaim. The server treats the JWT as invalid; the SDK surfacesunauthorized.token-auth/README.md:71-75. - Token valid with prefix
worker-1, but theResonateinstance was configured with a different prefix or the call site uses a non-prefixed ID. The server returns 403; the SDK surfacesResonateError: The request is forbidden.prefix-authz/index.ts:20-22. - Two clients trying to read each other's state. Each client only holds a JWT for its own prefix. Promise IDs created by client A are
tenantA:foo; client B'sworker.run("foo", ...)resolves totenantB:fooand never collides. Isolation is enforced by the server against the JWT's prefix claim, not by application code.README.md:17-21,prefix-authz/README.md:34-41.
When to reach for this pattern
- If you expose a Resonate server beyond a single trusted process and need to reject unauthenticated clients at the network boundary — use
token+--auth-publickey. - If every authenticated client is equally trusted and should see every promise, use the
token-authshape: passtokenand leaveprefixempty (or use a JWT with{"prefix":""}). - If you need multi-tenant isolation, per-worker isolation, or any case where one client must not read or write another client's promises, use the
prefix-authzshape: each client gets a JWT whoseprefixclaim matches the namespace it is allowed to touch, and theResonateinstance is constructed with the same prefix. - If you are deploying workers as services where the token lifecycle is managed by the platform, use
RESONATE_TOKEN/RESONATE_PREFIXenv vars instead of constructor arguments — the SDK reads them as fallbacks.token-auth/README.md:62-69,prefix-authz/README.md:88-97. - If you need richer claim validation, revocation, or rotation than a static RSA keypair, treat this example as the SDK-side shape and replace
jwt-cliwith a real identity provider; the Resonate side does not change.README.md:13-15.
Sources
- Example repo: https://github.com/resonatehq-examples/example-token-auth-ts
- SDK repo: https://github.com/resonatehq/resonate-sdk-ts
- SDK constructor (
token,prefixoptions,idPrefixapplication): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/resonate.ts - SDK HTTP network (Bearer token attach,
RESONATE_TOKENfallback): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/network/http.ts - SDK
Contextinterface (ctx.run=lfcalias): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/context.ts - Resonate docs: https://docs.resonatehq.io/
