Package Exports
- userdo
- userdo/client
- userdo/jwt
- userdo/server
- userdo/worker
Readme
UserDO
A Durable Object base class for building applications on Cloudflare Workers.
What You Get
- Authentication: Email based (JWT) auth with signup, login, password reset
- Key-Value Storage: Per-user KV storage with automatic broadcasting
- Database: Type-safe SQLite tables with Zod schemas and query builder
- Web Server: Pre-built Hono server with all endpoints configured
- Real-time: WebSocket connections with hibernation API support
- Organizations: Multi-user teams with roles and member management
Installation
bun install userdoQuick Start
1. Create Your Durable Object (Your Database + Logic)
A Durable Object is like a mini-server that lives on Cloudflare's edge. Each user gets their own instance with their own database. You extend UserDO to add your business logic:
import { UserDO, type Env } from "userdo/server";
import { z } from "zod";
// Define your data schema
const PostSchema = z.object({
title: z.string(),
content: z.string(),
});
// This is your Durable Object - each user gets one instance
export class BlogDO extends UserDO {
posts: any;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
// Create a table that's private to this user
this.posts = this.table('posts', PostSchema, { userScoped: true });
}
// Add your business methods
async createPost(title: string, content: string) {
return await this.posts.create({ title, content });
}
async getPosts() {
return await this.posts.orderBy('createdAt', 'desc').get();
}
}2. Create Your Worker (Your HTTP Gateway)
The Worker handles HTTP requests and routes them to the right user's Durable Object. It comes with built-in auth endpoints and you add your own:
import { createUserDOWorker, createWebSocketHandler } from 'userdo/server';
// Create the HTTP server with built-in auth endpoints
const app = createUserDOWorker('BLOG_DO');
const wsHandler = createWebSocketHandler('BLOG_DO');
// Add your custom endpoints
app.post('/api/posts', async (c) => {
const user = c.get('user');
if (!user) return c.json({ error: 'Unauthorized' }, 401);
const { title, content } = await c.req.json();
// Get this user's Durable Object instance
const blogDO = getUserDOFromContext(c, user.email, 'BLOG_DO') as BlogDO;
const post = await blogDO.createPost(title, content);
return c.json({ post });
});
// Export with WebSocket support
export default {
async fetch(request: Request, env: any, ctx: any): Promise<Response> {
if (request.headers.get('upgrade') === 'websocket') {
return wsHandler.fetch(request, env, ctx);
}
return app.fetch(request, env, ctx);
}
};Built-in HTTP endpoints (no code needed):
POST /api/signup- Create accountPOST /api/login- Sign inGET /api/me- Get current userGET /api/ws- WebSocket connection- See all endpoints
3. Configure wrangler.jsonc
{
"main": "src/index.ts",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"JWT_SECRET": "your-jwt-secret-here"
},
"durable_objects": {
"bindings": [
{ "name": "BLOG_DO", "class_name": "BlogDO" }
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["BlogDO"]
}
]
}Important: The migrations section with new_sqlite_classes is required to enable SQL database functionality. Without it, you'll get errors about SQL not being enabled.
4. Build Your Frontend
UserDO provides the backend API - you bring your own frontend (React, Vue, vanilla JS, etc.). Check out our examples for complete applications with frontend code.
Built-in API Endpoints
These endpoints work without additional configuration:
Authentication
POST /api/signup- Create user accountPOST /api/login- Authenticate userPOST /api/logout- End sessionGET /api/me- Get current user
Organizations (Multi-user Teams)
POST /api/organizations- Create organizationGET /api/organizations- Get owned organizationsGET /api/organizations/:id- Get specific organizationPOST /api/organizations/:id/members- Add member (auto-invites)DELETE /api/organizations/:id/members/:userId- Remove member
Data Storage
GET /data- Get user's key-value dataPOST /data- Set user's key-value data
Real-time
GET /api/ws- WebSocket connection for live updates
Organization-Scoped Applications
UserDO handles multi-user team applications:
export class TeamDO extends UserDO {
projects: any;
tasks: any;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
// Data automatically isolated per organization
this.projects = this.table('projects', ProjectSchema, { organizationScoped: true });
this.tasks = this.table('tasks', TaskSchema, { organizationScoped: true });
}
async createProject(name: string, organizationId: string) {
await this.getOrganization(organizationId); // Built-in access control
this.setOrganizationContext(organizationId); // Switch data scope
return await this.projects.create({ name }); // Auto-scoped to org
}
}
// Member management:
await teamDO.addOrganizationMember(orgId, 'user@example.com', 'admin');
// Stores invitation in target user's UserDO
const { memberOrganizations } = await userDO.getOrganizations();
// Returns all invitations/memberships for this userExamples
React + Vite
Modern React application with Vite - Full-stack task management app with authentication, real-time updates, and beautiful Tailwind UI. Shows proper Vite/Wrangler development workflow.
Organizations
Complete team project management system - Organizations → Projects → Tasks with member management, role-based access control, and real-time collaboration.
Hono Integration
Full-featured web application - Complete auth flows, data management, WebSocket integration, and browser client usage patterns.
Alchemy Deployment
Production deployment - Ready-to-deploy configuration for Alchemy.run with environment setup and scaling considerations.
Effect Integration
Functional programming - Integration with Effect library for advanced error handling and functional composition patterns.
Multi-tenant
Multiple isolated projects - How to run multiple independent applications using different UserDO binding names.
Browser Client
import { UserDOClient } from 'userdo/client';
const client = new UserDOClient('/api');
// Authentication
await client.signup('user@example.com', 'password');
await client.login('user@example.com', 'password');
// Real-time data
client.onChange('preferences', data => {
console.log('Preferences updated:', data);
});
// Organizations
const orgs = await client.get('/organizations');JWT Utilities
UserDO provides JWT utilities that match the internal token handling, so you don't need to reimplement JWT logic in your applications:
import {
verifyJWT,
decodeJWT,
isTokenExpired,
getEmailFromToken,
generateAccessToken,
generateRefreshToken,
generatePasswordResetToken,
type JwtPayload
} from 'userdo/server';
// Verify JWT with secret
const { ok, payload, error } = await verifyJWT(token, process.env.JWT_SECRET);
if (ok) {
console.log('Valid token for:', payload.email);
}
// Decode JWT without verification (useful for extracting info)
const payload = decodeJWT(token);
if (payload) {
console.log('Token email:', payload.email);
}
// Check if token is expired
const isExpired = isTokenExpired(payload);
// Extract email from token
const email = getEmailFromToken(token);
// Generate tokens (matches UserDO internal format)
const accessToken = await generateAccessToken(userId, email, secret);
const refreshToken = await generateRefreshToken(userId, secret);
const resetToken = await generatePasswordResetToken(userId, email, secret);These utilities are particularly useful for:
- SvelteKit/Next.js middleware: Verify tokens in server-side code
- Custom authentication flows: Generate tokens outside of UserDO
- Token validation: Check token validity without calling UserDO
- Email extraction: Get user email from tokens for routing
Development Setup with Custom WebSocket URL
For development environments where your frontend and backend run on different ports (e.g., Vite on 5173, Worker on 8787), you can specify a custom WebSocket URL:
// Development: Frontend on :5173, Backend on :8787
const isDev = window.location.port === '5173';
const client = new UserDOClient('/api', {
websocketUrl: isDev ? 'ws://localhost:8787/api/ws' : undefined
});This allows:
- ✅ HTTP requests to use proxied routes (
/api→localhost:8787) - ✅ WebSocket connections to connect directly to the worker
- ✅ No CORS issues for HTTP (handled by proxy)
- ✅ No proxy complexity for WebSockets
Benefits:
- Solves cross-origin WebSocket issues in development
- Works with any frontend framework (React, Vue, Svelte, etc.)
- No complex proxy configuration needed
- Backwards compatible - existing code continues to work
Production Usage
In production, omit the websocketUrl option for automatic behavior:
// Production: Uses current domain for WebSocket connections
const client = new UserDOClient('/api');Database Operations
Simple Tables
// User-scoped data (private to each user)
this.posts = this.table('posts', PostSchema, { userScoped: true });
// Organization-scoped data (shared within teams)
this.projects = this.table('projects', ProjectSchema, { organizationScoped: true });CRUD Operations
// Create
const post = await this.posts.create({ title, content });
// Read
const posts = await this.posts.orderBy('createdAt', 'desc').get();
const post = await this.posts.findById(id);
// Update
await this.posts.update(id, { title: 'New Title' });
// Delete
await this.posts.delete(id);
// Query
const results = await this.posts
.where('title', '==', 'Hello')
.limit(10)
.get();Real-time Events
Data changes automatically broadcast WebSocket events:
// Listen for specific data changes
client.onChange('preferences', data => console.log('Updated:', data));
// Listen for table changes
client.onChange('table:posts', event => {
console.log('Post changed:', event.type, event.data);
});Architecture
- Per-user isolation: Each user gets their own Durable Object instance
- Email-based routing: User emails determine Durable Object IDs
- WebSocket hibernation: Uses Cloudflare's hibernation API for WebSocket handling
- Type-safe schemas: Zod validation for all operations
- Automatic broadcasting: Real-time events for all data changes
Getting Started
Ready to build? Check out the examples directory for complete applications, or start with the quick start guide above.