Skip to content

Node.js BaaS App — REST + Realtime End-to-End

Stack: TypeScript + @heliosdb/client (Supabase-compatible fluent API) + ws HeliosDB build: default — no feature flag required


UVP

Building a real app on top of an embedded database usually means writing a server tier just to expose CRUD over HTTP, then bolting Socket.IO on top for live updates. HeliosDB Nano includes both in the same binary: a PostgREST-style REST surface at /rest/v1/{table} and a Supabase-style WebSocket channel at /realtime/v1/websocket. The TypeScript client @heliosdb/client is built from the HeliosDB SDKs source (it is not published on npm) and gives you the fluent db.from('table').select() API you already know. One createClient call. Auth + REST + Realtime, in TypeScript.


Prerequisites

Terminal window
npm init -y
# @heliosdb/client is not published on npm; build it from source
git clone https://github.com/HeliosDatabase/HeliosDB-SDKs.git
(cd HeliosDB-SDKs/sdks/typescript && npm install && npm run build)
npm install ./HeliosDB-SDKs/sdks/typescript
npm install ws
npm install -D typescript ts-node @types/node @types/ws
npx tsc --init

A Nano server with auth and HTTP enabled (default ports):

Terminal window
heliosdb-nano start \
--memory \
--auth scram-sha-256 \
--password s3cret

1. Connect

src/db.ts
import { createClient } from "@heliosdb/client";
export const db = createClient(
"http://localhost:8080",
"anon-key" // any string — used for the bearer token before login
);

The client speaks the same fluent API as @supabase/supabase-js; if you’ve used Supabase before, the surface is identical.


2. Schema (one-time, via SQL)

Schema management still goes through SQL. Use any PostgreSQL client — psql, pgAdmin, or a .sql file at boot:

-- schema.sql
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
author TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT,
created_at TIMESTAMP DEFAULT now()
);

Row-level security for REST clients is planned (see section 8), so this schema has no policy.

Terminal window
psql "postgresql://postgres:s3cret@127.0.0.1:5432/postgres" -f schema.sql

3. Auth — Sign Up + Login

src/auth.ts
import { db } from "./db.js";
export async function signup(email: string, password: string) {
const { data, error } = await db.auth.signUp({ email, password });
if (error) throw error;
return data;
}
export async function login(email: string, password: string) {
const { data, error } = await db.auth.signInWithPassword({ email, password });
if (error) throw error;
// data.session.access_token is now attached to every subsequent request
return data.session;
}

Behind the scenes the client is hitting:

  • POST /auth/v1/signup
  • POST /auth/v1/token?grant_type=password

Both endpoints are part of the BaaS surface; see BAAS_REST_API_TUTORIAL for the full curl-level reference.


4. Insert via REST

src/posts.ts
import { db } from "./db.js";
export async function createPost(title: string, body: string) {
const { data, error } = await db
.from("posts")
.insert({ title, body })
.select()
.single();
if (error) throw error;
return data;
}
export async function listMyPosts() {
// Returns every post: RLS on REST is planned, so filter by author in your service layer
const { data, error } = await db
.from("posts")
.select("*")
.order("created_at", { ascending: false });
if (error) throw error;
return data;
}
export async function searchPosts(term: string) {
return db.from("posts")
.select("id, title, body")
.ilike("title", `%${term}%`)
.limit(20);
}

The fluent chain compiles down to a single REST call:

GET /rest/v1/posts?select=*&order=created_at.desc
Authorization: Bearer <jwt>

5. Subscribe to Row-Level Changes (Realtime)

src/realtime.ts
import { db } from "./db.js";
export function watchPosts(onChange: (evt: any) => void) {
const channel = db
.channel("posts-changes")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "posts" },
(payload) => {
console.log("[realtime]", payload.eventType, payload.new ?? payload.old);
onChange(payload);
}
)
.subscribe();
return () => channel.unsubscribe();
}

Under the hood this opens a WebSocket to ws://localhost:8080/realtime/v1/websocket, joins a topic, and emits INSERT / UPDATE / DELETE envelopes shaped like Supabase’s. Known issue: realtime events are not delivered in the current release; tracked by the Nano team. Realtime subscriptions are not filtered by row-level security; secured realtime is planned.


6. Wire it Together

src/app.ts
import { signup, login } from "./auth.js";
import { createPost, listMyPosts } from "./posts.js";
import { watchPosts } from "./realtime.js";
async function main() {
// Create + login
await signup("alice@example.com", "s3cret");
const session = await login("alice@example.com", "s3cret");
console.log("logged in as", session.user.email);
// Subscribe before writing so we observe our own insert
const stop = watchPosts((evt) => console.log("change:", evt.eventType));
// Insert
const post = await createPost("Hello Nano", "First post over BaaS");
console.log("inserted:", post);
// Read back
const mine = await listMyPosts();
console.log("mine:", mine);
setTimeout(() => { stop(); process.exit(0); }, 1000);
}
main().catch((e) => { console.error(e); process.exit(1); });

Run:

Terminal window
npx ts-node src/app.ts
# logged in as alice@example.com
# [realtime] INSERT { id: 1, author: 'alice@example.com', title: 'Hello Nano', ... } (once the realtime known issue is fixed)
# inserted: { id: 1, ... }
# mine: [ { id: 1, ... } ]

7. Update / Delete with the Same Fluent API

// Update
await db.from("posts")
.update({ body: "edited" })
.eq("id", post.id)
.select();
// Delete
await db.from("posts")
.delete()
.eq("id", post.id);
// Compound filters
await db.from("posts")
.select("*")
.gte("created_at", "2026-04-01")
.ilike("title", "%nano%")
.order("created_at", { ascending: false })
.range(0, 9); // first 10

Nano’s REST filters support eq, neq, gt, gte, lt, lte, like, ilike, is and in, with not. negation; cs, cd, ov, full-text and or/and filters are planned, so the client’s chain methods for them fail against the current release.


8. Row-Level Security (planned for REST)

In the stock Nano server, row-level security is enforced through the embedded API and the REPL; SQL CREATE POLICY and enforcement for REST and wire clients are planned. The JWT from login() is attached to every request via Authorization: Bearer <jwt>, but REST requests are not filtered by RLS policies today, so enforce per-user access (for example author = <the signed-in user>) in your service layer.


Troubleshooting

SymptomCauseFix
401 Unauthorized after loginJWT expired (default 1h)Refresh: db.auth.refreshSession()
WebSocket disconnects immediatelyServer not running with HTTP enabledConfirm start (not dump/restore); HTTP is on by default at port 8080
db.from is not a functionWrong packageConfirm @heliosdb/client (not the generic heliosdb-nano)

Where Next