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
npm init -y# @heliosdb/client is not published on npm; build it from sourcegit clone https://github.com/HeliosDatabase/HeliosDB-SDKs.git(cd HeliosDB-SDKs/sdks/typescript && npm install && npm run build)npm install ./HeliosDB-SDKs/sdks/typescriptnpm install wsnpm install -D typescript ts-node @types/node @types/wsnpx tsc --initA Nano server with auth and HTTP enabled (default ports):
heliosdb-nano start \ --memory \ --auth scram-sha-256 \ --password s3cret1. Connect
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.sqlCREATE 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.
psql "postgresql://postgres:s3cret@127.0.0.1:5432/postgres" -f schema.sql3. Auth — Sign Up + Login
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/signupPOST /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
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.descAuthorization: Bearer <jwt>5. Subscribe to Row-Level Changes (Realtime)
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
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:
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
// Updateawait db.from("posts") .update({ body: "edited" }) .eq("id", post.id) .select();
// Deleteawait db.from("posts") .delete() .eq("id", post.id);
// Compound filtersawait db.from("posts") .select("*") .gte("created_at", "2026-04-01") .ilike("title", "%nano%") .order("created_at", { ascending: false }) .range(0, 9); // first 10Nano’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
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized after login | JWT expired (default 1h) | Refresh: db.auth.refreshSession() |
| WebSocket disconnects immediately | Server not running with HTTP enabled | Confirm start (not dump/restore); HTTP is on by default at port 8080 |
db.from is not a function | Wrong package | Confirm @heliosdb/client (not the generic heliosdb-nano) |
Where Next
- BAAS_REST_API_TUTORIAL — every REST verb at the curl level.
- REALTIME_WEBSOCKET — channel topology, presence, broadcast.
- TypeScript SDK reference — full fluent API surface.
- PYTHON_VECTOR_APP — semantic search end-to-end in Python.