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.
The Great Data Ownership Revolution: From Tenants to Homeowners
The Landlord Problem
For decades, we've built applications like digital feudalism. Users create accounts, upload photos, write posts, and build their digital lives—but who really owns this data? The server does. Every tweet, every photo, every personal note lives on someone else's computer, governed by someone else's rules.
In the traditional REST paradigm, your backend is the digital landlord. Users are merely tenants who must ask permission for every change. Want to update your profile? Submit a request to the landlord. Want to share a document? The landlord decides who gets access. Want to delete your own data? Hope the landlord approves.
This creates a fundamental mismatch between our mental model and the technical reality. We think of "my data" but build systems where the server owns everything and users are just temporary visitors with limited privileges.
The Jazz Paradigm: Digital Homeownership
Jazz flips this model on its head with a radical proposition: users should own their data, not rent it.
Think about it like the difference between renting and owning a home. In the REST world, you're always a tenant—you can decorate your apartment (update your data), but you need the landlord's permission for every change. In the Jazz world, you own the house. You can renovate whenever you want, and you only interact with city services (the server) for utilities and shared infrastructure.
The Security Revolution
Traditional security models are built around the fortress mentality: build walls around the server, authenticate everyone at the gate, and control every interaction from the center. This creates bottlenecks, single points of failure, and an inherent power imbalance.
Jazz's Group model represents a fundamentally different approach to security—one that mirrors how we naturally think about ownership and collaboration. Instead of the server being the supreme authority over all data, it becomes just another participant in the ecosystem, owning only what it legitimately should: shared infrastructure, business logic, and services.
When you create a document in Jazz, you're not asking the server to store it for you—you're creating something you own. When you want to collaborate, you're not asking the server to manage permissions—you're inviting others into your space. The server only gets involved when it needs to provide its own services, like processing payments or running business logic.
The Mental Model Shift
This shift requires rewiring how we think about application architecture:
From "What can this user do to my server?" to "What does my server need to do with user data?"
From "How do I protect my database?" to "How do I provide valuable services to data owners?"
From "Users are security risks" to "Users are autonomous agents with their own data sovereignty"
The server transforms from a jealous guardian of all data into a specialized service provider. It's the difference between being a controlling parent and being a trusted advisor—the relationship becomes collaborative rather than hierarchical.
The Natural Order
This model aligns with how we naturally think about ownership and privacy. Your thoughts belong to you. Your photos belong to you. Your documents belong to you. The tools you use to process, share, and collaborate with that data are services you engage with, not masters you serve.
Jazz doesn't just change the technical architecture—it restores the natural order of digital ownership. Users become digital citizens rather than digital subjects, with real agency over their data and genuine control over their digital lives.
The revolution isn't just technical; it's philosophical. We're moving from a world where data is feudal property to one where it's personal sovereignty. And that changes everything.