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

# 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.**

:::caution[Known issue]
The `/rest/v1` table routes are a known issue in the current release; see
[Known Limitations](https://www.heliosdb.com/docs/nano/reference/compatibility/known-limitations/#planned).
Until it is fixed, run reads and writes over the PostgreSQL wire protocol (for
example with the `pg` driver, Path B of the [TypeScript SDK page](https://www.heliosdb.com/docs/nano/api/typescript-sdk/)).
Row-level security is not applied to REST requests or realtime subscriptions;
both are planned.
:::

---

## Prerequisites

```bash
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):

```bash
heliosdb-nano start \
    --memory \
    --auth scram-sha-256 \
    --password s3cret
```

---

## 1. Connect

```ts
// 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:

```sql
-- 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](#8-row-level-security-planned-for-rest)), so this schema has no policy.

```bash
psql "postgresql://postgres:s3cret@127.0.0.1:5432/postgres" -f schema.sql
```

---

## 3. Auth — Sign Up + Login

```ts
// 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](https://www.heliosdb.com/docs/nano/guides/tutorials/baas_rest_api_tutorial/) for the full curl-level reference.

---

## 4. Insert via REST

```ts
// 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)

```ts
// 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

```ts
// 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:

```bash
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

```ts
// 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

| 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](https://www.heliosdb.com/docs/nano/guides/tutorials/baas_rest_api_tutorial/) — every REST verb at the curl level.
- [REALTIME_WEBSOCKET](https://www.heliosdb.com/docs/nano/guides/tutorials/realtime_websocket_tutorial/) — channel topology, presence, broadcast.
- [TypeScript SDK reference](https://www.heliosdb.com/docs/nano/api/typescript-sdk/) — full fluent API surface.
- [PYTHON_VECTOR_APP](https://www.heliosdb.com/docs/nano/guides/tutorials/sdk/python_vector_app/) — semantic search end-to-end in Python.
