Token authentication and prefix authorization in TypeScript on Resonate

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-17

The 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 through run, rpc, and each tick of schedule. prefix-authz/index.ts:11. The SDK assigns the prefix in resonate-sdk-ts/src/resonate.ts:133 as this.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's lfc (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 token is set on the constructor (or RESONATE_TOKEN is set in the environment), the HttpNetwork adapter resolves it and attaches it to every request head as the auth field — application code never touches the request envelope. resonate-sdk-ts/src/network/http.ts:65-66, 120-121.

When prefix is set on the constructor (or RESONATE_PREFIX is set in the environment), the Resonate instance stores this.idPrefix = "<prefix>:" and prepends it to every promise ID generated through run, rpc, and each tick of schedule (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 to get lookups. resonate-sdk-ts/src/resonate.ts:493.

When the server rejects a request, the SDK surfaces it as a ResonateError — The request is unauthorized for a missing or bad signature, The request is forbidden for 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":""}' for token-auth (empty claim grants access to all promises) and '{"prefix":"worker-1"}' for prefix-authz (claim scopes access to promise IDs starting with worker-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-publickey the server does not enforce JWTs.

Pick a prefix policy. Either set prefix on the Resonate instance (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 Resonate instance with no token and no RESONATE_TOKEN env var: the first request fails and the worker crashes with ResonateError: 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 unauthorized outcome. The bad token never gets to act on a promise. token-auth/README.md:55-58.

Token valid but missing the prefix claim. The server treats the JWT as invalid; the SDK surfaces unauthorized. token-auth/README.md:71-75.

Token valid with prefix worker-1, but the Resonate instance was configured with a different prefix or the call site uses a non-prefixed ID. The server returns 403; the SDK surfaces ResonateError: 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's worker.run("foo", ...) resolves to tenantB:foo and 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-auth shape: pass token and leave prefix empty (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-authz shape: each client gets a JWT whose prefix claim matches the namespace it is allowed to touch, and the Resonate instance 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_PREFIX env 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-cli with 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, prefix options, idPrefix application): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/resonate.ts

SDK HTTP network (Bearer token attach, RESONATE_TOKEN fallback): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/network/http.ts

SDK Context interface (ctx.run = lfc alias): https://github.com/resonatehq/resonate-sdk-ts/blob/v0.10.0/src/context.ts

Resonate docs: https://docs.resonatehq.io/