OneBase
v0.1.0 · self-hosted · Convex-style API

The reactive backend
in one file.

TypeScript functions, a typed document database and live queries, running as a single binary on SQLite, Postgres or Turso. Copy it to a server and run it.

$bun add github:abdullah-bl/onebase
~/my-app
$ onebase serve --backup s3://my-bucket/backups
onebase v0.1.0
  functions  9 (channels:list, messages:list, messages:send, …)
  crons      prune old messages
  database   onebase_data/db.sqlite
  files      onebase_data/files
  backups    s3://my-bucket/backups every 60 min, keep 24
  http       http://localhost:8090
  sync       ws://localhost:8090/api/sync
~40 MBmemory at startup, dashboard included
8.6 msper mutation, pushed to 100 live clients
1 fileto deploy: the binary, optionally with your app inside
5 KBbrowser client, served by the server

Measured on Linux arm64, Bun 1.4.2, SQLite, with the example chat app.

Live queries

Every client stays up to date. You don't write any sync code.

Clients subscribe to queries. When a mutation commits, onebase re-runs only the queries whose data changed and pushes the new results over WebSocket. Try it:

Aliceconnected
    Bobconnected

      Simulated in your browser. Waiting for a mutation…

      Write the backend in TypeScript

      A schema, some functions, and a hook. That's the whole backend.

      The API follows Convex: query, mutation, action, v.* validators, ctx.db. Everything is typed, from the schema to the React component.

      onebase/messages.tsserver
      import { v } from "onebase/values";
      import { mutation, query } from "./_generated/server";
      
      export const list = query({
        args: { channel: v.id("channels") },
        handler: (ctx, { channel }) =>
          ctx.db
            .query("messages")
            .withIndex("by_channel", (q) => q.eq("channel", channel))
            .collect(),
      });
      
      export const send = mutation({
        args: { channel: v.id("channels"), body: v.string() },
        handler: async (ctx, args) => {
          const user = await ctx.auth.getUserIdentity();
          const author = user?.name ?? "anonymous";
          return ctx.db.insert("messages", { ...args, author });
        },
      });
      app/Chat.tsxclient
      import { useMutation, useQuery } from "onebase/react";
      import { api } from "../onebase/_generated/api";
      
      export function Chat({ channel }) {
        // Re-renders whenever anyone sends a message.
        const messages = useQuery(api.messages.list, { channel });
        const send = useMutation(api.messages.send);
      
        return (
          <>
            {messages?.map((m) => (
              <p key={m._id}><b>{m.author}</b> {m.body}</p>
            ))}
            <button onClick={() => send({ channel, body: "hi" })}>
              Say hi
            </button>
          </>
        );
      }
      Batteries included

      The parts every app ends up needing.

      Fine-grained live queries

      A new message in channel A never re-runs the query for channel B. Identical subscriptions share a single execution.

      Real transactions

      Mutations are atomic and serializable. Throw an error and none of the writes happen, even across several servers.

      Node and npm everywhere

      Every function runs in Bun. Import any package from a query, a mutation or an action. There's no "use node".

      Typed end to end

      The types flow from the schema to ctx.db, to api.*, to useQuery. Arguments are validated at runtime too.

      Scheduler and crons

      runAfter, runAt and cron jobs survive restarts. Each run happens exactly once, even with several instances.

      File storage

      Uploads stream to disk or to S3, R2 or MinIO, so big files never sit in memory. Downloads come from presigned URLs.

      Sign-in built in

      Passwords, GitHub, Google or any OAuth provider, with sessions, in an API shaped like Convex Auth. Or bring Clerk, Auth0 or any OIDC issuer.

      SQLite, Postgres or Turso

      Start on SQLite. Switch with --db when you need more servers, and your functions don't change.

      Backups to S3

      Consistent SQLite snapshots, gzipped and uploaded on a schedule. onebase restore brings them back.

      Hard to get wrong

      Made for code written by people and by LLMs.

      The API is the one models already know from Convex. It forgives the usual mistakes, and when something is wrong it says how to fix it.

      • .filter({ done: false }) works, and it uses an index when one exists.
      • Every error names the problem and the fix: the wrong field, a missing index, an argument that doesn't match.
      • onebase doctor checks a project in one command, and the docs come as llms-full.txt to give your assistant.
      $ onebase doctor
      ✗ error posts.ts:2: Imports from the convex package
               instead of onebase.
               fix: Use "./_generated/server" and "onebase/values".
      ✗ error posts.ts:4: .filter() on a query gets a filter
               builder, not a document.
               fix: .filter(q => q.eq(q.field("x"), v)) or .filter({ x: v })
      ✗ error schema.ts › posts.index("by_athor"):
               Field "athor" doesn't exist on "posts".
               fix: Known fields: title, author
      ⚠ warn  Query on "posts" reads every document to apply
               .filter(). Add .index("by_author", ["author"])
               and it will be used automatically.
      Deploy

      From a $5 VPS to a fleet of servers.

      One binary. Pick the database that fits your setup today, and change it later without touching your functions.

      one server

      SQLite + S3 backups

      The default. Reads take microseconds, there's no database to run, and hourly snapshots go to any S3-compatible storage.

      onebase serve \
        --backup s3://bucket/backups
      many servers

      Postgres

      Run as many instances as you need behind a load balancer. Live queries are shared through LISTEN/NOTIFY, and mutations are serializable.

      onebase serve \
        --db postgres://…/app \
        --files s3://bucket/files
      managed SQLite

      Turso

      SQLite semantics without running a database yourself, and still as many instances as you like.

      TURSO_AUTH_TOKEN=… onebase serve \
        --db libsql://my-db.turso.io
      Compared to Convex

      Same model. Different trade-offs.

      Convex is a polished managed platform. onebase is for when you want to own the whole stack.

      Convexonebase
      HostingManaged cloud, or a self-hosted backendOne binary you run anywhere
      RuntimeV8 isolates; Node only in actionsBun in every function
      DatabaseConvex's ownSQLite, Postgres, MySQL or Turso
      .filter()Builder; scans the documentsBuilder or object; uses indexes
      DashboardIncludedIncluded, at /_admin in the binary
      Full-text searchIncludedIncluded, same API
      Vector searchIncludedIncluded, same API
      Portingn/aChange the imports; onebase doctor lists them

      Start with one file.

      Clone the example chat app and have a live, typed backend running in a minute.