@abloatai/ablo 0.64.1 → 0.64.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 CHANGED
@@ -4,6 +4,12 @@ Ablo lets AI agents and humans safely edit the same typed data without clobberin
4
4
 
5
5
  Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
6
6
 
7
+ Groups structure which shared state reaches each person or agent. Declare the
8
+ data's scope and participant authority, then let authorized subscriptions keep
9
+ reactive views current; HTTP agents explicitly read or observe changes. Start
10
+ with [Groups and shared context](./docs/groups.md) to connect membership, access,
11
+ loading and updates.
12
+
7
13
  ## Start here — scaffold with `ablo init`
8
14
 
9
15
  Before choosing among identifier claims, row claims, captured reads, atomic
package/CHANGELOG.md CHANGED
@@ -1,14 +1,120 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.3
4
+
5
+ ### Existing connections make room for new tables
6
+
7
+ `ablo connect apply --tables` now checks the requested tables even when an
8
+ existing connection reports ready. It reconciles publication membership before
9
+ returning, keeping existing credentials intact. If a snapshot needs recovery,
10
+ the requested tables are reconciled before that recovery begins; a snapshot
11
+ already in progress is left undisturbed.
12
+
13
+ Generated PostgreSQL setup also includes the replica identity needed to retain
14
+ old row values for updates and deletes. The companion sync-server changes track
15
+ snapshot coverage against the active schema, so adding a model cannot inherit
16
+ an older snapshot's readiness. Those server changes require a runtime rollout;
17
+ upgrading the CLI alone does not deploy them.
18
+
19
+ ### One session mint at browser startup
20
+
21
+ A cold reactive client now uses its first session credential to resolve identity,
22
+ avoiding a second call to the application's session endpoint and its membership
23
+ checks. Later refreshes still obtain a fresh credential. The React guide also
24
+ clarifies application-owned disposal, account switching and startup retry.
25
+
26
+ The browser packages now include their event emitter dependency. Applications
27
+ can bundle the React entry with an ordinary browser bundler without supplying a
28
+ Node `events` polyfill themselves.
29
+
30
+ ### A clearer boundary for customer accounts
31
+
32
+ The customer-isolation guide now follows one application connection, explicit
33
+ model subject rules and sessions minted from verified membership. It separates
34
+ your application's account IDs from Ablo organization, project and branch scope,
35
+ and explains what must change in writers, existing rows and database policies
36
+ when adopting that model. Session and API-key guidance use the same distinction.
37
+
38
+ ### Prepare schema SQL without a connection
39
+
40
+ `ablo migrate --offline` generates schema SQL without database credentials or an
41
+ Ablo API key. It prints the SQL or writes it with `--output`, using the same
42
+ planner as a connected migration. The output is explicitly unvalidated: applying
43
+ a migration still requires the connected deployment checks, as do previews that
44
+ omit `--offline`.
45
+
46
+ This release introduces no breaking API changes. Ablo, Transaction, Humans and
47
+ the CLI are published together at 0.64.3.
48
+
49
+ ## 0.64.2
50
+
51
+ ### More complete upgrade guidance
52
+
53
+ `ablo upgrade` now identifies removed `SyncProvider` imports and the removed
54
+ `AbloProvider` `userId` prop, including aliased imports. Its guidance points to
55
+ the current provider and authenticated client session. React examples and
56
+ identity documentation now use the supported props consistently.
57
+
58
+ ### Registration examples match the scaffold
59
+
60
+ The quickstart now includes the empty type import from `@abloatai/ablo` that
61
+ `ablo init` already generates. It loads the SDK declaration before augmenting
62
+ `Register`, so applications copying the example retain the existing module's
63
+ types. The groups guide also explains how shared context relates to membership
64
+ and subscription changes.
65
+
66
+ This patch introduces no new API changes.
67
+
3
68
  ## 0.64.1
4
69
 
5
- The React read and write boundaries are now explicit: `useAbloClient()` returns the writable client and `useAblo(selector)` returns render snapshots. The zero-argument `useAblo()` overload is removed. `ablo upgrade` reports migration hints; see [Upgrade Guide](./docs/migration.md).
70
+ ### A schema that survives the journey into a package
71
+
72
+ Sharing a schema through a compiled package could leave an application facing
73
+ pages of empty-object and unknown-type errors. The schema was present, but its
74
+ registration no longer reached the parts of Ablo that needed it. Registration
75
+ now carries through the published declarations into Transaction and Humans.
76
+ When it is missing, schema-free mutators report what the application needs to
77
+ supply.
78
+
79
+ Passing the schema explicitly is the recommended approach for mutators. The
80
+ integration guide also explains how to share a schema and React bindings across
81
+ packages, so a monorepo's source imports and its consumers' compiled imports
82
+ agree about the same application.
83
+
84
+ ### React makes the difference between reading and writing visible
85
+
86
+ A component reading a snapshot and an event handler issuing a write now ask for
87
+ different things. `useAblo(selector)` returns the render snapshot;
88
+ `useAbloClient()` returns the client used for operations. The zero-argument
89
+ `useAblo()` call is removed, making that choice visible at the call site.
90
+
91
+ Mutation failures have a dedicated hook again. `useMutationFailure` manages the
92
+ subscription and its cleanup, removing the effect each application otherwise
93
+ had to maintain. Framework adapters can obtain the supported store contract
94
+ through `getAbloStore(client)` from `@abloatai/ablo/client`.
95
+
96
+ ### Presence can leave the current session out
97
+
98
+ A view of who else is working on a record can now request
99
+ `{ excludeSelf: true }` through `usePresence` or `presence.forModel`. The filter
100
+ removes the current session; another tab belonging to the same person remains
101
+ visible. Applications no longer need to repeat that distinction themselves.
102
+
103
+ Generic collaboration subscriptions also retain the event arguments declared
104
+ by their adapters. Handlers that need authenticated sender context use
105
+ `collaboration.subscribe` on the session or `subscribeCollaboration` on the
106
+ transport.
6
107
 
7
- Public schema registration reaches Transaction and Humans across emitted package declarations. Ambient mutators without a registered schema now report a schema diagnostic; prefer explicit-schema overloads. Internal package dependencies are exact, and CLI/SDK compatibility is checked as one release family.
108
+ ### One release, with an explicit upgrade path
8
109
 
9
- `usePresence(..., { excludeSelf: true })` and `presence.forModel(model, id, { excludeSelf: true })` omit the current session. `useMutationFailure` owns React subscription cleanup. Custom framework adapters use `getAbloStore(client)` from `/client`. Generic collaboration subscriptions preserve their declared event tuples; attributed handlers use `collaboration.subscribe` (or transport `subscribeCollaboration`).
110
+ Ablo, Transaction, Humans and the CLI now declare exact compatibility
111
+ requirements, keeping their versions together. Provider terminology is
112
+ consistently `AbloProvider`, including the context used by framework adapters.
10
113
 
11
- `AbloProvider` is now the sole provider vocabulary. Advanced framework adapters use `useAbloStoreContext` and `AbloStoreContextValue`; the missing-provider error code is `ablo_context_missing_provider`.
114
+ Despite the patch number, 0.64.1 includes API removals. `ablo upgrade` reports
115
+ migration hints, and the
116
+ [upgrade guide](https://github.com/Abloatai/ablo/blob/v0.64.1/packages/ablo/docs/migration.md)
117
+ covers the hook replacements, provider context, presence fields and claims.
12
118
 
13
119
  ## 0.64.0
14
120
 
package/README.md CHANGED
@@ -29,6 +29,12 @@ Every write goes through it, so authority, idempotency, conflicts, ordering,
29
29
  and confirmation are enforced in one place. Your Postgres remains the source of
30
30
  truth.
31
31
 
32
+ Groups structure which shared state reaches each person or agent. Declare the
33
+ data's scope and participant authority, then let authorized subscriptions keep
34
+ reactive views current; HTTP agents explicitly read or observe changes. Start
35
+ with [Groups and shared context](./docs/groups.md) to connect membership, access,
36
+ loading and updates.
37
+
32
38
  ## Why Ablo
33
39
 
34
40
  Software used to have one writer: a human clicking through an application. AI
package/docs/api-keys.md CHANGED
@@ -234,7 +234,7 @@ Both management scopes are explicit grants on `mk_` credentials. Runtime
234
234
  `sk_`, `rk_`, `pk_`, and `ek_` credentials cannot become management
235
235
  credentials through an empty scope set or a CLI fallback.
236
236
 
237
- Branch binding remains an authority boundary even when a key has no granular
237
+ For ordinary application keys, branch binding remains an authority boundary even when a key has no granular
238
238
  scope strings: a temporary child key can act only inside that child. It cannot
239
239
  manage siblings or gain root authority.
240
240
 
@@ -253,11 +253,23 @@ Treat that key as a dedicated minting credential:
253
253
  - rotate it on a schedule and revoke it immediately after suspected exposure;
254
254
  - log the target `organizationId`, minted session id, and request id for audit.
255
255
 
256
- The scope's broad name describes the cross-organization check it passes, not
257
- the authority of each resulting session. The session can act only inside the
258
- named customer organization and only for the models/verbs in `can`. See
259
- [Customer Organizations](./customer-organizations.md) for the complete
260
- integration.
256
+ The issuer's `organization:act-as` grant can target **any organization**. Ablo
257
+ does not enforce an application-customer allowlist for that authority. A resulting
258
+ session is restricted by its target organization, model operations and expiry,
259
+ but restricting a session does not narrow the issuer's power to mint another.
260
+ Cross-organization mints select the target organization's root branch, including
261
+ when the issuer is bound to a child branch.
262
+
263
+ Normal dashboard key provisioning does not grant this scope. Ablo operators
264
+ provision it using `apps/sync-server/scripts/provision-platform-key.ts` with
265
+ `--org=<issuer-org> --branch=<issuer-branch> --env=sandbox` (or `production`). The
266
+ script requires control-plane database access and emits a new secret once.
267
+ The environment flag controls the credential prefix; it does not force target
268
+ sessions onto a staging branch.
269
+
270
+ For customers sharing one application, use model subject rules and verified
271
+ session groups with an ordinary application key. See
272
+ [Serving Many Customers](./customer-organizations.md).
261
273
 
262
274
  ## Current and legacy key spellings
263
275
 
package/docs/cli.md CHANGED
@@ -133,7 +133,7 @@ branch-bound runtime credential before starting application code.
133
133
  | `ablo branch list\|status\|check\|create\|ensure\|credential\|delete` | Manage and diagnose immutable branch planes and expiring credentials. | Run `ablo branch --help`; use `--json` for automation. |
134
134
  | `ablo logs` | Tail the resolved runtime credential's branch activity. Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow` |
135
135
  | `ablo push` | **Hosted**: upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
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` |
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. | `--offline`, `--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
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` |
@@ -266,9 +266,16 @@ server applies the change and version-gates connecting clients.
266
266
  ```bash
267
267
  ablo migrate --dry-run # preview the exact SQL
268
268
  ablo migrate # apply to DATABASE_URL
269
- ablo migrate --output schema.sql # write SQL to a file
269
+ ablo migrate --output schema.sql # write SQL to a file after connected validation
270
+ ablo migrate --offline --output schema.sql # generate SQL without credentials
270
271
  ```
271
272
 
273
+ `--offline` uses the same schema SQL generator without reading database or Ablo
274
+ credentials. Omit `--output` to print SQL. This is schema generation, not a
275
+ validated deployment plan: it cannot detect existing-data conflicts or drift.
276
+ Run `ablo migrate` with credentials to validate and apply; `--dry-run` and
277
+ `--output` without `--offline` retain connected validation.
278
+
272
279
  ### One database, two schemas
273
280
 
274
281
  `ablo migrate` does **not** own your whole database. It creates exactly the
@@ -1,184 +1,147 @@
1
1
  # Serving Many Customers
2
2
 
3
- > One account, one schema, and a session scoped to the customer whose data it may read.
3
+ > One application connection, customer subject rules, and sessions minted from verified membership.
4
4
 
5
- Serving many customers from one backend has two shapes, and the first question
6
- is whether isolating them is a security boundary or a routing convenience.
5
+ For a multi-account application, keep one Ablo organization, one project for the
6
+ application, and one branch per environment. Your customers remain rows in your
7
+ own database. Their existing account IDs can identify the subjects authorized by
8
+ Ablo; they do not need corresponding Ablo account registrations.
7
9
 
8
- **Use a model `subject` rule for account isolation inside one project.** Bind
9
- `accountId` to the `account` group, mint sessions only after verifying membership,
10
- and use those scoped sessions for server operations too. The complete
11
- [account multiplayer walkthrough](./examples/account-multiplayer.md) shows this
12
- composition and its verification path.
10
+ ## What each layer owns
13
11
 
14
- A separate Ablo organization per customer is another tenant boundary. It is not
15
- required merely because your application has accounts.
12
+ | Layer | Meaning | Boundary |
13
+ | --- | --- | --- |
14
+ | Ablo organization | Your team's Ablo account | Outer organization authority |
15
+ | Project | Your application and its schema | Application data plane |
16
+ | Branch | An environment of that application | Connection, retained log and branch-bound credentials |
17
+ | Customer subject | An account in your application | A model rule checks the row against verified session groups |
18
+ | Session | One human or agent's granted access | Subject membership, model operations and expiry |
16
19
 
17
- **Sync-group routing alone is not subject authorization.** The older routing-only
18
- example below illustrates delivery configuration; add subject rules before using
19
- that pattern as a customer security boundary. The limitations below describe
20
- models without subject rules, not subject-protected models.
20
+ `organizationId` on a session names the outer Ablo organization. It does not
21
+ select an account inside your application. A model's `subject` declares that
22
+ inner boundary; session `groups` supplies the authenticated membership needed
23
+ to pass it. Routing groups alone are not row authorization.
21
24
 
22
- ```ts
23
- // 1. src/ablo/schema.ts — your customer table is a scope root.
24
- import { defineSchema, identityRole, relation, model, z } from '@abloatai/ablo/schema';
25
-
26
- export const schema = defineSchema(
27
- {
28
- // Its rows form the group `customer:<id>`; the kind comes from `groups.root`.
29
- customers: model(
30
- { name: z.string() },
31
- { groups: { root: 'customer' } },
32
- ),
33
- // A child inherits its customer's group through the `parent` edge.
34
- decks: model(
35
- { customerId: z.string(), title: z.string() },
36
- { relations: { customer: relation.belongsTo('customers', 'customerId', { parent: true }) } },
37
- ),
38
- },
39
- {
40
- identityRoles: [
41
- identityRole({ kind: 'org', source: 'organizationId' }),
42
- identityRole({ kind: 'user', source: 'userId' }),
43
- ],
44
- },
45
- );
46
- ```
25
+ ## Declare the customer boundary
26
+
27
+ Use an explicit required account field on every customer-protected model:
47
28
 
48
29
  ```ts
49
- // 2. app/api/ablo-session/route.ts mint for one customer, on your backend.
50
- import { syncGroup } from '@abloatai/ablo/schema';
51
- import { sessions } from '@/ablo/sessions';
52
-
53
- export const POST = sessions.handler({
54
- authenticate: () => currentSignedInMember(),
55
- grant: ({ principal: member }) => ({
56
- user: { id: member.userId },
57
- can: { customers: ['read'], decks: ['read', 'create', 'update'] },
58
- groups: [syncGroup('customer', member.customerId)],
59
- }),
30
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
31
+
32
+ export const schema = defineSchema({
33
+ conversationSummaries: model({
34
+ accountId: z.string().min(1),
35
+ title: z.string(),
36
+ }, { subject: { field: 'accountId', group: 'account' }, load: 'instant' }),
60
37
  });
61
38
  ```
62
39
 
63
- That is the whole integration. The rest of this page is why each line is where
64
- it is.
65
-
66
- ## What each layer is
67
-
68
- Four things carry a name in this arrangement, and mixing two of them up is the
69
- one mistake worth spending a page to prevent.
70
-
71
- | Layer | What it is | Where it lives |
72
- |---|---|---|
73
- | Your account | The organization you signed up with. Colleagues join it with their own logins and share one bill. | Ablo |
74
- | Your application | A project. One per app you run, bound to one schema in your database. | Ablo |
75
- | Your customer | A row in your own table, with your own id on it. | Your database |
76
- | One person's session | An `ek_` your backend mints, cut to one customer's group. | Minted per sign-in |
77
-
78
- Your customers sit in the third row. They are not accounts, because an account
79
- is something you invite colleagues into. They are not projects, because a
80
- project binds to a Postgres schema and you run one application, not one per
81
- customer.
82
-
83
- Your `sk_` already carries your account, so a session never names it. What the
84
- session adds is which customer the person in front of it may read.
85
-
86
- ## Where the boundary is enforced
40
+ The subject rule requires the session to hold `account:<row.accountId>`. Keep
41
+ account IDs from Better Auth, or another membership system, in this field.
42
+ The outer tenancy column `organization_id` retains the owning Ablo organization;
43
+ it is not a second name for the customer account field.
87
44
 
88
- Two mechanisms do different jobs, and the difference is the whole of this page.
45
+ Push the schema to the application branch and apply subject policies through
46
+ [supported connection setup](./data-sources.md). Updating TypeScript alone does
47
+ not update the server schema, existing rows or database policies. For an existing
48
+ projection, migrate its writer and backfill as well as its model declaration.
89
49
 
90
- **Your account is the tenant boundary.** Every row Ablo stores carries your
91
- organization, project, and branch, and all three are compared on every read and
92
- every write, from the credential rather than the request. A client cannot reach
93
- past them by asking. This is the boundary that holds unconditionally.
50
+ ## Mint after verifying membership
94
51
 
95
- **Without a subject rule, sync groups are a routing cut inside your account.** They decide which changes are delivered and which rows a
96
- log-served read returns. That is routing. It is not a universal authorization
97
- boundary, and the gaps are specific:
98
-
99
- | Path | Group cut applied |
100
- |---|---|
101
- | Live delivery and fan-out | Yes |
102
- | HTTP read on a log-served plane (a connected database) | Yes |
103
- | HTTP read on a hosted or direct-query plane | **No.** Scoped by organization |
104
- | Writes | **No.** The groups are recorded on the change, never checked against the row |
105
- | Claim listings and presence | Yes |
106
-
107
- So a session cut to one customer, on a hosted plane, can read another
108
- customer's rows over HTTP; and on any plane it can write to them. What stops it
109
- today is the organization, which both customers share under this shape.
110
-
111
- If isolating your customers is a security requirement, give each one its own
112
- Ablo organization. The stronger row-and-subject authorization that would make
113
- this shape safe on every path is not in the engine yet.
114
-
115
- ## Naming a group
116
-
117
- Build a group with the `syncGroup(kind, id)` helper rather than a string. The
118
- kind is the one you declared in `groups.root`, and the id is your own
119
- identifier for the customer.
52
+ Your backend authenticates the person and verifies membership in the requested
53
+ account on every mint. Use the verified account ID, never an unchecked request
54
+ parameter:
120
55
 
121
56
  ```ts
122
- groups: [syncGroup('customer', member.customerId)]
57
+ import Sessions from '@abloatai/ablo/sessions';
58
+ import { syncGroup } from '@abloatai/ablo/schema';
59
+ import { schema } from './schema';
60
+
61
+ const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY! });
62
+ const session = await sessions.create({
63
+ user: { id: verifiedMember.userId },
64
+ groups: [syncGroup('account', verifiedMember.accountId)],
65
+ can: { conversationSummaries: ['read'] },
66
+ ttlSeconds: 300,
67
+ });
123
68
  ```
124
69
 
125
- Resolve `member.customerId` from the membership you just authenticated on the
126
- server. A signed-in person can put any value in a request body, and the session
127
- you mint is what decides what they can read.
70
+ This session inherits the issuer's organization, project and branch. No
71
+ `organizationId` override or `organization:act-as` grant is needed. The model's
72
+ subject rule authorizes rows; `can` authorizes operations. A list filter merely
73
+ selects a view.
128
74
 
129
- ## When a customer should be its own organization
75
+ The [account multiplayer walkthrough](./examples/account-multiplayer.md) contains
76
+ the complete server membership handler, browser lifecycle and scoped server
77
+ writes. A secret-key singleton must not substitute for a subject-scoped session
78
+ when performing customer operations.
130
79
 
131
- Whenever their isolation has to hold. Give each customer its own Ablo
132
- organization when one of them reading or writing another's rows would be an
133
- incident rather than a bug, when you cannot audit group coverage across every
134
- model, or when a customer is a separate paying business that signs in to Ablo
135
- itself and invites its own developers.
80
+ ## Share one database connection
136
81
 
137
- Your backend then names the customer's organization on the mint, which takes a
138
- secret key carrying `organization:act-as`. The customer never sees Ablo; the
139
- scope exists because the session leaves the organization the key belongs to.
82
+ Customers with subject-protected rows in the same application branch use that
83
+ branch's connected database and schema. There is no per-customer connection
84
+ registration. Ablo serves connected-plane reads from its retained log, populated
85
+ from the database; readiness and snapshot coverage still need verification.
140
86
 
141
- ## Onboarding a customer
87
+ A Data Source is selected by organization, project and branch. Neither a subject
88
+ ID nor a routing group selects a different connection.
142
89
 
143
- Insert the row. There is nothing to register with Ablo, because the group is
144
- derived from the row's id, and the first session minted against it is delivered
145
- its data.
90
+ ## Where isolation is enforced
146
91
 
147
- Add a project only when you add an application. `npx ablo projects create` takes
148
- a management credential from `ablo login`, and one project holds one schema.
92
+ For models with subject rules, the server checks subject membership beneath the
93
+ UI across bootstrap, known-ID and list HTTP reads, live delivery and catch-up.
94
+ Write authorization checks the row's subject too. Routing membership in another
95
+ group does not bypass the subject rule. Models without subject rules do not gain
96
+ customer isolation merely because a session names an account group.
149
97
 
150
- ## Troubleshooting
98
+ Before enabling browser reads, test two real accounts against the deployed SDK,
99
+ server and connection configuration:
151
100
 
152
- ### A session reads nothing
101
+ - Bootstrap exposes only the authorized account's rows.
102
+ - Reading another account's known row ID over HTTP is denied.
103
+ - Live updates and reconnect catch-up do not expose foreign rows.
104
+ - Switching accounts disposes the previous client and does not display its
105
+ IndexedDB data in the newly authorized account.
153
106
 
154
- Check the groups the session was minted with against the kind in `groups.root`.
155
- A group whose kind is not declared matches nothing, which reads as an empty
156
- database rather than an error.
107
+ The maintained example and authorization journeys provide runnable checks;
108
+ local tests are not evidence that a deployed fleet has the same behavior.
109
+ [Groups and shared context](./groups.md) describes cache clearing and membership
110
+ changes. An external membership change does not automatically revoke every
111
+ already-issued session, and offline cached copies cannot be remotely retracted.
157
112
 
158
- ### A session reads another customer's rows
113
+ ## When to use separate Ablo organizations
159
114
 
160
- Check that the model declares a `parent` edge up to the scope root. A model with
161
- no group of its own and no parent belongs to no group, so a group cut does not
162
- narrow it.
115
+ Choose separate organizations when customers own separate Ablo accounts or need
116
+ separate outer data planes. The engine can use an unregistered organization
117
+ scope ID; this does not register an Ablo billing account or connect a database.
163
118
 
164
- ### The mint is refused
119
+ Cross-organization minting requires a secret key explicitly granted
120
+ `organization:act-as`. That authority can target any organization; there is no
121
+ application-customer allowlist on the grant. Normal dashboard key creation does
122
+ not grant it. It is operator-provisioned platform authority, not a prerequisite
123
+ for serving customers through the shared-application pattern above.
165
124
 
166
- Naming `organizationId` reaches into a different account and takes
167
- `organization:act-as`. A platform serving its own customers names groups
168
- instead, and its key needs no scope at all.
125
+ A cross-organization session uses the target organization's default project and
126
+ root branch. It does **not** inherit the issuer's staging branch. Its schema may
127
+ come from the issuer's project, but its data connection does not: every target
128
+ plane needs its own supported data-source setup. Sharing a schema artifact alone
129
+ does not establish shared Aurora reads.
169
130
 
170
- ## See it yourself
131
+ See [Sessions](./sessions.md) for schema binding and [API keys](./api-keys.md) for
132
+ the authority and lifecycle of a platform mint key.
171
133
 
172
- ```
173
- npx ablo whoami --json
174
- ```
134
+ ## Troubleshooting
175
135
 
176
- The `syncGroups` it reports are the cut the engine will apply. If a customer's
177
- group is missing there, no read will show its rows.
136
+ **The mint is refused.** For customers inside one application, omit
137
+ `organizationId`, declare subject rules and grant verified account groups. Do
138
+ not remove the override without migrating a model that currently uses the
139
+ customer ID as its outer tenancy column.
178
140
 
179
- ## Related guides
141
+ **A session reads nothing.** Check the branch, pushed subject rule, stored account
142
+ field, granted groups and connection readiness. A schema shared across
143
+ organizations does not share the source connection.
180
144
 
181
- - [Identity & Sync Groups](/identity) how groups are declared and resolved.
182
- - [Sessions](/sessions) session lifetime, refresh, and revocation.
183
- - [API Keys](/api-keys) credential classes and scopes.
184
- - [Projects](/projects) — one project per application.
145
+ **A session reads another customer's row.** Check the actual pushed model's
146
+ subject declaration and the session's verified groups. A parent edge, routing
147
+ group or client filter alone is not an authorization rule.
@@ -7,6 +7,9 @@ The maintained reference lives in
7
7
  Start at `src/index.ts`, then follow its owned `accounts`, `agent` and `workspace`
8
8
  boundaries. Its README contains the install, isolated-branch setup and test commands.
9
9
 
10
+ For the full participant lifecycle and its current cache behavior, see
11
+ [Groups and shared context](../groups.md).
12
+
10
13
  ## One account, one authorization rule
11
14
 
12
15
  “People in this account can see its chats” requires both a row rule and a
@@ -47,7 +50,8 @@ Your application authenticates the person and verifies membership on every
47
50
  mint. The subject rule authorizes rows; `groups` proves membership; `can`
48
51
  authorizes operations. Synchronization delivers the authorized data. A list
49
52
  filter helps select a view but does not establish any of these permissions.
50
- The provider's `userId` prop is informational, not an authentication mechanism.
53
+ React reads application identity from your auth context; the provider accepts
54
+ no `userId` prop.
51
55
 
52
56
  ## Browser reads and server writes share the scope
53
57
 
@@ -104,7 +104,7 @@ const ablo = Ablo({
104
104
  });
105
105
 
106
106
  // The agent run is mounted on behalf of its triggering user.
107
- <AbloProvider client={ablo} userId={triggeringUser.id}>
107
+ <AbloProvider client={ablo}>
108
108
  {children}
109
109
  </AbloProvider>
110
110
  ```
package/docs/groups.md CHANGED
@@ -1,16 +1,137 @@
1
- # Change Propagation
1
+ # Groups and shared context
2
2
 
3
- > How one row's change reaches the rows and actors that depend on it.
3
+ > Structure which shared state reaches each participant, and how their view stays current.
4
4
 
5
- > How a change to one row reaches the rows and actors that depend on it, and how
6
- > to keep a chain of dependent work fresh. This is the propagation half of sync
7
- > groups; [`identity.md`](./identity.md) is the access half (who may read a
8
- > group), and [`concurrency-convention.md`](./concurrency-convention.md) is the
9
- > convention this rests on.
5
+ Groups connect the structure of your data to the people and agents who receive
6
+ it. A group names a shared context, such as `account:acme` or `workspace:abc`.
7
+ Membership and authorization determine the eligible view; subscriptions and
8
+ client loading determine how that view reaches a participant.
10
9
 
11
- ---
10
+ Start here to understand **why this participant receives this record**. Use
11
+ [Identity](./identity.md) for authentication and credential issuance, and the
12
+ [account multiplayer walkthrough](./examples/account-multiplayer.md) for the
13
+ maintained application that puts these pieces together.
14
+
15
+ ## One context, several decisions
16
+
17
+ Ablo has existing declarations for these decisions; there is no single group
18
+ object that configures all of them.
19
+
20
+ | Decision | Existing declaration or behavior |
21
+ | --- | --- |
22
+ | Which records form a context? | Model `groups.root` creates a group per root record; children inherit through `belongsTo` relationships marked `parent: true`. Ordinary references do not propagate membership. `groups.roles` supplies explicit field-based routes. |
23
+ | Who belongs? | Schema `groups.grants` declares a membership edge through its `subject` and `scope` relations within an organization. A trusted backend can also issue session `groups` after verifying application membership. |
24
+ | Which rows may they access? | Model `policy` establishes the read/tenant boundary; `subject` requires a matching credential group for the named row field. Delivery routing alone is not a read policy. |
25
+ | What may they do? | Session `can` grants model operations. Read membership does not grant update or claim authority. |
26
+ | Which changes reach them? | Server-authorized subscriptions match the row's delivery groups. Requested groups cannot widen credential authority. |
27
+ | What is local? | Reactive clients bootstrap and maintain local state; HTTP clients explicitly fetch data. Client loading is distinct from permission to read. |
28
+
29
+ For a model with `subject`, its subject group is the **exclusive delivery
30
+ route**. Parent groups, explicit roles and additional routes cannot provide an
31
+ alternate path to that row. For other routed models, delivery matches any
32
+ eligible group; declaring a narrow route does not narrow an otherwise broad
33
+ read policy. `groups.routingOnly: true` acknowledges that deliberate difference,
34
+ not an authorization grant.
35
+
36
+ ## Follow one conversation
37
+
38
+ The account multiplayer reference declares this model:
39
+
40
+ ```ts
41
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
42
+
43
+ const schema = defineSchema({
44
+ conversations: model({
45
+ accountId: z.string().min(1),
46
+ title: z.string(),
47
+ executionOwner: z.string().nullable(),
48
+ executionState: z.enum(['idle', 'generating']),
49
+ }, { subject: { field: 'accountId', group: 'account' } }),
50
+ });
51
+ ```
52
+
53
+ A conversation whose `accountId` is `acme` requires `account:acme`. The
54
+ application verifies Alice's membership before issuing her browser session with
55
+ that group and `can: { conversations: ['read'] }`. The agent gets the same group
56
+ with `read` and `update` authority. The reference verifies account membership in
57
+ application code; it does not use a schema `groups.grants` membership model.
58
+
59
+ Both can read the conversation. Alice's browser cannot update it with its
60
+ read-only credential: the reference performs human writes through a separately
61
+ scoped server client. The agent may update it, subject to the write's claims and
62
+ read checks. An outsider cannot gain access by supplying `accountId: 'acme'` in
63
+ a filter or by requesting an unauthorized subscription.
64
+
65
+ ```mermaid
66
+ flowchart TD
67
+ R["Conversation: accountId = acme"] --> S["Subject: account:acme"]
68
+ S --> G["Trusted groups + operation grants"]
69
+ G -->|Bootstrap and live updates| H["Alice's reactive local view"]
70
+ G -->|Explicit reads or log requests| A["Agent's working context"]
71
+ ```
12
72
 
13
- ## Start from the problem
73
+ The diagram describes data flow. Group membership does not prove that a
74
+ participant is connected, has loaded every record, or has acted on an update.
75
+ Presence describes activity; it does not grant authority or locate cached bytes.
76
+
77
+ ## A participant's lifecycle
78
+
79
+ | Event | What happens today |
80
+ | --- | --- |
81
+ | Join | The backend authenticates the participant and verifies membership before minting a scoped session. Issuance does not itself load records. |
82
+ | Load | Alice's reactive client loads its authorized baseline and consumes updates. An HTTP agent calls model reads/lists or observes the ordered log; it has no reactive local graph. |
83
+ | Change | A confirmed conversation change routes through `account:acme` to eligible subscribers. An HTTP agent must explicitly read again or consume log changes to update its working context. |
84
+ | Gain a group | On the incremental group-added path, the reactive client records membership and receives covering deltas for newly visible rows. The full-diff path instead requests re-bootstrap. |
85
+ | Reconnect | The reactive client compares current server-issued groups with stored subscription metadata. Detected shrinkage clears local storage and memory and marks a full bootstrap as required; otherwise normal catch-up applies. |
86
+ | Lose a group | On a group-removal notification, the reactive client clears its managed database and object pool, updates subscription metadata and requests re-bootstrap. It does not selectively evict that group's rows. |
87
+ | Switch account | The reference disposes the previous account client and creates a client using the newly authorized account endpoint. |
88
+
89
+ Group-change handling is a runtime path, not a promise that every change in an
90
+ external membership database immediately invalidates every issued credential.
91
+ The application must connect its membership and credential lifecycle to Ablo.
92
+ An offline participant cannot process a revocation notification until it
93
+ reconnects; managed-cache clearing cannot retract copies retained by application
94
+ code or an agent. Clients configured without automatic bootstrap do not fetch
95
+ a full baseline after a group-change notification; they rely on covering deltas or
96
+ explicit reads for data.
97
+
98
+ Consider a participant authorized for both `account:acme` and `account:beta`.
99
+ Losing Acme currently clears the client's whole managed cache, including cached
100
+ Beta records, before rebuilding the remaining authorized view. Beta records
101
+ remain eligible for loading. For non-subject routing where one row belongs to
102
+ several groups, losing one matching group likewise does not alone establish
103
+ that the row is inaccessible; remaining authorization and routes matter.
104
+
105
+ ## Understand the living system
106
+
107
+ Inspect a participant through three separate questions: **what may they see,
108
+ what are they subscribed to, and what have they loaded?** To explain an individual
109
+ record, follow its model's subject or routing declaration, the participant's
110
+ trusted groups and operation grants, then its client transport and lifecycle.
111
+
112
+ These distinctions also help assess a group design:
113
+
114
+ | Symptom | Design question |
115
+ | --- | --- |
116
+ | Many irrelevant updates | Is the delivery group broader than the participant's work? |
117
+ | One task needs many groups | Has the shared context been fragmented too far? |
118
+ | One change reaches many subscribers | Is that fan-out useful, and do all subscribers need live delivery? |
119
+ | Frequent group-premise rejection | Does the decision depend on the whole group, or only particular rows/fields? |
120
+ | Slow loading or catch-up | How much authorized state is being materialized, and how much changed while offline? |
121
+
122
+ These are evaluation questions, not a built-in group score or per-participant
123
+ cache dashboard. A group does not configure blob prefetch, cache placement or
124
+ selective eviction. Those would be additional capabilities built on these scope
125
+ and update signals.
126
+
127
+ ## Changes and decisions
128
+
129
+ Receiving an update keeps a live view current. Declaring a read premise checks
130
+ whether a particular decision is still valid when written. A group can serve
131
+ both purposes, but membership alone neither locks records nor makes them
132
+ mutually consistent.
133
+
134
+ ### Protect a decision based on a group
14
135
 
15
136
  An agent reads workspace `A` to write document `B`. A moment later it reads `B` to write
16
137
  block `C`. Between those steps someone else edits `A`. The agent is now building
@@ -74,18 +195,17 @@ does not retain the row contents as read evidence.
74
195
  for you and leaves the third to you — on purpose.
75
196
 
76
197
  **Routing — who hears about a change.** Every row belongs to one or more sync
77
- groups, and a write fans out to all of them. A row also inherits its ancestors'
78
- groups: editing a block stamps the delta with `block:…`, `document:…`, *and*
79
- `workspace:…`, so everyone watching the workspace sees the block move. This is delivery,
198
+ groups, and a write fans out to its delivery groups. For a model without an
199
+ exclusive `subject` route, declared scope roots and
200
+ relationships can route a block change to `block:…`, `document:…`, and
201
+ `workspace:…`, so authorized workspace subscribers receive it. This is delivery,
80
202
  resolved by walking the ownership tree at commit time. It routes the change; it
81
203
  never recomputes a value.
82
204
 
83
- **Structural cascade — what disappears with a change.** Deleting a workspace removes
84
- its documents and blocks. The database does that through `ON DELETE CASCADE`, but a
85
- database-level cascade emits no delta, so open clients would quietly hold rows
86
- that no longer exist. The engine closes that gap: before the delete it snapshots
87
- the subtree and emits a tombstone for each descendant, routed to the right
88
- group. Watchers see the whole subtree vanish.
205
+ **Structural cascade — what disappears with a change.** A declared ownership
206
+ relationship can make deleting a parent remove its descendants. Clients need
207
+ routed deletion deltas to remove those records from their views. This follows
208
+ the relationship and delete path; sharing a group alone does not cascade deletes.
89
209
 
90
210
  **Value recomputation — what a change implies for derived state.** If `B` holds a
91
211
  number rolled up from `A`, the engine does not recompute `B` when `A` changes. It
@@ -116,8 +236,8 @@ decides what that means for its own state, commits, and *its* commit is what
116
236
  reaches `C`.
117
237
 
118
238
  The direction matters. The signal flows forward, A to B to C, and each hop is a
119
- real write an actor chose to make. The engine supplies the edges (group
120
- membership) and a stale signal on each edge (the premise check); the actors are
239
+ real write an actor chose to make. Group routing supplies the delivery edges, and declared read premises add
240
+ stale-work checks to writes; the actors are
121
241
  the runtime that walks them. It is closer to a dataset an analyst
122
242
  recalculates cell by cell than to a reactive engine that recomputes the whole
123
243
  column for you.
@@ -167,11 +287,10 @@ group premise fires when *anything* in the group moves — so a group that is to
167
287
  broad wakes actors for changes they don't care about, and one that is too narrow
168
288
  misses the dependency you meant to track.
169
289
 
170
- The rule of thumb: **make a group the smallest set of rows that must stay
171
- mutually consistent.** A workspace and its documents belong together because editing one
172
- changes what the others mean; two unrelated workspaces do not. Reach for finer,
173
- overlapping groups when you genuinely have a dependency chain to track, and keep
174
- them coarse everywhere else.
290
+ Choose groups around shared work and authorized audiences. Use a group premise
291
+ when a decision depends on that whole context; use row or field premises when
292
+ it depends on less. Overlapping routing groups can express useful audiences,
293
+ but do not create transaction boundaries or a consistency guarantee.
175
294
 
176
295
  ---
177
296
 
package/docs/identity.md CHANGED
@@ -1,7 +1,11 @@
1
- # Identity & Sync Groups
1
+ # Identity and credentials
2
2
 
3
3
  > Who is connecting, and which slice of state they are allowed to see.
4
4
 
5
+ Start with [Groups and shared context](./groups.md) for how data membership,
6
+ authorization, subscriptions and local state fit together. This guide owns
7
+ authentication, credential issuance and the schema wiring behind that view.
8
+
5
9
  This is the doc the Quickstart skips: **who is connecting, and which slice
6
10
  of shared state do they get?** If you've wired `<AbloProvider client={ablo}>`
7
11
  and wondered where org / team / user actually come from — start here.
@@ -15,7 +19,7 @@ Ablo is not an identity provider. It has no login, no password store, no
15
19
  session of its own. You keep whatever you already use — Clerk, Auth0,
16
20
  NextAuth, WorkOS, your own session table. Ablo's job begins **after** you've
17
21
  authenticated the user: you hand Ablo the already-authenticated identity, and
18
- Ablo decides which **sync groups** that identity may read and write.
22
+ Ablo enforces that credential's groups, model read policies and operation grants.
19
23
 
20
24
  ## Inspect the credential the application is actually using
21
25
 
@@ -75,13 +79,12 @@ that.
75
79
 
76
80
  ## What a sync group is
77
81
 
78
- A **sync group** is a named channel of shared state a string like
79
- `org:acme` or `workspace:abc123`. It is simultaneously:
80
-
81
- - **the unit of fan-out:** a confirmed write to a row publishes a delta to
82
- every participant subscribed to that row's sync group(s), and
83
- - **the unit of access:** a participant receives a row's deltas *only if* the
84
- row's sync group is in their allowed set.
82
+ A **sync group** names shared state, such as `org:acme` or `workspace:abc123`.
83
+ The server checks allowed groups for delivery; model `policy` and `subject`
84
+ rules govern row access, and capability operations govern permitted actions.
85
+ Routing a row to a group does not itself authorize an HTTP read or write.
86
+ See [the group lifecycle](./groups.md#a-participants-lifecycle) for loading,
87
+ updates, reconnects and removal.
85
88
 
86
89
  There is no built-in `org` / `team` / `user` concept in the engine. Those are
87
90
  *your* domain words. Ablo only knows sync-group strings. The mapping from "this
@@ -133,7 +136,7 @@ export const schema = defineSchema(
133
136
  // 2. app/providers.tsx — a HUMAN gets their full org / team scope.
134
137
  // teamIds is set on the client you build (Ablo({ schema, teamIds: user.teamIds })),
135
138
  // not passed to the provider; the provider just takes that client.
136
- <AbloProvider client={ablo} userId={user.id}>
139
+ <AbloProvider client={ablo}>
137
140
  {children}
138
141
  </AbloProvider>
139
142
  ```
@@ -255,9 +258,11 @@ Delivery scoping is two declarations that meet in the middle. One describes the
255
258
  changes when the row's sync groups intersect the participant's allowed set.
256
259
 
257
260
  That intersection does not itself authorize an HTTP read. A model's `policy`
258
- governs read access. Treat sync-groups as change routing and `policy` (plus the
259
- organization boundary beneath it) as authorization; declaring one never
260
- silently creates the other.
261
+ governs read access, while `subject` can require a credential group for a row
262
+ field. Operation grants bound the actions. A subject-scoped row uses only its
263
+ subject delivery group; other routes cannot bypass that boundary. See
264
+ [Groups and shared context](./groups.md#one-context-several-decisions) for how
265
+ these declarations fit together.
261
266
 
262
267
  ### Half 1 (`identityRoles`): identity → allowed groups
263
268
 
@@ -408,8 +413,7 @@ server, never by the browser.**
408
413
 
409
414
  ## Wiring the provider
410
415
 
411
- The identity your server resolved is carried by the client you build and the
412
- `userId` prop. In a Next.js app, resolve the user in a Server Component and pass
416
+ The identity your server resolved is carried by the authenticated client session. In a Next.js app, resolve the user in a Server Component and pass
413
417
  it down. Build the client once (the schema, `teamIds`, and the `apiKey` resolver
414
418
  live here; entity narrowing rides the minted session's `groups`), then hand
415
419
  it to the provider:
@@ -450,7 +454,7 @@ export function Providers({
450
454
  }) {
451
455
  const ablo = useMemo(() => makeAblo(user), [user.id]);
452
456
  return (
453
- <AbloProvider client={ablo} userId={user.id} fallback={<AppSkeleton />}>
457
+ <AbloProvider client={ablo} fallback={<AppSkeleton />}>
454
458
  {children}
455
459
  </AbloProvider>
456
460
  );
@@ -461,11 +465,11 @@ What carries identity — and just as importantly, what does *not* set the bound
461
465
 
462
466
  | Where | Purpose |
463
467
  | ------------ | ------------------------------------------------------------------------------------------------ |
464
- | `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary: the server enforces scope from the authenticated request. |
468
+ | Application authentication context | Supplies identity for app-owned fields and UI. The provider has no `userId` prop; the server enforces scope from the authenticated session. |
465
469
  | `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
466
470
  | `groups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows: it can never widen it. Passed to `sessions.create({ user \| agent, groups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('workspace', 'abc123')]`. |
467
471
 
468
- Because the server is the boundary, a client that changes `userId` to another
472
+ Because the server is the boundary, a client that changes application identity state to another
469
473
  user's id does not gain their data — the server resolves and enforces the real
470
474
  identity on the connection. These are how your app *tells* Ablo who it
471
475
  already authenticated, not how it *proves* it.
@@ -644,9 +648,9 @@ The best practices Ablo inherits from that lineage:
644
648
  the line precisely: [token parameters are trusted and usable for access
645
649
  control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
646
650
  In Ablo terms, the identity your server vouches for — and the session's
647
- `groups`, minted server-side — are the *trusted* claims that set scope; the
648
- `userId` prop is *untrusted client input* — convenient for app-owned fields, but
649
- never the boundary. This is why changing `userId` in the browser grants nothing.
651
+ `groups`, minted server-side — are the *trusted* claims that set scope; application
652
+ identity state is *untrusted client input* — convenient for app-owned fields, but
653
+ never the boundary. Changing that state in the browser grants nothing.
650
654
 
651
655
  3. **Scope by a hierarchical naming convention, declared once.** Ablo's `kind:id`
652
656
  group naming (`org:…` / `team:…` from `identityRoles`, `workspace:…` from a model's
package/docs/index.md CHANGED
@@ -27,6 +27,10 @@ interfaces.
27
27
  Keep the authoritative transaction, locks, constraints, and direct SQL paths your application already owns.
28
28
  </Card>
29
29
 
30
+ <Card title="Groups and shared context" icon="share-2" href="/groups">
31
+ Structure which state reaches each participant, from membership and access to loading, updates and removal.
32
+ </Card>
33
+
30
34
  <Card title="Participant identity" icon="fingerprint" href="/identity">
31
35
  Give agents, people, and services distinct scoped credentials instead of treating every worker as the same caller.
32
36
  </Card>
@@ -172,7 +172,7 @@ export const ablo = Ablo({
172
172
 
173
173
  Browser apps should use the React provider or a scoped session token, not a
174
174
  server API key in the bundle. Build the client first, then hand it to the
175
- provider — `AbloProvider` takes `{ client, userId?, onError?, fallback? }`, and
175
+ provider — `AbloProvider` takes `{ client, onError?, fallback? }`, and
176
176
  nothing else (`schema`, `teamIds`, and `apiKey` all live on the
177
177
  client now).
178
178
 
@@ -16,6 +16,12 @@ schema** — your migration tool stays in charge of the shape of your database.
16
16
  > or separate database, and connect your production one when you're ready for it
17
17
  > to be the system of record.
18
18
 
19
+ Groups structure which shared state reaches each person or agent. Declare the
20
+ data's scope and participant authority, then let authorized subscriptions keep
21
+ reactive views current; HTTP agents explicitly read or observe changes. Start
22
+ with [Groups and shared context](./groups.md) to connect membership, access,
23
+ loading and updates.
24
+
19
25
  ## 1. Install and initialize
20
26
 
21
27
  ```bash
@@ -82,6 +88,7 @@ every type is one parameter away — no `typeof schema` re-stating, anywhere:
82
88
 
83
89
  ```ts
84
90
  // ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
91
+ import type {} from '@abloatai/ablo';
85
92
  import type { schema } from './schema';
86
93
  declare module '@abloatai/ablo' {
87
94
  interface Register { Schema: typeof schema }
@@ -89,11 +96,11 @@ declare module '@abloatai/ablo' {
89
96
  export {};
90
97
  ```
91
98
 
92
- It's a regular `.ts` module, not a hand-authored `.d.ts`. The top-level
93
- `import type { schema }` makes the `declare module` block *merge* into (augment)
94
- the SDK's `Register` interface instead of colliding with it the same shape
95
- [TanStack Router uses in `src/router.tsx`](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety). Any `.ts` file in your
96
- `tsconfig` `include` works; it never needs to be imported.
99
+ It's a regular `.ts` module, not a hand-authored `.d.ts`. The empty
100
+ `import type {} from '@abloatai/ablo'` loads the SDK's module declaration so the
101
+ `declare module` block augments its existing `Register` interface. The schema
102
+ import supplies your schema's type. Both imports are erased at runtime. Any `.ts`
103
+ file in your `tsconfig` `include` works; it never needs a runtime import.
97
104
 
98
105
  ```ts
99
106
  import type { Model } from '@abloatai/ablo/schema';
package/docs/react.md CHANGED
@@ -110,25 +110,24 @@ import { ablo } from '@/lib/ablo';
110
110
 
111
111
  export function Providers({
112
112
  children,
113
- user, // resolved server-side from YOUR auth
114
113
  }: {
115
114
  children: React.ReactNode;
116
- user: { id: string };
117
115
  }) {
118
116
  return (
119
- <AbloProvider client={ablo} userId={user.id} fallback={<AppSkeleton />}>
117
+ <AbloProvider client={ablo} fallback={<AppSkeleton />}>
120
118
  {children}
121
119
  </AbloProvider>
122
120
  );
123
121
  }
124
122
  ```
125
123
 
126
- `client` is the only required prop. The rest are situational:
124
+ `client` is the only required prop. The removed `userId` prop is no longer accepted;
125
+ read application identity from your authentication context. Ablo authority comes
126
+ from the client session. The remaining props are situational:
127
127
 
128
128
  | Prop | Default | Purpose |
129
129
  | ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
130
130
  | `client` |: | **Required.** The `Ablo({ schema, apiKey })` instance. It carries the schema and connection config. |
131
- | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
132
131
  | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
133
132
  | `onError` |: | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
134
133
 
@@ -146,7 +145,7 @@ session owner calls `await ablo.dispose()` on logout or before replacing that
146
145
  client. Provider remounts can reuse it. Never share a browser singleton across
147
146
  server requests. When changing accounts, remove the old account UI and create a
148
147
  fresh client whose session endpoint grants the newly verified membership.
149
- A query filter and the provider's `userId` prop do not change authorization.
148
+ Query filters and application identity state do not change authorization.
150
149
 
151
150
  For a component-owned client, create and dispose the instance in the same effect.
152
151
  React Strict Mode can replay setup and cleanup, so each setup creates a fresh
@@ -182,6 +181,12 @@ for the requested account on every mint; the URL itself grants no access.
182
181
  See the [account multiplayer walkthrough](./examples/account-multiplayer.md)
183
182
  for the complete ownership boundary and runnable component.
184
183
 
184
+ Use `onError` to show a startup failure outside the bootstrap gate. After a
185
+ transient failure, remount the provider with the same client to retry readiness;
186
+ failed `ready()` attempts are retryable. After logout or an account change,
187
+ create a fresh client instead. Strict Mode may create two client instances in
188
+ development; each instance owns its own credential lifecycle.
189
+
185
190
  ## Render immediately with connection status
186
191
 
187
192
  `useAblo(ablo => ablo.status)` works during provider startup, in passthrough children and in
package/docs/sessions.md CHANGED
@@ -182,7 +182,7 @@ for the actor.
182
182
  |---|---|---|
183
183
  | `user` / `agent` | both | The actor. `id` becomes the token's `participantId`. Pass exactly one. |
184
184
  | `can` | both | Required non-empty per-model operation allowlist, typed off the schema. |
185
- | `organizationId` | user | Mint into a customer organization instead of the key's own. Requires `organization:act-as`. |
185
+ | `organizationId` | user | Select another Ablo organization, not an application account. Requires `organization:act-as`; uses the target root branch. |
186
186
  | `schemaProject` | user | Override the schema project for a cross-org mint. Usually omitted because the owning key's project is the default. |
187
187
  | `groups` | both | Narrow the session below its default scope. Omit to inherit. |
188
188
  | `ttlSeconds` | both | Lifetime in seconds. Defaults to `900` (15m). |
@@ -273,34 +273,24 @@ Some apps need each customer to be its **own** tenant — a hard data boundary
273
273
  scoping. The law-firm shape (Legora): every firm is its own org, many users
274
274
  inside it.
275
275
 
276
- Choose the boundary before minting sessions:
276
+ For customers sharing one application connection, declare a model `subject`
277
+ rule and mint verified account `groups` with the ordinary application key. Keep
278
+ its organization, project and branch. The [customer guide](./customer-organizations.md)
279
+ owns the complete tenancy model, migration requirements and isolation checks.
277
280
 
278
- | Customer model | Isolation guarantee | Use when |
279
- |---|---|---|
280
- | One Ablo organization, customer scope roots | Every model's declared `policy` | Cross-customer access is intentional or every model explicitly partitions by the customer root |
281
- | One Ablo organization per customer | Structural organization filtering and RLS on every row | Customers must be isolated even when a model has no customer policy |
282
-
283
- Sync-group routing controls which changes are delivered; it does not grant or
284
- deny reads. Do not use scope roots as a tenant security boundary unless every
285
- model declares the matching policy. If that invariant is difficult to audit,
286
- use one organization per customer.
287
-
288
- For the complete key, backend-route, browser, lifecycle, and troubleshooting
289
- flow, see [Customer Organizations](./customer-organizations.md).
290
-
291
- The problem that creates: if each customer is a separate org, a naïve setup would
292
- make you re-push your schema into every new customer's org. You don't have to.
293
- Keep **one** project as the home of your schema. When its key mints into another
294
- organization, Ablo automatically resolves the session's *schema* from that key's
295
- project while its *data* stays in the customer's own org:
281
+ Separate Ablo organizations are separate data planes. A cross-organization user
282
+ session can reuse the issuer's schema artifact, but it uses the target
283
+ organization's default project and root branch. It does not inherit the issuer's
284
+ staging branch or database connection. Each target plane needs its own supported
285
+ data-source setup.
296
286
 
297
287
  ```ts
298
- const ablo = Ablo({ schema, apiKey: process.env.ABLO_PLATFORM_KEY });
299
- const { token } = await sessions.create({
288
+ const sessions = Sessions({ schema, apiKey: process.env.ABLO_PLATFORM_KEY! });
289
+ const session = await sessions.create({
300
290
  user: { id: userId },
301
- organizationId, // DATA this customer's isolated org
291
+ organizationId, // A separate Ablo organization and data plane.
302
292
  can: { records: ['read', 'update'] },
303
- ttlSeconds: 3600,
293
+ ttlSeconds: 300,
304
294
  });
305
295
  ```
306
296
 
@@ -322,10 +312,10 @@ your users in the key's own organization.
322
312
 
323
313
  ## Security
324
314
 
325
- The whole safety argument is the short TTL: a session token leaked from a
326
- browser (XSS) is valid for minutes, scoped to one actor's data, and can't mint
327
- anything or touch the control plane. Contrast `sk_`, which would be a full org
328
- compromise which is exactly why it never leaves your server.
315
+ Session authority combines the organization/project/branch boundary, model
316
+ subject rules, verified groups, the `can` allowlist and expiry. A short TTL limits
317
+ exposure; it does not create row isolation. Keep issuer secret keys on the
318
+ server and verify membership whenever issuing or renewing customer sessions.
329
319
 
330
320
  ## User vs. agent sessions
331
321
 
package/llms.txt CHANGED
@@ -174,7 +174,7 @@ coordination until the app reports it through Data Source events.
174
174
 
175
175
  ## Change propagation
176
176
 
177
- A change to one row reaches other rows three ways. ROUTING: a write fans out to every sync group the row belongs to, INCLUDING its ancestors' groups (editing a block routes to `block:` + `document:` + `workspace:`), so everyone watching the workspace sees it — delivery, not recomputation. DELETE CASCADE: deleting a parent emits explicit tombstone deltas for its descendants, so open clients never silently hold rows that are gone. VALUE: derived values are NOT recomputed server-side — Ablo surfaces that the source moved and the actor decides. To keep dependent work fresh, pass rows returned by `ablo.<model>.read({ id })` in the mutation's `reads` array. Ablo records compact model/id/readAt evidence, not row contents. At commit the server checks whether anything moved past the read watermark; if so, the mutation does not land. Use `get` when no such relationship exists. To chain A→B→C, put A+B in one group and B+C in another: A's change reaches B, and C hears it only once B ITSELF writes. No transitive auto-recompute, no convergence guarantee for cycles.
177
+ A change to one row reaches other rows three ways. ROUTING: a write fans out to every sync group the row belongs to, including declared ancestor routes for models without `subject`; subject-scoped rows route exclusively through their required subject group, so only eligible subscribers receive them — delivery, not recomputation. DELETE CASCADE: deleting a parent emits explicit tombstone deltas for its descendants, so open clients never silently hold rows that are gone. VALUE: derived values are NOT recomputed server-side — Ablo surfaces that the source moved and the actor decides. To keep dependent work fresh, pass rows returned by `ablo.<model>.read({ id })` in the mutation's `reads` array. Ablo records compact model/id/readAt evidence, not row contents. At commit the server checks whether anything moved past the read watermark; if so, the mutation does not land. Use `get` when no such relationship exists. To chain A→B→C, put A+B in one group and B+C in another: A's change reaches B, and C hears it only once B ITSELF writes. No transitive auto-recompute, no convergence guarantee for cycles.
178
178
 
179
179
  ## Nouns
180
180
 
@@ -322,3 +322,7 @@ Canonical docs to read before integrating, in this order. Read each with `npx ab
322
322
  - [Upgrade Guide](https://docs.abloatai.com/migration): when upgrading an existing integration; every breaking change, what to change, and which version introduced it.
323
323
  - [Session Settings](https://docs.abloatai.com/session-settings): when the customer's database has row-level-security policies; the identity context Ablo sets before every write, and how to map it to the setting names those policies read.
324
324
  - [Every page, one line each](https://docs.abloatai.com/llms.txt), or [the full docs as one file](https://docs.abloatai.com/llms-full.txt).
325
+
326
+ ## Groups and shared context
327
+
328
+ Groups connect data membership to authorized participant subscriptions. Model roots, relations and grants describe routing and membership; policy, subject and capability operations establish access. Subject-scoped rows route exclusively through their subject group. Reactive clients load and maintain local state; HTTP agents read or observe changes explicitly. Group removal currently clears the managed client cache and requests re-bootstrap, rather than selectively evicting rows. Membership does not imply loaded data, activity, or a lock. Start with `ablo docs groups` for the lifecycle and the account multiplayer example; use `ablo docs identity` for authentication and credential issuance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.64.1",
3
+ "version": "0.64.3",
4
4
  "description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -120,6 +120,7 @@
120
120
  "verify:context-package": "node scripts/verify-context-package.mjs",
121
121
  "typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json && tsc -p examples/tsconfig.json",
122
122
  "test": "vitest run",
123
+ "test:browser-bundle": "node scripts/check-browser-bundle.mjs",
123
124
  "generate:errors": "tsx --conditions=@ablo/source scripts/generate-error-docs.mts",
124
125
  "lint:errors": "tsx --conditions=@ablo/source scripts/check-error-docs.mts",
125
126
  "generate:pricing": "tsx scripts/generate-pricing-docs.mts",
@@ -146,8 +147,8 @@
146
147
  "directory": "packages/ablo"
147
148
  },
148
149
  "dependencies": {
149
- "@abloatai/humans": "0.64.1",
150
- "@abloatai/transaction": "0.64.1",
150
+ "@abloatai/humans": "0.64.3",
151
+ "@abloatai/transaction": "0.64.3",
151
152
  "zod": "^4.4.3"
152
153
  },
153
154
  "peerDependencies": {