Home
Softono
zeroback

zeroback

Open source TypeScript
16
Stars
0
Forks
1
Issues
0
Watchers
2 months
Last Commit

About zeroback

An open-source Convex-style backend you deploy to your own Cloudflare account. Real-time queries, mutations, type-safe codegen β€” all running on Cloudflare Workers, Durable Objects, and SQLite.

Platforms

Web Self-hosted Cloud

Languages

TypeScript

Zeroback

License: MIT

An open-source Convex-style backend you deploy to your own Cloudflare account. Real-time queries, mutations, type-safe codegen β€” all running on Cloudflare Workers, Durable Objects, and SQLite.

πŸ“– Why Zeroback β€” the backstory

Zeroback demo β€” real-time sync across two browser windows

Get Started

npx @zeroback/cli init my-app
cd my-app
npm install @zeroback/server
zeroback dev

Edit zeroback/schema.ts and zeroback/tasks.ts, and you have a real-time backend. Add @zeroback/client and @zeroback/react when you're ready to connect your frontend.

Why Zeroback?

Convex introduced a great developer experience: define your backend as plain TypeScript functions, get real-time subscriptions and a type-safe client for free. Zeroback brings that same model to Cloudflare's edge infrastructure β€” giving you full control over your data and deployment.

  • Your Cloudflare account β€” data lives in your Durable Objects, not a third-party service
  • Real-time subscriptions β€” queries re-run and push updates over WebSocket when data changes
  • Type-safe codegen β€” generated api object gives you end-to-end type safety from database to UI
  • Optimistic concurrency control β€” mutations are checked for conflicts before committing
  • Database indexes β€” declare indexes in your schema, query them with .withIndex() for efficient lookups
  • Full-text search β€” declare search indexes in your schema, query with .search() for relevance-ranked results powered by SQLite FTS5
  • Count queries β€” efficient SELECT COUNT(*) via .count() on any query β€” no need to fetch all documents
  • Pagination β€” built-in cursor-based pagination with .paginate()
  • Authentication β€” built-in auth via better-auth with email/password and social providers (Google, GitHub). defineAuth() in your schema, useAuth() in React, ctx.auth in functions
  • Branded Id<T> type β€” IDs are typed per-table (Id<"tasks">) for compile-time safety across your entire stack
  • Single Durable Object β€” all state, transactions, and WebSocket connections in one place for strong consistency
  • Offline support β€” opt-in IndexedDB persistence for instant cached renders, offline reads, and mutation replay
  • SSR support β€” preload queries server-side with preloadQuery for instant hydration without a loading flash

Quick Start

1. Scaffold a new project

npx @zeroback/cli init my-app
cd my-app
npm install @zeroback/server

2. Define your schema

// zeroback/schema.ts
import { defineSchema, defineTable, v } from "@zeroback/server"

export const schema = defineSchema({
  tasks: defineTable({
    text: v.string(),
    isCompleted: v.boolean(),
  }),
})

3. Write your functions

// zeroback/tasks.ts
import { query, mutation, v } from "./_generated/server"

export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("tasks").order("desc").take(50)
  },
})

export const create = mutation({
  args: {
    text: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.insert("tasks", { text: args.text, isCompleted: false })
  },
})

export const toggle = mutation({
  args: {
    id: v.id("tasks"),
  },
  handler: async (ctx, args) => {
    const task = await ctx.db.get(args.id)
    if (!task) throw new Error("Task not found")
    await ctx.db.patch(args.id, { isCompleted: !task.isCompleted })
  },
})

4. Use in React

First, install the client package:

npm install @zeroback/react

Then use them in your React app:

import { ZerobackClient, ZerobackProvider, useQuery, useMutation } from "@zeroback/react"
import { api } from "../zeroback/_generated/api"

const client = new ZerobackClient("ws://localhost:8788/ws")

function Tasks() {
  const tasks = useQuery(api.tasks.list)
  const create = useMutation(api.tasks.create)
  const toggle = useMutation(api.tasks.toggle)

  return (
    <div>
      {tasks?.map((task) => (
        <p key={task._id} onClick={() => toggle({ id: task._id })}>
          {task.isCompleted ? "βœ…" : "⬜"} {task.text}
        </p>
      ))}
      <button onClick={() => create({ text: "New task" })}>
        Add Task
      </button>
    </div>
  )
}

function App() {
  return (
    <ZerobackProvider client={client}>
      <Tasks />
    </ZerobackProvider>
  )
}

5. Start development

zeroback dev

This will:

  1. Analyze your zeroback/ directory for schema and function definitions
  2. Generate type-safe code in zeroback/_generated/
  3. Start a local Cloudflare Worker with Durable Objects
  4. Watch for changes and rebuild automatically

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  React App                                                β”‚
β”‚  useQuery(api.tasks.list)                                 β”‚
β”‚  useMutation(api.tasks.create)                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ WebSocket
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Cloudflare Worker                                        β”‚
β”‚  Routes requests to Durable Object                        β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚  ZerobackDO (Durable Object)                          β”‚ β”‚
β”‚ β”‚                                                       β”‚ β”‚
β”‚ β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚
β”‚ β”‚  β”‚ User         β”‚ β”‚ Transaction  β”‚ β”‚ Subscription  β”‚  β”‚ β”‚
β”‚ β”‚  β”‚ Functions    β”‚ β”‚ Store        β”‚ β”‚ Manager       β”‚  β”‚ β”‚
β”‚ β”‚  β”‚ (bundled)    β”‚ β”‚ (OCC)        β”‚ β”‚ (realtime)    β”‚  β”‚ β”‚
β”‚ β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚
β”‚ β”‚                                                       β”‚ β”‚
β”‚ β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚
β”‚ β”‚  β”‚  SQLite (Durable Object Storage)                β”‚  β”‚ β”‚
β”‚ β”‚  β”‚  documents + indexes + scheduled jobs           β”‚  β”‚ β”‚
β”‚ β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Single Durable Object. Everything β€” queries, mutations, subscriptions, and WebSocket connections β€” runs inside one Durable Object instance. This gives you strong consistency without distributed coordination, but it also means your app is bound by the limits of a single DO.

User functions run in-process. Your zeroback/ functions are bundled into the worker and executed directly inside the Durable Object β€” no inter-service RPCs.

Limits of a single Durable Object

Zeroback runs entirely within one Cloudflare Durable Object. This keeps the architecture simple and strongly consistent, but comes with inherent platform constraints:

Limit Value Notes
Concurrent WebSocket connections ~1,000 Self-imposed (MAX_CONNECTIONS). The real bottleneck is the single-threaded CPU β€” each message is processed sequentially
SQLite storage 10 GB Cloudflare Durable Object storage limit
CPU per request 30s (Workers paid plan) Each mutation/query must complete within this budget
Single-threaded execution 1 core All queries, mutations, and subscription invalidations share one thread

For many apps (internal tools, collaborative docs, moderate-traffic SaaS), these limits are more than enough. If you need to scale beyond a single DO, you would need to shard across multiple Durable Objects β€” this is not built-in today.

Packages

Package Description
@zeroback/server Define schemas, queries, mutations. Database reader/writer, query builder, filter DSL
@zeroback/client WebSocket client with auto-reconnect, subscription management, mutation queue, IndexedDB persistence. Also exports preloadQuery for SSR.
@zeroback/react ZerobackProvider, useQuery, useMutation, useAction, usePaginatedQuery, useQueryWithStatus, useConnectionState, usePreloadedQuery, useAuth
@zeroback/solid Solid.js bindings: ZerobackProvider, createQuery, createQueryWithStatus, createMutation, createAction, createPaginatedQuery, createConnectionState
@zeroback/values Validator library (v.string(), v.number(), v.object(), etc.) for schema and args
@zeroback/cli zeroback init, zeroback dev, zeroback deploy, zeroback codegen, zeroback run, zeroback reset β€” scaffold, develop, deploy

Documentation

  • Schema & Validators β€” defineSchema, defineTable, v.* validators, indexes, search indexes
  • Functions β€” queries, mutations, actions, internal functions, HTTP actions, cron jobs, codegen
  • Database β€” reading, writing, QueryBuilder, filters, indexes, pagination, full-text search
  • Client SDK β€” ZerobackClient, subscriptions, optimistic updates, persistence
  • React Hooks β€” useQuery, useMutation, useAction, usePaginatedQuery
  • Solid.js β€” createQuery, createMutation, createAction, createPaginatedQuery
  • CLI β€” zeroback init, zeroback dev, zeroback deploy, zeroback codegen, zeroback run, zeroback reset
  • Authentication β€” better-auth integration, email/password and social providers, ctx.auth in functions
  • Scheduling β€” scheduler.runAfter, scheduler.runAt, cron jobs
  • File Storage β€” upload, serve, and manage files via Cloudflare R2
  • Deployment β€” deploy to Cloudflare Workers
  • How It Works β€” real-time subscriptions, OCC, type-safe codegen

Project Structure

your-project/
β”œβ”€β”€ zeroback/                      # Your backend code
β”‚   β”œβ”€β”€ schema.ts             # Table definitions
β”‚   β”œβ”€β”€ tasks.ts              # Query & mutation functions
β”‚   └── _generated/           # Auto-generated (don't edit)
β”‚       β”œβ”€β”€ api.ts            # Typed API references
β”‚       β”œβ”€β”€ server.ts         # Typed query/mutation factories
β”‚       └── dataModel.ts      # TypeScript types for tables
β”œβ”€β”€ src/                      # Your frontend code
β”‚   └── App.tsx
β”œβ”€β”€ wrangler.toml             # Scaffolded by zeroback init, user can customize
β”œβ”€β”€ package.json
└── .zeroback/
    └── entry.ts              # Scaffolded by zeroback init, user can customize

Both wrangler.toml and .zeroback/entry.ts are scaffolded once by zeroback init and owned by the user β€” you can customize them freely. The entry file imports from zeroback/_generated/manifest.ts (regenerated on every build), which wires your functions and schema to the @zeroback/server/runtime.

The wrangler.toml at project root points to .zeroback/entry.ts as the Worker entry point. Wrangler's bundler (esbuild) handles all import resolution from there.

Development

Prerequisites

  • Bun (package manager and runtime)
  • Wrangler (Cloudflare Workers CLI)

Running Locally

# Install dependencies
bun install

# Start the backend (from your app directory)
zeroback dev

# In another terminal, start the frontend
cd examples/task-manager
bun run dev

Open http://localhost:5173 to see the example task manager app.

Testing

Zeroback includes an end-to-end test suite that starts a local dev server and exercises the full stack over WebSocket:

# Run the E2E test suite
bun run test

# Watch mode
bun run test:watch

Tests cover mutations, index queries, pagination, real-time subscriptions, multi-client scenarios, argument validation, and document structure.

Deployment

Deploy your Zeroback backend to Cloudflare with a single command:

zeroback deploy

This runs codegen and then wrangler deploy. You can pass flags through to wrangler:

zeroback deploy --dry-run                    # codegen only, skip deploy
zeroback deploy -- --env production          # pass flags to wrangler

Then point your client to the production URL:

const client = new ZerobackClient("wss://your-worker.your-subdomain.workers.dev/ws");

Building Packages

bunx tsc --build

License

MIT