@basictech/react 0.8.0-beta.4 → 0.9.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,6 +1,30 @@
1
1
  # @basictech/react
2
2
 
3
- React SDK for [Basic](https://basic.tech) - add authentication and real-time database to your React app in minutes.
3
+ React SDK for [Basic](https://basic.tech) authentication and an offline-first, synced database for your React app.
4
+
5
+ Basic gives every user their own personal datastore (a PDS). Your app authenticates with OAuth, gets its own sandboxed slice of the user's datastore, and this SDK keeps a local replica of that data in the browser — reads are instant and local, writes work offline, and everything syncs live across the user's devices over the Basic **Sync/2** protocol.
6
+
7
+ > **Breaking change (0.9):** this version is a ground-up rewrite for the new Basic auth system and the Sync/2 sync engine. The 0.8.x sync client speaks a protocol the server no longer supports. See [Migrating from 0.8](#migrating-from-08).
8
+
9
+ ---
10
+
11
+ ## Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Quick start](#quick-start)
15
+ - [How it works](#how-it-works)
16
+ - [Authentication](#authentication)
17
+ - [Database](#database)
18
+ - [Live queries](#live-queries)
19
+ - [Sync status, offline behavior & errors](#sync-status-offline-behavior--errors)
20
+ - [Shares (multiplayer)](#shares-multiplayer)
21
+ - [Typed schemas](#typed-schemas)
22
+ - [REST mode](#rest-mode)
23
+ - [API reference](#api-reference)
24
+ - [Advanced usage](#advanced-usage)
25
+ - [Migrating from 0.8](#migrating-from-08)
26
+
27
+ ---
4
28
 
5
29
  ## Installation
6
30
 
@@ -8,13 +32,16 @@ React SDK for [Basic](https://basic.tech) - add authentication and real-time dat
8
32
  npm install @basictech/react
9
33
  ```
10
34
 
11
- ## Quick Start
35
+ Requires React 16.8+ (hooks). React 17, 18, and 19 are supported.
36
+
37
+ ## Quick start
12
38
 
13
- ### 1. Create a Schema
39
+ ### 1. Define your schema
14
40
 
15
- Create a `basic.config.ts` file with your project configuration:
41
+ Create a `basic.config.ts` with your project id (from [app.basic.tech](https://app.basic.tech)) and your tables:
16
42
 
17
43
  ```typescript
44
+ // basic.config.ts
18
45
  export const schema = {
19
46
  project_id: "YOUR_PROJECT_ID",
20
47
  version: 1,
@@ -22,7 +49,7 @@ export const schema = {
22
49
  todos: {
23
50
  type: "collection",
24
51
  fields: {
25
- title: { type: "string", indexed: true },
52
+ title: { type: "string", indexed: true, required: true },
26
53
  completed: { type: "boolean", indexed: true },
27
54
  },
28
55
  },
@@ -30,318 +57,354 @@ export const schema = {
30
57
  };
31
58
  ```
32
59
 
33
- ### 2. Add the Provider
60
+ Field types: `string`, `number`, `boolean`, `json`. Publish the schema from the Basic dashboard — sync is enabled once the published version matches your local one.
34
61
 
35
- Wrap your app with `BasicProvider`:
62
+ ### 2. Wrap your app
36
63
 
37
64
  ```tsx
65
+ // main.tsx
38
66
  import { BasicProvider } from "@basictech/react";
39
67
  import { schema } from "./basic.config";
40
68
 
41
- function App() {
42
- return (
43
- <BasicProvider schema={schema}>
44
- <YourApp />
45
- </BasicProvider>
46
- );
47
- }
69
+ createRoot(document.getElementById("root")!).render(
70
+ <BasicProvider schema={schema} devToolbar>
71
+ <App />
72
+ </BasicProvider>,
73
+ );
48
74
  ```
49
75
 
50
- ### 3. Use the Hook
51
-
52
- Access auth and database in any component:
76
+ ### 3. Sign in and use the database
53
77
 
54
78
  ```tsx
55
79
  import { useBasic, useQuery } from "@basictech/react";
56
80
 
57
- function TodoList() {
58
- const { db, isSignedIn, signIn, signOut, user } = useBasic();
59
-
60
- // Live query - automatically updates when data changes
61
- const todos = useQuery(() => db.collection("todos").getAll());
62
-
63
- const addTodo = async () => {
64
- await db.collection("todos").add({
65
- title: "New todo",
66
- completed: false,
67
- });
68
- };
81
+ function Todos() {
82
+ const { isSignedIn, signIn, signOut, user, db } = useBasic();
83
+ const todos = useQuery(() => db.table("todos").getAll());
69
84
 
70
85
  if (!isSignedIn) {
71
- return <button onClick={signIn}>Sign In</button>;
86
+ return <button onClick={() => signIn()}>Sign in with Basic</button>;
72
87
  }
73
88
 
74
89
  return (
75
90
  <div>
76
- <p>Welcome, {user?.email}</p>
77
- <button onClick={addTodo}>Add Todo</button>
78
- <ul>
79
- {todos?.map((todo) => (
80
- <li key={todo.id}>{todo.title}</li>
81
- ))}
82
- </ul>
83
- <button onClick={signOut}>Sign Out</button>
91
+ <p>hi {user?.email} <button onClick={() => signOut()}>sign out</button></p>
92
+
93
+ <button onClick={() => db.table("todos").create({ title: "hello", completed: false })}>
94
+ add todo
95
+ </button>
96
+
97
+ {todos?.map((todo) => (
98
+ <label key={todo.id}>
99
+ <input
100
+ type="checkbox"
101
+ checked={!!todo.completed}
102
+ onChange={() => db.table("todos").patch(todo.id, { completed: !todo.completed })}
103
+ />
104
+ {String(todo.title)}
105
+ <button onClick={() => db.table("todos").delete(todo.id)}>×</button>
106
+ </label>
107
+ ))}
84
108
  </div>
85
109
  );
86
110
  }
87
111
  ```
88
112
 
89
- ---
113
+ That's the whole loop: `signIn()` runs the OAuth flow, `db.table(...)` writes apply instantly to the local replica (and queue offline), and `useQuery` re-renders whenever local data changes — whether the change came from this tab, another tab, another device, or another user via a share.
90
114
 
91
- ## API Reference
115
+ ## How it works
92
116
 
93
- ### `<BasicProvider>`
117
+ In the default `sync` mode, the SDK maintains a **local replica** of your app's slice of the user's datastore, in IndexedDB, and syncs it over one WebSocket:
94
118
 
95
- Root provider component. Must wrap your entire app.
119
+ 1. **Bootstrap** on first run the SDK fetches a snapshot of current state, then subscribes for live changes from that point.
120
+ 2. **Writes are ops** — every write is a `put` (create/replace), `patch` (shallow field merge), or `delete` operation with a client-minted unique id. Ops apply to the local view immediately (optimistic), queue in IndexedDB, and push to the server when online. Retries are always safe: the server deduplicates by op id.
121
+ 3. **Reads are local** — `get`/`getAll`/`find` read the local view: confirmed server state with your pending writes layered on top. No network round-trip.
122
+ 4. **The server is the authority** — it orders all ops (last-writer-wins per record; `patch`es merge per field), validates them against your published schema, and echoes them to every connected device. If it rejects a write, the SDK rolls it back locally and surfaces it (see [rejected ops](#rejected-ops)).
123
+ 5. **Sign-out is clean** — local data is deleted, the session is revoked server-side, and other tabs update in place. No page reloads.
124
+
125
+ ## Authentication
126
+
127
+ Basic is the OAuth provider. `signIn()` redirects to the user's Basic sign-in page; after consent the user lands back on your app with a code the SDK exchanges automatically (PKCE, rotating refresh tokens — all handled for you).
96
128
 
97
129
  ```tsx
98
- <BasicProvider
99
- schema={schema} // Required: Your Basic schema
100
- debug={false} // Optional: Enable console logging
101
- dbMode="sync" // Optional: "sync" (default) or "remote"
102
- devToolbar={false} // Optional: Floating dev status bar (localhost / dev / debug)
103
- />
130
+ const auth = useAuth(); // or grab the same fields from useBasic()
131
+
132
+ auth.status; // 'bootstrapping' | 'authenticated' | 'recovering' | 'reauth_required' | 'signed_out'
133
+ auth.isSignedIn; // boolean (stays true during reauth_required so you can show user info)
134
+ auth.user; // { sub, email, name, picture } | null
135
+ auth.did; // the user's decentralized id
136
+ auth.scope; // scopes granted to your app
137
+
138
+ await auth.signIn(); // redirect flow (current URL is the redirect URI)
139
+ await auth.signIn("https://app.com/done"); // custom redirect URI
140
+ await auth.signInWithHandle("alice.basic.id"); // federated: resolve the user's own PDS first
141
+ await auth.signOut(); // revokes server-side + clears local data
104
142
  ```
105
143
 
106
- #### Props
144
+ ### Auth status lifecycle
107
145
 
108
- | Prop | Type | Default | Description |
109
- | ------------ | -------------------- | -------- | --------------------------------------------------------------------------------------------------- |
110
- | `schema` | `object` | required | Schema with `project_id` and `tables` |
111
- | `debug` | `boolean` | `false` | Enable debug logging |
112
- | `dbMode` | `"sync" \| "remote"` | `"sync"` | Database mode |
113
- | `devToolbar` | `boolean` | `false` | Show the Basic dev toolbar (only when `localhost`, `NODE_ENV === "development"`, or `debug={true}`) |
146
+ | Status | Meaning | What to render |
147
+ | --- | --- | --- |
148
+ | `bootstrapping` | SDK is initializing | splash / nothing (provider hides children by default) |
149
+ | `authenticated` | Healthy session | your app |
150
+ | `recovering` | Session likely exists but unconfirmed (offline, mid-refresh) | your app (data still works locally) |
151
+ | `reauth_required` | Session definitively invalid user must sign in again | user info + a sign-in prompt |
152
+ | `signed_out` | No session | sign-in screen |
114
153
 
115
- #### Database Modes
154
+ ### Calling your own API
116
155
 
117
- - **`sync`** - Local-first with IndexedDB + real-time sync via WebSocket
118
- - **`remote`** - Direct REST API calls (no local storage)
156
+ `getToken()` returns a valid access token, refreshing it automatically (tokens are short-lived never cache them yourself):
119
157
 
120
- ---
158
+ ```typescript
159
+ const token = await auth.getToken();
160
+ await fetch("https://your-api.example/thing", {
161
+ headers: { Authorization: `Bearer ${token}` },
162
+ });
163
+ // after a 401 from your API:
164
+ await auth.getToken({ forceRefresh: true });
165
+ ```
121
166
 
122
- ### `useBasic()`
167
+ ### Scopes
123
168
 
124
- Main hook for accessing auth and database.
169
+ Default requested scopes are `profile,email,app:admin`. Check what was actually granted:
125
170
 
126
- ```tsx
127
- const {
128
- // Auth state
129
- isReady, // boolean - SDK initialized
130
- isSignedIn, // boolean - Session is valid
131
- user, // { id, email, ... } | null (may be null briefly during profile refresh/recovery)
132
-
133
- // Auth methods
134
- signIn, // () => void - Redirect to login
135
- signOut, // () => void - Clear session
136
- signInWithCode, // (code, state?) => Promise - Manual OAuth
137
- getSignInUrl, // (redirectUri?) => string - Get OAuth URL
138
- getToken, // () => Promise<string> - Get access token
139
-
140
- // Database
141
- db, // Database instance
142
- dbStatus, // DBStatus - see below
143
- dbMode, // "sync" | "remote"
144
-
145
- // Dev / schema snapshot (for custom tooling)
146
- devInfo, // BasicSchemaDevInfo | null — local vs remote schema status
147
- refreshSchemaStatus, // () => Promise<void> — re-fetch schema status from API
148
- } = useBasic();
171
+ ```typescript
172
+ auth.hasScope("app:db:read"); // note: app:admin implies all app:db:* scopes
173
+ auth.missingScopes(); // requested but not granted
149
174
  ```
150
175
 
151
- #### `DBStatus` (sync connection state)
176
+ ## Database
152
177
 
153
- When `dbMode === "sync"`, `dbStatus` is one of:
178
+ `db.table(name)` returns the table API (name must exist in your schema):
154
179
 
155
- | Value | Description |
156
- | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
157
- | `LOADING` | SDK initializing |
158
- | `OFFLINE` | Not connected |
159
- | `CONNECTING` | Connecting to sync server |
160
- | `ONLINE` | Connected and idle |
161
- | `SYNCING` | Syncing data |
162
- | `ERROR` | Sync error |
163
- | `ERROR_WILL_RETRY` | Sync error but client will retry (e.g. expired token). Use this to show "Reconnecting…" or trigger token refresh. |
180
+ ```typescript
181
+ const todos = db.table("todos");
164
182
 
165
- Import the enum for comparisons: `import { useBasic, DBStatus } from '@basictech/react'`.
183
+ const created = await todos.create({ title: "buy milk" });
184
+ // → { id: "0198…", title: "buy milk" } — id minted locally, works offline
166
185
 
167
- ---
186
+ await todos.put(id, { title: "replace", completed: false });
187
+ // create-or-replace the WHOLE record with these fields
188
+
189
+ await todos.patch(id, { completed: true });
190
+ // shallow-merge the given fields; returns null if the record doesn't exist
191
+
192
+ await todos.delete(id); // idempotent — deleting a missing record is fine
193
+
194
+ await todos.get(id); // record or null
195
+ await todos.getAll(); // all records
196
+ await todos.find((t) => !t.completed);
197
+ ```
198
+
199
+ Semantics worth knowing:
168
200
 
169
- ### Development toolbar
201
+ - **`put` replaces, `patch` merges.** Two devices patching *different* fields of the same record both win; patching the *same* field, the last write wins.
202
+ - **`json` fields are atomic** — patching a `json` field replaces its whole value (no deep merge).
203
+ - Writes are **validated locally** against your schema before queueing (same rules the server enforces), so typos fail fast with a thrown error instead of a round-trip.
204
+ - In sync mode, `create` mints the id client-side — you get the full record back synchronously-ish, even offline.
170
205
 
171
- A small floating bar (similar in spirit to Next.js dev indicators) shows **auth**, **database mode**, **sync status**, and **schema vs server** health. It is **opt-in** and only appears in development: `localhost` / `127.0.0.1` / `.local`, `NODE_ENV === "development"`, or when `debug` is `true` on the provider or on the standalone component.
206
+ ## Live queries
172
207
 
173
- **Option A provider flag**
208
+ `useQuery` re-runs your query and re-renders whenever the underlying data changes:
174
209
 
175
210
  ```tsx
176
- <BasicProvider schema={schema} devToolbar debug>
177
- <App />
178
- </BasicProvider>
211
+ const todos = useQuery(() => db.table("todos").getAll()); // undefined while loading
212
+ const open = useQuery(() => db.table("todos").find((t) => !t.completed));
213
+ const one = useQuery(() => db.table("todos").get(selectedId), [selectedId]); // deps like useEffect
179
214
  ```
180
215
 
181
- **Option B place the component yourself** (must be under `BasicProvider`; respects the same visibility rules, or pass `debug` to force):
216
+ For advanced queries, `table.ref` exposes the underlying [Dexie](https://dexie.org) table over the local view (sync mode only) indexed fields from your schema are Dexie indexes:
182
217
 
183
218
  ```tsx
184
- import { BasicDevToolbar } from "@basictech/react";
219
+ const recent = useQuery(() =>
220
+ db.table("todos").ref!.where("completed").equals(0).limit(10).toArray(),
221
+ );
222
+ ```
223
+
224
+ ## Sync status, offline behavior & errors
185
225
 
186
- <BasicProvider schema={schema}>
187
- <App />
188
- <BasicDevToolbar />
189
- </BasicProvider>;
226
+ ```tsx
227
+ const sync = useSyncStatus();
228
+
229
+ sync.status; // 'idle' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
230
+ sync.enabled; // false when the schema is invalid/unpublished
231
+ sync.pendingCount; // local writes not yet confirmed by the server
190
232
  ```
191
233
 
192
- The expanded panel includes **Refresh schema** (re-runs the remote schema check) and **Copy debug info** (JSON snapshot **without** raw access tokens). You can also read `devInfo` and call `refreshSchemaStatus()` from `useBasic()` for your own UI.
234
+ **Offline is not an error.** Writes keep working and queue in IndexedDB; reads serve the local view; everything pushes on reconnect (safe to retry ops are idempotent). The queue survives page reloads.
193
235
 
194
- ---
236
+ `revoked` means the user disconnected your app from their account — data access is gone until they sign in and consent again.
195
237
 
196
- ### `useQuery()`
238
+ ### Rejected ops
197
239
 
198
- Live query hook - automatically re-renders when data changes.
240
+ If the server terminally rejects a write (schema validation, permissions, payload too large), the SDK **rolls the record back**, parks the op in a rejected store, and will never retry it:
199
241
 
200
242
  ```tsx
201
- import { useQuery } from "@basictech/react";
243
+ const sync = useSyncStatus();
244
+ const rejected = await sync.listRejected();
245
+ // [{ op_id, op: { type, table, record_id, data }, error: 'SCHEMA_VALIDATION_FAILED', message, rejected_at }]
246
+ await sync.clearRejected();
247
+ ```
202
248
 
203
- // Get all items
204
- const todos = useQuery(() => db.collection("todos").getAll());
249
+ You can also listen live:
205
250
 
206
- // With type safety
207
- interface Todo {
208
- id: string;
209
- title: string;
210
- completed: boolean;
211
- }
212
- const todos = useQuery(() => db.collection<Todo>("todos").getAll());
251
+ ```typescript
252
+ const { client } = useBasic();
253
+ useEffect(() => client.engine?.on("rejected", ({ rejection }) => {
254
+ toast.error(`write rejected: ${rejection.error}`);
255
+ }), [client]);
213
256
  ```
214
257
 
215
- > **Note:** Only works in `sync` mode. In `remote` mode, use manual fetching.
258
+ ## Shares (multiplayer)
216
259
 
217
- ---
260
+ Users can grant other users access to a slice of their data — a whole table or specific records, read-only or read-write. Grants are created by the user on basic.id (apps can never share a user's data on their own); your app **lists** and **mounts** them:
218
261
 
219
- ### Database Methods
220
-
221
- #### `db.collection(name)`
262
+ ```tsx
263
+ function SharedTodos() {
264
+ const { received } = useShares(); // shares other users granted to me
265
+ const share = received[0];
266
+ const { db: sharedDb, status } = useShare(share?.id); // 'mounting' | 'mounted' | 'error' | 'revoked'
267
+ const todos = useQuery(() => sharedDb?.table("todos").getAll(), [sharedDb]);
222
268
 
223
- Access a collection by name.
269
+ if (!share) return <p>nothing shared with you yet</p>;
270
+ if (status !== "mounted") return <p>{status}…</p>;
224
271
 
225
- ```tsx
226
- const { db } = useBasic();
227
- const todos = db.collection("todos");
272
+ return (
273
+ <ul>
274
+ {todos?.map((t) => (
275
+ <li key={t.id} onClick={() => sharedDb!.table("todos").patch(t.id, { completed: true })}>
276
+ {String(t.title)}
277
+ </li>
278
+ ))}
279
+ </ul>
280
+ );
281
+ }
228
282
  ```
229
283
 
230
- #### Collection Methods
284
+ Mounted shares live in their **own local keyspace** — separate replica, separate offline queue — and are never merged with the user's own data. Writes work when the share permission is `write` (the owner sees your changes attributed to the guest). When the owner revokes the share, the mount status flips to `revoked` and the local copy is purged.
231
285
 
232
- | Method | Returns | Description |
233
- | ------------------ | -------------------- | ----------------------------------- |
234
- | `getAll()` | `Promise<T[]>` | Get all records |
235
- | `get(id)` | `Promise<T \| null>` | Get one record by ID |
236
- | `add(data)` | `Promise<T>` | Create new record (returns with ID) |
237
- | `put(data)` | `Promise<T>` | Upsert record (requires ID) |
238
- | `update(id, data)` | `Promise<T \| null>` | Partial update |
239
- | `delete(id)` | `Promise<boolean>` | Delete record |
240
- | `filter(fn)` | `Promise<T[]>` | Filter with predicate |
286
+ ## Typed schemas
241
287
 
242
- #### Examples
288
+ `@basictech/schema` (a dependency of this package) can infer record types from your schema:
243
289
 
244
- ```tsx
245
- // Create
246
- const todo = await db.collection("todos").add({
247
- title: "Buy milk",
248
- completed: false,
290
+ ```typescript
291
+ import { defineSchema, type InferRecord } from "@basictech/schema";
292
+
293
+ export const schema = defineSchema({
294
+ project_id: "…",
295
+ version: 1,
296
+ tables: {
297
+ todos: {
298
+ fields: {
299
+ title: { type: "string", required: true },
300
+ completed: { type: "boolean" },
301
+ },
302
+ },
303
+ },
249
304
  });
250
- console.log(todo.id); // Auto-generated ID
251
305
 
252
- // Read
253
- const allTodos = await db.collection("todos").getAll();
254
- const oneTodo = await db.collection("todos").get("some-id");
306
+ type Todo = InferRecord<typeof schema, "todos">;
307
+ // { id: string; title: string; completed?: boolean }
308
+
309
+ const todos = db.table<Todo>("todos"); // fully typed CRUD
310
+ ```
255
311
 
256
- // Update
257
- await db.collection("todos").update("some-id", { completed: true });
312
+ ## REST mode
258
313
 
259
- // Delete
260
- await db.collection("todos").delete("some-id");
314
+ Don't want a local replica? `mode="rest"` makes every table call a direct API request — no IndexedDB, no WebSocket, no offline support:
261
315
 
262
- // Filter
263
- const incomplete = await db.collection("todos").filter((t) => !t.completed);
316
+ ```tsx
317
+ <BasicProvider schema={schema} mode="rest">
264
318
  ```
265
319
 
266
- ---
320
+ The table API is identical, with REST semantics: `create` gets its id from the server, `put`/`patch`/`delete` on missing records return null/no-op, reads hit the network. `useQuery` won't live-update in this mode (re-run queries with deps).
267
321
 
268
- ## Advanced Usage
322
+ ## API reference
269
323
 
270
- ### Manual OAuth Flow
324
+ ### `<BasicProvider>` props
271
325
 
272
- For custom OAuth handling (mobile apps, popups, etc.):
326
+ | Prop | Default | Description |
327
+ | --- | --- | --- |
328
+ | `schema` | — | Your Basic schema document (`project_id` is read from it) |
329
+ | `mode` | `"sync"` | `"sync"` (local replica) or `"rest"` (direct API) |
330
+ | `auth.scopes` | `"profile,email,app:admin"` | OAuth scopes to request |
331
+ | `auth.pds_url` | `https://pds.basic.id` | The Basic server (auth + data + sync) |
332
+ | `auth.sync_url` | `wss://<pds>/sync/` | Sync WebSocket URL (rarely needed) |
333
+ | `storage` | `localStorage` | Custom `BasicStorage` adapter for auth state |
334
+ | `debug` | `false` | Verbose console logging |
335
+ | `devToolbar` | `false` | Floating status toolbar (dev environments only) |
336
+ | `renderWhileLoading` | `false` | Render children before auth bootstrap finishes |
273
337
 
274
- ```tsx
275
- const { signInWithCode, getSignInUrl } = useBasic();
338
+ ### Hooks
276
339
 
277
- // Get OAuth URL with custom redirect
278
- const url = getSignInUrl("myapp://callback");
340
+ | Hook | Returns |
341
+ | --- | --- |
342
+ | `useBasic()` | Everything: auth fields + actions, `db`, `sync`, `devInfo`, `client` |
343
+ | `useAuth()` | `{ isReady, isSignedIn, status, user, did, scope, hasScope, missingScopes, signIn, signInWithHandle, signInWithCode, signOut, getToken, getSignInUrl }` |
344
+ | `useDb()` | The table API for the user's own data |
345
+ | `useQuery(fn, deps?)` | Live query result (`undefined` while loading) |
346
+ | `useSyncStatus()` | `{ status, enabled, pendingCount, listRejected, clearRejected }` |
347
+ | `useShares()` | `{ granted, received, isLoading, error, refresh }` |
348
+ | `useShare(shareId)` | `{ db, status, error }` — a mounted share |
279
349
 
280
- // Exchange code for session
281
- const result = await signInWithCode(code, state);
282
- if (result.success) {
283
- console.log("Signed in!");
284
- }
285
- ```
350
+ ### Table API
286
351
 
287
- ### Remote Mode
352
+ | Method | Sync mode | REST mode |
353
+ | --- | --- | --- |
354
+ | `create(data)` | client-minted id, offline-capable | server-minted id |
355
+ | `put(id, data)` | create **or replace** | replace; throws if missing |
356
+ | `patch(id, partial)` | merge; `null` if missing locally | merge; `null` if missing |
357
+ | `delete(id)` | idempotent | no-op if missing |
358
+ | `get(id)` / `getAll()` / `find(fn)` | local view | network |
359
+ | `ref` | Dexie table (live queries, indexes) | `undefined` |
288
360
 
289
- For server-rendered apps or when you don't need offline support:
361
+ ## Advanced usage
290
362
 
291
- ```tsx
292
- <BasicProvider schema={schema} dbMode="remote">
293
- <App />
294
- </BasicProvider>
295
- ```
363
+ ### Outside React
296
364
 
297
- In remote mode:
365
+ Everything is available without React — `BasicClient` is the framework-agnostic core (`AuthManager` + `SyncEngine` + `RestClient`):
298
366
 
299
- - Data is fetched via REST API
300
- - No IndexedDB storage
301
- - `useQuery` won't auto-update (use manual refresh)
302
- - Requires authentication for all operations
303
- - Auth request failures are surfaced to the caller; recoverable 401s do not automatically sign the user out
367
+ ```typescript
368
+ import { createBasicClient } from "@basictech/react";
304
369
 
305
- ### Error Handling
370
+ const client = createBasicClient({ schema, debug: true });
371
+ await client.start();
372
+ client.subscribe(() => console.log(client.getSnapshot()));
373
+ await client.db.table("todos").create({ title: "from anywhere" });
374
+ ```
306
375
 
307
- ```tsx
308
- import { NotAuthenticatedError } from "@basictech/react";
309
-
310
- try {
311
- await db.collection("todos").add({ title: "Test" });
312
- } catch (error) {
313
- if (error instanceof NotAuthenticatedError) {
314
- // User needs to sign in
315
- signIn();
316
- }
317
- }
376
+ The Sync/2 protocol layer (`SyncEngine`, `SyncConnection`, `SyncStore`, and all wire types) is exported too, for building custom clients or other runtimes — pass a `WebSocketImpl` (e.g. the `ws` package) to run in Node.
377
+
378
+ ### Events
379
+
380
+ ```typescript
381
+ const { client } = useBasic();
382
+ client.engine?.on("status", (s) => {}); // sync status changes
383
+ client.engine?.on("change", ({ sub, tables }) => {}); // records changed
384
+ client.engine?.on("rejected", ({ sub, rejection }) => {}); // terminal write rejection
385
+ client.engine?.on("revoked", ({ code }) => {}); // app connection revoked
318
386
  ```
319
387
 
320
- ---
388
+ ### Custom storage
321
389
 
322
- ## TypeScript
390
+ Auth state lives in `localStorage` by default. Provide any `{ get, set, remove }` async adapter via the `storage` prop (see `BasicStorage`). `STORAGE_KEYS` lists every key the SDK uses.
323
391
 
324
- Full TypeScript support with generics:
392
+ ## Migrating from 0.8
325
393
 
326
- ```tsx
327
- interface Todo {
328
- id: string;
329
- title: string;
330
- completed: boolean;
331
- createdAt: number;
332
- }
394
+ 0.9 is a breaking rewrite — the server's legacy sync endpoint was removed, so 0.8 sync no longer functions. Changes you'll make:
333
395
 
334
- // Type-safe collection
335
- const todos = db.collection<Todo>("todos");
396
+ | 0.8 | 0.9 |
397
+ | --- | --- |
398
+ | `db.collection("todos")` | `db.table("todos")` |
399
+ | `.add(data)` | `.create(data)` |
400
+ | `.update(id, partial)` | `.patch(id, partial)` |
401
+ | `.put({ id, ...fields })` | `.put(id, fields)` |
402
+ | `dbStatus` / `DBStatus` enum | `useSyncStatus().status` |
403
+ | `dbMode="remote"` | `mode="rest"` |
404
+ | `auth.ws_url` | `auth.sync_url` (you almost never need it) |
405
+ | `signOut()` reloads the page | in-place teardown, no reload |
336
406
 
337
- // All methods are typed
338
- const todo = await todos.add({
339
- title: "Test",
340
- completed: false,
341
- createdAt: Date.now(),
342
- });
343
- // todo is typed as Todo
344
- ```
407
+ Local data migrates by re-bootstrap: the old `basicdb` IndexedDB database is deleted automatically and 0.9 builds fresh replicas in `basic-sync:<project_id>` databases from the server (the server is the source of truth, so nothing is lost).
345
408
 
346
409
  ---
347
410
 
@@ -1,24 +0,0 @@
1
-
2
- 
3
- > @basictech/react@0.8.0-beta.1 build
4
- > tsup
5
-
6
- CLI Building entry: src/index.ts
7
- CLI Using tsconfig: tsconfig.json
8
- CLI tsup v8.5.1
9
- CLI Using tsup config: /Users/raz/codebook/basic/libs/client-ts/packages/react/tsup.config.ts
10
- CLI Target: es2022
11
- CLI Cleaning output folder
12
- CJS Build start
13
- ESM Build start
14
- CJS dist/index.js 97.02 KB
15
- CJS dist/index.js.map 187.81 KB
16
- CJS ⚡️ Build success in 36ms
17
- ESM dist/index.mjs 93.09 KB
18
- ESM dist/index.mjs.map 187.75 KB
19
- ESM ⚡️ Build success in 37ms
20
- DTS Build start
21
- DTS ⚡️ Build success in 1091ms
22
- DTS dist/index.d.ts 12.66 KB
23
- DTS dist/index.d.mts 12.66 KB
24
- ⠙