@abloatai/ablo 0.59.2 → 0.61.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/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.61.0
4
+
5
+ ### Existing database connections repair in place
6
+
7
+ `ablo connect apply` is now the single repeatable operation for both a new
8
+ database connection and an existing registration. A healthy connection is a
9
+ no-op. When a registration predates current publication, replica-identity,
10
+ grant, or row-level-security requirements, the same command reconciles those
11
+ database-owned invariants through the transient owner URL while preserving
12
+ Ablo's working scoped passwords.
13
+
14
+ If that repair means an earlier initial snapshot may have omitted rows, the
15
+ operation requests the required fresh snapshot and subsequent reruns report its
16
+ loading or ready state. Automation can select `--json` for stable lifecycle and
17
+ per-step codes instead of parsing human output. Credential rotation remains an
18
+ explicit operation for an actually incomplete or invalid role pair.
19
+
20
+ ## 0.60.0
21
+
22
+ ### Sessions are the connection boundary for people and agents
23
+
24
+ `Sessions({ schema, apiKey })` is now the dedicated session issuer. Backends create scoped
25
+ agent sessions with `sessions.create({ agent, can, groups })` and expose browser
26
+ sessions with `sessions.handler({ authenticate, grant })`. Both return the same
27
+ short-lived session contract, and both are supplied to clients through
28
+ `Ablo({ schema, session })`:
29
+
30
+ ```ts
31
+ const workerAccess = {
32
+ records: ['read', 'update'],
33
+ } as const;
34
+
35
+ import Sessions from '@abloatai/ablo/sessions';
36
+
37
+ const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
38
+
39
+ const session = () =>
40
+ sessions.create({
41
+ agent: { id: stableWorkerId },
42
+ groups: [workspaceGroup],
43
+ can: workerAccess,
44
+ });
45
+
46
+ const agent = Ablo({ schema, session });
47
+ ```
48
+
49
+ Session clients default to one reconnecting WebSocket for commits, claims,
50
+ observation, presence, and collaboration. API-key clients remain HTTP by
51
+ default, and bounded session work can select `transport: 'http'` explicitly.
52
+ Model calls do not open additional sockets.
53
+
54
+ An async session provider represents one renewable logical identity. The client
55
+ caches each short-lived credential until it approaches `expiresAt`, pre-mints a
56
+ replacement, and reconnects with that replacement when necessary. Durable
57
+ observation resumes from its acknowledged cursor across socket replacement.
58
+ A provider resolving `null` means the application login ended and terminates the
59
+ session; a thrown error remains transient. A static session object cannot renew
60
+ itself and ends when its bearer expires. In-flight commits whose outcome became
61
+ ambiguous still reject and can be retried with their original idempotency key.
62
+
63
+ The browser client now names its session route as
64
+ `session: { endpoint: '/api/ablo-session' }`; `authEndpoint` is removed. Public
65
+ connection scope is `groups`; public `syncGroups` is removed. The overlapping
66
+ `agents.create`, `join`, and `useJoin` lifecycles are also removed: connection
67
+ groups define visibility, `usePeers` reads presence, and row claims own
68
+ exclusion.
69
+
70
+ Internally, session contract, creation, handler, source normalization, and
71
+ credential renewal now live beneath one `sessions` boundary. HTTP bootstrap and
72
+ the live socket consume the same normalized session access, so credential
73
+ identity and renewal policy cannot diverge.
74
+
75
+ Session issuance no longer occupies a property on `Ablo(...)`. That client owns
76
+ the schema model namespace, so an application model named `sessions` works as
77
+ `ablo.sessions` like any other model. Issuance and lifecycle administration stay
78
+ server-only behind the explicit `@abloatai/ablo/sessions` import.
79
+
3
80
  ## 0.59.2
4
81
 
5
82
  ### Patch Changes
package/README.md CHANGED
@@ -127,7 +127,7 @@ flattening the implementation into `packages/ablo`:
127
127
  - `packages/ablo` is the branded public facade. Its files mostly re-export the
128
128
  package that owns each API.
129
129
  - `packages/transaction` owns the shared model-operation contracts and the
130
- stateless HTTP implementation.
130
+ headless HTTP/WebSocket transport implementation.
131
131
  - `packages/humans` owns the reactive WebSocket/local/React implementation.
132
132
 
133
133
  That means searching only inside `packages/ablo/src` will not find the
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAYH,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAC5B,KAA2B,EAC3B,OAAO,GAAiC,EAAE;IAE1C,MAAM,EAAE,OAAO,EAAE,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CACjC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAC5B,QAAQ,EACR,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EACjE,CAAC,CACF,CAAC;IACF,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,0DAA0D,OAAO,EAAE;KAC7E,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAYH,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAC5B,KAA2B,EAC3B,UAAwC,EAAE;IAE1C,MAAM,EAAE,OAAO,EAAE,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CACjC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAC5B,QAAQ,EACR,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EACjE,CAAC,CACF,CAAC;IACF,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,0DAA0D,OAAO,EAAE;KAC7E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { Sessions, Sessions as default } from '@abloatai/transaction/sessions';
2
+ export type { AbloSession, CreateAgentSessionParams, CreateSessionParams, CreateUserSessionParams, RevokeSessionParams, RotateSessionParams, SessionCredential, SessionEndpoint, SessionHandler, SessionHandlerOptions, SessionProvider, SessionProviderResult, SessionRevocation, SessionRotation, SessionScope, SessionSource, SessionsClient, SessionsOptions, } from '@abloatai/transaction/sessions';
3
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC/E,YAAY,EACV,WAAW,EACX,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Sessions, Sessions as default } from '@abloatai/transaction/sessions';
2
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO,EAAE,MAAM,gCAAgC,CAAC"}
@@ -75,11 +75,12 @@ await ablo.ready();
75
75
  ```
76
76
 
77
77
  The secret `apiKey` is server-only. Browser clients must not receive it; live UIs
78
- use the default WebSocket transport with a minted user/session token.
78
+ use the reactive client with `session.endpoint`, which owns their WebSocket and
79
+ short-lived user-session renewal.
79
80
 
80
81
  If your backend mints restricted agent tokens, register the database once from a
81
82
  secret-key server process as above. Workers using the restricted token can then
82
- construct `Ablo({ schema, authToken, transport: "http" })` because the project
83
+ construct `Ablo({ schema, session, transport: "http" })` because the project
83
84
  already has a registered data plane.
84
85
 
85
86
  ## Link a message to a claim
package/docs/agents.md CHANGED
@@ -22,21 +22,33 @@ These are observational reads. Use `read({ id })` only when a later Ablo write
22
22
  depends on that exact version and will pass it through `reads`.
23
23
 
24
24
  Most agents wake on a trigger, read what they need, write a result, and go idle.
25
- That is a request/response workload, so they use plain HTTP. A resident agent
26
- that needs pushed deltas, queued-claim grants, or presence selects
27
- `transport: 'websocket'`. The credential is the identity on both carriers; the
28
- server resolves the org, scope, and actor from it.
29
-
30
- Short-lived agents use HTTP. Long-running agents may add a multiplexed
31
- WebSocket without installing the human materializer. People add the `humans()`
32
- plugin for a local reactive graph. All three operate on the same typed,
25
+ Trusted service work uses an API key and plain HTTP. An agent that needs its own
26
+ scoped identity receives a session; session clients use one reconnecting
27
+ WebSocket by default. The server resolves the org, scope, and actor from the
28
+ credential on both carriers.
29
+
30
+ Create scoped credentials with the server-only issuer:
31
+
32
+ ```ts
33
+ import Sessions from '@abloatai/ablo/sessions';
34
+
35
+ const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
36
+ ```
37
+
38
+ API-key services use HTTP. Session agents use a multiplexed WebSocket without
39
+ installing the human materializer. People add the `humans()` plugin for a local reactive graph. All three operate on the same typed,
33
40
  coordinated state and enter the same server-side commit and claim paths.
34
41
 
35
42
  ```ts
43
+ const session = () => sessions.create({
44
+ agent: { id: stableWorkerId },
45
+ can: workerAccess,
46
+ groups: [workspaceGroup],
47
+ });
48
+
36
49
  const ablo = Ablo({
37
50
  schema,
38
- apiKey: process.env.ABLO_API_KEY,
39
- transport: 'websocket',
51
+ session,
40
52
  cursorStore,
41
53
  });
42
54
 
@@ -73,14 +85,14 @@ authenticates; the [schema](/installation) defines what you can call.
73
85
 
74
86
  ## The agent client
75
87
 
76
- Same `Ablo()` entry point as everywhere else pass `transport: 'http'`. No
77
- socket, no connection state just your schema (for types) and an API key.
88
+ Same `Ablo()` entry point as everywhere else. An API-key client is HTTP: no
89
+ socket or connection state, just your schema and service credential.
78
90
 
79
91
  ```ts
80
92
  import Ablo from "@abloatai/ablo";
81
93
  import { schema } from "./schema";
82
94
 
83
- const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http" });
95
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
84
96
 
85
97
  // Reads + writes, fully typed off your schema.
86
98
  // `get` resolves to the row, or `undefined` when none matches.
@@ -99,12 +111,12 @@ and `claim`. It does **not** expose stateful-only `local` reads or model
99
111
  selected WebSocket transport, or holds one POST/SSE response until the context
100
112
  changes on the HTTP transport.
101
113
 
102
- ## Managed scoped agents
114
+ ## Scoped agent sessions
103
115
 
104
- When this process owns the secret client and also runs the agent, prefer
105
- `agents.create`. It mints the restricted credential, returns a schema-typed
106
- client, and renews that credential for a long run. `sessions.create({ agent })`
107
- is the raw-token path for handing identity to another runtime.
116
+ `Sessions(...).create({ agent })` is the one issuance path. It returns a restricted
117
+ credential; construct the schema-typed client in the runtime that executes the
118
+ agent. This keeps session minting, revocation, and rotation under one resource
119
+ for both people and agents.
108
120
 
109
121
  Derive identity and groups from the run row or trusted job payload—not from
110
122
  model output or an HTTP request body. A serverless handler normally creates and
@@ -114,12 +126,14 @@ disposes one child per invocation:
114
126
  const run = await control.runs.read({ id: verifiedRunId });
115
127
  if (!run) throw new Error('run not found');
116
128
 
117
- const agent = await control.agents.create({
118
- id: `run:${run.id}`,
119
- name: 'run-worker',
129
+ const session = await sessions.create({
130
+ agent: { id: `run:${run.id}` },
120
131
  can: { records: ['read', 'update'] },
121
- syncGroups: [`workspace:${run.workspaceId}`],
132
+ groups: [`workspace:${run.workspaceId}`],
133
+ userMeta: { name: 'run-worker' },
122
134
  });
135
+
136
+ const agent = Ablo({ schema, session, transport: 'http' });
123
137
  try {
124
138
  await executeRun(agent, run);
125
139
  } finally {
@@ -128,29 +142,32 @@ try {
128
142
  ```
129
143
 
130
144
  Use a stable id only when one logical run is serialized; two concurrent workers
131
- that share an id appear as the same participant. For independent concurrent
132
- work, omit `id` and let Ablo create distinct identities.
145
+ that share an id appear as the same participant. Generate a different id for
146
+ each independent concurrent run.
133
147
 
134
- A long-running worker may cache one managed client per stable scope, but the
135
- cache owns lifecycle: evict idle clients, call `dispose()` on eviction, and
136
- dispose every client during graceful shutdown. Never cache a client and later
137
- reuse it for a different workspace or capability set.
148
+ A long-running worker may cache one client per stable scope and provide an
149
+ async credential resolver that re-mints the same session identity. The cache
150
+ owns lifecycle: evict idle clients, call `dispose()` on eviction, and dispose
151
+ every client during graceful shutdown. Never reuse a client for a different
152
+ workspace or capability set.
138
153
 
139
154
  ```ts
140
155
  const agents: Record<
141
156
  string,
142
- Awaited<ReturnType<typeof control.agents.create>> | undefined
157
+ ReturnType<typeof Ablo> | undefined
143
158
  > = {};
144
159
 
145
160
  async function agentFor(run: Run) {
146
161
  const key = `${run.workspaceId}:${run.workerSlot}`;
147
162
  const cached = agents[key];
148
163
  if (cached) return cached;
149
- const created = await control.agents.create({
150
- id: `worker:${key}`,
164
+ const agentId = `worker:${key}`;
165
+ const session = () => sessions.create({
166
+ agent: { id: agentId },
151
167
  can: { records: ['read', 'update'] },
152
- syncGroups: [`workspace:${run.workspaceId}`],
168
+ groups: [`workspace:${run.workspaceId}`],
153
169
  });
170
+ const created = Ablo({ schema, session });
154
171
  agents[key] = created;
155
172
  return created;
156
173
  }
package/docs/api-keys.md CHANGED
@@ -28,7 +28,7 @@ and remap them before each command.
28
28
  | Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; headless CI supplies an `mk_` credential through `ABLO_API_KEY`. |
29
29
  | Run the production backend | `sk_` bound to the production root | Store it as the deployment's `ABLO_API_KEY`. |
30
30
  | Read in a browser | `pk_` | Publishable, read-only key. |
31
- | Write in a browser as a user | short-lived `ek_` | Your backend exposes `authEndpoint` and mints it. |
31
+ | Write in a browser as a user | short-lived `ek_` | Your backend exposes a session endpoint and mints it. |
32
32
 
33
33
  The everyday loop is therefore:
34
34
 
@@ -71,16 +71,18 @@ already carries the target.
71
71
 
72
72
  ## Which credential to pass to the SDK
73
73
 
74
- There's **one field `apiKey`** and what you pass depends on **where the code runs**.
75
- Pick your row:
74
+ There are two ordinary identity inputs: `apiKey` for a key the process owns and
75
+ `session` for a scoped actor. Pick your row:
76
76
 
77
77
  | Where your code runs | What to pass | Example |
78
78
  |---|---|---|
79
79
  | **Server / worker / agent** (can hold a secret) | your secret `sk_`: it defaults to `ABLO_API_KEY`, so usually pass **nothing** | `Ablo({ schema })` |
80
80
  | **Browser: read-only** | a publishable `pk_` (safe to ship) | `Ablo({ schema, apiKey: process.env.NEXT_PUBLIC_ABLO_PUBLISHABLE_KEY })` |
81
- | **Browser: writing as the signed-in user** | `authEndpoint`: the route on your own backend that mints a short-lived per-user token | `Ablo({ schema, authEndpoint: '/api/ablo-session' })` |
81
+ | **Browser: writing as the signed-in user** | `session.endpoint`: the route on your own backend that mints a short-lived per-user token | `Ablo({ schema, session: { endpoint: '/api/ablo-session' } })` |
82
82
 
83
- That's the whole story: one knob, filled by audience.
83
+ The names follow ownership: a process owns an API key; an actor runs through a
84
+ session, regardless of whether that session is already minted, renewable, or
85
+ fetched from a browser endpoint.
84
86
 
85
87
  The `mk_` credential created by `ablo login` is different: it is a CLI
86
88
  control-plane credential, not an application API key. It can manage projects
@@ -104,24 +106,28 @@ For an `ek_`, the server mints and the client holds the short-lived result.
104
106
  public `pk_` is **read-only** — it can't carry one specific user's write authority. So when
105
107
  the browser writes *as the logged-in user*, your backend (which holds the secret `sk_` and
106
108
  knows who's signed in) mints a short-lived per-user token with `sessions.create({ user, can })`,
107
- and the browser's `apiKey` function fetches it. You don't manage refresh — the SDK calls the
109
+ and the browser's `session.endpoint` fetches it. You don't manage refresh — the SDK calls the
108
110
  function once before connecting and then keeps the token fresh (re-mint before expiry, and on
109
111
  tab-focus / network-online / device-wake). For a read-only app you don't need
110
112
  any of this — just the `pk_` above.
111
113
 
112
114
  Server-side, because `apiKey` defaults to `process.env.ABLO_API_KEY`, most backend and agent
113
115
  code passes nothing. The secret `sk_` is **server-only** — never in a
114
- browser bundle. There is no `getToken` or `as` option `apiKey` (the key a server holds)
115
- and `authEndpoint` (the mint route a browser points at) are the two credential
116
- knobs, and you set exactly one.
116
+ browser bundle. There is no `getToken`, `as`, or separate auth-endpoint option:
117
+ `apiKey` is the key a process owns, while `session` is a minted resource, a
118
+ renewal provider, or `{ endpoint }`. Set exactly one identity input.
117
119
 
118
120
  ### Minting per-user / agent tokens (server-side, with your `sk_`)
119
121
 
122
+ Construct the dedicated issuer with
123
+ `Sessions({ schema, apiKey: process.env.ABLO_API_KEY })` from
124
+ `@abloatai/ablo/sessions`. It is server-only and does not create a participant
125
+ connection.
126
+
120
127
  | Mint | Call | Result |
121
128
  |---|---|---|
122
- | Human end-user session | `await server.sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
123
- | Ready agent client | `await server.agents.create({ can: { records: ['update'] } })` | Auto-refreshing client scoped to `can` |
124
- | Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: { records: ['update'] } })` | `rk_` for another runtime |
129
+ | Human end-user session | `await sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
130
+ | Agent session | `await sessions.create({ agent: { id }, can: { records: ['update'] } })` | Scoped `rk_` for the agent runtime |
125
131
 
126
132
  The principal kind comes from *which* shape you pass — `{ user, can }` → `user`, `{ agent, can }` → `agent`.
127
133
 
package/docs/cli.md CHANGED
@@ -136,6 +136,7 @@ branch-bound runtime credential before starting application code.
136
136
  | `ablo migrate` | **Direct Postgres**: provision just the synced models (plus the adapter's `ablo_outbox` / `ablo_idempotency`) in your own `DATABASE_URL`. Leaves your other tables alone. | `--dry-run`, `--output <file>`, `--schema`, `--export` |
137
137
  | `ablo pull` | **Direct Postgres**: generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
138
138
  | `ablo check` | **Direct Postgres**: verify your _existing_ tables fit the schema (read-only, no schema changes). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
139
+ | `ablo connect plan\|apply\|check\|rotate\|deregister` | **Direct Postgres**: register and maintain a database plane. Here `--schema` means the existing PostgreSQL namespace, never a TypeScript file. | `--url <postgres-url>`, `--schema <postgres-schema>`, `--env-file <path>`, `--yes` |
139
140
  | `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
140
141
  | `ablo docs` | Read these pages for the version you installed: offline, no network (see [`ablo docs`](#ablo-docs)). | `--json` |
141
142
 
@@ -28,10 +28,12 @@ const ablo = Ablo({
28
28
  });
29
29
  ```
30
30
 
31
- The package-root export is the stateless HTTP client for agents, workers, route
32
- handlers, and other server operations. See [Options](./options.md) for its exact
33
- constructor reference. Live state and local reads are added through the
34
- [React client](./react.md).
31
+ The package-root export is the headless coordination client for agents, workers,
32
+ route handlers, and other server operations. Trusted API-key clients use HTTP.
33
+ Scoped session clients use one reconnecting WebSocket for commits and live
34
+ coordination; point reads and administration remain HTTP. See [Transports](./transports.md)
35
+ for the lifecycle and [Options](./options.md) for the constructor. A human-facing
36
+ local graph is added through the [React client](./react.md).
35
37
 
36
38
  Your database connects out of band — through logical replication (`npx ablo
37
39
  connect`), or the signed [Data Source](./data-sources.md) endpoint as the
@@ -206,11 +206,15 @@ same credential represent the same participant and do not exclude one another.
206
206
  Mint a distinct scoped session for each independently coordinated agent:
207
207
 
208
208
  ```ts
209
- const { token } = await server.sessions.create({
209
+ import Sessions from '@abloatai/ablo/sessions';
210
+
211
+ const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
212
+ const session = await sessions.create({
210
213
  agent: { id: `forecast-agent-${workerId}` },
214
+ can: { records: ['read', 'update'] },
211
215
  });
212
216
 
213
- const agent = Ablo({ schema, apiKey: token });
217
+ const agent = Ablo({ schema, session });
214
218
  ```
215
219
 
216
220
  Functional updates do not require distinct participant identities because they
@@ -305,7 +309,6 @@ The main methods are:
305
309
  | `claim.state({ id })` | Read the current holder without blocking. |
306
310
  | `claim.queue({ id })` | Read the current wait order. |
307
311
  | `claim.release({ id })` | Release early when you do not hold a handle. |
308
- | `join({ scope })` | Observe presence for a broader scope. |
309
312
 
310
313
  This page owns row-backed claims: `model.claim({ id })` reads and claims an Ablo
311
314
  model row, and the handle carries fresh data. Identifier-only claims before an
@@ -47,27 +47,16 @@ export const schema = defineSchema(
47
47
  ```ts
48
48
  // 2. app/api/ablo-session/route.ts — mint for one customer, on your backend.
49
49
  import { syncGroup } from '@abloatai/ablo/schema';
50
- import { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
51
- import { ablo } from '@/ablo/server';
50
+ import { sessions } from '@/ablo/sessions';
52
51
 
53
- export async function POST() {
54
- const member = await requireSignedInMember();
55
-
56
- const session = await ablo.sessions.create({
52
+ export const POST = sessions.handler({
53
+ authenticate: () => currentSignedInMember(),
54
+ grant: ({ principal: member }) => ({
57
55
  user: { id: member.userId },
58
56
  can: { customers: ['read'], decks: ['read', 'create', 'update'] },
59
- syncGroups: [syncGroup('customer', member.customerId)],
60
- });
61
-
62
- return Response.json(
63
- credentialEndpointSuccessSchema.parse({
64
- token: session.token,
65
- expiresAt: session.expiresAt,
66
- credentialKind: 'ephemeral',
67
- }),
68
- { headers: { 'Cache-Control': 'no-store' } },
69
- );
70
- }
57
+ groups: [syncGroup('customer', member.customerId)],
58
+ }),
59
+ });
71
60
  ```
72
61
 
73
62
  That is the whole integration. The rest of this page is why each line is where
@@ -130,7 +119,7 @@ kind is the one you declared in `groups.root`, and the id is your own
130
119
  identifier for the customer.
131
120
 
132
121
  ```ts
133
- syncGroups: [syncGroup('customer', member.customerId)]
122
+ groups: [syncGroup('customer', member.customerId)]
134
123
  ```
135
124
 
136
125
  Resolve `member.customerId` from the membership you just authenticated on the
@@ -132,6 +132,27 @@ npx ablo connect apply --env-file .env.local --yes
132
132
  The explicit flag makes the credential choice visible and loads both the
133
133
  branch-bound key and database URL. Shell environment variables take precedence.
134
134
 
135
+ ### Two schema flags, two ownership boundaries
136
+
137
+ `migrate` and `push` read your Ablo TypeScript contract. `connect apply` also
138
+ reads it to derive the mapped Postgres tables; `--tables` is an explicit
139
+ override:
140
+
141
+ ```bash
142
+ # TypeScript contract and export; app-schema selects its Postgres namespace.
143
+ npx ablo migrate --schema ablo/schema.ts --export schema --app-schema public
144
+
145
+ # Existing Postgres namespace only. Never pass ablo/schema.ts here.
146
+ npx ablo connect apply --schema public --yes
147
+
148
+ # Activate the TypeScript contract after the database is connected.
149
+ npx ablo push --schema ablo/schema.ts --export schema
150
+ ```
151
+
152
+ If the database uses its default namespace, omit `connect --schema`; it defaults
153
+ to `public`. Keep the three operations in this order in deployment automation:
154
+ expand tables, connect the database, then activate the hosted schema.
155
+
135
156
  ### One database, several projects
136
157
 
137
158
  Provider database URLs and Postgres schemas solve different isolation jobs:
@@ -190,13 +211,18 @@ npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
190
211
  ```
191
212
 
192
213
  Pass an admin connection string with `--url` and select the application namespace
193
- with `--schema` (default `public`). It creates a per-binding publication, two
194
- per-binding scoped roles, and the grants, turns on logical decoding where it can, registers
195
- both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
214
+ with `--schema` (default `public`). It inspects and reconciles a per-binding
215
+ publication, two per-binding scoped roles, grants, replica identity,
216
+ registration, and the initial snapshot. On an existing healthy registration it
217
+ is a no-op and preserves both passwords. A policy-only repair also preserves
218
+ them; credential rotation happens only through the explicit `connect rotate`
219
+ operation.
220
+
196
221
  The admin credential is used on this machine only and never persisted — nothing is
197
- written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass `--show-sql`
198
- to see every statement first, or drop `--apply` to print the SQL and run it
199
- yourself. Rotate the scoped passwords any time with `ablo connect rotate`.
222
+ written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass
223
+ `--show-sql` to see every statement first, `--json` for stable result and step
224
+ codes, or drop `--apply` to print the SQL and run it yourself. Rotate the scoped
225
+ passwords any time with `ablo connect rotate`.
200
226
 
201
227
  The rest of this page is what that command sets up, step by step, for when you want
202
228
  to run it by hand or review exactly what changes.
@@ -215,19 +241,20 @@ the row to be visible already, and touching application rows is neither necessar
215
241
  nor a safe bootstrap mechanism.
216
242
 
217
243
  If a connection was snapshotted with an older replication role whose row-level
218
- security hid historical rows, repair that role and request the load again without
219
- deregistering or rotating credentials:
244
+ security hid historical rows, rerun the same operation. It repairs the owned
245
+ policy and requests the necessary fresh load without deregistering or rotating
246
+ credentials:
220
247
 
221
248
  ```bash
222
- npx ablo connect rotate # reasserts BYPASSRLS and safely re-registers both roles
223
- npx ablo connect resnapshot # recreates only the slot; the load is asynchronous
224
- npx ablo connect check # repeat until the existing-row load is complete
249
+ npx ablo connect apply --url postgres://owner:...@host/db --yes --json
250
+ # Rerun the same command: loading becomes ready; a healthy rerun is a no-op.
225
251
  ```
226
252
 
227
- Use the same `resnapshot` step after adding an existing populated table to the
228
- publication. Following its future WAL changes is not enough to load rows written
229
- before publication membership; the snapshot coverage guard therefore refuses to
230
- record completion when even one mapped table is absent. Relation matching is
253
+ Rerun `connect apply` after adding an existing populated table to the contract.
254
+ Following its future WAL changes is not enough to load rows written before
255
+ publication membership, so reconciliation updates membership and requests the
256
+ fresh snapshot together. The snapshot coverage guard refuses to record
257
+ completion when even one mapped table is absent. Relation matching is
231
258
  schema-qualified using the DataSource's configured `schema` (default `public`):
232
259
  an identically named table in another Postgres schema neither counts as coverage
233
260
  nor enters the snapshot or WAL stream for your model.
@@ -120,16 +120,17 @@ is genuinely unavailable.
120
120
 
121
121
  ## 2. The credential each runtime holds
122
122
 
123
- There is one field, `apiKey`, and what goes in it follows from where the code
124
- runs. In production that resolves to four rows:
123
+ Credential configuration follows the runtime. Long-lived root keys use
124
+ `apiKey`; scoped actors use `session`; browser login exchange uses
125
+ `session.endpoint`:
125
126
 
126
127
  | Runtime | Credential | Notes |
127
128
  |---|---|---|
128
129
  | Server, worker, agent, cron | `sk_` in `ABLO_API_KEY` | Defaults from the environment, so most code passes nothing. |
129
- | Serverless function | `sk_` in `ABLO_API_KEY`, with `transport: 'http'` | Stateless request/response; nothing held open across invocations. |
130
- | Resident agent | restricted `rk_` or agent-scoped credential, with `transport: 'websocket'` | One multiplexed WebSocket per client and Ablo cell; checkpoint durable deltas before acknowledging. |
130
+ | Serverless function | `sk_` in `ABLO_API_KEY` | Stateless request/response; nothing held open across invocations. |
131
+ | Long-running agent | `session: () => sessions.create(...)` | One renewable identity and one multiplexed WebSocket per client and Ablo cell; checkpoint durable deltas before acknowledging. |
131
132
  | Browser, read-only | root-bound `pk_` | Publishable, safe to ship, and read-only. |
132
- | Browser, writing as the signed-in user | `authEndpoint` | A route on your backend mints a short-lived `ek_` per user. |
133
+ | Browser, writing as the signed-in user | `session: { endpoint }` | A route on your backend mints a short-lived `ek_` per user. |
133
134
 
134
135
  [API Keys](./api-keys.md) covers the model; [Sessions](./sessions.md) covers
135
136
  minting. Two things bite specifically at deploy time.
@@ -284,7 +285,7 @@ and what each promises.
284
285
  browser bundle.
285
286
  3. `ablo plan` reviewed, followed by fingerprint-gated `ablo push --yes`.
286
287
  4. `ablo status --json` gating the deploy on an empty `blockers` array.
287
- 5. Browser clients on a root-bound `pk_` or an `authEndpoint`, not a secret key.
288
+ 5. Browser clients on a root-bound `pk_` or `session.endpoint`, not a secret key.
288
289
  6. Webhook endpoints registered at their deployed URLs, with the signing secret
289
290
  in your environment.
290
291
 
@@ -48,12 +48,18 @@ export const schema = defineSchema({
48
48
  ```ts
49
49
  // web/ablo.ts — SERVER-ONLY client (holds the sk_ key; never imported in the browser).
50
50
  import Ablo from '@abloatai/ablo';
51
+ import Sessions from '@abloatai/ablo/sessions';
51
52
  import { schema } from './ablo/schema';
52
53
 
53
54
  export const ablo = Ablo({
54
55
  schema,
55
56
  apiKey: process.env.ABLO_API_KEY,
56
57
  });
58
+
59
+ export const sessions = Sessions({
60
+ schema,
61
+ apiKey: process.env.ABLO_API_KEY,
62
+ });
57
63
  ```
58
64
 
59
65
  Mount the React provider near the app root. Build the browser client first —
@@ -69,11 +75,11 @@ import { Ablo } from '@abloatai/ablo/react';
69
75
  import { AbloProvider } from '@abloatai/ablo/react';
70
76
  import { schema } from '@/ablo/schema';
71
77
 
72
- // Browser client: no secret key — `authEndpoint` points at the session route
78
+ // Browser client: no secret key — `session.endpoint` points at the session route
73
79
  // your server exposes (below); the SDK fetches and refreshes the token.
74
80
  const ablo = Ablo({
75
81
  schema,
76
- authEndpoint: '/api/ablo-session',
82
+ session: { endpoint: '/api/ablo-session' },
77
83
  });
78
84
 
79
85
  export function Providers({ children }: { children: React.ReactNode }) {
@@ -86,26 +92,17 @@ browser only ever sees the short-lived token:
86
92
 
87
93
  ```ts
88
94
  // web/app/api/ablo-session/route.ts
89
- import { ablo } from '@/ablo';
90
- import { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
95
+ import { sessions } from '@/ablo';
91
96
 
92
97
  export const runtime = 'nodejs';
93
98
 
94
- export async function POST() {
95
- const userId = await currentUserId(); // your auth
96
- const { token, expiresAt } = await ablo.sessions.create({
97
- user: { id: userId },
99
+ export const POST = sessions.handler({
100
+ authenticate: () => currentUser(), // your auth; null when signed out
101
+ grant: ({ principal: user }) => ({
102
+ user: { id: user.id },
98
103
  can: { records: ['read', 'update'] },
99
- });
100
- return Response.json(
101
- credentialEndpointSuccessSchema.parse({
102
- token,
103
- expiresAt,
104
- credentialKind: 'ephemeral',
105
- }),
106
- { headers: { 'Cache-Control': 'no-store' } },
107
- );
108
- }
104
+ }),
105
+ });
109
106
  ```
110
107
 
111
108
  ## 2. Add Live Reads In The UI