Before implementing inbox-based endpoints, you need to set up a Jazz server worker.
Server workers have Jazz accounts with static credentials. Generate new credentials by running:
npx jazz-run account create --name "Balance Service Worker"
This will output:
JAZZ_WORKER_ACCOUNT=co_zH3i4n2V8X...
JAZZ_WORKER_SECRET=sealerSecret_a5DyE7H9c2...
Store these credentials as environment variables. Handle the Account Secret like any other secret (DB password, API key, etc.)
# .env file
JAZZ_WORKER_ACCOUNT=co_zH3i4n2V8X...
JAZZ_WORKER_SECRET=sealerSecret_a5DyE7H9c2...
import { startWorker } from 'jazz-tools/worker';
const { worker, experimental: { inbox } } = await startWorker({
syncServer: 'wss://cloud.jazz.tools/[email protected]',
// Credentials are automatically read from JAZZ_WORKER_ACCOUNT and JAZZ_WORKER_SECRET
// Or pass them explicitly:
// accountID: process.env.JAZZ_WORKER_ACCOUNT,
// accountSecret: process.env.JAZZ_WORKER_SECRET,
});
console.log("Worker started:", worker.id);
The Jazz inbox system enables RPC-like communication for operations requiring server-side validation, atomic updates, or controlled access. Unlike traditional request/response patterns, Jazz leverages automatic data synchronization for reads while using inbox messages only for writes.
Create a balance system where only the worker can modify balances, but users can subscribe to changes:
import { co, z, Group } from "jazz-tools";
// User balance - readable by owner, writable only by worker
export const UserBalance = co.map({
userId: z.string(),
balance: z.number(),
lastUpdated: z.number(),
});
// User's private root to store balance reference
export const UserPrivateRoot = co.map({
balanceId: z.string(),
});
// Purchase record - immutable audit trail
export const PurchaseRecord = co.map({
userId: z.string(),
itemId: z.string(),
amount: z.number(),
timestamp: z.number(),
balanceAfter: z.number(),
});
// Write operations via inbox
export const BuyItemRequest = co.map({
type: z.literal("buy"),
itemId: z.string(),
amount: z.number(),
});
export const CreateAccountRequest = co.map({
type: z.literal("createAccount"),
initialBalance: z.number(),
});
export const InboxMessage = z.discriminatedUnion("type", [
BuyItemRequest,
CreateAccountRequest,
]);
The worker handles only write operations through the inbox:
inbox.subscribe(
InboxMessage,
async (message, senderAccountID) => {
const senderAccount = await co.account().load(senderAccountID, { loadAs: worker });
if (!senderAccount) return;
switch (message.type) {
case "createAccount": {
// Create balance with restricted permissions
const balanceGroup = Group.create({ owner: worker });
balanceGroup.addMember(senderAccount, "reader");
const balance = UserBalance.create(
{
userId: senderAccountID,
balance: message.initialBalance,
lastUpdated: Date.now(),
},
{ owner: balanceGroup }
);
// Store balance ID in user's private root for easy access
const userRoot = senderAccount.root;
if (userRoot) {
userRoot.balanceId = balance.id;
await userRoot.waitForSync();
}
return balance;
}
case "buy": {
// Load user's balance from their root
const userRoot = senderAccount.root as UserPrivateRoot | undefined;
if (!userRoot?.balanceId) throw new Error("No account found");
const balance = await UserBalance.load(userRoot.balanceId, { loadAs: worker });
if (!balance) throw new Error("Balance not found");
// Validate and update atomically
if (balance.balance < message.amount) {
throw new Error("Insufficient balance");
}
balance.balance -= message.amount;
balance.lastUpdated = Date.now();
// Create audit record
const purchaseGroup = Group.create({ owner: worker });
purchaseGroup.addMember(senderAccount, "reader");
const purchase = PurchaseRecord.create(
{
userId: senderAccountID,
itemId: message.itemId,
amount: message.amount,
timestamp: Date.now(),
balanceAfter: balance.balance,
},
{ owner: purchaseGroup }
);
await Promise.all([
balance.waitForSync(),
purchase.waitForSync()
]);
return purchase;
}
}
},
{ retries: 3 }
);
import { InboxSender, useCoState } from "jazz-tools";
import { UserBalance, BuyItemRequest } from "./schema";
// Initialize once - for write operations
const purchaseService = await InboxSender.load(
WORKER_ACCOUNT_ID,
currentUserAccount
);
// Create account (one-time setup)
const balance = await purchaseService.sendMessage(
CreateAccountRequest.create({
type: "createAccount",
initialBalance: 100,
})
);
// Subscribe to balance changes (real-time updates)
// In React:
const balance = useCoState(UserBalance, currentUserAccount.root?.balanceId);
// In vanilla JS:
const unsubscribe = UserBalance.subscribe(
currentUserAccount.root?.balanceId,
(balance) => {
console.log("Current balance:", balance?.balance);
}
);
// Make purchases through inbox
try {
const purchase = await purchaseService.sendMessage(
BuyItemRequest.create({
type: "buy",
itemId: "item-123",
amount: 25,
})
);
// Balance will auto-update via subscription
} catch (error) {
console.error("Purchase failed:", error.message);
}
- Reads are automatic: Clients subscribe directly to CoValues they have permission to read
- Writes go through inbox: Only operations that modify state need inbox messages
- No "get" methods needed: Jazz's automatic sync eliminates request/response for reads
- Real-time updates: All permitted clients see changes immediately
Instead of request/response, use state transitions:
export const Order = co.map({
status: z.enum(["draft", "processing", "completed", "failed"]),
items: z.array(z.string()),
total: z.number(),
error: z.optional(z.string()),
});
// Client creates order in "draft" state
const order = Order.create({
status: "draft",
items: ["item-1"],
total: 50
});
// Client transitions to "processing"
order.status = "processing";
// Worker subscribes to orders and processes them
Order.subscribe(orderId, (order) => {
if (order?.status === "processing") {
processOrder(order);
}
});
1. Worker-Only Write
const group = Group.create({ owner: worker });
group.addMember(user, "reader");
// User can read but not write
2. Shared Write Access
const group = Group.create({ owner: worker });
group.addMember(user, "writer");
// Both can modify
3. Public Read
group.addMember("everyone", "reader");
// Anyone can read
1. Batch Processing
const batchProcessor = new Map<string, BuyItemRequest[]>();
inbox.subscribe(InboxMessage, async (message, senderID) => {
if (message.type === "buy") {
// Accumulate requests
const batch = batchProcessor.get(senderID) || [];
batch.push(message);
batchProcessor.set(senderID, batch);
// Process when batch is full or on timeout
if (batch.length >= 10) {
await processBatch(senderID, batch);
batchProcessor.delete(senderID);
}
}
});
2. External Service Integration
case "buy": {
// Call external API
const paymentResult = await stripeAPI.charge({
amount: message.amount,
currency: "usd",
});
if (paymentResult.success) {
balance.balance -= message.amount;
balance.paymentId = paymentResult.id;
} else {
throw new Error("Payment failed: " + paymentResult.error);
}
}
3. Rate Limiting with CoMaps
export const RateLimitMap = co.map({
requests: z.record(z.number()), // userId -> timestamp
});
const rateLimits = RateLimitMap.create({}, { owner: worker });
inbox.subscribe(InboxMessage, async (message, senderID) => {
const now = Date.now();
const userRequests = Object.values(rateLimits.requests)
.filter(time => now - time < 60000) // Last minute
.length;
if (userRequests >= 10) {
throw new Error("Rate limit exceeded");
}
rateLimits.requests[senderID] = now;
// ... process message
});
// Integration test
const testWorker = await startWorker({ ... });
const testAccount = await Account.create({ ... });
// Subscribe to balance before making changes
let balanceUpdates = 0;
UserBalance.subscribe(testAccount.root?.balanceId, () => {
balanceUpdates++;
});
// Send purchase request
const sender = await InboxSender.load(testWorker.id, testAccount);
await sender.sendMessage(BuyItemRequest.create({
type: "buy",
itemId: "test-item",
amount: 10,
}));
// Verify real-time update
expect(balanceUpdates).toBeGreaterThan(0);
- Optimistic UI: Update UI immediately, revert on error
- Offline support: Inbox messages queue automatically
- Horizontal scaling: Multiple workers can share inbox processing
- Event sourcing: Purchase records create an immutable audit log
Based on the Jazz documentation, workers are full Jazz accounts that participate in the permission system, making them ideal for controlled server-side operations while maintaining Jazz's real-time collaborative nature.
Uh oh!
There was an error while loading. Please reload this page.