@abloatai/ablo 0.9.2 → 0.9.3
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/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +40 -22
- package/dist/BaseSyncedStore.d.ts +2 -36
- package/dist/BaseSyncedStore.js +5 -53
- package/dist/NetworkMonitor.js +4 -1
- package/dist/SyncClient.d.ts +10 -5
- package/dist/SyncClient.js +63 -1
- package/dist/SyncEngineContext.js +5 -1
- package/dist/auth/index.js +3 -1
- package/dist/cli.cjs +302645 -0
- package/dist/client/Ablo.d.ts +12 -3
- package/dist/client/Ablo.js +28 -2
- package/dist/client/ApiClient.js +39 -6
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.d.ts +9 -0
- package/dist/client/createModelProxy.js +34 -10
- package/dist/client/persistence.d.ts +6 -1
- package/dist/client/persistence.js +1 -1
- package/dist/client/registerDataSource.d.ts +4 -4
- package/dist/client/registerDataSource.js +39 -31
- package/dist/client/writeOptionsSchema.d.ts +50 -0
- package/dist/client/writeOptionsSchema.js +57 -0
- package/dist/core/index.d.ts +18 -26
- package/dist/core/index.js +22 -46
- package/dist/errorCodes.d.ts +13 -0
- package/dist/errorCodes.js +16 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/mutators/UndoManager.d.ts +31 -5
- package/dist/mutators/UndoManager.js +113 -1
- package/dist/schema/ddl.js +2 -1
- package/dist/schema/field.js +2 -1
- package/dist/schema/serialize.js +2 -1
- package/dist/server/storage-mode.d.ts +7 -0
- package/dist/server/storage-mode.js +6 -0
- package/dist/source/adapters/drizzle.js +3 -2
- package/dist/source/adapters/kysely.d.ts +68 -0
- package/dist/source/adapters/kysely.js +210 -0
- package/dist/source/adapters/memory.js +2 -1
- package/dist/source/adapters/prisma.js +3 -2
- package/dist/source/index.js +2 -1
- package/dist/transactions/TransactionQueue.d.ts +6 -7
- package/dist/transactions/TransactionQueue.js +33 -9
- package/dist/utils/duration.js +3 -2
- package/docs/client-behavior.md +1 -1
- package/docs/data-sources.md +61 -42
- package/docs/guarantees.md +2 -2
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +4 -7
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +84 -37
- package/docs/schema-contract.md +2 -4
- package/llms-full.txt +360 -0
- package/llms.txt +14 -9
- package/package.json +22 -2
package/docs/mcp.md
CHANGED
|
@@ -83,7 +83,7 @@ Per-client walkthroughs:
|
|
|
83
83
|
| `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
|
|
84
84
|
| `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
|
|
85
85
|
| `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
|
|
86
|
-
| `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with
|
|
86
|
+
| `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with a `data-source` (your own database) endpoint. |
|
|
87
87
|
|
|
88
88
|
#### Resources
|
|
89
89
|
|
package/docs/quickstart.md
CHANGED
|
@@ -1,74 +1,121 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Build with Ablo on **your own database**. You declare a small Ablo schema for the
|
|
4
|
+
models humans and agents edit together, hand the client your Postgres
|
|
5
|
+
`DATABASE_URL`, and coordinate every write through `ablo.<model>`. Your database
|
|
6
|
+
is the system of record — Ablo never hosts your data. It is the transaction
|
|
7
|
+
layer on top: it registers your connection, commits every write there behind
|
|
8
|
+
row-level security, and fans the confirmed rows out to every connected client.
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
is the same; [Integration Guide](./integration-guide.md) explains when to use
|
|
8
|
-
Ablo-managed state versus a Data Source that calls your existing API service.
|
|
9
|
-
|
|
10
|
-
## 1. Install and set a sandbox key
|
|
10
|
+
## 1. Install and get a key
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install @abloatai/ablo
|
|
14
|
+
npx ablo login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`ablo login` opens the browser — sign in (or sign up) and a `sk_test_` key is
|
|
18
|
+
saved locally for the CLI. Later, `npx ablo dev` (step 4) writes
|
|
19
|
+
`ABLO_API_KEY` into your `.env.local` so the SDK finds it too — no manual
|
|
20
|
+
copy-paste. In CI, or to manage it by hand, set it yourself instead:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
14
23
|
export ABLO_API_KEY=sk_test_...
|
|
15
24
|
```
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
Every SDK and CLI call needs a key. Test and live keys work like Stripe's —
|
|
27
|
+
except both point at databases *you* own: `sk_test_*` for your dev database,
|
|
28
|
+
`sk_live_*` for production. There is no keyless mode; the public `/sandbox` page
|
|
29
|
+
is a hosted demo, not your app.
|
|
19
30
|
|
|
20
|
-
## 2. Declare
|
|
31
|
+
## 2. Declare your Ablo schema
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
server actions, agents, React reads
|
|
33
|
+
The schema is the contract — it generates `ablo.<model>` methods for app code,
|
|
34
|
+
server actions, agents, and React reads. Declare **only the synced models** Ablo
|
|
35
|
+
coordinates; your auth, billing, and other tables stay in your own Drizzle schema,
|
|
36
|
+
owned by your own migrations.
|
|
24
37
|
|
|
25
38
|
```ts
|
|
26
|
-
|
|
39
|
+
// ablo/schema.ts
|
|
27
40
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
28
41
|
|
|
29
|
-
const schema = defineSchema({
|
|
42
|
+
export const schema = defineSchema({
|
|
30
43
|
weatherReports: model({
|
|
31
44
|
location: z.string(),
|
|
32
45
|
status: z.enum(['pending', 'ready']),
|
|
33
46
|
forecast: z.string().optional(),
|
|
34
47
|
}),
|
|
35
48
|
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 3. Point Ablo at your database
|
|
52
|
+
|
|
53
|
+
The client takes your schema, your key, and your `DATABASE_URL`. On first
|
|
54
|
+
connect Ablo registers the connection (sent once over TLS, stored sealed, never
|
|
55
|
+
echoed back) and from then on commits every write directly to your Postgres.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# .env — server runtime only, never the browser
|
|
59
|
+
DATABASE_URL=postgres://ablo_app:...@host:5432/db
|
|
60
|
+
ABLO_API_KEY=sk_test_...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// ablo/client.ts
|
|
65
|
+
import Ablo from '@abloatai/ablo';
|
|
66
|
+
import { schema } from './schema';
|
|
36
67
|
|
|
37
68
|
export const ablo = Ablo({
|
|
38
69
|
schema,
|
|
39
70
|
apiKey: process.env.ABLO_API_KEY,
|
|
71
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here, never with Ablo
|
|
40
72
|
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use a dedicated **non-superuser role** for the connection — Ablo enforces
|
|
76
|
+
tenant isolation with row-level security, so the server rejects superuser or
|
|
77
|
+
`BYPASSRLS` roles outright.
|
|
78
|
+
|
|
79
|
+
Don't want a connection string to leave your infrastructure? Keep
|
|
80
|
+
`DATABASE_URL` in your app only and expose one signed **Data Source endpoint**
|
|
81
|
+
built from an ORM adapter instead — same product, same writes, see
|
|
82
|
+
[Connect Your Database](./data-sources.md). In that setup, omit `databaseUrl`
|
|
83
|
+
from `Ablo(...)`.
|
|
84
|
+
|
|
85
|
+
## 4. Provision your tables, then push the schema
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx ablo migrate # creates your synced-model tables (with row-level security)
|
|
89
|
+
# in YOUR database — your other tables are left untouched
|
|
90
|
+
npx ablo dev # pushes the schema (test mode), writes ABLO_API_KEY to
|
|
91
|
+
# .env.local, and re-pushes on every save — the dev loop
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`ablo dev` (or one-shot `npx ablo push`) uploads the schema *definition* —
|
|
95
|
+
model names, fields, types. That metadata is the only thing Ablo keeps; the
|
|
96
|
+
rows stay in your database. Skipping the push makes every write to a new or
|
|
97
|
+
changed model fail with `server_execute_unknown_model` — that error literally
|
|
98
|
+
means "run `npx ablo push`."
|
|
99
|
+
|
|
100
|
+
## 5. Write through the model
|
|
101
|
+
|
|
102
|
+
The rows land in your Postgres; every connected client sees them live.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { ablo } from './ablo/client';
|
|
106
|
+
|
|
41
107
|
await ablo.ready();
|
|
42
108
|
|
|
43
109
|
const created = await ablo.weatherReports.create({
|
|
44
|
-
data: {
|
|
45
|
-
location: 'Stockholm',
|
|
46
|
-
status: 'pending',
|
|
47
|
-
},
|
|
110
|
+
data: { location: 'Stockholm', status: 'pending' },
|
|
48
111
|
});
|
|
49
112
|
|
|
50
113
|
const updated = await ablo.weatherReports.update({
|
|
51
114
|
id: created.id,
|
|
52
|
-
data: {
|
|
53
|
-
status: 'ready',
|
|
54
|
-
forecast: 'Light rain, 13C',
|
|
55
|
-
},
|
|
115
|
+
data: { status: 'ready', forecast: 'Light rain, 13C' },
|
|
56
116
|
});
|
|
57
117
|
|
|
58
|
-
console.log({ id: updated.id, status: updated.status });
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Expected output:
|
|
62
|
-
|
|
63
|
-
```txt
|
|
64
|
-
{ id: '...', status: 'ready' }
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Run the example
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
cd packages/sync-engine
|
|
71
|
-
ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
|
|
118
|
+
console.log({ id: updated.id, status: updated.status }); // { id: '...', status: 'ready' }
|
|
72
119
|
```
|
|
73
120
|
|
|
74
121
|
## Add coordination for slow work
|
|
@@ -146,5 +193,5 @@ Keep using the schema client for app and agent writes.
|
|
|
146
193
|
- [Schema Contract](./schema-contract.md) explains what the schema drives across SDK, React, agents, Data Source, and schema push.
|
|
147
194
|
- [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
|
|
148
195
|
- [Client Behavior](./client-behavior.md) covers errors, retries, and public imports.
|
|
149
|
-
- [Connect Your Database](./data-sources.md) covers
|
|
196
|
+
- [Connect Your Database](./data-sources.md) covers both connection shapes — `databaseUrl` and the signed Data Source endpoint.
|
|
150
197
|
- [AI SDK Tool](./examples/ai-sdk-tool.md) shows the same write path inside a tool call.
|
package/docs/schema-contract.md
CHANGED
|
@@ -88,10 +88,8 @@ the fresh row. Reads stay open; only acting on the row serializes.
|
|
|
88
88
|
|
|
89
89
|
## Storage boundary
|
|
90
90
|
|
|
91
|
-
Every schema model
|
|
92
|
-
|
|
93
|
-
- Use Ablo-managed state when the row can live in Ablo.
|
|
94
|
-
- Use a Data Source when your app database remains canonical.
|
|
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.
|
|
95
93
|
|
|
96
94
|
Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
|
|
97
95
|
Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# Ablo Full Context
|
|
2
|
+
|
|
3
|
+
Ablo is the state coordination layer for apps where humans and agents edit the same data.
|
|
4
|
+
|
|
5
|
+
Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist to the database, coordinate with concurrent human or agent work, and leave an audit trail.
|
|
6
|
+
|
|
7
|
+
The product surface should read like Stripe, Turbopuffer, or Reducto: one product-name import, models by dot access, optional subpaths only for schema, React, and tests.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import Ablo from '@abloatai/ablo';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Public Surface
|
|
14
|
+
|
|
15
|
+
Public imports:
|
|
16
|
+
|
|
17
|
+
- `@abloatai/ablo` — `Ablo`, errors, typed model clients, claims, `dataSource`, and advanced schema-less protocol models.
|
|
18
|
+
- `@abloatai/ablo/schema` — `defineSchema`, `model`, `z`, relations, schema types.
|
|
19
|
+
- `@abloatai/ablo/react` — React provider and hooks.
|
|
20
|
+
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
|
21
|
+
|
|
22
|
+
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).
|
|
23
|
+
|
|
24
|
+
The canonical integration doc is `integration-guide`. It explains the end-to-end
|
|
25
|
+
path for schema, React selectors, your Data Source-backed database, multiplayer,
|
|
26
|
+
and agent workers. Use it before inventing a new setup
|
|
27
|
+
flow.
|
|
28
|
+
|
|
29
|
+
## Production Guarantees
|
|
30
|
+
|
|
31
|
+
`wait: 'confirmed'` resolves after the server accepts the write. Schema model
|
|
32
|
+
writes are optimistic; a server rejection rolls back local state and raises a
|
|
33
|
+
typed `AbloError`.
|
|
34
|
+
|
|
35
|
+
Use `snapshot(...)` and `readAt` when an agent write depends on state it already
|
|
36
|
+
read. `onStale: 'reject'` prevents lost updates by rejecting if the target
|
|
37
|
+
changed after the snapshot.
|
|
38
|
+
|
|
39
|
+
Claims are live coordination signals, not database locks. Schema clients wait
|
|
40
|
+
from the realtime claim stream. Schema-less HTTP clients must pass an explicit
|
|
41
|
+
`claimedPollInterval` for `ifClaimed: 'wait'`; no hidden hard-coded polling.
|
|
42
|
+
|
|
43
|
+
Agent run bookkeeping is internal. Most users should not create run ledger
|
|
44
|
+
records or scoped access credentials manually.
|
|
45
|
+
|
|
46
|
+
## Primary App Path
|
|
47
|
+
|
|
48
|
+
Declare models in a schema, then use typed model clients:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import Ablo from '@abloatai/ablo';
|
|
52
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
53
|
+
|
|
54
|
+
const schema = defineSchema({
|
|
55
|
+
weatherReports: model({
|
|
56
|
+
id: z.string(),
|
|
57
|
+
location: z.string(),
|
|
58
|
+
status: z.enum(['pending', 'ready']),
|
|
59
|
+
forecast: z.string().optional(),
|
|
60
|
+
updatedAt: z.string(),
|
|
61
|
+
}),
|
|
62
|
+
projects: model({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
name: z.string(),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
69
|
+
|
|
70
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
71
|
+
if (!report) throw new Error('Row not found');
|
|
72
|
+
|
|
73
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
74
|
+
const updated = await ablo.weatherReports.update({
|
|
75
|
+
id: claim.data.id,
|
|
76
|
+
data: { status: 'ready', forecast: await getForecast(claim.data) },
|
|
77
|
+
wait: 'confirmed',
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The normal app API (every verb takes ONE options object):
|
|
82
|
+
|
|
83
|
+
- `ablo.<model>.retrieve({ id })` — async read of one row from the server
|
|
84
|
+
- `ablo.<model>.list({ where })` — async read of many from the server
|
|
85
|
+
- `ablo.<model>.get(id)` / `getAll({ where })` / `getCount({ where })` — synchronous local-graph reads (reactive in React render)
|
|
86
|
+
- `ablo.<model>.create({ data, id? })`
|
|
87
|
+
- `ablo.<model>.update({ id, data })`
|
|
88
|
+
- `ablo.<model>.delete({ id })`
|
|
89
|
+
- `await using claim = await ablo.<model>.claim({ id })` — disposable claim handle; read the fresh row off `claim.data`; auto-releases on scope exit
|
|
90
|
+
- `ablo.<model>.claim.state({ id })` / `claim.queue({ id })` / `claim.release({ id })` / `claim.reorder({ id, order })`
|
|
91
|
+
|
|
92
|
+
The synchronous reads (`getAll`/`getCount`) accept `where`, `filter`, `orderBy`,
|
|
93
|
+
`limit`, `offset`, and `state`; state defaults to `'live'`, with `'archived'`
|
|
94
|
+
and `'all'` for lifecycle-aware reads.
|
|
95
|
+
|
|
96
|
+
Use `ablo.model<T>(name)` only when the caller intentionally does not have a schema, such as custom server agents, MCP routes, migration scripts, or generic admin tools.
|
|
97
|
+
|
|
98
|
+
React reads should use selector `useAblo`:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
const report = useAblo((ablo) => ablo.weatherReports.get(id)) ?? serverReport;
|
|
102
|
+
const visibleReports = useAblo((ablo) =>
|
|
103
|
+
ablo.weatherReports.getAll({ where: { projectId } }),
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use zero-argument `useAblo()` only for callbacks, effects, and writes that need
|
|
108
|
+
the provider-owned client:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const ablo = useAblo();
|
|
112
|
+
await ablo?.weatherReports.update({ id, data: patch, wait: 'confirmed' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Treat `useQuery`, `useOne`, `useReader`, and `useMutate` as compatibility hooks
|
|
116
|
+
for older string-keyed integrations. Do not teach them as the first React API.
|
|
117
|
+
|
|
118
|
+
## Multiplayer
|
|
119
|
+
|
|
120
|
+
Multiplayer is an effect of the normal model API, not a separate setup path.
|
|
121
|
+
When human UI, server actions, and agents use the same
|
|
122
|
+
`Ablo({ schema, apiKey })` client and write through `ablo.<model>`, Ablo
|
|
123
|
+
coordinates the shared model stream:
|
|
124
|
+
|
|
125
|
+
- confirmed model writes fan out as realtime deltas,
|
|
126
|
+
- React hooks read the latest row and active claims,
|
|
127
|
+
- claims announce active work before a commit,
|
|
128
|
+
- `snapshot(...)` plus `readAt` protects against stale writes.
|
|
129
|
+
|
|
130
|
+
Direct database writes outside Ablo are not coordinated until the app reports
|
|
131
|
+
them through Data Source events. In Data Source mode, writes made through Ablo
|
|
132
|
+
still coordinate normally because Ablo sends the signed commit request, receives
|
|
133
|
+
canonical rows, and fans out the resulting deltas.
|
|
134
|
+
|
|
135
|
+
For existing Python backends, keep the Python service layer and database as the
|
|
136
|
+
source of truth. Add one signed Data Source endpoint, keep initial page loads on
|
|
137
|
+
existing APIs if needed, use `useAblo((ablo) => ablo.<model>.get(id)) ?? serverRow`
|
|
138
|
+
for live rows, then migrate buttons one at a time from direct Python endpoint
|
|
139
|
+
writes to `ablo.<model>.update(...)`.
|
|
140
|
+
|
|
141
|
+
This applies to any API-backed app: Python, Rails, Go, or Node. The backend keeps
|
|
142
|
+
its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
|
|
143
|
+
`ABLO_API_KEY`, not a database URL.
|
|
144
|
+
|
|
145
|
+
## Client Behavior
|
|
146
|
+
|
|
147
|
+
Important options: `schema`, `apiKey`, `baseURL`, `persistence`, `fetch`,
|
|
148
|
+
`defaultHeaders`, `defaultQuery`, `logger`, and `dangerouslyAllowBrowser`.
|
|
149
|
+
|
|
150
|
+
There is intentionally no `databaseURL` option on `Ablo(...)`. Application and
|
|
151
|
+
agent code use `ABLO_API_KEY`. Customer-owned app databases stay private behind
|
|
152
|
+
a signed Data Source endpoint.
|
|
153
|
+
|
|
154
|
+
Important per-write options: `wait`, `readAt`, `onStale`,
|
|
155
|
+
`idempotencyKey`, and `timeout`.
|
|
156
|
+
|
|
157
|
+
Errors: `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`,
|
|
158
|
+
`AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`,
|
|
159
|
+
`AbloServerError`, `AbloStaleContextError`, and `AbloClaimedError`.
|
|
160
|
+
|
|
161
|
+
Retry transport failures and 5xx with backoff only when the operation is
|
|
162
|
+
idempotent. Do not blindly retry validation, permission, idempotency, claimed, or
|
|
163
|
+
stale-context errors without changing the request.
|
|
164
|
+
|
|
165
|
+
## Sandboxes
|
|
166
|
+
|
|
167
|
+
There are two sandbox surfaces:
|
|
168
|
+
|
|
169
|
+
- Public `/sandbox` is a deterministic visual playground. It demonstrates shared
|
|
170
|
+
state, active claims, stale-write rejection, receipts, and deltas without
|
|
171
|
+
making real Ablo calls. It is agent-first: it should expose a prompt that can
|
|
172
|
+
be pasted into Claude Code or Codex to wire one real model through Ablo.
|
|
173
|
+
- Authenticated org sandboxes are real test environments. The default sandbox is
|
|
174
|
+
the Stripe-style test mode for an org. It has an isolated sync group prefix,
|
|
175
|
+
can mint `sk_test_*` keys, and can be reset without touching live state.
|
|
176
|
+
|
|
177
|
+
Additional org sandboxes can start blank or copy live configuration. Keep
|
|
178
|
+
customer production traffic on `sk_live_*`; use `sk_test_*` for app setup, Data
|
|
179
|
+
Source endpoint wiring, and agent integration testing.
|
|
180
|
+
|
|
181
|
+
The coding-agent handoff should ask for a narrow integration: one schema model,
|
|
182
|
+
one Ablo client module, one React selector read, one typed model write with
|
|
183
|
+
`readAt`/`onStale: 'reject'`/`wait: 'confirmed'`, and one smoke test where two
|
|
184
|
+
writers prove claim/stale behavior.
|
|
185
|
+
|
|
186
|
+
## Data Sources
|
|
187
|
+
|
|
188
|
+
Use these public environment names:
|
|
189
|
+
|
|
190
|
+
- `ABLO_API_KEY` — SDK authentication for app and agent code. Where it comes
|
|
191
|
+
from: the human runs `npx ablo login` once (browser; an agent must not run
|
|
192
|
+
it), and `npx ablo dev` then writes `ABLO_API_KEY=sk_test_…` into
|
|
193
|
+
`.env.local` automatically. Check the environment and `.env.local` before
|
|
194
|
+
asking the human for a key.
|
|
195
|
+
|
|
196
|
+
Do not ask customers to paste their app database URL into Ablo. If their app
|
|
197
|
+
database is canonical, they expose a Data Source endpoint and keep database
|
|
198
|
+
credentials inside their app.
|
|
199
|
+
|
|
200
|
+
Every schema model is backed by the customer's own database. The SDK methods
|
|
201
|
+
`ablo.<model>.create/update/delete` produce a signed commit request to the
|
|
202
|
+
customer's Data Source route, and that route writes the app database.
|
|
203
|
+
|
|
204
|
+
When the customer's database is canonical, expose one signed source route:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
208
|
+
import { prismaDataSource } from '@abloatai/ablo/source';
|
|
209
|
+
import { schema } from './ablo.schema';
|
|
210
|
+
import { prisma } from './lib/prisma';
|
|
211
|
+
|
|
212
|
+
// The adapter owns commit / idempotency / outbox — no hand-written commit.
|
|
213
|
+
export const { POST } = dataSourceNext({
|
|
214
|
+
schema,
|
|
215
|
+
apiKey: process.env.ABLO_API_KEY!,
|
|
216
|
+
adapter: prismaDataSource(prisma, schema), // or drizzleDataSource(db, schema) / kyselyDataSource(db, schema)
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Commit request shape:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
{
|
|
224
|
+
type: 'commit',
|
|
225
|
+
clientTxId: 'tx_...',
|
|
226
|
+
operations: [
|
|
227
|
+
{ type: 'UPDATE', model: 'weatherReports', id: 'report_stockholm', input, readAt, onStale: 'reject' },
|
|
228
|
+
],
|
|
229
|
+
scope: { participantId, participantKind, organizationId, requiredSyncGroups, mode: 'live' },
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Return `{ rows }` for normal apps or `{ deltas }` for sources that already
|
|
234
|
+
compute canonical change events. External writes come back through the source
|
|
235
|
+
`events` handler or `POST /api/source/events`.
|
|
236
|
+
|
|
237
|
+
## Advanced Schema Options
|
|
238
|
+
|
|
239
|
+
Teach schema as model fields and relations first:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const schema = defineSchema({
|
|
243
|
+
weatherReports: model({
|
|
244
|
+
id: z.string(),
|
|
245
|
+
location: z.string(),
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Advanced helpers such as `mutable`, `readOnly`, `field`, `indexed`, query definitions, `load` strategies, parent metadata, and sync-group formats are still part of the schema package for offline/cache/indexing-heavy apps. Do not put them in the first example unless the user asks for local persistence, offline sync, custom loading, or low-level indexing behavior.
|
|
251
|
+
|
|
252
|
+
## Server Agent Path
|
|
253
|
+
|
|
254
|
+
Server agents should import the same schema as the app and use the same model
|
|
255
|
+
methods. Claim the row around slow work, then write through `ablo.<model>`.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import Ablo from '@abloatai/ablo';
|
|
259
|
+
|
|
260
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
261
|
+
|
|
262
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
263
|
+
const forecast = await getForecast(claim.data);
|
|
264
|
+
await ablo.weatherReports.update({
|
|
265
|
+
id: claim.data.id,
|
|
266
|
+
data: { status: 'ready', forecast },
|
|
267
|
+
wait: 'confirmed',
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Do not require users to manually create protocol bookkeeping objects in the
|
|
272
|
+
common agent path.
|
|
273
|
+
|
|
274
|
+
## Model
|
|
275
|
+
|
|
276
|
+
Every model read returns state plus coordination metadata:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
type ModelRead<T> = {
|
|
280
|
+
data: T;
|
|
281
|
+
stamp: number;
|
|
282
|
+
claims: ModelClaim[];
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`stamp` is the state watermark. Pass it to writes as `readAt`.
|
|
287
|
+
|
|
288
|
+
`claims` lists active work on the target. Reads are allowed while another participant is working. The caller decides whether to return, fail, or wait.
|
|
289
|
+
|
|
290
|
+
## Claimed Behavior
|
|
291
|
+
|
|
292
|
+
Claimed behavior is explicit:
|
|
293
|
+
|
|
294
|
+
- `ifClaimed: 'return'` returns the current claims with the read.
|
|
295
|
+
- `ifClaimed: 'fail'` throws `AbloClaimedError`.
|
|
296
|
+
- `ifClaimed: 'wait'` waits for matching claims to clear.
|
|
297
|
+
|
|
298
|
+
Schema clients use the realtime claim stream for waits.
|
|
299
|
+
|
|
300
|
+
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`.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
await api.model('reports').retrieve({
|
|
304
|
+
id: 'report_stockholm',
|
|
305
|
+
ifClaimed: 'wait',
|
|
306
|
+
claimedPollInterval: 1_000,
|
|
307
|
+
claimedTimeout: 30_000,
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
No hidden hard-coded claimed polling. `claimedTimeout` is a maximum wait, not the coordination mechanism.
|
|
312
|
+
|
|
313
|
+
## Write
|
|
314
|
+
|
|
315
|
+
Use model methods as the normal write path:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
319
|
+
const updated = await ablo.weatherReports.update({
|
|
320
|
+
id: 'report_stockholm',
|
|
321
|
+
data: { status: 'ready' },
|
|
322
|
+
readAt: snap.stamp,
|
|
323
|
+
onStale: 'reject',
|
|
324
|
+
wait: 'confirmed',
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Write checks:
|
|
329
|
+
|
|
330
|
+
- authorization
|
|
331
|
+
- stale state via `readAt`
|
|
332
|
+
- active claim conflicts
|
|
333
|
+
- idempotency when an idempotency key is provided
|
|
334
|
+
|
|
335
|
+
Use `commits.create(...)` only for low-level batch writes.
|
|
336
|
+
|
|
337
|
+
## Receipt
|
|
338
|
+
|
|
339
|
+
Schema model writes return the updated row. Passing `wait: 'confirmed'` waits for
|
|
340
|
+
the server confirmation before resolving. Lower-level model writes return a
|
|
341
|
+
receipt object for custom runtimes that need protocol-level proof.
|
|
342
|
+
|
|
343
|
+
## Interaction Model
|
|
344
|
+
|
|
345
|
+
The common agent run is:
|
|
346
|
+
|
|
347
|
+
1. Read the row through `ablo.<model>.retrieve({ id })` (or `list({ where })`).
|
|
348
|
+
2. Claim it: `await using claim = await ablo.<model>.claim({ id })` — waits if held, hands you `claim.data`.
|
|
349
|
+
3. Do the slow work from `claim.data`.
|
|
350
|
+
4. Write with `ablo.<model>.update({ id, data })`.
|
|
351
|
+
5. The claim auto-releases when it goes out of scope (`await using`).
|
|
352
|
+
|
|
353
|
+
The underlying primitives are Model, Claim, Commit, and Receipt. Most schema
|
|
354
|
+
users should not call `commits.create(...)` directly.
|
|
355
|
+
|
|
356
|
+
## Public HTTP Routes
|
|
357
|
+
|
|
358
|
+
- `GET /v1/models/{model}/{id}` — read state plus active claims
|
|
359
|
+
- `POST /v1/commits` — apply mutations
|
|
360
|
+
Use `Authorization: Bearer <api key>` for platform calls.
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,9 @@ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist
|
|
|
10
10
|
|
|
11
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.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Second: make sure a key exists. Check `ABLO_API_KEY` in the environment, then `.env.local`. If neither has one, 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` for you.
|
|
14
|
+
|
|
15
|
+
Then PUSH — this is the step everything depends on. The server keeps its OWN copy of the schema. Run `npx ablo dev --no-watch`: it pushes `ablo/schema.ts` (test mode) AND writes `ABLO_API_KEY` into `.env.local` from the stored login. Until the schema is pushed, EVERY write to a new or changed model fails with `server_execute_unknown_model`. Re-run it after schema changes (`npx ablo push` also works once the key is wired; bare `npx ablo dev` watches forever — don't, you have no TTY).
|
|
14
16
|
|
|
15
17
|
## Use this API
|
|
16
18
|
|
|
@@ -50,7 +52,7 @@ hosted schema push, and schema-version gating. Do not invent a parallel
|
|
|
50
52
|
string-keyed write path for rows that belong to a schema model.
|
|
51
53
|
|
|
52
54
|
For full integrations, use `integration-guide` as the canonical doc. It covers
|
|
53
|
-
the same model API across
|
|
55
|
+
the same model API across your own Data Source-backed app databases,
|
|
54
56
|
React selectors, multiplayer, and future agent workers.
|
|
55
57
|
|
|
56
58
|
Reads come in two flavors, and you pick by whether you can wait. `retrieve({ id })`
|
|
@@ -116,23 +118,25 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
|
|
|
116
118
|
|
|
117
119
|
Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
|
|
118
120
|
|
|
119
|
-
Every schema model
|
|
121
|
+
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.
|
|
120
122
|
|
|
121
123
|
```ts
|
|
122
124
|
// app/api/ablo/source/route.ts
|
|
123
125
|
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
124
|
-
import {
|
|
126
|
+
import { drizzleDataSource } from '@abloatai/ablo/source/drizzle';
|
|
125
127
|
import { schema } from '@/ablo/schema';
|
|
126
|
-
import {
|
|
128
|
+
import { db } from '@/db';
|
|
129
|
+
|
|
130
|
+
export const runtime = 'nodejs'; // the route touches your database
|
|
127
131
|
|
|
128
132
|
export const { POST } = dataSourceNext({
|
|
129
133
|
schema,
|
|
130
134
|
apiKey: process.env.ABLO_API_KEY!,
|
|
131
|
-
adapter:
|
|
135
|
+
adapter: drizzleDataSource(db, schema), // or prismaDataSource(prisma, schema) / kyselyDataSource(db, schema)
|
|
132
136
|
});
|
|
133
137
|
```
|
|
134
138
|
|
|
135
|
-
`npx ablo init`
|
|
139
|
+
`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.
|
|
136
140
|
|
|
137
141
|
## Sandboxes
|
|
138
142
|
|
|
@@ -163,6 +167,7 @@ Import from these public paths only:
|
|
|
163
167
|
- `@abloatai/ablo/source` — `dataSource`, the `DataSourceAdapter` spine, `prismaDataSource`. For a customer-canonical Data Source endpoint.
|
|
164
168
|
- `@abloatai/ablo/source/next` — `dataSourceNext` (Next.js App Router `{ POST }`).
|
|
165
169
|
- `@abloatai/ablo/source/drizzle` — `drizzleDataSource`.
|
|
170
|
+
- `@abloatai/ablo/source/kysely` — `kyselyDataSource`.
|
|
166
171
|
- `@abloatai/ablo/source/conformance` — `runDataSourceTests` to prove a custom adapter/handler.
|
|
167
172
|
|
|
168
173
|
Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subpaths. (`/source` IS public — it's the Data Source endpoint surface above.)
|
|
@@ -172,8 +177,8 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
|
|
|
172
177
|
`ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
|
|
173
178
|
|
|
174
179
|
- `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.
|
|
175
|
-
-
|
|
180
|
+
- Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo dev` writes `.env.local`).
|
|
176
181
|
- Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
|
|
177
|
-
- `npx ablo dev --no-watch` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode test|live` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
|
182
|
+
- `npx ablo dev --no-watch` pushes the schema (test mode) AND writes `ABLO_API_KEY` to `.env.local` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode test|live` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
|
178
183
|
|
|
179
184
|
Canonical docs to read before integrating: `quickstart`, `schema-contract`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.
|