Supalive, live queries for Postgres and MySQL

rebaz941 pts0 comments

@supalive/core - npm

npm

Search<br>Sign UpSign In

@supalive/core

0.1.4 • Public • Published 11 days ago<br>Readme<br>Code Beta<br>12 Dependencies<br>1 Dependents<br>5 Versions

@supalive/core

Core package for supalive — a reactive database framework.

Reactive queries for any Postgres/MySQL.

Write a typed query once. The client re-renders the moment a mutation touches a row that would change the result — no triggers, no LISTEN/NOTIFY wiring, no manual cache invalidation.

Live demo · How it works · Architecture

Installation

npm install @supalive/core

Exports

@supalive/core/procedure - Procedure definitions

@supalive/core/db/mysql - MySQL adapter

@supalive/core/db/pg - PostgreSQL adapter

@supalive/core/types - Type definitions

@supalive/core/server - Server implementation

@supalive/core/client - Client implementation

What it is

Supalive sits between your app and your database and turns any query into a live query.

You define procedures the same way you'd define a tRPC router. Mark one as a query and any client that calls useLiveQuery on it gets a stream of updates. When a mutation commits and its writes overlap that query's read set, Supalive recomputes and pushes the new result. Everything is typed end-to-end from the database column to the React hook.

It does not replace your database. It runs on top of plain Postgres or MySQL — including Supabase Postgres, Neon, RDS, PlanetScale — without schema changes.

The 30-second version

1. Define a query and a mutation.

// procedures.ts<br>import { z } from "zod";<br>import { createQueryBuilder, createMutationBuilder, defineSchema, router } from "@supalive/core/procedure";

const query = createQueryBuilderServerContext>();<br>const mutation = createMutationBuilderServerContext>();

const ItemSchema = defineSchema({<br>name: "items",<br>schema: {<br>id: z.string(),<br>title: z.string(),<br>status: z.enum(["todo", "in_progress", "done"]),<br>ownerId: z.string(),<br>},<br>columns: { ownerId: "owner_id" },<br>});

export const listItems = query({<br>args: z.object({ status: z.enum(["todo", "in_progress", "done"]).optional() }),<br>handler: async (ctx, { status }) => {<br>let q = ctx.db.query(ItemSchema).select()<br>.where((f) => f.eq("ownerId", ctx.serverCtx.user.userId));<br>if (status) q = q.where((f) => f.eq("status", status));<br>return q.orderBy("createdAt", "desc").limit(50).get();<br>},<br>});

export const createItem = mutation({<br>args: z.object({ id: z.string(), title: z.string(), status: z.enum(["todo", "in_progress", "done"]) }),<br>handler: async (ctx, args) => {<br>await ctx.db.insert(ItemSchema, args.id, {<br>...args,<br>ownerId: ctx.serverCtx.user.userId,<br>createdAt: new Date().toISOString(),<br>updatedAt: new Date().toISOString(),<br>});<br>},<br>});

export const appRouter = router({ procedures: { listItems, createItem } });

2. Start the server.

// server.ts<br>import { SupaliveWebSocketServer } from "@supalive/core/server";<br>import { appRouter } from "./procedures";

const server = new SupaliveWebSocketServer({<br>port: 3000,<br>database: { type: "postgres", connectionString: process.env.DATABASE_URL! },<br>upstash: { url: process.env.REDIS_URL!, token: "", redisUrl: process.env.REDIS_URL! },<br>subManagerUrl: process.env.SUB_MANAGER_URL!,<br>verifyAuth: async (data) => verifyJwt(data.token),<br>});<br>server.registerRouter(appRouter);<br>await server.start();

3. Use it on the client.

// App.tsx<br>import { useLiveQuery, useMutation } from "./supalive";

function Todos() {<br>const { data, status } = useLiveQuery((c) => c.listItems, { status: "todo" });<br>const [createItem] = useMutation((c) => c.createItem);

return (<br>{data?.map((item) => li key={item.id}>{item.title}li>)}<br>button onClick={() => createItem({ id: crypto.randomUUID(), title: "new", status: "todo" })}><br>add<br>button><br>);

Open this page in two tabs. Click add in one. Watch the list grow in both, automatically.

Why this exists

Postgres LISTEN/NOTIFY is row-level — you get a notification, you still have to figure out which queries it affects. Supabase Realtime is great for broadcast, presence, and a row-level changefeed, but it doesn't answer the question every reactive app actually has: "re-run this exact query when its result would change."

The frameworks that do answer that question — Convex, Zero, Replicache, PowerSync — each ship their own database, sync engine, or client-side store. That's a lot to adopt when all you wanted was reactivity on top of the Postgres you already have.

Supalive is the smallest possible "reactive query" layer that works on top of your existing database. It's the Convex DX without the Convex lock-in.

How it works

client supalive server sub-manager (1 process) postgres<br>────── ──────────────── ───────────────────────── ────────<br>┌── worker 0 ──┐<br>│ subs slice 0 │<br>├── worker 1 ──┤ ← FNV-1a(subId) % N<br>│ subs slice 1 │ routes ops here<br>├── … ────┤<br>│ worker N-1 │<br>└──────────────┘

subscribe ────────▶ register subId ───────────▶ slot in worker; if metadata<br>is still valid vs commit log,<br>skip recompute<br>liveQuery ────────────────────────────────────────────▶ SELECT<br>cache { data,...

supalive core query status server const

Related Articles