@abloatai/ablo 0.41.0 → 0.43.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.
@@ -6,20 +6,17 @@ One command answers the question this page exists for — would a write succeed
6
6
  right now, and if not, why:
7
7
 
8
8
  ```bash
9
- ABLO_API_KEY=sk_live_… npx ablo status
9
+ ABLO_API_KEY=sk_… npx ablo status
10
10
  ```
11
11
 
12
12
  ```text
13
13
  ablo status
14
14
 
15
- key sk_live_51H8… (ABLO_API_KEY env — overrides stored)
16
- mode production
15
+ key sk_51H8… (ABLO_API_KEY env — overrides stored)
17
16
  org org_3nKq…
18
17
  project checkout (prj_7Yb2…)
19
- acts on production
20
- sandbox sk_test_9fJd· expires in 71d
21
- ● production — no key
22
- push production with sk_live_51H8… (env)
18
+ acts on production root
19
+ push production with sk_51H8(env)
23
20
  api https://api.abloatai.com reachable
24
21
  data ✓ database connected to this plane (direct)
25
22
  schema 4 models pushed (rev 12) hash 3f9a2c81 @ 2026-07-18
@@ -45,7 +42,7 @@ three pieces pointed at the same plane.
45
42
 
46
43
  | Ingredient | Who runs it | What "deploying" means for it |
47
44
  |---|---|---|
48
- | **Your Postgres** | You (or your provider) | Registering it against your production plane, once, with a production key. |
45
+ | **Your Postgres** | You (or your provider) | Registering it against the production root, once, with a root-bound key. |
49
46
  | **Ablo** | Hosted at `api.abloatai.com` | Nothing to run. You choose a project, a plane, and the keys that reach them. |
50
47
  | **Your app and agents** | You | Holding the right credential for the runtime, and pushing the schema in the deploy. |
51
48
 
@@ -62,10 +59,9 @@ per-plane, and knowing which three is most of what production readiness means:
62
59
  your dev database are separate registrations.
63
60
  - **The active schema artifact:** the model shapes the engine actually routes on.
64
61
 
65
- A key's plane is fixed at mint: `sk_live_` acts on production, while `sk_test_`
66
- is bound to one development branch. The immutable branch id,
67
- not a user-supplied slug, is the selector. This is why application code never
68
- passes an environment.
62
+ Every `sk_` is fixed to one branch at mint. The persisted branch id—not a
63
+ suffix or user-supplied slug—says whether that is the production root or one
64
+ development child. This is why application code never passes an environment.
69
65
 
70
66
  A child copies its parent's active schema when it is created, then owns its
71
67
  schema history. A child push never changes production. Production gets new
@@ -78,12 +74,12 @@ There is no shared development plane. New credentials are branch-bound; follow
78
74
 
79
75
  Your production database joins Ablo the same way your dev database did — logical
80
76
  replication so Ablo can read and confirm, a scoped writer role so Ablo can land
81
- rows — run once, with a production key so the registration attaches to the
82
- production plane:
77
+ rows — run once, with a root-bound key so the registration attaches to the
78
+ production root:
83
79
 
84
80
  ```bash
85
- ABLO_API_KEY=sk_live_… npx ablo connect apply --url postgres://admin:…@host:5432/db
86
- ABLO_API_KEY=sk_live_… npx ablo connect check
81
+ ABLO_API_KEY=sk_… npx ablo connect apply --url postgres://admin:…@host:5432/db
82
+ ABLO_API_KEY=sk_… npx ablo connect check
87
83
  ```
88
84
 
89
85
  [Connect Your Database](./data-sources.md) is the full walkthrough — the SQL, the
@@ -129,28 +125,41 @@ runs. In production that resolves to four rows:
129
125
 
130
126
  | Runtime | Credential | Notes |
131
127
  |---|---|---|
132
- | Server, worker, agent, cron | `sk_live_` in `ABLO_API_KEY` | Defaults from the environment, so most code passes nothing. |
133
- | Serverless function | `sk_live_` in `ABLO_API_KEY`, with `transport: 'http'` | Stateless request/response; nothing held open across invocations. |
134
- | Browser, read-only | `pk_live_` | Publishable and safe to ship, like a Stripe `pk_`. Reads only. |
128
+ | 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
+ | Browser, read-only | root-bound `pk_` | Publishable, safe to ship, and read-only. |
135
131
  | Browser, writing as the signed-in user | `authEndpoint` | A route on your backend mints a short-lived `ek_` per user. |
136
132
 
137
133
  [API Keys](./api-keys.md) covers the model; [Sessions](./sessions.md) covers
138
134
  minting. Two things bite specifically at deploy time.
139
135
 
140
- **The live key `ablo login` gives you cannot push schema.** It is a restricted,
141
- observe-only `rk_live_` by design, so a stolen CLI config cannot write to
142
- production. A production deploy needs a **secret** `sk_live_` from the dashboard,
143
- supplied as `ABLO_API_KEY`. You do not have to discover this from a failed
144
- deploy: `ablo login` and `ablo status` name what the key in hand does, and
145
- `ablo status --json` reports it as `effectiveKey.kind` for a pipeline to check
146
- before it pushes. The deploy itself uses an explicit dashboard `sk_live_` in
147
- `ABLO_API_KEY`; there is no local mode switch.
148
-
149
- **An explicit key always wins.** The CLI resolves `ABLO_API_KEY`, then
150
- `.env.local`, then `.env`, then the stored login — and `ablo status` prints which
151
- one it found under `key`, with its source. When a deploy lands somewhere
136
+ **`ablo login` does not give the application production authority.** It stores
137
+ a project-scoped `mk_` management credential that manages branches and exchanges
138
+ for temporary child credentials; it cannot read or write application data or
139
+ push the production schema. A production deploy needs a **secret** `sk_`
140
+ from the dashboard, supplied as `ABLO_API_KEY`. `ablo whoami` names what an
141
+ explicit key acts on, and `ablo status --json` reports its capability as
142
+ `runtimeKey.kind` for a pipeline to check before pushing. There is no local
143
+ mode switch.
144
+
145
+ **An explicit key always wins.** Application-facing commands resolve
146
+ `ABLO_API_KEY`, then `.env.local`, then `.env`, then a legacy stored runtime
147
+ credential never the `mk_` management login for data operations. `ablo status`
148
+ prints what it found under `key`, with its source. When a deploy lands somewhere
152
149
  surprising, that line is usually the answer.
153
150
 
151
+ Use `ablo whoami` when you need only the authoritative identity:
152
+
153
+ ```bash
154
+ ABLO_API_KEY=sk_… npx ablo whoami
155
+ ```
156
+
157
+ For a deployment secret already stored under another name, prefer
158
+ `ablo whoami --key-env <NAME>` over copying it into argv or permanently
159
+ maintaining parallel local variables. The command reads the explicitly named
160
+ value from the process or project env files and reports its project and branch
161
+ without printing the full secret.
162
+
154
163
  ## 3. Pushing the schema is a deploy step
155
164
 
156
165
  The server keeps its own copy of your schema and routes on that copy. Until it
@@ -159,7 +168,9 @@ has yours, a write to a new model fails with `server_execute_unknown_model` —
159
168
  depends on the new models goes live.
160
169
 
161
170
  ```bash
162
- ABLO_API_KEY=sk_live_… npx ablo push --yes
171
+ ABLO_API_KEY=sk_… npx ablo push --yes
172
+ # or, explicitly:
173
+ npx ablo push --env-file .env.production --yes
163
174
  ```
164
175
 
165
176
  Production requires confirmation: interactively you type the destination
@@ -210,8 +221,8 @@ deployed HTTPS endpoint is registered once, and Ablo returns the signing secret
210
221
  a single time:
211
222
 
212
223
  ```bash
213
- ABLO_API_KEY=sk_live_… npx ablo webhooks create https://yourapp.com/api/ablo/[...all]
214
- ABLO_API_KEY=sk_live_… npx ablo webhooks list # endpoints + delivery health
224
+ ABLO_API_KEY=sk_… npx ablo webhooks create https://yourapp.com/api/ablo/[...all]
225
+ ABLO_API_KEY=sk_… npx ablo webhooks list # endpoints + delivery health
215
226
  ```
216
227
 
217
228
  `webhooks list` reports each endpoint's status, cursor, and last error — the
@@ -220,9 +231,9 @@ handler, the Standard Webhooks signature, and rolling a secret.
220
231
 
221
232
  ## What to watch once it is live
222
233
 
223
- - **`ablo logs`:** commit activity as it happens, scoped by the key, so a live
224
- key streams the org and a test key streams only its sandbox. `--json` emits
225
- NDJSON for piping.
234
+ - **`ablo logs`:** commit activity as it happens, scoped by the key. A
235
+ root-bound key streams production; a child-bound key streams only that child.
236
+ `--json` emits NDJSON for piping.
226
237
  - **`ablo status`:** the readiness verdict. Cheap enough to run from a health
227
238
  check on your own side.
228
239
  - **The [audit log](./audit.md):** every confirmed write traced back to the key
@@ -244,17 +255,17 @@ and what each promises.
244
255
  | `server_execute_unknown_model` | The plane's active schema does not carry that model. | `ablo push` with a key for that plane. |
245
256
  | Clients rejected at connect | The deployed schema and the client's schema disagree. | Push this tree, or deploy the revision the server is running. |
246
257
  | `project_scope_denied` (403) | The model belongs to another project in your org. | Use a key minted for that project: a push cannot cross projects. |
247
- | 403 on `ablo push` | The key authenticated but cannot author schema. | A secret `sk_live_`; the `ablo login` live key is observe-only. |
258
+ | 403 on `ablo push` | The key authenticated but cannot author schema. | A secret `sk_`; the `mk_` stored by `ablo login` is management-only. |
248
259
 
249
260
  ## The checklist
250
261
 
251
262
  1. Production database registered against the production plane, direct host, and
252
263
  `ablo connect check` all green.
253
- 2. A secret `sk_live_` in the deploy environment as `ABLO_API_KEY` — never in a
264
+ 2. A secret `sk_` in the deploy environment as `ABLO_API_KEY` — never in a
254
265
  browser bundle.
255
266
  3. `ablo push --yes` in the pipeline, ahead of the code that needs the new models.
256
267
  4. `ablo status --json` gating the deploy on an empty `blockers` array.
257
- 5. Browser clients on a `pk_live_` or an `authEndpoint`, not a secret key.
268
+ 5. Browser clients on a root-bound `pk_` or an `authEndpoint`, not a secret key.
258
269
  6. Webhook endpoints registered at their deployed URLs, with the signing secret
259
270
  in your environment.
260
271
 
@@ -150,7 +150,7 @@ https://api.example.com/api/ablo/source
150
150
  Store the Ablo API key in the Python server:
151
151
 
152
152
  ```bash
153
- ABLO_API_KEY=sk_live_...
153
+ ABLO_API_KEY=sk_...
154
154
  ```
155
155
 
156
156
  Then expose one route that verifies the signed request and calls the existing
@@ -93,7 +93,7 @@ npx ablo connect apply --url postgres://admin:...@host:5432/db
93
93
 
94
94
  ```bash
95
95
  # Your app's environment holds only the API key — never a connection string.
96
- ABLO_API_KEY=sk_live_...
96
+ ABLO_API_KEY=sk_...
97
97
  ```
98
98
 
99
99
  ```ts
@@ -127,8 +127,9 @@ here is the current, correct reading:
127
127
  It is not the write path. You write **through** Ablo.
128
128
  - **"Ablo is out of the write path."** Not anymore. Ablo is the write chokepoint;
129
129
  every write flows through its coordination and then into your database.
130
- - **"Ablo holds my rows."** Only in the no-database-yet sandbox. Once you connect a
131
- database, your rows live in *your* Postgres — Ablo keeps just the log.
130
+ - **"Ablo holds my rows."** Only on a development branch where you have not
131
+ connected a database yet. Once you connect one, your rows live in *your*
132
+ Postgres — Ablo keeps just the log.
132
133
  - **"Ablo migrates my schema."** No. Ablo writes rows through a scoped role and runs
133
134
  no DDL; your migrations own the shape of your database.
134
135
 
@@ -62,7 +62,7 @@ use a new key.
62
62
 
63
63
  ## Failures are not replayed: they re-run
64
64
 
65
- This is where Ablo deliberately differs from Stripe and from most payment APIs, and it is the
65
+ This is where Ablo deliberately differs from most request/response APIs, and it is the
66
66
  behaviour most likely to surprise you.
67
67
 
68
68
  **Only successful writes are recorded.** A write that failed leaves no idempotency record, so
package/docs/identity.md CHANGED
@@ -318,7 +318,7 @@ session" glossed over. Concretely:
318
318
 
319
319
  1. **Your `ABLO_API_KEY` lives only on your trusted server**, scoped to your
320
320
  account. It signs your app's relationship with Ablo. It must never reach a
321
- browser bundle — treat it like a Stripe secret key.
321
+ browser bundle.
322
322
  2. **Your server authenticates the user with your own system.** That's the
323
323
  request that knows "this is user `U`, org `O`, teams `[...]`".
324
324
  3. **Your server hands that authenticated identity to Ablo**, and the browser
@@ -342,9 +342,8 @@ server, never by the browser.**
342
342
  > the client could name its own org or sync groups, any user could read another
343
343
  > tenant's data by editing a request. By keeping the API key server-side and
344
344
  > deriving scope from the identity your server already authenticated, the trust
345
- > boundary lands in the one place you control. This is the same reason
346
- > Liveblocks resolves scope in `prepareSession` and Stripe mints ephemeral keys
347
- > server-side.
345
+ > boundary lands in the one place you control. Scope resolution and session
346
+ > minting therefore stay server-side.
348
347
 
349
348
  ## Wiring the provider
350
349
 
@@ -59,7 +59,7 @@ confirms it over the WAL. Application and agent code hold only `ABLO_API_KEY`
59
59
  client never sees a connection string. [Connect Your Database](./data-sources.md)
60
60
  is the single source of truth for both paths.
61
61
 
62
- ## Test With Sandboxes
62
+ ## Try the public sandbox demo
63
63
 
64
64
  Use the public `/sandbox` page to understand the state flow. It is a visual,
65
65
  deterministic demo; it does not call your API key or mutate hosted Ablo data.
@@ -69,7 +69,7 @@ Codex and ask it to wire one real model through the schema model API.
69
69
  Use `npx ablo dev` for real integration work. It derives an immutable branch
70
70
  from Git, inherits the parent schema, and writes a temporary branch credential
71
71
  to `.env.local`. Each developer or pull request gets independent schema, rows,
72
- claims, and logs. Use an explicit `sk_live_*` root credential only in the
72
+ claims, and logs. Use an explicit `sk_*` root credential only in the
73
73
  reviewed production deployment.
74
74
 
75
75
  When handing this to a coding agent, give it a concrete target:
@@ -231,9 +231,9 @@ export async function POST() {
231
231
 
232
232
  ### Why two credential shapes
233
233
 
234
- `ABLO_API_KEY` is your long-lived account credential. Treat it like a
235
- Stripe secret key: it stays on trusted servers, never reaches a browser
236
- bundle, and signs server-to-server requests. It is the right credential
234
+ `ABLO_API_KEY` is your long-lived account credential. It stays on trusted
235
+ servers, never reaches a browser bundle, and signs server-to-server requests.
236
+ It is the right credential
237
237
  for trusted runtimes (Next.js server actions, background workers,
238
238
  migration scripts) where the code reading it is yours.
239
239
 
@@ -250,10 +250,8 @@ ABLO_API_KEY ─exchange─► scoped token ────────► narrow
250
250
  server only) revocable)
251
251
  ```
252
252
 
253
- This is the same shape as Stripe's
254
- ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
255
- AssumeRole (returns time-bounded creds with the minimal needed scope).
256
- You never type that token into your app; the SDK mints one when it needs one and
253
+ You never type that token into your app; the SDK mints a time-bounded,
254
+ minimally scoped token when it needs one and
257
255
  refreshes before expiry.
258
256
 
259
257
  ## 3. Read State
@@ -440,7 +438,7 @@ Ablo needs your Data Source endpoint and API key. Your app
440
438
  stores one Ablo credential:
441
439
 
442
440
  ```bash
443
- ABLO_API_KEY=sk_live_...
441
+ ABLO_API_KEY=sk_...
444
442
  ```
445
443
 
446
444
  The API key verifies Ablo's request. It is not a database credential.
@@ -3,8 +3,8 @@
3
3
  Maintainer scoping doc. Closes the one real day-one DX gap in Data Source
4
4
  mode: the `commit`/`load`/`list` legs are inbound webhooks (Ablo → your
5
5
  endpoint), so on `localhost` they need a tunnel (ngrok/cloudflared). This
6
- scopes a built-in reverse channel so Data Source works on localhost the way
7
- managed mode already does — the Stripe-CLI `stripe listen` pattern.
6
+ scopes a built-in reverse channel so Data Source works on localhost without a
7
+ public tunnel.
8
8
 
9
9
  ## The gap, precisely
10
10
 
@@ -26,10 +26,9 @@ managed mode "just works" locally and Data Source doesn't.
26
26
 
27
27
  ## Prior art
28
28
 
29
- - **Stripe CLI `stripe listen`:** the canonical fix. The CLI opens an
30
- *outbound* WebSocket to Stripe; Stripe drains webhook events down it and the
31
- CLI forwards them to `localhost`. No public URL, no tunnel. We want the same
32
- for the `commit`/`load`/`list` leg.
29
+ - **Outbound relay:** the CLI opens a WebSocket to the hosted service, receives
30
+ events over that connection, and forwards them to `localhost`. No public URL
31
+ or tunnel is required. We want the same for the `commit`/`load`/`list` leg.
33
32
  - **Our own `createPushQueue`:** already proves the outbound-from-customer
34
33
  pattern for the `events` leg. The reverse channel is the symmetric primitive
35
34
  for the other direction.
@@ -78,7 +77,7 @@ import { dataSource, createSourceConnector } from '@abloatai/ablo';
78
77
  import { sourceOptions } from './ablo.source'; // shared with route.ts
79
78
 
80
79
  const connector = createSourceConnector({
81
- apiKey: process.env.ABLO_API_KEY!, // sk_test_*
80
+ apiKey: process.env.ABLO_API_KEY!, // sk_* bound to a child branch
82
81
  handler: dataSource(sourceOptions), // the unchanged (Request)=>Response
83
82
  });
84
83
  await connector.run(abortSignal);
@@ -90,8 +89,8 @@ await connector.run(abortSignal);
90
89
  ### Server side (sync-server)
91
90
 
92
91
  - New WS endpoint `/v1/source/listen`. Auth: project API key → resolves the
93
- source. Reject if the key isn't `sk_test_*` unless the source explicitly
94
- opts into reverse-channel for production (see "Production" below).
92
+ source. Reject if the persisted key binding is the root branch unless the
93
+ source explicitly opts into a production reverse channel (see below).
95
94
  - Per-source request queue. When a `commit`/`load`/`list` needs the customer
96
95
  and a connector is attached, enqueue + drain down the socket instead of
97
96
  POSTing the webhook URL. Reuse the same signed-envelope shape so the
@@ -106,18 +105,16 @@ await connector.run(abortSignal);
106
105
  project key, so the connector verifies them through the existing
107
106
  `verifyAbloSourceRequest` with no special-casing. The transport changes; the
108
107
  trust model does not.
109
- - Gate to `sk_test_*` by default. The DB still stays canonical in the
110
- customer's process; nothing here gives Ablo the `DATABASE_URL`.
111
-
112
- ## Test-mode interplay
113
-
114
- `SourceRequestContext.mode` (`src/source/types.ts`) already distinguishes
115
- `test`/`live`. The reverse channel is the natural home for `mode: 'test'`
116
- traffic: a local connector attached with an `sk_test_*` key receives the
117
- source's test commits, runs them against the customer's test DB, and the SDK
118
- sees confirmed rows + fan-out exactly as in production. This is the missing
119
- piece that makes `sk_test_*` a complete local loop rather than just a data
120
- namespace.
108
+ - Gate to non-root branch bindings by default. The DB still stays canonical in
109
+ the customer's process; nothing here gives Ablo the `DATABASE_URL`.
110
+
111
+ ## Development-branch interplay
112
+
113
+ The reverse channel is the natural home for child-branch traffic: a local
114
+ connector attached with a child-bound `sk_*` receives that branch's commits,
115
+ runs them against the customer's development DB, and the SDK sees confirmed
116
+ rows and fan-out exactly as on the root. The server resolves child versus root
117
+ from the persisted branch binding, never from the key spelling.
121
118
 
122
119
  ## Production stance
123
120
 
package/docs/migration.md CHANGED
@@ -13,13 +13,14 @@ change when you upgrade.
13
13
 
14
14
  | Version | What changed | What to do |
15
15
  |---|---|---|
16
+ | **Next** | New keys no longer encode `live`/`test`; branch binding is authoritative | Accept the new `sk_…`/`rk_…`/`pk_…`/`ek_…` spellings. Do not route or authorize from a key prefix. Old `_live_`/`_test_` keys continue to authenticate; rotate them to mint the current form |
16
17
  | **0.36.0** | `ttlSeconds` deprecated on the join surfaces in favour of `ttl` | `useJoin({ scope, ttlSeconds: '5m' })` → `useJoin({ scope, ttl: '5m' })`; same for `ParticipantJoinOptions`. Both spellings work until 0.37.0 |
17
18
  | **0.35.0** | Synchronous reads moved under `local`, mirroring the async verbs | `get(id)` → `local.get(id)`; `getAll(options)` → `local.list(options)`; `getCount(options)` → `local.count(options)` |
18
19
  | **0.35.0** | `causedByTaskId` write option + seven `turn_*` error codes removed | Delete the `causedByTaskId` argument from writes; a branch on `turn_validation_failed` was unreachable and can go with it |
19
20
  | **0.34.0** | Presence verb renamed `watch` → `join` | `ablo.<model>.watch(ids)` → `ablo.<model>.join(ids)`; `useWatch` → `useJoin`; the `WatchOptions` / `UseWatchOptions` / `UseWatchReturn` types → `JoinOptions` / `UseJoinOptions` / `UseJoinReturn`; error code `model_watch_not_configured` → `model_join_not_configured` |
20
21
  | **0.28.0** | Removed React placeholders that had no working runtime | `usePresence` → `usePeers` or `useJoin`; `useClaim` → `ablo.<model>.claim`; `SyncGroupProvider` / `useSyncGroup` → `useJoin({ scope })` |
21
22
  | **0.11.0** | Historical `intent` → `claim` rename | The hook renamed in that release was later removed in 0.28.0. Current code uses `ablo.<model>.claim` or `useJoin` |
22
- | **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Update code that branches on the environment (e.g. source `mode`): `'test'`→`'sandbox'`, `'live'`→`'production'`. Key prefixes `sk_test_`/`sk_live_` are unchanged |
23
+ | **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Historical: update code that branches on the environment (`'test'`→`'sandbox'`, `'live'`→`'production'`). That release retained `sk_test_`/`sk_live_`; current releases mint mode-free, branch-bound keys |
23
24
  | **0.9.2** | `turn` primitive + agent-work `tasks` resource removed | Coordinate with `claim`; mint a scoped session instead of `agent().run()` |
24
25
  | **0.9.2** | `intents` deprecated in favor of `claim` | Use `ablo.<model>.claim`; `ablo.intents` is now `@internal` |
25
26
  | **0.9.0** | One options object per verb | `update(id, data, opts)` → `update({ id, data, ...opts })` |
@@ -32,6 +33,27 @@ change when you upgrade.
32
33
 
33
34
  ---
34
35
 
36
+ ## Next: key spelling follows capability, branch binding follows authority
37
+
38
+ New runtime credentials have one prefix per capability class:
39
+
40
+ ```diff
41
+ - sk_live_… / sk_test_…
42
+ + sk_…
43
+ ```
44
+
45
+ The same applies to `rk_`, `pk_`, and `ek_`. This does **not** merge branch
46
+ data or weaken production safety. Every key row is still immutably bound to one
47
+ project and one branch, and the server-confirmed `branchRoot` flag distinguishes
48
+ the protected production root from a development child.
49
+
50
+ Remove code that chooses an endpoint, environment, or confirmation policy by
51
+ matching `_live_` or `_test_`. Use the resolved identity (`ablo whoami` in the
52
+ CLI) instead. Existing suffixed credentials remain valid compatibility inputs,
53
+ so rollout does not require an immediate rotation.
54
+
55
+ ---
56
+
35
57
  ## 0.36.0: one lease is spelled `ttl`
36
58
 
37
59
  ```diff
@@ -187,14 +209,18 @@ on presence and claim state.
187
209
 
188
210
  ## 0.10.0: environment enum `sandbox` / `production`; stateless HTTP transport
189
211
 
212
+ > Historical migration note: current development uses immutable branches, not a
213
+ > shared sandbox selector. The enum and compatibility fields below remain
214
+ > relevant to integrations written against 0.10, but new applications let the
215
+ > branch-bound credential select the plane.
216
+
190
217
  ### Environment enum rename (the only breaking change)
191
218
 
192
- The canonical environment values are now **`production`** and **`sandbox`** (was
193
- `live` and `test`). This is a *vocabulary* change at the type/API layer — the
194
- on-the-wire key prefixes are **unchanged**: keys are still `sk_test_…` /
195
- `sk_live_…` and parse exactly as before. What changed is the enum you see in
196
- code: `Environment`, the source-handler `mode` field, and `ApiKeyEnv` now read
197
- `production` / `sandbox`.
219
+ In 0.10 the canonical environment values became **`production`** and
220
+ **`sandbox`** (from `live` and `test`). At that release this was only a
221
+ vocabulary change at the type/API layer: on-the-wire keys still used
222
+ `sk_test_…` / `sk_live_…`. Current releases accept those legacy spellings but
223
+ mint mode-free `sk_…` keys whose persisted branch binding is authoritative.
198
224
 
199
225
  You only need to act if your code branches on the environment value — most
200
226
  commonly a Data Source handler keyed on `mode`. The mapping is exactly
package/docs/projects.md CHANGED
@@ -4,20 +4,20 @@
4
4
 
5
5
  A **project** is the isolation unit inside your organization — the shape you
6
6
  know from Neon or Supabase. Each app you build gets its own project, and each
7
- project gets its own schema, its own sandbox/production data planes, and its
8
- own API keys. Two teams in one org can ship two apps that never see each
7
+ project gets its own root and child branches, schemas, data planes, and API
8
+ keys. Two teams in one org can ship two apps that never see each
9
9
  other's models, keys, or rows.
10
10
 
11
11
  ```text
12
12
  organization
13
13
  ├── project: default ← every org has one; pre-project apps live here
14
- │ ├── schema (sandbox + production artifacts)
15
- │ ├── data planes (registered databases)
16
- │ └── keys (sk_/rk_/ek_/pk_)
14
+ │ ├── production root
15
+ │ ├── development/preview branches
16
+ │ └── branch-bound schema, data plane, and credentials
17
17
  └── project: my-app ← npx ablo init creates this for a new app
18
- ├── schema
19
- ├── data planes
20
- └── keys
18
+ ├── production root
19
+ ├── development/preview branches
20
+ └── branch-bound schema, data plane, and credentials
21
21
  ```
22
22
 
23
23
  ## The default project
@@ -40,19 +40,23 @@ Schema pushes, database registrations, reads, and writes all act on the
40
40
 
41
41
  - `npx ablo push` activates the schema for the pushing key's project — it
42
42
  can never demote another project's schema.
43
- - Registering a database (`DATABASE_URL`) attaches it to the key's project
44
- and environment.
43
+ - Registering a database (`DATABASE_URL`) attaches it to the key's project and
44
+ immutable branch.
45
45
  - A write or read against a model that belongs to **another** project in
46
46
  your org fails with a typed `project_scope_denied` — never a silent empty
47
47
  result, and never the misleading "unknown model, run ablo push".
48
48
 
49
- ## Sandboxes belong to a project
49
+ ## Branches belong to a project
50
50
 
51
- Production is singular per project; sandboxes are many. Each sandbox of a
52
- project gets its **own data plane** (its own registered dev database) but
53
- they all share the project's **one** sandbox schema — pushing the same
54
- schema from a second sandbox doesn't create a second artifact, it just
55
- provisions that sandbox's database.
51
+ Production is the protected root branch; development and preview branches are
52
+ children. Each branch has its own rows, active schema artifact, claims, log,
53
+ credentials, and optional registered database. A child copies the parent's
54
+ active schema when created and owns its history afterward, so a feature-branch
55
+ push cannot change a sibling or production.
56
+
57
+ `ablo dev` derives a child from Git and mints an expiring `sk_` credential
58
+ bound to its immutable branch id. Production runtimes use `sk_` bound to
59
+ the root. There is no shared sandbox schema and no runtime environment switch.
56
60
 
57
61
  ## CLI
58
62
 
@@ -68,15 +72,16 @@ npx ablo projects use default # back to the org default
68
72
  npx ablo status # shows the active project
69
73
  ```
70
74
 
71
- The active project is a local targeting preference (stored next to `mode` in
72
- your CLI config): new keys you mint pick it up. It never changes what an
73
- existing key can reach — a key's project scope is decided server-side at
74
- mint.
75
+ The active project is a local targeting preference in your CLI config: new
76
+ management and branch credentials you mint pick it up. It never changes what
77
+ an existing key can reach — project and branch scope are decided server-side at
78
+ mint. Use `npx ablo whoami` to confirm the exact target of the active
79
+ credential.
75
80
 
76
81
  ## API
77
82
 
78
- Projects are a Stripe-shaped control-plane resource, authenticated with a
79
- secret (`sk_`) key:
83
+ Projects are a control-plane resource authenticated with a management (`mk_`)
84
+ key:
80
85
 
81
86
  ```bash
82
87
  curl https://api.abloatai.com/api/v1/projects \
@@ -11,7 +11,7 @@ which stays the system of record. Ablo writes rows but **runs no DDL and owns no
11
11
  schema** — your migration tool stays in charge of the shape of your database.
12
12
 
13
13
  > No database yet? Pass an `apiKey` only and Ablo keeps your rows in its own log,
14
- > so you can build the whole app today — like Stripe test mode. Point it at a
14
+ > so you can build the whole app today. Point it at a
15
15
  > separate or local Postgres for a development branch, or at your production
16
16
  > database when you're ready.
17
17
 
@@ -35,12 +35,18 @@ export ABLO_MANAGEMENT_KEY=mk_...
35
35
 
36
36
  Every runtime call needs a branch-bound API key. `ablo dev` exchanges the
37
37
  stored management credential for
38
- an expiring `sk_test_*` key bound to the current development branch.
39
- Production runtimes use `sk_live_*`. In production a key points at the database
38
+ an expiring `sk_*` key bound to the current development branch.
39
+ Production runtimes use `sk_*`. In production a key points at the database
40
40
  *you* own; on a development branch you can skip the database entirely and let
41
41
  Ablo host the rows (apiKey only). There is no keyless mode — a key is always
42
42
  required. (The public `/sandbox` page is a separate hosted demo, not your app.)
43
43
 
44
+ You do not create a separate named variable per development environment.
45
+ Application code always reads `ABLO_API_KEY`; `ablo dev` wires the value for the
46
+ current branch. Run `npx ablo whoami` whenever you want the server-confirmed
47
+ project and branch for that value. See [API Keys](./api-keys.md) for the full
48
+ project → branch → credential model.
49
+
44
50
  ## 2. Your Ablo schema (init scaffolded it)
45
51
 
46
52
  The schema is the contract — it generates `ablo.<model>` methods for app code,
@@ -149,7 +155,7 @@ Your **app** holds only the API key — never a connection string:
149
155
 
150
156
  ```bash
151
157
  # .env — server runtime only, never the browser
152
- ABLO_API_KEY=sk_test_...
158
+ ABLO_API_KEY=sk_...
153
159
  ```
154
160
 
155
161
  ```ts
package/docs/react.md CHANGED
@@ -21,8 +21,7 @@ import { useAblo } from '@abloatai/ablo/react';
21
21
  You build the Ablo client once — that's where the schema, the session endpoint,
22
22
  and connection config live — then hand it to the provider. The provider takes
23
23
  the already-built `client`; it no longer takes `schema`, `url`, `apiKey`, etc.
24
- as props. This mirrors Stripe's `<Elements stripe={stripePromise}>`: construct
25
- the thing, then pass it.
24
+ as props. Construct the client once, then pass that instance to the provider.
26
25
 
27
26
  ```ts
28
27
  // lib/ablo.ts
@@ -106,11 +106,11 @@ the fresh row. Reads stay open; only acting on the row serializes.
106
106
 
107
107
  Every schema model is backed by your own database, and you write to it through
108
108
  `ablo.<model>`. There are three start states, all covered in [Connect Your
109
- Database](./data-sources.md) (the single source of truth): the sandbox (`apiKey`
110
- only, no database — Ablo keeps your rows in its own log), `npx ablo connect` (a
111
- scoped writer role plus logical replication, so Ablo writes your rows and confirms
112
- them over the WAL), or a signed Data Source endpoint when your database can't grant
113
- replication.
109
+ Database](./data-sources.md) (the single source of truth): a development branch
110
+ with no database yet (`apiKey` only — Ablo keeps that branch's rows in its own
111
+ log), `npx ablo connect` (a scoped writer role plus logical replication, so Ablo
112
+ writes your rows and confirms them over the WAL), or a signed Data Source
113
+ endpoint when your database can't grant replication.
114
114
 
115
115
  Your database connects out of band, so the client holds only `ABLO_API_KEY` —
116
116
  never a connection string. Browser code goes through `<AbloProvider>` or a scoped
@@ -45,8 +45,8 @@ context, whether or not you map anything:
45
45
  | --- | --- |
46
46
  | `app.current_org_id` | The organization the credential acts for |
47
47
  | `app.current_project_id` | The project |
48
- | `app.current_environment` | The environment |
49
- | `app.current_sandbox_id` | The sandbox, when the write is in one |
48
+ | `app.current_environment` | The key's test/live trust class (`sandbox`/`production` on this compatibility surface) |
49
+ | `app.current_sandbox_id` | Legacy compatibility coordinate for older test planes; not a branch selector |
50
50
  | `app.current_participant_id` | The participant making the write |
51
51
  | `app.current_participant_kind` | Whether that participant is a person, an agent, or the system |
52
52
  | `app.current_user_id` | The person on whose behalf the write is made |
@@ -70,6 +70,11 @@ authenticated key and the plane:
70
70
  `orgId` · `projectId` · `environment` · `sandboxId` · `participantId` ·
71
71
  `participantKind`
72
72
 
73
+ `environment` and `sandboxId` retain their published names for compatibility
74
+ with existing RLS policies. Current development isolation comes from the
75
+ credential's immutable branch binding; callers do not choose a sandbox or
76
+ branch through session settings.
77
+
73
78
  Because none of them come from the caller, a mapping can forward the tenant
74
79
  identity Ablo already trusts, but cannot widen what a writer sees. A setting name
75
80
  takes exactly one source — the name is the key — so naming the same setting twice