Start here

@hulla/db

Read and write your own PostgreSQL database with a typed SDK, and refresh subscribed browser queries after committed writes.

On this page

@hulla/db gives your application typed PostgreSQL access and live query results. Define your tables once. Use db in server code, or declare queries and mutations for the browser.

Edit the table or the app. Both writes go through the server model.

Databaseproducts table
Servervalidate · commit · publish
Clientsubscribed storefront

products

Edit a name or stock level directly.

namestock

Committed revision 0

Storefront

Rename a product, or buy one to change stock.

5 available
2 available
Sold out

Subscribed to products.list · revision 0

Try setting the first row’s stock to 10, then buy one in the storefront.

Local model with separate client and database state connected by message ports. No PostgreSQL connection.

db/tasks.ts
import { query, mutation } from './_generated/functions'
import { z } from 'zod'

export const list = query((ctx) => ctx.db.select('tasks').limit(100))

export const create = mutation({
  input: z.object({ title: z.string().min(1) }),
  handler: (ctx, input) => ctx.db.insert('tasks', input),
})

In a React component, observe the query and call the mutation:

src/Tasks.tsx
import { useMutation, useQuery } from '@hulla/db-react'
import { create, list } from '../db/_generated/client/tasks'

export function Tasks() {
  const tasks = useQuery(list)
  const add = useMutation(create)
  return (
    <>
      {tasks.data?.map((task) => (
        <p key={task.id}>{task.title}</p>
      ))}
      <button
        onClick={() => {
          void add({ title: 'Write the guide' })
        }}>
        Add task
      </button>
    </>
  )
}

After a write commits, the server reevaluates affected queries and the component receives the new result. You do not invalidate query keys or synchronize a browser database. Vue, Svelte and Solid have their own UI bindings; plain JavaScript can subscribe directly.

The examples above assume a table and a configured connection. The quick start takes you from those files to a working live task list. Installation covers npm packages, supported native platforms and build/runtime prerequisites.

An API route, server action or background job can use ordinary database builders:

server/tasks.ts
import { db } from '../db/_generated/server'

const task = await db.insert('tasks', { title: 'Write the guide' })
const tasks = await db.select('tasks').orderBy('title').limit(100)

Your application owns authentication, authorization and external services in this code. Use db.transaction when several writes must commit or roll back together. Direct writes also refresh matching browser subscriptions.

You need to…Use
Read or write from trusted server codeDirect database access
Keep a browser view currentQueries and mutations with a UI binding
Render on the server, then subscribe in the browserA framework recipe
Show a change before its mutation finishesAn optimistic mutation wrapper

A server-only application does not need browser declarations or a subscription service. A browser application calls declared functions; it never receives the PostgreSQL credentials or direct database handle.

PostgreSQL stores the data. You can self-host it or use a suitable managed PostgreSQL endpoint. Your existing framework owns routes, server functions, loading boundaries and deployment. Your auth provider owns login and sessions; the optional Better Auth adapter stores its records in the same database.

For live browser queries, run the supplied persistent service alongside your application. Server database access and that service require Node/Bun with native process support. See the mental model for the boundaries and deployment guide for hosting requirements.

Start with the working React example, or go directly to Next.js, Nuxt, SvelteKit, SolidStart or Astro. Native Rust applications have a separate guide.