Patterns and techniques worth applying to professional projects, extracted from the moltworker codebase.
Instead of leaking internal errors, rewrite them at the proxy layer:
// src/index.ts:37-46
function transformErrorMessage(message: string, host: string): string {
if (message.includes('gateway token missing')) {
return `Invalid or missing token. Visit https://${host}?token={YOUR_TOKEN}`;
}
if (message.includes('pairing required')) {
return `Pairing required. Visit https://${host}/_admin/`;
}
return message;
}Applied transparently in WebSocket relay — clients get actionable errors without backend changes.
Same auth middleware, different response format based on client:
// src/auth/middleware.ts
export function createAccessMiddleware(options: { type: 'json' | 'html', redirectOnMissing?: boolean }) {
return async (c, next) => {
const jwt = extractJWT(c);
if (!jwt) {
if (options.type === 'html' && options.redirectOnMissing) {
return c.redirect(`https://${teamDomain}`, 302);
}
return options.type === 'json'
? c.json({ error: 'Unauthorized', hint: '...' }, 401)
: c.html('<h1>Unauthorized</h1>', 401);
}
// ...
};
}Browser gets redirect to login, API client gets JSON error.
Return loading page immediately, start container in background:
// src/index.ts:230-242
if (!isGatewayReady && acceptsHtml) {
// Start in background (don't await!)
c.executionCtx.waitUntil(
ensureMoltbotGateway(sandbox, c.env).catch(console.error)
);
// Return loading page immediately
return c.html(loadingPageHtml);
}The loading page polls /api/status and reloads when ready. User sees progress, not a hanging request.
Prevent overwriting good backup with empty/corrupt data:
// src/gateway/sync.ts:39-58
// Verify source has critical files BEFORE rsync
const checkProc = await sandbox.startProcess('test -f /root/.clawdbot/clawdbot.json && echo "ok"');
await waitForProcess(checkProc, 5000);
if (!checkLogs.stdout?.includes('ok')) {
return {
success: false,
error: 'Sync aborted: source missing clawdbot.json',
details: 'Could indicate corruption or incomplete setup.',
};
}Don't just check status, wait for actual readiness:
// src/gateway/process.ts:56-77
const existingProcess = await findExistingMoltbotProcess(sandbox);
if (existingProcess) {
// ALWAYS use full timeout — process can be "running" but not ready
// (started by another concurrent request)
try {
await existingProcess.waitForPort(MOLTBOT_PORT, { timeout: STARTUP_TIMEOUT_MS });
return existingProcess;
} catch (e) {
// Stuck — kill and restart
await existingProcess.kill();
}
}The comment explains why — prevents premature kills from race conditions.
Composable mocks with sensible defaults:
// src/test-utils.ts
export function createMockEnv(overrides: Partial<MoltbotEnv> = {}): MoltbotEnv {
return {
Sandbox: {} as any,
ASSETS: {} as any,
MOLTBOT_BUCKET: {} as any,
...overrides,
};
}
export function createMockEnvWithR2(overrides = {}) {
return createMockEnv({
R2_ACCESS_KEY_ID: 'test-key-id',
R2_SECRET_ACCESS_KEY: 'test-secret-key',
CF_ACCOUNT_ID: 'test-account-id',
...overrides,
});
}Test code becomes: const env = createMockEnvWithR2({ DEBUG_ROUTES: 'true' }).
// src/index.ts:203-209
app.use('/debug/*', async (c, next) => {
if (c.env.DEBUG_ROUTES !== 'true') {
return c.json({ error: 'Debug routes are disabled' }, 404);
}
return next();
});Debug routes include /debug/processes?logs=true, /debug/env, /debug/ws-test (interactive WebSocket tester). Disabled by default, enable with wrangler secret put DEBUG_ROUTES → true.
// src/index.ts:55-82
function validateRequiredEnv(env: MoltbotEnv): string[] {
const missing: string[] = [];
// Conditional: AI Gateway requires BOTH key and URL
if (env.AI_GATEWAY_API_KEY) {
if (!env.AI_GATEWAY_BASE_URL) {
missing.push('AI_GATEWAY_BASE_URL (required when using AI_GATEWAY_API_KEY)');
}
} else if (!env.ANTHROPIC_API_KEY) {
missing.push('ANTHROPIC_API_KEY or AI_GATEWAY_API_KEY');
}
return missing;
}Error message explains the dependency: "required when using AI_GATEWAY_API_KEY".
// src/index.ts:284-365
const [clientWs, serverWs] = Object.values(new WebSocketPair());
const containerWs = (await sandbox.wsConnect(request, port)).webSocket;
serverWs.accept();
containerWs.accept();
// Bidirectional relay with transformation
serverWs.addEventListener('message', e => containerWs.send(e.data));
containerWs.addEventListener('message', e => {
let data = e.data;
// Transform errors here
serverWs.send(data);
});
return new Response(null, { status: 101, webSocket: clientWs });Transparent proxy — client has no idea messages are being intercepted.
src/routes/cdp.ts is ~1800 lines translating Chrome DevTools Protocol to Puppeteer calls. Key patterns:
- Session state management — tracks nodeIds, objectIds, frames
- Synthetic ID generation — CDP expects specific ID formats
- Proactive events — sends
Page.frameNavigated,Page.loadEventFiredafter navigation - Timing-safe auth —
crypto.timingSafeEqual()for secret comparison
This is a masterclass in protocol translation if you ever need to build a shim layer.
| Pattern | Benefit |
|---|---|
| Error transformation at boundary | Users get help, not stack traces |
| Content-type aware responses | HTML vs JSON automatically |
| Background init + loading page | No hanging cold starts |
| Sanity check before sync | Prevent data loss |
| Port-based health checks | Process "running" ≠ ready |
| Mock builders with variants | Less test boilerplate |
| Feature-flagged debug routes | Safe introspection in prod |
| Conditional config validation | Clear dependency errors |
| WebSocket interception | Transparent message rewriting |
The project uses @cloudflare/sandbox to run a full Docker container inside a Durable Object:
import { getSandbox, Sandbox } from '@cloudflare/sandbox';
const sandbox = getSandbox(env.Sandbox, 'moltbot', { keepAlive: true });
await sandbox.startProcess('/usr/local/bin/start-moltbot.sh', { env: envVars });
await process.waitForPort(18789, { mode: 'tcp', timeout: 120000 });Container filesystem is ephemeral. R2 mounted via s3fs provides persistence:
await sandbox.mountBucket('moltbot-data', '/data/moltbot', {
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: { accessKeyId, secretAccessKey },
});Limitations:
- No inotify/fswatch (FUSE limitation)
- No distributed locking
- Eventual consistency on listings
- Use for backup/restore, not live collaboration
Every 5 minutes: rsync local state → R2. On cold start: restore from R2 if newer.