@abloatai/ablo 0.11.0 → 0.11.2

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +72 -25
  3. package/dist/Model.d.ts +39 -0
  4. package/dist/Model.js +68 -0
  5. package/dist/auth/credentialPolicy.d.ts +145 -0
  6. package/dist/auth/credentialPolicy.js +130 -0
  7. package/dist/cli.cjs +154 -25
  8. package/dist/client/Ablo.d.ts +39 -88
  9. package/dist/client/Ablo.js +54 -99
  10. package/dist/client/ApiClient.d.ts +10 -1
  11. package/dist/client/ApiClient.js +23 -12
  12. package/dist/client/auth.d.ts +21 -9
  13. package/dist/client/auth.js +42 -6
  14. package/dist/client/createModelProxy.d.ts +74 -10
  15. package/dist/client/createModelProxy.js +85 -4
  16. package/dist/client/httpClient.d.ts +17 -3
  17. package/dist/client/httpClient.js +1 -0
  18. package/dist/client/identity.js +134 -122
  19. package/dist/client/index.d.ts +1 -1
  20. package/dist/client/sessionMint.d.ts +15 -0
  21. package/dist/client/sessionMint.js +86 -0
  22. package/dist/errorCodes.d.ts +2 -0
  23. package/dist/errorCodes.js +3 -1
  24. package/dist/errors.d.ts +3 -2
  25. package/dist/errors.js +3 -2
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +4 -7
  28. package/dist/mutators/RecordingTransaction.js +14 -42
  29. package/dist/react/AbloProvider.d.ts +1 -6
  30. package/dist/react/AbloProvider.js +1 -5
  31. package/dist/react/context.d.ts +1 -31
  32. package/dist/react/context.js +2 -2
  33. package/dist/react/index.d.ts +0 -6
  34. package/dist/react/index.js +0 -7
  35. package/dist/react/useSyncStatus.d.ts +1 -1
  36. package/dist/realtime/index.d.ts +1 -1
  37. package/dist/schema/generate.js +1 -2
  38. package/dist/schema/schema.d.ts +16 -5
  39. package/dist/schema/schema.js +26 -0
  40. package/dist/surface.d.ts +29 -0
  41. package/dist/surface.js +60 -0
  42. package/dist/sync/ConnectionManager.d.ts +16 -5
  43. package/dist/sync/ConnectionManager.js +42 -7
  44. package/dist/transactions/TransactionQueue.js +22 -10
  45. package/dist/types/global.d.ts +11 -3
  46. package/dist/types/global.js +8 -3
  47. package/dist/types/streams.d.ts +0 -22
  48. package/dist/utils/mobx-setup.js +1 -0
  49. package/docs/api-keys.md +49 -0
  50. package/docs/api.md +6 -5
  51. package/docs/client-behavior.md +7 -3
  52. package/docs/coordination.md +88 -24
  53. package/docs/data-sources.md +29 -9
  54. package/docs/examples/existing-python-backend.md +9 -5
  55. package/docs/examples/scoped-agent.md +1 -1
  56. package/docs/guarantees.md +4 -3
  57. package/docs/identity.md +89 -82
  58. package/docs/integration-guide.md +19 -10
  59. package/docs/migration.md +49 -2
  60. package/docs/quickstart.md +65 -33
  61. package/docs/react.md +49 -3
  62. package/docs/schema-contract.md +23 -5
  63. package/llms-full.txt +43 -24
  64. package/llms.txt +17 -15
  65. package/package.json +1 -1
  66. package/dist/api/index.d.ts +0 -10
  67. package/dist/api/index.js +0 -9
  68. package/dist/principal.d.ts +0 -44
  69. package/dist/principal.js +0 -49
  70. package/dist/react/SyncGroupProvider.d.ts +0 -19
  71. package/dist/react/SyncGroupProvider.js +0 -44
  72. package/dist/react/useClaim.d.ts +0 -29
  73. package/dist/react/useClaim.js +0 -42
  74. package/dist/react/usePresence.d.ts +0 -32
  75. package/dist/react/usePresence.js +0 -41
package/docs/react.md CHANGED
@@ -31,7 +31,7 @@ import { schema } from '@/ablo/schema';
31
31
  // from your own server route (see Identity below).
32
32
  export const ablo = Ablo({
33
33
  schema,
34
- authEndpoint: '/api/ablo-session',
34
+ apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
35
35
  });
36
36
  ```
37
37
 
@@ -65,13 +65,13 @@ export function Providers({
65
65
 
66
66
  | Prop | Default | Purpose |
67
67
  | ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
68
- | `client` | — | **Required.** The `Ablo({ schema, authEndpoint })` instance. It carries the schema and connection config. |
68
+ | `client` | — | **Required.** The `Ablo({ schema, apiKey })` instance. It carries the schema and connection config. |
69
69
  | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
70
70
  | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
71
71
  | `onError` | — | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
72
72
 
73
73
  Everything that used to be a provider prop — `schema`, `url`, `apiKey`,
74
- `teamIds`, `syncGroups`/`scope`, `persistence`, `bootstrapMode` — now lives on
74
+ `teamIds`, `syncGroups`, `persistence`, `bootstrapMode` — now lives on
75
75
  the `Ablo({ ... })` client you build before mounting the provider. Where the
76
76
  identity comes from, and why the API key never reaches the browser, is the whole
77
77
  of [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how
@@ -178,6 +178,52 @@ imperative work after an event or effect.
178
178
 
179
179
  See [API reference](/docs/api) for the full options surface.
180
180
 
181
+ ## useClaim — named-claim dispatcher
182
+
183
+ `useClaim` (renamed from `useIntent` in 0.11.0) is typed sugar for invoking a
184
+ *named* claim from your own coordination vocabulary — distinct from the
185
+ row-level `ablo.<model>.claim({ id })` resource claim. Use it when you want to
186
+ broadcast a semantic claim like "I'm editing this layer" or "the agent is
187
+ generating here" and let your transport turn it into a network effect.
188
+
189
+ Declare the vocabulary once via module augmentation on the `Register` interface
190
+ (the `Claims` key — previously `Intents`):
191
+
192
+ ```ts
193
+ declare module '@abloatai/ablo' {
194
+ interface Register {
195
+ Claims: {
196
+ editLayer: { slideId: string; layerId: string };
197
+ generateWithAI: { entityId: string; tool: string };
198
+ };
199
+ }
200
+ }
201
+ ```
202
+
203
+ Then `useClaim('editLayer')` returns a function whose sole argument is the
204
+ `editLayer` shape — purely compile-time narrowing, no runtime checks:
205
+
206
+ ```tsx
207
+ 'use client';
208
+
209
+ import { useClaim } from '@abloatai/ablo/react';
210
+
211
+ export function LayerToolbar({ slideId, layerId }: { slideId: string; layerId: string }) {
212
+ const claimEditLayer = useClaim('editLayer');
213
+
214
+ return (
215
+ <button onClick={() => claimEditLayer({ slideId, layerId })}>
216
+ Edit layer
217
+ </button>
218
+ );
219
+ }
220
+ ```
221
+
222
+ The hook is pure sugar: the actual network effect lives in the `beginClaim`
223
+ function wired into the provider (bound to your transport). If no `beginClaim`
224
+ is wired, the returned invoker throws `AbloValidationError` with code
225
+ `claim_not_wired`.
226
+
181
227
  ## Next.js
182
228
 
183
229
  The Next.js [App Router landing](/nextjs) walks through Server Components
@@ -49,6 +49,20 @@ The model key (`weatherReports`) becomes the client namespace
49
49
  contract. You should not create a parallel string-keyed write path for the same
50
50
  data.
51
51
 
52
+ ### Reserved fields
53
+
54
+ The SDK provides these on every row automatically — do **not** declare them in
55
+ your `model(...)` fields:
56
+
57
+ - `id`
58
+ - `createdAt`
59
+ - `updatedAt`
60
+ - `organizationId`
61
+ - `createdBy`
62
+
63
+ Declare only your own fields; the reserved ones are still present on the row and
64
+ readable, you just don't author them.
65
+
52
66
  ## Reads and writes
53
67
 
54
68
  Use async reads when the row may not be local:
@@ -88,12 +102,16 @@ the fresh row. Reads stay open; only acting on the row serializes.
88
102
 
89
103
  ## Storage boundary
90
104
 
91
- Every schema model is backed by your own database through a Data Source — Ablo
92
- coordinates each write and your app commits it to your Postgres.
105
+ Every schema model is backed by your own database. There are three start states,
106
+ all covered in [Connect Your Database](./data-sources.md) (the single source of
107
+ truth): the sandbox (`apiKey` only, no database), a direct connection string
108
+ (`databaseUrl` passed to `Ablo(...)`, a live, server-only option), or a signed
109
+ Data Source endpoint where your app keeps the database credential and commits
110
+ each write itself.
93
111
 
94
- Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
95
- Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
96
- API key.
112
+ If your database stays canonical behind a Data Source endpoint, do not pass
113
+ `databaseUrl` to `Ablo(...)` trusted runtimes use `ABLO_API_KEY`. Browser code
114
+ goes through `<AbloProvider>` or a scoped session route, never a raw API key.
97
115
 
98
116
  ## Rules of thumb
99
117
 
package/llms-full.txt CHANGED
@@ -19,7 +19,7 @@ Public imports:
19
19
  - `@abloatai/ablo/react` — React provider and hooks.
20
20
  - `@abloatai/ablo/testing` — test harnesses and mocks.
21
21
 
22
- TYPES: have the project register its schema ONCE via declaration merging (init scaffolds `ablo.d.ts`): `declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema.
22
+ TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
23
23
 
24
24
 
25
25
  Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or `internal/*` as public imports. The Data Source surface — `/source`, `/source/next`, `/source/drizzle`, `/source/kysely`, `/source/conformance` — IS public (it's how a customer-owned database is wired).
@@ -39,9 +39,9 @@ Use `snapshot(...)` and `readAt` when an agent write depends on state it already
39
39
  read. `onStale: 'reject'` prevents lost updates by rejecting if the target
40
40
  changed after the snapshot.
41
41
 
42
- Claims are live coordination signals, not database locks. Schema clients wait
43
- from the realtime claim stream. Schema-less HTTP clients must pass an explicit
44
- `claimedPollInterval` for `ifClaimed: 'wait'`; no hidden hard-coded polling.
42
+ Claims are live coordination signals, not database locks. Reads never block on a
43
+ claim to wait for a row to free up, `claim({ id })` it and the claim queues
44
+ fairly behind the current holder.
45
45
 
46
46
  Agent run bookkeeping is internal. Most users should not create run ledger
47
47
  records or scoped access credentials manually.
@@ -145,9 +145,21 @@ This applies to any API-backed app: Python, Rails, Go, or Node. The backend keep
145
145
  its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
146
146
  `ABLO_API_KEY`, not a database URL.
147
147
 
148
- When the user DOES use the connection-string path (`Ablo({ databaseUrl })` /
149
- `DATABASE_URL` at init): the role must be NON-superuser and NON-BYPASSRLS
150
- Ablo enforces row-level security and rejects owner roles with
148
+ The connection-string path is the PRIMARY one: pass `databaseUrl` explicitly to
149
+ `Ablo({ databaseUrl })`. It is NOT auto-read from the environment a
150
+ `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored
151
+ unless you pass `databaseUrl`. The typical user ALREADY has a Postgres (often
152
+ Prisma-managed for auth/audit/log tables that are NOT in the Ablo schema); Ablo
153
+ syncs a SUBSET of models against it. Most users do NOT run `ablo migrate` — they
154
+ ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing
155
+ tables with their own migration tool. Run `npx ablo migrate` only when Ablo
156
+ should OWN the tables. NOTE: Ablo's CLOUD connects to your Postgres over the
157
+ NETWORK, so a localhost / private-range DB is unreachable and rejected — for a
158
+ local dev DB, expose a signed Data Source endpoint (your app proxies to it) or
159
+ use the hosted sandbox (no DB needed).
160
+
161
+ When the user DOES use the connection-string path: the role must be NON-superuser
162
+ and NON-BYPASSRLS — Ablo enforces row-level security and rejects owner roles with
151
163
  `database_role_cannot_enforce_rls`. Neon's and Supabase's default dashboard
152
164
  strings use the database OWNER (e.g. `neondb_owner`) and ARE rejected. EASIEST: have the user run `npx ablo migrate` — it detects the unsafe role and creates the scoped one automatically from their machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first (`CREATE ROLE ablo_app LOGIN PASSWORD '...'
153
165
  NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app;
@@ -162,9 +174,11 @@ The options that matter: `schema` and `apiKey`. Everything else
162
174
  breaks the connection. It exists only for self-hosted/proxy setups the human
163
175
  explicitly asks for.
164
176
 
165
- There is intentionally no `databaseURL` option on `Ablo(...)`. Application and
166
- agent code use `ABLO_API_KEY`. Customer-owned app databases stay private behind
167
- a signed Data Source endpoint.
177
+ `databaseUrl` is an OPTIONAL, server-only option on `Ablo(...)`. It is NOT
178
+ auto-read from the environment pass it EXPLICITLY to register your Postgres
179
+ directly (the primary connection-string path). Omit it when you expose a signed
180
+ Data Source endpoint (so customer-owned app databases stay private), or when
181
+ trying Ablo against the hosted sandbox (apiKey only, no database).
168
182
 
169
183
  Important per-write options: `wait`, `readAt`, `onStale`,
170
184
  `idempotencyKey`, and `timeout`.
@@ -187,7 +201,10 @@ There are two sandbox surfaces:
187
201
  be pasted into Claude Code or Codex to wire one real model through Ablo.
188
202
  - Authenticated org sandboxes are real test environments. The default sandbox is
189
203
  the Stripe-style sandbox for an org. It has an isolated sync group prefix,
190
- can mint `sk_test_*` keys, and can be reset without touching live state.
204
+ can mint `sk_test_*` keys, and can be reset without touching live state. The
205
+ sandbox CAN host rows in Ablo's test plane, so you can try Ablo with NO
206
+ database — `apiKey` only, no `databaseUrl` — like Stripe test mode. (In
207
+ production, your own Postgres is the system of record.)
191
208
 
192
209
  Additional org sandboxes can start blank or copy live configuration. Keep
193
210
  customer production traffic on `sk_live_*`; use `sk_test_*` for app setup, Data
@@ -290,7 +307,10 @@ common agent path.
290
307
 
291
308
  ## Model
292
309
 
293
- Every model read returns state plus coordination metadata:
310
+ The read shape depends on whether the client has a local graph:
311
+
312
+ - The stateful `ablo.<model>.retrieve({ id })` returns the BARE row (`T | undefined`), and `list({ where })` returns `T[]`. Read coordination metadata (state watermark, active claims) separately via `ablo.snapshot(...)` and `ablo.<model>.claim.state({ id })`.
313
+ - The stateless HTTP / `.model(name)` `retrieve({ id })` returns a `ModelRead<T>` envelope because it has no local graph to carry the watermark; `list({ where })` still returns `T[]`.
294
314
 
295
315
  ```ts
296
316
  type ModelRead<T> = {
@@ -300,32 +320,31 @@ type ModelRead<T> = {
300
320
  };
301
321
  ```
302
322
 
303
- `stamp` is the state watermark. Pass it to writes as `readAt`.
323
+ `stamp` is the state watermark. Pass it to writes as `readAt` (from `ablo.snapshot(...)` on the stateful client, or from the envelope on the HTTP client).
304
324
 
305
325
  `claims` lists active work on the target. Reads are allowed while another participant is working. The caller decides whether to return, fail, or wait.
306
326
 
307
327
  ## Claimed Behavior
308
328
 
309
- Claimed behavior is explicit:
329
+ Claimed behavior is explicit, and there are only two policies:
310
330
 
311
- - `ifClaimed: 'return'` returns the current claims with the read.
331
+ - `ifClaimed: 'return'` (the default) returns the row plus the current claims with the read.
312
332
  - `ifClaimed: 'fail'` throws `AbloClaimedError`.
313
- - `ifClaimed: 'wait'` waits for matching claims to clear.
314
-
315
- Schema clients use the realtime claim stream for waits.
316
333
 
317
- Schema-less HTTP clients cannot know when a claim clears unless the caller opts into polling. When using `ifClaimed: 'wait'` over HTTP, provide `claimedPollInterval` and usually `claimedTimeout`.
334
+ Reads never block on a claim. To wait for a row to free up, `claim({ id })` it
335
+ the claim queues fairly behind the current holder and is granted when the row
336
+ frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
318
337
 
319
338
  ```ts
339
+ // Read a claimed row without blocking, surfacing who holds it:
320
340
  await api.model('reports').retrieve({
321
341
  id: 'report_stockholm',
322
- ifClaimed: 'wait',
323
- claimedPollInterval: 1_000,
324
- claimedTimeout: 30_000,
342
+ ifClaimed: 'return',
325
343
  });
326
- ```
327
344
 
328
- No hidden hard-coded claimed polling. `claimedTimeout` is a maximum wait, not the coordination mechanism.
345
+ // Or wait for it to free up by queueing your own claim:
346
+ await api.model('reports').claim({ id: 'report_stockholm' });
347
+ ```
329
348
 
330
349
  ## Write
331
350
 
package/llms.txt CHANGED
@@ -8,7 +8,7 @@ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist
8
8
 
9
9
  ## Start here
10
10
 
11
- First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla> --storage datasource`. Agents have no TTY — `--yes` is REQUIRED or it HANGS. It scaffolds `ablo/schema.ts`, the client, the Data Source endpoint, and (for Next.js) the browser provider + session route, all on the current API. Edit the generated files rather than hand-writing from this doc.
11
+ First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla>`. Agents have no TTY — `--yes` is REQUIRED or it HANGS. The default `--storage direct` (connection string) scaffolds `ablo/schema.ts`, the client, and (for Next.js) the browser provider + session route; pass `--storage endpoint` instead when database credentials must stay inside the app — that variant scaffolds a signed Data Source endpoint. All on the current API. Edit the generated files rather than hand-writing from this doc.
12
12
 
13
13
  Second: make sure a key exists — WITHOUT printing it. The key is a secret; it must never appear in your output, your reasoning, or a file you echo (it would live in the conversation history forever). Check PRESENCE only: `[ -n "$ABLO_API_KEY" ] && echo set` and `grep -cq '^ABLO_API_KEY=' .env.local && echo wired` — never `cat .env.local`, never `echo $ABLO_API_KEY`. If neither check passes, ask the HUMAN to run `npx ablo login` once — it opens a browser and saves a `sk_test_` key locally; an agent must NOT run it. You never copy the key by hand: the next step writes it into `.env.local` (and gitignores it) for you.
14
14
 
@@ -24,7 +24,7 @@ Each app gets its own PROJECT inside the org — its own schema, its own sandbox
24
24
  import Ablo from '@abloatai/ablo';
25
25
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
26
26
 
27
- TYPES: have the project register its schema ONCE via declaration merging (init scaffolds `ablo.d.ts`): `declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema.
27
+ TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
28
28
 
29
29
 
30
30
  const schema = defineSchema({
@@ -99,13 +99,13 @@ coordination until the app reports it through Data Source events.
99
99
  ## Claimed Behavior
100
100
 
101
101
  Reads never silently block. Schema reads stay open while a row is claimed.
102
- Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
103
- receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
104
- `ifClaimed: 'wait'` to wait until the active claim clears.
102
+ Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` (the
103
+ default — returns the row plus active claim metadata) or `ifClaimed: 'fail'`
104
+ to throw `AbloClaimedError`.
105
105
 
106
- Schema clients learn when a claim clears by listening to the live claim stream, so they don't need to poll. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
107
-
108
- Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
106
+ Reads never block on a claim. To wait for a row to free up, `claim({ id })` it
107
+ the claim queues fairly behind the current holder and is granted when the row
108
+ frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
109
109
 
110
110
  ## Guarantees
111
111
 
@@ -123,9 +123,9 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
123
123
 
124
124
  ## Storage Boundary
125
125
 
126
- Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
126
+ `databaseUrl` is an OPTIONAL, server-only constructor option on `Ablo(...)`. It is NOT auto-read from the environment — pass it EXPLICITLY to register your Postgres directly. Omit it when you expose a signed Data Source endpoint, or when trying Ablo against the hosted sandbox (apiKey only). A `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored unless you pass `databaseUrl` explicitly.
127
127
 
128
- Every schema model is backed by a database, and the default is YOUR OWN. Keep your rows in your Postgres and expose a Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`). Your `DATABASE_URL` lives in your app, never in `Ablo(...)`; run `npx ablo migrate` to provision the synced-model tables in your DB.
128
+ In production, every schema model is backed by YOUR OWN database. The PRIMARY path is the connection string: pass `databaseUrl` (most users already have a Postgres — often Prisma- or Drizzle-managed for auth/audit/log tables that are NOT in the Ablo schema; Ablo syncs a SUBSET of models against it). Most users do NOT run `ablo migrate` — they ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing tables with their own migration tool. Run `npx ablo migrate` only when Ablo should OWN the tables (it provisions the synced-model tables in your DB). The alternative to the connection string is a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`); use it when database credentials must never leave your infrastructure (or for a local/private-range DB, which Ablo's cloud cannot reach over the network).
129
129
 
130
130
  GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
131
131
 
@@ -145,7 +145,7 @@ export const { POST } = dataSourceNext({
145
145
  });
146
146
  ```
147
147
 
148
- `npx ablo init` defaults to this — it scaffolds the endpoint and the `DATABASE_URL` for you (see CLI below). Your app database credentials stay private — Ablo only calls the endpoint.
148
+ `npx ablo init` defaults to `--storage direct` (the connection-string path — it scaffolds the client carrying `databaseUrl` and the `DATABASE_URL` env entry; see CLI below). Pass `--storage endpoint` to scaffold the signed Data Source endpoint above instead, when app database credentials must stay private — Ablo only calls the endpoint.
149
149
 
150
150
  ## Sandboxes
151
151
 
@@ -156,9 +156,11 @@ when an agent is asked to "make Ablo work" in an existing app.
156
156
 
157
157
  Authenticated org sandboxes are real test environments. Treat the default
158
158
  sandbox like Stripe test mode: it has an isolated sync group prefix and mints
159
- `sk_test_*` keys. Extra sandboxes can start blank or copy live configuration.
160
- Resetting a sandbox creates a clean future stream without touching live data.
161
- Use `sk_live_*` only for production.
159
+ `sk_test_*` keys. The sandbox CAN host rows in Ablo's test plane, so you can try
160
+ Ablo with NO database `apiKey` only, no `databaseUrl`. (In production, your own
161
+ Postgres is the system of record.) Extra sandboxes can start blank or copy live
162
+ configuration. Resetting a sandbox creates a clean future stream without
163
+ touching live data. Use `sk_live_*` only for production.
162
164
 
163
165
  For coding agents, the sandbox success path is: pick one shared model,
164
166
  declare schema, create the Ablo client, replace one direct mutation with a typed
@@ -185,7 +187,7 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
185
187
 
186
188
  `ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
187
189
 
188
- - `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the `ablo/data-source.ts` endpoint above.
190
+ - `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage direct|endpoint` (default `direct`), `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the client; `--storage direct` (default) wires `databaseUrl`, `--storage endpoint` scaffolds the `ablo/data-source.ts` endpoint above instead.
189
191
  - Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo push` writes `.env.local`).
190
192
  - Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
191
193
  - `npx ablo push --no-watch` pushes the schema (sandbox) AND writes `ABLO_API_KEY` to `.env.local` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode sandbox|production` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,10 +0,0 @@
1
- /**
2
- * Internal compatibility entrypoint for the stateless hosted protocol client.
3
- *
4
- * Use this build for serverless functions, scripts, and backends that want
5
- * model reads/writes and commits over HTTP without the realtime sync runtime.
6
- */
7
- export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiClaims, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, } from '../client/ApiClient.js';
8
- export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, ClaimCreateOptions, ClaimHandle, ClaimWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
9
- import { createProtocolClient } from '../client/ApiClient.js';
10
- export default createProtocolClient;
package/dist/api/index.js DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Internal compatibility entrypoint for the stateless hosted protocol client.
3
- *
4
- * Use this build for serverless functions, scripts, and backends that want
5
- * model reads/writes and commits over HTTP without the realtime sync runtime.
6
- */
7
- export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
8
- import { createProtocolClient } from '../client/ApiClient.js';
9
- export default createProtocolClient;
@@ -1,44 +0,0 @@
1
- /**
2
- * Principal constructors — a thin typed façade over the raw
3
- * `SessionRef` / `AgentRef` shapes so call sites don't have to memorize
4
- * the discriminated-union tags.
5
- *
6
- * ```ts
7
- * import Ablo, { session } from '@abloatai/ablo';
8
- *
9
- * const ablo = Ablo({ schema, apiKey });
10
- * const participant = await ablo.participants.join({
11
- * type: 'Matter',
12
- * id: 'deal-1',
13
- * });
14
- * ```
15
- *
16
- * Browser-human flows use `session(...)`. Agent-spawn-agent flows use
17
- * `agent(...)`, but those rarely appear in customer code because the
18
- * participant layer handles attenuation.
19
- *
20
- * These are pure — no I/O, no hidden state. If the shape ever grows a
21
- * required field (say, a scope hint for the restricted key), the helper
22
- * is the one place to flag migrations.
23
- */
24
- import type { AgentRef, SessionRef } from './types/streams.js';
25
- /**
26
- * Build a `SessionRef` from the identifiers your auth system already
27
- * holds. Typical inputs: the Better Auth session id, the user id, and
28
- * the organization the session is scoped to.
29
- */
30
- export declare function session(params: {
31
- id: string;
32
- userId: string;
33
- organizationId: string;
34
- }): SessionRef;
35
- /**
36
- * Build an `AgentRef` from an agent id + the capability token that
37
- * authenticates it. Rare in application code — the common path is
38
- * `participant.join(child)` where the parent's token is attenuated
39
- * automatically.
40
- */
41
- export declare function agent(params: {
42
- id: string;
43
- capabilityToken: string;
44
- }): AgentRef;
package/dist/principal.js DELETED
@@ -1,49 +0,0 @@
1
- /**
2
- * Principal constructors — a thin typed façade over the raw
3
- * `SessionRef` / `AgentRef` shapes so call sites don't have to memorize
4
- * the discriminated-union tags.
5
- *
6
- * ```ts
7
- * import Ablo, { session } from '@abloatai/ablo';
8
- *
9
- * const ablo = Ablo({ schema, apiKey });
10
- * const participant = await ablo.participants.join({
11
- * type: 'Matter',
12
- * id: 'deal-1',
13
- * });
14
- * ```
15
- *
16
- * Browser-human flows use `session(...)`. Agent-spawn-agent flows use
17
- * `agent(...)`, but those rarely appear in customer code because the
18
- * participant layer handles attenuation.
19
- *
20
- * These are pure — no I/O, no hidden state. If the shape ever grows a
21
- * required field (say, a scope hint for the restricted key), the helper
22
- * is the one place to flag migrations.
23
- */
24
- /**
25
- * Build a `SessionRef` from the identifiers your auth system already
26
- * holds. Typical inputs: the Better Auth session id, the user id, and
27
- * the organization the session is scoped to.
28
- */
29
- export function session(params) {
30
- return {
31
- kind: 'session',
32
- id: params.id,
33
- userId: params.userId,
34
- organizationId: params.organizationId,
35
- };
36
- }
37
- /**
38
- * Build an `AgentRef` from an agent id + the capability token that
39
- * authenticates it. Rare in application code — the common path is
40
- * `participant.join(child)` where the parent's token is attenuated
41
- * automatically.
42
- */
43
- export function agent(params) {
44
- return {
45
- kind: 'agent',
46
- id: params.id,
47
- capabilityToken: params.capabilityToken,
48
- };
49
- }
@@ -1,19 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- export interface SyncGroupProviderProps {
3
- /** The sync-group identifier — e.g., `matter:abc-123`, `deck:xyz`. */
4
- id: string;
5
- children: ReactNode;
6
- }
7
- export declare function SyncGroupProvider({ id, children }: SyncGroupProviderProps): import("react").JSX.Element;
8
- /**
9
- * Returns the ID of the nearest `<SyncGroupProvider>`. Throws if
10
- * called outside one — sync-group awareness is mandatory by design,
11
- * so the error points the consumer at the provider instead of
12
- * returning undefined and letting downstream code silently miss scope.
13
- *
14
- * If a component legitimately renders both inside and outside a
15
- * group, structure the tree so the hook is only called on the
16
- * inside path (e.g., split into two components). Silent nulls are
17
- * never the right answer.
18
- */
19
- export declare function useSyncGroup(): string;
@@ -1,44 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext, useMemo } from 'react';
4
- import { AbloValidationError } from '../errors.js';
5
- /**
6
- * Narrow context for a per-entity sync-group scope. Maps directly onto
7
- * Liveblocks' `<RoomProvider id="...">`: wrap a subtree, and any hooks
8
- * inside can read `useSyncGroup()` to discover "which entity am I
9
- * scoped to?" without threading the ID through props.
10
- *
11
- * Typical IDs follow the multiplayer sync-group convention: `matter:<id>`,
12
- * `deck:<id>`, `project:<id>`. The ID is an opaque string — the
13
- * provider doesn't parse it.
14
- *
15
- * v0.3.0 scope: this is a thin passthrough. Future versions will
16
- * scope `useQuery` / `useOne` results to the group automatically.
17
- */
18
- const SyncGroupContext = createContext(null);
19
- export function SyncGroupProvider({ id, children }) {
20
- // Stabilize the context value so consumers memoized on it don't
21
- // re-render when the provider re-renders for unrelated reasons.
22
- const value = useMemo(() => id, [id]);
23
- return _jsx(SyncGroupContext.Provider, { value: value, children: children });
24
- }
25
- /**
26
- * Returns the ID of the nearest `<SyncGroupProvider>`. Throws if
27
- * called outside one — sync-group awareness is mandatory by design,
28
- * so the error points the consumer at the provider instead of
29
- * returning undefined and letting downstream code silently miss scope.
30
- *
31
- * If a component legitimately renders both inside and outside a
32
- * group, structure the tree so the hook is only called on the
33
- * inside path (e.g., split into two components). Silent nulls are
34
- * never the right answer.
35
- */
36
- export function useSyncGroup() {
37
- const id = useContext(SyncGroupContext);
38
- if (!id) {
39
- throw new AbloValidationError('useSyncGroup: no <SyncGroupProvider> mounted above this component. ' +
40
- 'Wrap your tree with <SyncGroupProvider id="matter:..."> from ' +
41
- '@abloatai/ablo/react.', { code: 'no_sync_group_provider' });
42
- }
43
- return id;
44
- }
@@ -1,29 +0,0 @@
1
- import type { ResolveClaims } from '../types/global.js';
2
- /**
3
- * Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
4
- *
5
- * The consumer declares their claim vocabulary in the global:
6
- *
7
- * ```ts
8
- * declare module '@abloatai/ablo' {
9
- * interface Register {
10
- * Claims: {
11
- * editLayer: { slideId: string; layerId: string };
12
- * generateWithAI: { entityId: string; tool: string };
13
- * };
14
- * }
15
- * }
16
- * ```
17
- *
18
- * Then `useClaim('editLayer')` returns a function whose sole argument
19
- * is the `editLayer` claim shape — no runtime checks, purely compile-
20
- * time narrowing.
21
- *
22
- * The SDK doesn't own what happens next: the `beginClaim` function on
23
- * the React context (supplied via `SyncProvider`) is where the claim
24
- * claim turns into a network effect. A Node-backed consumer wires it
25
- * through `SyncAgent.beginClaim`; a browser-backed consumer may
26
- * broadcast it through their own WebSocket. This hook is pure sugar
27
- * that adds the typed name + claim narrowing.
28
- */
29
- export declare function useClaim<Name extends keyof ResolveClaims & string>(claimName: Name): (claim: ResolveClaims[Name]) => unknown;
@@ -1,42 +0,0 @@
1
- 'use client';
2
- import { useCallback } from 'react';
3
- import { useSyncContext } from './context.js';
4
- import { AbloValidationError } from '../errors.js';
5
- /**
6
- * Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
7
- *
8
- * The consumer declares their claim vocabulary in the global:
9
- *
10
- * ```ts
11
- * declare module '@abloatai/ablo' {
12
- * interface Register {
13
- * Claims: {
14
- * editLayer: { slideId: string; layerId: string };
15
- * generateWithAI: { entityId: string; tool: string };
16
- * };
17
- * }
18
- * }
19
- * ```
20
- *
21
- * Then `useClaim('editLayer')` returns a function whose sole argument
22
- * is the `editLayer` claim shape — no runtime checks, purely compile-
23
- * time narrowing.
24
- *
25
- * The SDK doesn't own what happens next: the `beginClaim` function on
26
- * the React context (supplied via `SyncProvider`) is where the claim
27
- * claim turns into a network effect. A Node-backed consumer wires it
28
- * through `SyncAgent.beginClaim`; a browser-backed consumer may
29
- * broadcast it through their own WebSocket. This hook is pure sugar
30
- * that adds the typed name + claim narrowing.
31
- */
32
- export function useClaim(claimName) {
33
- const { beginClaim } = useSyncContext();
34
- return useCallback((claim) => {
35
- if (!beginClaim) {
36
- throw new AbloValidationError(`useClaim: no \`beginClaim\` wired into SyncProvider. Pass ` +
37
- `a \`beginClaim\` prop (typically bound to your transport) ` +
38
- `to enable claim invocations.`, { code: 'claim_not_wired' });
39
- }
40
- return beginClaim(claimName, claim);
41
- }, [beginClaim, claimName]);
42
- }