Skip to content
Merged
60 changes: 54 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
# Changelog

## 0.2.0-next.5
## 0.2.0-next.6

Fix: `onAuthStateChange` (and thus `<AuthGate>`) no longer hangs forever when
the initial `/users/me` session check rejects (cross-origin/network failure —
e.g. the sandbox-preview context used for project-card screenshots). A rejected
check now fires `SIGNED_OUT` instead of leaving `loading` stuck, so the app
renders its sign-in screen rather than a blank page. Adds a regression test.
Combines the entities data layer (next.0–next.4) with the auth fail-safe fix
that shipped separately as next.5, so the canary `next` channel carries both.

Fix (from next.5): `onAuthStateChange` (and thus `<AuthGate>`) no longer hangs
forever when the initial `/users/me` session check rejects (cross-origin/network
failure — e.g. the sandbox-preview context used for project-card screenshots). A
rejected check now fires `SIGNED_OUT` instead of leaving `loading` stuck, so the
app renders its sign-in screen rather than a blank page. Adds a regression test.

## 0.2.0

Adds the **entities data layer** — a Base44-parity data API over the gateway so
apps read/write data without touching Supabase, SQL, or credentials directly:

```ts
const todos = await bool.entities.todos.list("-created_at");
const one = await bool.entities.todos.create({ title: "hi" });
await bool.entities.todos.update(one.id, { done: true });
await bool.entities.todos.filter({ status: "active", count: { $gte: 10 } });
```

`bool.entities.<table>` mirrors Base44's entity surface one-to-one:
- **Reads:** `list`, `filter`, `get` — with `sort` (`-col`), `limit`, `skip`,
and `fields` (column selection).
- **Writes:** `create`, `bulkCreate`, `update`, `bulkUpdate`, `delete`.
- **Bulk-by-query:** `updateMany(query, { $set })`, `deleteMany(query)`.
- **Import:** `importEntities(csvFile)` (parsed client-side → `bulkCreate`).
- **Realtime:** `subscribe(cb)` (gateway doorbell).
- **Filter DSL:** MongoDB-style — `$eq $ne $gt $gte $lt $lte $in $nin $exists
$regex $all $not` per field, `$and`/`$or`/`$nor` at the root, array shorthand,
and `null` → IS NULL.

Methods return row data directly and throw on error. Additive and
backward-compatible — `bool.db` / `supabase` still work.

Known gaps vs. Base44 (documented, follow-ups): `updateMany` with
`$inc/$mul/$push/$pull` is read-modify-write (not atomic under concurrent
writers — a Postgres RPC would make it atomic); `$size` (filter by array
length) isn't expressible over PostgREST and is omitted.

**`EntitiesModule` is now an augmentable `interface`** (was a `type` alias), so
generated apps can type each entity via `declare module "bool-sdk"`:

```ts
declare module "bool-sdk" {
interface EntitiesModule { board_games: EntityHandler<BoardGames> }
}
```

That makes `bool.entities.board_games` typed (field names, enum values, types)
while the string index signature keeps un-declared tables usable as
`EntityHandler<any>`. Bool's `define_entity` tool writes one such `.d.ts` per
model. No runtime change.

## 0.1.1

Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ tested, and upgradable independently of any one app.

## What it does

- **Entities data API.** `client.entities.<table>` is the recommended way to
read/write data — a one-to-one mirror of Base44's entity surface: `list`,
`filter`, `get`, `create`, `bulkCreate`, `update`, `bulkUpdate`, `updateMany`,
`delete`, `deleteMany`, `importEntities`, `subscribe`. It hides Supabase/SQL
entirely; methods return rows directly and throw on error:
```ts
const todos = await bool.entities.todos.list("-created_at");
const one = await bool.entities.todos.create({ title: "hi" });
await bool.entities.todos.update(one.id, { done: true });
await bool.entities.todos.filter({ status: "active", count: { $gte: 10 } });
await bool.entities.todos.updateMany({ done: false }, { $set: { done: true } });
```
Filters use MongoDB-style operators (`$eq $ne $gt $gte $lt $lte $in $nin
$exists $regex $all $not`, plus `$and`/`$or`/`$nor`); sort is a `-col` string.
- **Data + Storage through the Bool gateway.** `client.db` is a standard
[supabase-js](https://supabase.com/docs/reference/javascript) client whose
REST and Storage traffic is routed to the Bool gateway (`/_bool/v1/db`). The
gateway injects the real credential server-side and pins the app's private
Postgres schema — the anon key in the bundle has no data grants and can't
read anything directly.
[supabase-js](https://supabase.com/docs/reference/javascript) client (what
`entities` is built on) whose REST and Storage traffic is routed to the Bool
gateway (`/_bool/v1/db`). The gateway injects the real credential server-side
and pins the app's private Postgres schema — the anon key in the bundle has
no data grants and can't read anything directly.
- **Realtime "doorbell".** Postgres changes broadcast a row-data-free
`{table, op}` ping on the app's public channel; `subscribeToChanges` wraps
the subscription. Refetch on each ping — the ping never carries row data.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bool-sdk",
"version": "0.2.0-next.5",
"version": "0.2.0-next.6",
"description": "Client SDK for apps built on Bool — gateway data access, end-user auth, and the React auth layer.",
"type": "module",
"main": "./dist/index.js",
Expand Down
41 changes: 25 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// Keep this in sync with the gateway data route (/_bool/v1/db) and users route
// (/_bool/v1/users) in the Bool platform repo.
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
import { createEntitiesModule, type EntitiesModule } from "./entities.js";

/** Matches the server's append-only gateway path version. */
const GATEWAY_API = "v1";
Expand Down Expand Up @@ -99,6 +100,9 @@ export type BoolClient = {
* Use it exactly like a normal supabase-js client: `.from(...)`, `.storage`,
* `.channel(...)`. Do NOT use `db.auth` — end-user auth is `client.auth`. */
db: BoolDb;
/** The data API: `entities.<table>.list/filter/get/create/update/delete`.
* The recommended way to read/write app data — hides Supabase entirely. */
entities: EntitiesModule;
/** End-user auth for this app (gateway users plane). */
auth: BoolAuth;
/** This app's private Postgres schema name. */
Expand Down Expand Up @@ -426,26 +430,31 @@ export function createBoolClient(config: BoolClientConfig): BoolClient {
},
};

// Realtime "doorbell": the app schema's grants are revoked, so Supabase
// `postgres_changes` never fires. Instead the server broadcasts a
// row-data-free ping on the PUBLIC channel "bool:" + schema whenever any
// row changes. Subscribe with the anon key (no token needed) and REFETCH
// on each ping.
const subscribeToChanges = (
listener: (payload: BoolChangePayload) => void,
): (() => void) => {
const channel = db
.channel("bool:" + schema)
.on("broadcast", { event: "*" }, (msg) =>
listener((msg as { payload?: BoolChangePayload }).payload ?? {}),
)
.subscribe();
return () => {
void db.removeChannel(channel);
};
};

const client: BoolClient = {
db,
entities: createEntitiesModule(db, subscribeToChanges),
auth,
schema,
// Realtime "doorbell": the app schema's grants are revoked, so Supabase
// `postgres_changes` never fires. Instead the server broadcasts a
// row-data-free ping on the PUBLIC channel "bool:" + schema whenever any
// row changes. Subscribe with the anon key (no token needed) and REFETCH
// on each ping.
subscribeToChanges(listener) {
const channel = db
.channel("bool:" + schema)
.on("broadcast", { event: "*" }, (msg) =>
listener((msg as { payload?: BoolChangePayload }).payload ?? {}),
)
.subscribe();
return () => {
void db.removeChannel(channel);
};
},
subscribeToChanges,
};

setDefaultBoolClient(client);
Expand Down
Loading
Loading