@basictech/react 0.9.0-beta.0 → 0.9.0-beta.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basictech/react",
3
- "version": "0.9.0-beta.0",
3
+ "version": "0.9.0-beta.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/readme.md CHANGED
@@ -13,6 +13,8 @@ Basic gives every user their own personal datastore (a PDS). Your app authentica
13
13
  - [Installation](#installation)
14
14
  - [Quick start](#quick-start)
15
15
  - [How it works](#how-it-works)
16
+ - [Anonymous & local-first](#anonymous--local-first)
17
+ - [Multiple users](#multiple-users)
16
18
  - [Authentication](#authentication)
17
19
  - [Database](#database)
18
20
  - [Live queries](#live-queries)
@@ -110,17 +112,63 @@ function Todos() {
110
112
  }
111
113
  ```
112
114
 
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.
115
+ That's the whole loop and note the app **works before sign-in too**: `db.table(...)` writes go to a local anonymous workspace and merge into the account automatically when the user signs in (see [Anonymous & local-first](#anonymous--local-first)). `useQuery` re-renders whenever local data changes — whether the change came from this tab, another device, or another user via a share.
114
116
 
115
117
  ## How it works
116
118
 
117
119
  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:
118
120
 
119
- 1. **Bootstrap** — on first run the SDK fetches a snapshot of current state, then subscribes for live changes from that point.
121
+ 1. **Local first** — the local database opens immediately, with no session and no network. Reads and writes work from the first render, including offline cold starts.
120
122
  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
123
  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
+ 4. **Bootstrap on connect** — when a session exists, the SDK fetches a snapshot of the account's current state, layers your pending writes on top, then subscribes for live changes.
125
+ 5. **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)).
126
+ 6. **Sign-out is clean** — the user's local data is wiped, the session is revoked server-side, and the app continues in a fresh anonymous workspace. No page reloads.
127
+
128
+ ## Anonymous & local-first
129
+
130
+ Anonymous mode is **on by default**: users can use your app without signing in. Their data lives in a local-only workspace (sync status `local`), fully readable and writable, surviving reloads.
131
+
132
+ When they sign in, the anonymous workspace **becomes** their account: every anonymous write is already a queued op, so the normal bootstrap-and-push flow merges them into the account — no copy step, no data loss, and live queries keep working through the transition.
133
+
134
+ ```tsx
135
+ const { isAnonymous, isSignedIn, signIn, db } = useBasic();
136
+
137
+ // works signed in or not:
138
+ await db.table("todos").create({ title: "works offline & anonymous" });
139
+
140
+ // prompt when you want the data to start syncing:
141
+ {isAnonymous && <button onClick={() => signIn()}>Sign in to sync</button>}
142
+ ```
143
+
144
+ Safety guarantees:
145
+
146
+ - A keyspace is **stamped with the account DID** at first bootstrap. Local data from one account can never merge into another — if a different account signs in over it, the local cache is wiped first (it's server-backed; nothing is lost).
147
+ - Signing out wipes that user's local data and drops into a fresh anonymous workspace (nothing readable is left behind on shared machines).
148
+
149
+ Set `anonymous={false}` on the provider to require sign-in before any local data exists (classic behavior).
150
+
151
+ ## Multiple users
152
+
153
+ Several local users — anonymous or signed-in — can exist side by side, one active at a time. The active user is **per tab** (two tabs can be on different users); the profiles themselves are shared across tabs. Each user has an isolated keyspace and session.
154
+
155
+ ```tsx
156
+ const { users, activeUser, switchUser, addUser, removeUser } = useUsers();
157
+
158
+ <select value={activeUser?.id} onChange={(e) => switchUser(e.target.value)}>
159
+ {users.map((u) => (
160
+ <option key={u.id} value={u.id}>
161
+ {u.kind === "anon" ? "Anonymous" : u.email || u.name || u.did}
162
+ </option>
163
+ ))}
164
+ </select>
165
+ <button onClick={() => addUser()}>Add user</button>
166
+ ```
167
+
168
+ - `switchUser(id)` swaps the active session, database, and sync connection in place. Live queries via `useQuery` re-attach automatically.
169
+ - `addUser()` creates a fresh anonymous user and switches to it (sign in from there to add an account).
170
+ - `removeUser(id)` wipes that user's local data and revokes its session; removing the active user is the same as `signOut()`.
171
+ - `signOut()` removes the active user and falls through to the next one (or a fresh anonymous workspace).
124
172
 
125
173
  ## Authentication
126
174
 
@@ -148,8 +196,8 @@ await auth.signOut(); // revokes server-side + clears local
148
196
  | `bootstrapping` | SDK is initializing | splash / nothing (provider hides children by default) |
149
197
  | `authenticated` | Healthy session | your app |
150
198
  | `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 |
199
+ | `reauth_required` | Session definitively invalid — user must sign in again | your app (local reads/writes still work) + a re-sign-in prompt |
200
+ | `signed_out` | No session — anonymous local workspace (default mode) | your app + a "sign in to sync" prompt (`isAnonymous` is true) |
153
201
 
154
202
  ### Calling your own API
155
203
 
@@ -226,12 +274,14 @@ const recent = useQuery(() =>
226
274
  ```tsx
227
275
  const sync = useSyncStatus();
228
276
 
229
- sync.status; // 'idle' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
277
+ sync.status; // 'idle' | 'local' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
230
278
  sync.enabled; // false when the schema is invalid/unpublished
231
279
  sync.pendingCount; // local writes not yet confirmed by the server
232
280
  ```
233
281
 
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.
282
+ `local` means the database is working without a connection an anonymous workspace, an unpublished (version 0) schema during development, or a paused session.
283
+
284
+ **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 — including offline cold starts: reloading with no network serves all data from the local replica.
235
285
 
236
286
  `revoked` means the user disconnected your app from their account — data access is gone until they sign in and consent again.
237
287
 
@@ -327,6 +377,7 @@ The table API is identical, with REST semantics: `create` gets its id from the s
327
377
  | --- | --- | --- |
328
378
  | `schema` | — | Your Basic schema document (`project_id` is read from it) |
329
379
  | `mode` | `"sync"` | `"sync"` (local replica) or `"rest"` (direct API) |
380
+ | `anonymous` | `true` | Local workspace without sign-in; merges into the account on sign-in |
330
381
  | `auth.scopes` | `"profile,email,app:admin"` | OAuth scopes to request |
331
382
  | `auth.pds_url` | `https://pds.basic.id` | The Basic server (auth + data + sync) |
332
383
  | `auth.sync_url` | `wss://<pds>/sync/` | Sync WebSocket URL (rarely needed) |
@@ -339,14 +390,17 @@ The table API is identical, with REST semantics: `create` gets its id from the s
339
390
 
340
391
  | Hook | Returns |
341
392
  | --- | --- |
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) |
393
+ | `useBasic()` | Everything: auth fields + actions, `db`, `sync`, `users`, `activeUser`, `devInfo`, `client` |
394
+ | `useAuth()` | `{ isReady, isSignedIn, isAnonymous, status, user, did, scope, hasScope, missingScopes, signIn, signInWithHandle, signInWithCode, signOut, getToken, getSignInUrl }` |
395
+ | `useDb()` | The table API for the active user's own data |
396
+ | `useQuery(fn, deps?)` | Live query result (`undefined` while loading); re-attaches automatically on user switch |
346
397
  | `useSyncStatus()` | `{ status, enabled, pendingCount, listRejected, clearRejected }` |
398
+ | `useUsers()` | `{ users, activeUser, isAnonymous, switchUser, addUser, removeUser }` |
347
399
  | `useShares()` | `{ granted, received, isLoading, error, refresh }` |
348
400
  | `useShare(shareId)` | `{ db, status, error }` — a mounted share |
349
401
 
402
+ Note on `useQuery` and user switching: the active user's identity is appended to your deps automatically, so queries that read `db` from the current render (via `useBasic()`/`useDb()`) re-attach to the new user's database on switch. If your query closure captures `db` from outside render, pass `[db]` in deps explicitly — or remount the subtree with `key={activeUser?.id}`.
403
+
350
404
  ### Table API
351
405
 
352
406
  | Method | Sync mode | REST mode |