| name | resonate-token-authentication-typescript |
| description | Secure Resonate applications with JWT-based authentication and prefix-based authorization. Use for multi-tenant systems, role-based access control, and isolating workers or services from accessing each other's promises. |
| license | Apache-2.0 |
Resonate Token Authentication & Authorization (TypeScript)
Overview
Resonate supports JWT-based authentication and prefix-based authorization to secure access to the Resonate server and control which promises clients can access. This enables multi-tenant systems, role-based access control, and isolation between different workers or services.
Core principle: Generate JWT tokens signed with a private key, configure the Resonate server with the public key, and clients present tokens to authenticate. Optionally use the prefix claim for fine-grained authorization.
Mental Model
Without Auth:
Client → Resonate Server → All Promises Accessible
With Token Auth:
Client + Valid JWT → Resonate Server → All Promises Accessible
Client + Invalid JWT → Resonate Server → REJECTED (401 Unauthorized)
With Prefix Auth (Multi-Tenant):
Client + JWT(prefix="tenant-1") → Access only "tenant-1:*" promises
Client + JWT(prefix="tenant-2") → Access only "tenant-2:*" promises
Client + JWT(prefix="worker-a") → Access only "worker-a:*" promises
Setup: Generate Keys and Tokens
1. Generate RSA Key Pair
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem
Security:
- Store
private_key.pem securely (never commit to git, use secrets manager)
public_key.pem can be deployed with server configuration
2. Install JWT CLI Tool
brew install mike-engel/jwt-cli/jwt-cli
3. Generate JWT Tokens
Basic authentication token (no prefix):
jwt encode --secret @private_key.pem -A RS256 '{}'
Token with prefix claim (for authorization):
jwt encode --secret @private_key.pem -A RS256 '{"prefix":"tenant-1"}'
Store token in environment variable:
export MY_TOKEN=$(jwt encode --secret @private_key.pem -A RS256 '{"prefix":"worker-1"}')
Server Configuration
Start Resonate Server with Authentication
resonate dev --auth-publickey public_key.pem
What this does:
- Enables JWT authentication on all HTTP API endpoints
- Validates JWT signatures using the provided public key
- Rejects requests with invalid or missing tokens (401 Unauthorized)
- Enforces prefix-based authorization if
prefix claim is present
Production Deployment
docker run -v $(pwd)/public_key.pem:/keys/public_key.pem \
resonatehq/resonate \
serve --auth-publickey /keys/public_key.pem
resonate serve \
--auth-publickey /etc/resonate/public_key.pem \
--storage-type postgres \
--storage-postgres-url postgres://user:pass@db.example.com/resonate
For fully worked deploys of the server with auth wired in, see resonate-server-deployment (Linux/systemd) or resonate-server-deployment-cloud-run (GCP Cloud Run + Cloud SQL).
Pattern 1: Basic Token Authentication
Use when: You want to restrict access to trusted clients only, but all authenticated clients have equal access.
import { Resonate, Context } from "@resonatehq/sdk";
function* helloAuth(ctx: Context, greeting: string) {
const result = yield* ctx.run((ctx: Context) => {
return `${greeting} world!`;
});
return result;
}
const resonate = new Resonate({
url: "http://localhost:8001",
token: process.env.MY_TOKEN
});
const workflow = resonate.register("workflow", helloAuth);
const result = await workflow.run("workflow.id", "hello");
console.log(result);
resonate.stop();
Without token:
const resonateNoAuth = new Resonate({
url: "http://localhost:8001"
});
const workflow = resonateNoAuth.register("workflow", helloAuth);
await workflow.run("workflow.id", "hello");
Pattern 2: Prefix-Based Authorization (Multi-Tenant)
Use when: You need to isolate promises between tenants, workers, or services.
Server-Side Prefix Enforcement
When a client connects with a token containing a prefix claim, the Resonate server enforces that the client can only:
- Create promises with IDs starting with that prefix
- Access promises with IDs starting with that prefix
Generate tenant-specific tokens:
export TENANT1_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"tenant-1"}')
export TENANT2_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"tenant-2"}')
export WORKER_A_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"worker-a"}')
Client with Automatic Prefix
const tenant1 = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT1_TOKEN,
prefix: "tenant-1"
});
const workflow1 = tenant1.register("workflow", processOrder);
await workflow1.run("order-123", orderData);
const tenant2 = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT2_TOKEN,
prefix: "tenant-2"
});
const workflow2 = tenant2.register("workflow", processOrder);
await workflow2.run("order-123", orderData);
Client with Manual Prefix
const resonate = new Resonate({
url: "http://localhost:8001",
token: process.env.TENANT1_TOKEN
});
const workflow = resonate.register("workflow", processOrder);
await workflow.run("tenant-1:order-123", orderData);
Why manual prefixing?
- More control over promise ID structure
- Can use different prefixes for different workflows
- Useful for migration or complex ID schemes
Pattern 3: Multi-Worker Isolation
Use when: Multiple worker groups should not interfere with each other's promises.
const workerA = new Resonate({
url: "http://localhost:8001",
token: process.env.WORKER_A_TOKEN,
prefix: "worker-a",
group: "workers-a"
});
workerA.register("processTask", processTask);
const workerB = new Resonate({
url: "http://localhost:8001",
token: process.env.WORKER_B_TOKEN,
prefix: "worker-b",
group: "workers-b"
});
workerB.register("processTask", processTask);
Pattern 4: Environment Variable Configuration
Use when: You want to avoid hardcoding credentials in source code.
const resonate = new Resonate({
url: "http://localhost:8001"
});
const workflow = resonate.register("workflow", myWorkflow);
await workflow.run("order-123", data);
Environment variables:
RESONATE_TOKEN: JWT token
RESONATE_PREFIX: Promise ID prefix
Pattern 5: Role-Based Access Control (RBAC)
Use when: Different roles need different levels of access.
export ADMIN_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{}')
export SERVICE_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"service-api"}')
export USER_TOKEN=$(jwt encode -S @private_key.pem -A RS256 '{"prefix":"user-${userId}"}')
Client code:
const admin = new Resonate({
url: "http://localhost:8001",
token: process.env.ADMIN_TOKEN
});
await admin.promises.get("tenant-1:order-123");
await admin.promises.get("tenant-2:order-456");
const service = new Resonate({
url: "http://localhost:8001",
token: process.env.SERVICE_TOKEN,
prefix: "service-api"
});
await service.promises.get("request-123");
Token Claims and Validation
Required claims:
- None (empty JWT
{} is valid for basic authentication)
Optional claims:
prefix (string): Restricts promise access to IDs starting with this prefix
exp (number): Token expiration timestamp (Unix epoch)
iat (number): Token issued-at timestamp
Token validation:
jwt encode -S @private_key.pem -A RS256 \
--exp='+1 hour' \
'{"prefix":"tenant-1"}'
jwt decode -S @public_key.pem $MY_TOKEN
Security Best Practices
1. Protect Private Keys
git add private_key.pem
aws secretsmanager create-secret \
--name resonate-jwt-private-key \
--secret-string file://private_key.pem
echo "*.pem" >> .gitignore
echo "*.key" >> .gitignore
2. Rotate Keys Periodically
openssl genrsa -out private_key_v2.pem 2048
openssl rsa -in private_key_v2.pem -pubout -out public_key_v2.pem
resonate serve --auth-publickey public_key_v2.pem
jwt encode -S @private_key_v2.pem -A RS256 '{"prefix":"tenant-1"}'
3. Use Token Expiration
jwt encode -S @private_key.pem -A RS256 --exp='+1 hour' '{}'
jwt encode -S @private_key.pem -A RS256 --exp='+30 days' '{}'
4. Least Privilege Prefixes
const client = new Resonate({
token: token,
prefix: "user-123"
});
const client = new Resonate({
token: token,
prefix: "user"
});
Production Recommendations
1. Use External Identity Provider
For production systems, integrate with identity providers like:
- Keycloak: Open-source identity and access management
- Auth0: Managed authentication service
- Okta: Enterprise identity platform
- AWS Cognito: AWS-managed identity service
2. Token Refresh Strategy
let token = await getInitialToken();
let resonate = new Resonate({ url: serverUrl, token });
setInterval(async () => {
token = await refreshToken(token);
resonate = new Resonate({ url: serverUrl, token });
}, 30 * 60 * 1000);
3. Audit Logging
resonate.on("error", (error) => {
if (error.message.includes("unauthorized")) {
logger.warn("Authentication failed", {
timestamp: Date.now(),
error: error.message
});
}
});
Common Pitfalls
1. Forgetting to Configure Server
resonate dev
resonate dev --auth-publickey public_key.pem
2. Mismatched Prefix in Token and SDK
const token = generateToken({ prefix: "tenant-1" });
const resonate = new Resonate({
token: token,
prefix: "tenant-2"
});
const resonate = new Resonate({
token: token,
prefix: "tenant-1"
});
3. Exposing Tokens in Logs
console.log(`Token: ${process.env.MY_TOKEN}`);
console.log(`Token: ${process.env.MY_TOKEN?.slice(0, 10)}...`);
Decision Tree
Use token authentication when:
- Restricting access to trusted clients only
- Deploying to production environments
- Exposing Resonate server over public networks
Use prefix-based authorization when:
- Multi-tenant systems (isolate tenant data)
- Role-based access control
- Isolating worker groups
- Implementing data access policies
Don't use authentication when:
- Local development (use
resonate dev without flags)
- Internal private networks (though still recommended)
- Single-tenant systems with trusted clients
Summary
Token authentication and prefix-based authorization enable:
- Secure access control with JWT tokens
- Multi-tenant isolation via prefix claims
- Role-based access control
- Fine-grained promise access policies
- Integration with external identity providers
Core recipe: Generate RSA key pair → Configure server with public key → Generate JWT tokens with prefix claims → Clients authenticate with tokens → Server enforces prefix-based access control