Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .changeset/staff-the-demo-org.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
'hotcrm': patch
---

Give the demo org people, so the position-based mechanisms this app ships stop
resolving to an empty recipient set. A new `pnpm demo:staff` command creates
three non-admin demo users on a local dev server — an NA rep, an EU rep and a
sales manager — assigns the positions they hold, and re-evaluates the sharing
rules so the already-seeded records materialise grants.

This was the last dark layer of the same gap #621 and #638 closed from the other
two sides. The rules installed and the records matched them, but nobody held any
position, so a matching account still granted nothing: on a fresh install
`sys_user_position` had 0 rows and `sys_record_share` 0 rows, every
position-based sharing rule granted nobody anything, and submitting a deal for
approval opened `opportunity_approval`'s `manager_review` with an empty approver
slate while `lockRecord` held the record with no in-product recovery. After
staffing, the same fresh install shows `north_america_territory` granting its 6
accounts and `europe_territory` its 2, the NA rep reading exactly the six US/CA
accounts she does not own (and neither the two EU ones nor the one account in no
territory), and `manager_review` routing to a real approver.

Who exists and which positions they hold is a table
(`src/sharing/demo-staffing.ts`) — adding a person is adding a row. The two reps
must be users who do NOT own the accounts, because `crm_account` is `private`
and the OWD baseline already admits a record's owner, so a share to the owner
would prove nothing; ownership stays with `demo_bootstrap`'s first user and the
script exits non-zero if that ever stops being true. The other seven positions
stay unstaffed on purpose: a real deployment staffs its own people.

**These accounts can never reach a customer org.** Staffing is a repo script
that drives a local dev server through the platform's own admin endpoints, not
metadata: nothing in the published artifact can create a user, and
`test/demo-staffing.test.ts` fails if a seed dataset or a flow node ever writes
`sys_user`, `sys_member` or `sys_user_position`. (It could not have worked as
metadata either — identity tables are `managedBy: 'better-auth'`, so a row
inserted around that surface has no credential and nobody can sign in as it.)

Two platform behaviours worth carrying forward, both measured here.

`plugin-sharing` materialises rule grants from a record-write hook that returns
early on `isSystem` writes, and every seeded row is written with
`isSystem: true`. So staffing alone leaves `sys_record_share` empty until a rule
is re-evaluated — which the script does, and which a server restart also does
via the boot backfill.

And what bounds a rep to their territory is not their profile. Object-level read
on `crm_account` comes from `member_default`, the additive baseline every org
member holds (ADR-0090 D5); the row set comes from `crm_account` being `private`
(rows are owner-visible only, and the reps own nothing) plus the shares their
territory rule materialised. Each rep also holds `sales_rep`, which widens no
rows — `viewAllRecords: false, readScope: 'own'` is the same depth the baseline
computes — but makes the persona a sales rep rather than a generic member. The
corollary is now a test: a set bound to a position a territory rep holds must
never grant `viewAllRecords` on `crm_account`, or the rep reads all nine
accounts and the territory grant proves nothing while the org still looks
staffed. Fixes #640. Refs #621, #638, #622, #488.
48 changes: 48 additions & 0 deletions docs/MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,54 @@ After any platform upgrade, or whenever Studio shows validation banners:
4. If a banner persists, fix the offending fixture in `src/data/`, not the
designer or the platform.

### 4.1 Staffing the demo org (`pnpm demo:staff`)

A reseeded org has records but no PEOPLE. On a fresh install exactly one user
exists (the dev admin), `demo_bootstrap` claims every seeded record for them,
and `sys_user_position` is empty — so every position-based sharing rule this app
ships grants nobody anything, and `opportunity_approval`'s `manager_review` node
opens with an empty approver slate while `lockRecord` holds the record ([#640]).

```bash
pnpm dev # terminal 1 — leave running
pnpm demo:staff # terminal 2 — once, after the server is up
```

That creates three non-admin demo users (`na.rep@` / `eu.rep@` /
`sales.manager@objectos.ai`, all `demo1234`), assigns their positions, and
re-evaluates every sharing rule so the already-seeded accounts materialise
grants. It is idempotent, self-verifying (non-zero exit if the layers do not
connect) and prints what each user can see:

```
north_america_territory matched= 6 holders=1 granted=6
europe_territory matched= 2 holders=1 granted=2
na.rep@objectos.ai sees 6 account(s) · countries: [CA, US]
eu.rep@objectos.ai sees 2 account(s) · countries: [DE, UK]
```

Who exists and which positions they hold is a table —
[`src/sharing/demo-staffing.ts`](../src/sharing/demo-staffing.ts). Adding a
person is adding a row.

Three things worth knowing before changing any of it:

- **It is a script, not metadata, on purpose.** A real deployment must install
none of these accounts, so nothing in the published artifact may be able to
create a user. `test/demo-staffing.test.ts` fails if a seed dataset or a flow
node ever writes `sys_user` / `sys_member` / `sys_user_position`.
- **Re-evaluating the rules is not optional.** `plugin-sharing` materialises
grants from a record-write hook that returns early on `isSystem` writes, and
every seeded row is written with `isSystem: true`. Staffing alone therefore
leaves `sys_record_share` empty until a rule is re-evaluated (a server restart
does it too, via the boot backfill).
- **The reps must not own the accounts.** `crm_account` is `private`, so the OWD
baseline already admits a record's owner — a share to the owner demonstrates
nothing. Ownership stays with `demo_bootstrap`'s first user; the script exits
non-zero if a demo user turns out to own a seeded account.

[#640]: https://github.com/objectstack-ai/hotcrm/issues/640

## 5. Releasing

HotCRM ships as **one** app package (`hotcrm` / `app.objectstack.hotcrm`). Before
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"changeset:status": "changeset status --since=origin/main",
"publish:marketplace": "node scripts/publish-marketplace.mjs",
"publish:marketplace:dry-run": "DRY_RUN=1 node scripts/publish-marketplace.mjs",
"demo:reset": "rm -rf .objectstack/data && pnpm build && echo '✅ DB reset — start the server with: pnpm dev (or pnpm start). Seed data loads on first boot.'"
"demo:reset": "rm -rf .objectstack/data && pnpm build && echo '✅ DB reset — start the server with: pnpm dev (or pnpm start). Seed data loads on first boot, then run: pnpm demo:staff'",
"demo:staff": "tsx scripts/demo-staff.ts"
},
"packageManager": "pnpm@10.33.0",
"dependencies": {
Expand Down
Loading
Loading