@odla-ai/chapter 0.20.1 → 0.20.2

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/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # @odla-ai/chapter
2
2
 
3
- A foundation for **membership sites**. One `defineChapter({...})` config stands up
4
- a full public member site — join/apply → Stripe membership → Google booking
5
- member area — plus an admin console and a CRM, or an **admin-only hub**, on
3
+ A foundation for **membership sites**. One `defineChapter({...})` config resolves
4
+ the shared application engine — join/apply → Stripe membership → Google booking
5
+ member area — plus an admin console and CRM, or an **admin-only hub**, on
6
6
  odla-db + Clerk + [@odla-ai/crm](https://odla.ai/docs/packages/crm) + calendar +
7
- email.
7
+ email. The host still builds the public pages, routing, and brand presentation;
8
+ Chapter supplies their application mechanics.
8
9
 
9
10
  ```sh
10
- npm i @odla-ai/chapter
11
+ npm i --save-exact @odla-ai/chapter@0.20.2
11
12
  ```
12
13
 
13
14
  > **Agentic experiment.** Built and maintained by AI agents from bounded runbooks
@@ -17,12 +18,32 @@ npm i @odla-ai/chapter
17
18
  > and leader → follower record delivery ship. APIs may still tighten before 1.0;
18
19
  > pin the package and run schema/route contract tests in every site.
19
20
 
21
+ ## Agent runbooks
22
+
23
+ The API examples below are not a website-build plan. An LLM or coding agent must
24
+ choose and read one of these version-matched runbooks from the installed npm
25
+ package before changing a site:
26
+
27
+ - **Empty repo or a genuinely new product:** read
28
+ [`runbooks/greenfield.md`](./runbooks/greenfield.md) completely. It covers the
29
+ brand brief, public information architecture, Chapter configuration,
30
+ member/admin surfaces, Cloudflare/ODLA setup, and visual + journey acceptance.
31
+ - **Existing or deployed site:** read
32
+ [`runbooks/adopt-existing.md`](./runbooks/adopt-existing.md) completely. It
33
+ preserves the existing product, URLs, visual identity, data, auth, jobs, and
34
+ rollback boundary while Chapter becomes the primary implementation.
35
+
36
+ Do not combine the two flows. A provisioned canary is not a completed adoption,
37
+ and a greenfield build does not need migration machinery. Both runbooks keep
38
+ secrets out of source and require development proof before production.
39
+
20
40
  ## The shape
21
41
 
22
42
  - **One config, two profiles.** `defineChapter()` validates at import and returns
23
- a resolved engine. `mode: "chapter"` is the full public member site; `mode:
24
- "hub"` is admin-only and CRM-focused. The mode gates the member/join/payment
25
- route surface; everything else (auth, CRM, chrome, provisioning) is shared.
43
+ a resolved engine. `mode: "chapter"` enables the complete public-member
44
+ application profile; `mode: "hub"` is admin-only and CRM-focused. The mode
45
+ gates the member/join/payment route surface; everything else (auth, CRM,
46
+ chrome, provisioning) is shared.
26
47
  `chapter.brand` and `chapter.network` are resolved from this same config, so
27
48
  the browser UI does not need a second identity or follower registry.
28
49
  - **The worker is the package.** `chapterWorker({ chapter })` is the whole
@@ -31,11 +52,15 @@ npm i @odla-ai/chapter
31
52
  `/api/admin/network/{targets,push}`. Chapter mode adds the public
32
53
  member surface (`/api/join-config`, `/api/applications`,
33
54
  `/api/schedule/{slots,book}`, `/api/payments/subscription`,
34
- `/api/webhooks/stripe`) and the admin operational routes (`/api/admin/*`), then
35
- falls back to static assets. Your `src/worker.ts` is ~3 lines. Observability is
36
- a host concern wrap it with `withObservability` from `@odla-ai/o11y`.
37
- Everything brand-specific (prices, policy copy, email templates, scheduling
38
- rules) is read at runtime from a single odla-db `groups` row, never hardcoded.
55
+ `/api/webhooks/stripe`). The `/api/admin/*` handlers are registered in both
56
+ modes; expose only the sections compatible with the selected profile and its
57
+ provisioned namespaces. The Worker then falls back to static assets. Your
58
+ `src/worker.ts` is ~3 lines. Observability is a host concern — wrap it with
59
+ `withObservability` from `@odla-ai/o11y`.
60
+ Operational values that owners may change (prices, policy copy, email
61
+ templates, scheduling rules) are read at runtime from a single odla-db
62
+ `groups` row. Brand identity and build-time tokens remain in the checked-in
63
+ Chapter config.
39
64
  - **A route seam, not a black box.** `chapterWorker({ chapter, routes })` runs
40
65
  your handlers *before* the built-ins (add routes, or override/alias a path),
41
66
  each receiving the same context the built-ins get. The worker entry also exports
@@ -46,13 +71,15 @@ npm i @odla-ai/chapter
46
71
  *or* an odla-db `admins` allowlist, plus a read-only `superAdmins` tier —
47
72
  selected by `auth.source`, defaulting per mode. Escalation guards
48
73
  (`canChangeRole`) are package-enforced.
49
- - **Correctness is packaged, not per-site.** Exactly-once email
50
- (`sendTemplated`/`isAlreadySent`), a non-prod delivery fail-safe
51
- (`planDelivery`), status-never-backwards (`canTransition`), Stripe webhook
52
- integrity (`verifyStripeSignature`) with the webhook as the authoritative writer
53
- of paid/refunded, one-subscription-per-application idempotency, meetings-as-
54
- canonical booking (a rebooking *reschedules* the event, preserving the Meet
55
- link), Google-edit adoption (`reconcileMeetings`), and a one-way CRM projection.
74
+ - **Correctness is packaged, not per-site.** Replay-deduplicated email
75
+ (`sendTemplated`/`isAlreadySent`; concurrent sends still need a serialized
76
+ outbox/provider idempotency for a true exactly-once guarantee), a non-prod
77
+ delivery fail-safe (`planDelivery`), status-never-backwards (`canTransition`),
78
+ Stripe webhook integrity (`verifyStripeSignature`) with the webhook as the
79
+ authoritative writer of paid/refunded, one-subscription-per-application
80
+ idempotency, meetings-as-canonical booking (a rebooking *reschedules* the
81
+ event, preserving the Meet link), Google-edit adoption (`reconcileMeetings`),
82
+ and a one-way CRM projection.
56
83
  - **Provisioning is declarative.** `createChapterIntegration(chapter)` composes
57
84
  the crm namespaces + the chapter namespaces (`applications`, `groups`,
58
85
  `meetings`, `emailLog`, plus the auth tables) + a guarded group-row seed. Drop
@@ -103,7 +130,7 @@ console instead of failing silently. To brand it, pass `brand.palette` /
103
130
  override the shared visual grammar in both modes. When the `chapter` prop is
104
131
  present, `ChapterAdmin` installs those brand tokens itself.
105
132
 
106
- ## Quick start
133
+ ## API quick start
107
134
 
108
135
  ```ts
109
136
  // src/chapter.config.mjs
@@ -153,11 +180,10 @@ export default {
153
180
  };
154
181
  ```
155
182
 
156
- That is the reusable application shell. The follower's public pages remain
157
- site-owned: start from a reference design and `@odla-ai/ui` marketing
158
- components, then change the wordmark, palette, copy, imagery, and investment
159
- thesis. Do not fork auth, admin routing, CRM, payment, booking, or account logic
160
- to achieve a different brand.
183
+ That is the reusable application shell. Public pages remain site-owned: start
184
+ from the approved product and brand brief plus `@odla-ai/ui` marketing
185
+ components. Do not copy a reference site's identity or fork auth, admin routing,
186
+ CRM, payment, booking, or account logic to achieve a different brand.
161
187
 
162
188
  ### Leader → follower delivery
163
189
 
@@ -314,6 +340,10 @@ ordinary static subdirectories. Legacy path links still open, and
314
340
 
315
341
  ## Adopting into an existing site
316
342
 
343
+ For the complete ordered conversion and cutover procedure, use
344
+ [`runbooks/adopt-existing.md`](./runbooks/adopt-existing.md). The notes below
345
+ are the package-specific behavior reference, not a complete adoption plan.
346
+
317
347
  A real conversion (the site this was extracted from) went from a 2,094-line
318
348
  worker to 6 lines and deleted ~2,500 lines. The order that worked:
319
349
 
@@ -419,13 +449,15 @@ These bite silently — a smoke test won't catch them:
419
449
 
420
450
  ### Install + scope notes
421
451
 
422
- - **Your first admin is seeded in odla Studio, by hand — and the email must be
423
- lowercased.** Neither `admins` nor `superAdmins` is ever written by a worker
424
- route or a provisioning seed; that is deliberate, so nothing running in the app
425
- (or injected into a page) can grant admin. The gate looks the email up
426
- lowercased, so a row saved as `Ada@Example.com` matches nothing and the account
427
- silently isn't an admin with no error to tell you. Save it lowercase, then
428
- confirm by signing in, not by looking at the row.
452
+ - **Bootstrap the first admin according to the selected auth source.** Chapter
453
+ mode defaults to Clerk claims: set the first operator's Clerk
454
+ `public_metadata.role` to `admin`. If that operator also needs the
455
+ read-only cross-site tier, add their lowercase email to `superAdmins` in odla
456
+ Studio. Hub mode defaults to table auth: add the first operator's lowercase
457
+ email to `admins` in Studio. Neither allowlist is ever written by a worker
458
+ route or provisioning seed; that is deliberate, so the running app cannot
459
+ grant itself admin. Confirm the result by signing in, not merely by inspecting
460
+ the row.
429
461
  - **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
430
462
  this package's manifest, because only one entry imports it: the worker verifies
431
463
  JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
@@ -434,11 +466,18 @@ These bite silently — a smoke test won't catch them:
434
466
  `@odla-ai/auth-clerk/invitations` when you want to send your own branded
435
467
  invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
436
468
  half, so prefer the narrower entry.
437
- - **Known-good version set** (installs clean, no flags): `@odla-ai/chapter` 0.20.0,
438
- `@odla-ai/ui` 0.10.0, `@odla-ai/crm` 0.1.4, `@odla-ai/db` 0.6.6,
439
- `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1, CLI 0.17.1, plus `jose` and
440
- react (or `preact` + `preact/compat`). chapter is the package that pulls the
441
- others together, so this is the line to check when an install complains.
469
+ - **Known-good application set** (installs clean, no flags):
470
+ `@odla-ai/chapter` 0.20.2, `@odla-ai/ui` 0.10.0, `@odla-ai/crm` 0.1.4,
471
+ `@odla-ai/db` 0.6.6,
472
+ `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1,
473
+ `@odla-ai/auth-clerk` 0.4.0, `@odla-ai/o11y` 2.2.2, `jose` 6.2.3, React
474
+ 19.2.7, and `react-dom` 19.2.7.
475
+ - **Known-good React host toolchain:** `@odla-ai/cli` 0.17.1,
476
+ `@odla-ai/security` 0.3.1, `@cloudflare/workers-types` 4.20260702.1,
477
+ `@types/react` 19.2.17, `@types/react-dom` 19.2.3,
478
+ `@vitejs/plugin-react` 6.0.3, TypeScript 6.0.3, Vite 8.1.4, Vitest 4.1.10,
479
+ and Wrangler 4.107.0. The greenfield runbook standardizes on this exact React
480
+ matrix; a Preact host needs its own tested compatibility set.
442
481
  - **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
443
482
  the peer conflict that tells you a pair is unsupported. If you need it, find out
444
483
  why first.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odla-ai/chapter",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "description": "A leader/follower foundation for branded membership sites: shared CRM, admin, auth, payments, booking, and explicit record delivery from one defineChapter config.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://odla.ai/docs/packages/chapter",
@@ -40,7 +40,8 @@
40
40
  },
41
41
  "files": [
42
42
  "dist",
43
- "README.md"
43
+ "README.md",
44
+ "runbooks"
44
45
  ],
45
46
  "sideEffects": false,
46
47
  "engines": {
@@ -0,0 +1,505 @@
1
+ # Adoption runbook: convert an existing site to Chapter
2
+
3
+ Audience: an LLM or coding agent converting an existing or deployed website,
4
+ Worker, database, auth flow, or operating membership product.
5
+
6
+ Read this file completely before changing the application. Also read the
7
+ installed package `README.md`, `package.json` exports, and `dist/*.d.ts`. The
8
+ existing product stays live until measured Chapter parity and cutover are
9
+ approved.
10
+
11
+ If the repository is empty and no operating state exists, use
12
+ [`greenfield.md`](./greenfield.md).
13
+
14
+ ## Completion means conversion, not installation
15
+
16
+ The following are canary evidence only:
17
+
18
+ - installing `@odla-ai/chapter`;
19
+ - writing `defineChapter()` or rendering `ChapterAdmin`;
20
+ - provisioning an ODLA tenant;
21
+ - serving a login screen or one Chapter API route;
22
+ - deploying a parallel Worker with copied sample data.
23
+
24
+ Adoption is complete only when the real product routes and journeys use Chapter
25
+ as the primary implementation, live data is reconciled and continuously
26
+ handled, production reads and writes have crossed explicit gates, and both
27
+ runtime and data rollback remain proven.
28
+
29
+ ## Non-negotiable boundaries
30
+
31
+ 1. Never mutate, reset, delete, detach, or repoint current production resources
32
+ during development adoption. Preserve Workers, Pages projects, domains,
33
+ databases, R2/KV, queues, Workflows, cron, webhooks, Clerk, Stripe, email,
34
+ Google calendars, source repositories, and immutable history.
35
+ 2. Never print or commit secrets. Dev and production use separate credentials
36
+ and explicitly targeted Worker environments.
37
+ 3. Preserve existing public URLs, content, SEO, header/navigation, responsive
38
+ layout, theme behavior, accessibility, and branded assets unless the human
39
+ approves a product change. Framework replacement is not redesign authority.
40
+ 4. Preserve data semantics before deleting code. Chapter schema, routes, sends,
41
+ auth, account, payment, and scheduling defaults are hypotheses until parity
42
+ tests prove them.
43
+ 5. Runtime rollback and data rollback are different. Restoring an old Worker
44
+ cannot recover writes accepted after Chapter became authoritative.
45
+ 6. No silent fallback. A Chapter-primary failure is explicit and measured; a
46
+ legacy fallback, if approved, is named, time-bounded, and visible.
47
+ 7. Keep ODLA rules default-deny and every admin/provider mutation
48
+ server-mediated.
49
+
50
+ ## Phase 0 — Freeze the operating baseline
51
+
52
+ Create or extend root `MIGRATION.md`; never replace its history. Record state
53
+ `inventoried` only after the human approves the baseline.
54
+
55
+ ### Inventory the product
56
+
57
+ - every public, member, admin, API, webhook, health, asset, feed, and 404 route;
58
+ - current production/dev origins, Worker/Pages names and version ids, routes,
59
+ DNS, compatibility settings, build automation, and rollback versions;
60
+ - D1/SQL/ODLA/KV/R2/files and every reader/writer;
61
+ - queues, Workflow definitions, cron, provider webhooks, retry/outbox behavior;
62
+ - Clerk app/instances, JWT claims, roles/allowlists, redirects, and first-admin
63
+ process;
64
+ - Stripe products/prices/webhooks/idempotency, Google calendars, Email Service;
65
+ - current schema/rules, row counts, earliest/latest timestamps, natural keys,
66
+ relations, projections, and owner-edited fields;
67
+ - public/private content boundaries and every raw HTML/Markdown/inline-JSON
68
+ render sink;
69
+ - telemetry, freshness/error alerts, deploy commands, and CI auto-deploys.
70
+
71
+ ### Freeze visual and behavior evidence
72
+
73
+ Capture representative production screenshots and measured layouts at 390,
74
+ 768, 1280, and 1440 pixels in every supported theme. Record:
75
+
76
+ - header height, wordmark/nav alignment, responsive breakpoints, page geometry;
77
+ - form fields/labels/errors, Clerk containment, tables/cards/rails, loading,
78
+ empty, error, unauthorized, and 404 states;
79
+ - full public route/link inventory, titles, descriptions, canonical URLs,
80
+ structured data, robots/sitemap/feed behavior;
81
+ - join, payment, booking, member, admin, CRM, email, and refund journeys.
82
+
83
+ Do not rely on visual memory. These fixtures are acceptance authority until a
84
+ human explicitly approves a redesign.
85
+
86
+ ### Define migration budgets
87
+
88
+ Record accepted count/hash differences, maximum sync lag/newest-data age, route
89
+ and status parity, latency/error budget, visual tolerances, identity merge
90
+ policy, deterministic synthetic user/application ids, and an observation
91
+ window. Record both rollback plans before any Chapter write.
92
+
93
+ Maintain a migration matrix:
94
+
95
+ | Track | Legacy authority | Chapter/ODLA dev target | Current reads | Current writes | Evidence/freshness | Rollback | State |
96
+ | --- | --- | --- | --- | --- | --- | --- | --- |
97
+ | runtime/bindings | | | | | | | |
98
+ | membership data/CRM | | | | | | | |
99
+ | jobs/webhooks/outboxes | | | | | | | |
100
+ | Clerk/auth | | | | | | | |
101
+ | Stripe/payments | | | | | | | |
102
+ | calendar/booking | | | | | | | |
103
+ | email | | | | | | | |
104
+ | public/member/admin UI | | | | | | | |
105
+ | o11y/alerts | | | | | | | |
106
+
107
+ Allowed states: `not-started`, `canary`, `parallel`, `verified`,
108
+ `primary-dev`, `primary-prod`, `retired`, or `n/a` with a reason.
109
+
110
+ ## Phase 1 — Model Chapter without deleting anything
111
+
112
+ Install the version-matched agent guidance and inspect the automation boundary:
113
+
114
+ ```sh
115
+ npx @odla-ai/cli@0.17.1 setup
116
+ npx @odla-ai/cli@0.17.1 capabilities --json
117
+ ```
118
+
119
+ Pin the exact known-good package matrix from the installed Chapter README.
120
+ For this runbook, first require
121
+ `npm view @odla-ai/chapter@0.20.2 version` to succeed. Install every runtime,
122
+ toolchain, CLI, and security package with the exact versions in that README;
123
+ never use `latest`, a range, or `--legacy-peer-deps`.
124
+
125
+ Write `src/chapter.config.mjs` from the inventory. Make every behavior decision
126
+ explicit:
127
+
128
+ - `mode`, `services`, complete light/dark brand tokens;
129
+ - price/currency/interval and owner-approved policy copy;
130
+ - email addresses/templates/debug redirect and send trigger;
131
+ - scheduling/timezone and pipeline stages/bookable/approvable subsets;
132
+ - exact application fields, caps, disclaimer requirement, Clerk-visible
133
+ `profileFields`, and CRM-projected `crmFields`;
134
+ - claim/table auth, ladder/super-admin tier;
135
+ - account side effect (`none`, `create`, or emailed `invite`);
136
+ - approve/refund policy and any network targets/field allowlists.
137
+
138
+ ### Schema/rule parity gate
139
+
140
+ Before deleting old schema/rules/provisioning:
141
+
142
+ 1. freeze the legacy schema and rules as reviewed test fixtures;
143
+ 2. normalize only approved representational differences;
144
+ 3. compare `createChapterIntegration(chapter)` namespaces, attrs, links, rules,
145
+ and seeds to the fixtures;
146
+ 4. fail on missing fields, widened rules, cardinality changes, renamed natural
147
+ keys, new client-readable data, or unapproved seed differences;
148
+ 5. record every intentional delta and migration mapping in `MIGRATION.md`.
149
+
150
+ `createChapterIntegration()` inserts the group seed only when absent. Existing
151
+ runtime owner edits do not update from later config changes. Compare the current
152
+ group row field by field, classify ownership, and write an explicit migration
153
+ for approved differences.
154
+
155
+ ### Behavior-delta gate
156
+
157
+ At minimum audit:
158
+
159
+ - services default (`db`, `calendar`, `o11y`);
160
+ - per-field and request-body caps;
161
+ - account default `none` and outbound effects of `create`/`invite`;
162
+ - stock `JoinIsland` generates a fresh `submissionId` on each invocation, so an
163
+ ambiguous retry after a lost response is not idempotent even though a
164
+ concurrent click is disabled;
165
+ - Clerk-client-readable `public_metadata.profile`;
166
+ - application-to-CRM projection fields;
167
+ - required disclaimer acknowledgement and `disclaimerAckAt`;
168
+ - initial/bookable/approvable stages and never-backwards transitions;
169
+ - when each lifecycle email fires;
170
+ - Chapter recognizes only the literal `prod` environment as live mail; the
171
+ CLI-valid alias `production` follows the non-production fail-safe;
172
+ - approve promotion/email and refund/subscription behavior;
173
+ - Chapter route names and member/admin routing;
174
+ - Chapter's insert-only group seed and runtime-edited fields;
175
+ - payment readiness versus actual webhook readiness;
176
+ - calendar's HTTP-200 `schedulingReady: false` degradation.
177
+
178
+ The stock `applications` namespace is fixed. Its configurable form strings are
179
+ limited to `firstName`, `lastName`, `email`, `referral`, `referralName`,
180
+ `whoYouAre`, `linkedin`, `message`, `phone`, and `state`; `focus` and
181
+ `disclaimerAck` are special. An unknown configured field can pass config
182
+ validation and then fail at the database. Preserve a host schema/submit route
183
+ for every legacy field that cannot be mapped without loss.
184
+
185
+ The current `prices.currency` and `prices.interval` values do not control
186
+ storage, join config, formatting, or the Stripe subscription. Treat the live
187
+ Stripe Price as authoritative and make provider-side amount/currency/interval
188
+ equality a cutover gate.
189
+
190
+ Keep one test per accepted behavior decision so a package upgrade cannot change
191
+ the site silently.
192
+
193
+ ### Make the dev descriptor executable
194
+
195
+ Before Phase 2, create or adapt `odla.config.mjs`. A repository with no ODLA
196
+ config may use the installed exact CLI's `init` command as a starting point,
197
+ but the reviewed result must declare:
198
+
199
+ - the existing product's one app id/name and only `envs: ["dev"]`;
200
+ - explicit `chapter.services`;
201
+ - `integrations: [createChapterIntegration(chapter)]` plus any compatible,
202
+ non-overlapping host integrations;
203
+ - the intended dev Clerk publishable key, calendar ids, o11y service, local
204
+ credential paths, and a null/off-route dev link.
205
+
206
+ Do not provision both a legacy inline schema and the Chapter integration for the
207
+ same namespaces. After the schema/rule parity gate passes, freeze the legacy
208
+ descriptor as a test fixture and make the reviewed Chapter integration the
209
+ active dev descriptor. Production configuration remains absent.
210
+
211
+ Create an off-route dev Worker entry using
212
+ `chapterWorker({ chapter, routes: legacyRoutes })`, wrapped with o11y, and an
213
+ explicit `wrangler ... --env dev` target. Keeping all legacy handlers as host
214
+ routes at this point changes no route ownership; it only makes the canary and
215
+ Chapter data surface executable.
216
+
217
+ ### Add a fail-closed migration readiness route
218
+
219
+ The built-in `/api/health` is only `{ "ok": true }`; it does not touch the
220
+ database. Add an admin-only host route or executable command that returns `503`
221
+ until all of these match the acceptance manifest:
222
+
223
+ - literal tenant/app/env and Chapter/package release;
224
+ - expected schema/rules digest and required group/config rows;
225
+ - reconciled counts, natural-key collisions, relation integrity, and newest
226
+ source timestamp within the freshness budget;
227
+ - zero unhandled quarantines and bounded/zero pending outbox work;
228
+ - healthy continuous sync and the Chapter-primary-to-legacy rollback mirror.
229
+
230
+ Return only redacted counts/digests. This gate is allowed to fail during initial
231
+ canary work; it must be mechanically green before switching either reads or
232
+ writes, and after every rollback recovery.
233
+
234
+ ## Phase 2 — Create an isolated dev canary
235
+
236
+ Run:
237
+
238
+ ```sh
239
+ npx odla-ai doctor
240
+ npx odla-ai provision --dry-run
241
+ ```
242
+
243
+ The dev topology must be distinct from production:
244
+
245
+ - `envs: ["dev"]`, tenant `<app-id>--dev`;
246
+ - explicit dev Worker name and `wrangler ... --env dev`;
247
+ - no production route, DNS, database binding, queue, Workflow, cron, webhook,
248
+ Git writer, Stripe live resource, email recipient, or Google write authority;
249
+ - `debugEmail` enabled and Stripe test mode only;
250
+ - dev Clerk `pk_test_...` from the intended workspace/application;
251
+ - o11y wrapped around the real canary handler with safe low-cardinality labels.
252
+
253
+ Show the dry-run target, schema/rules, services, link, and secret recipients.
254
+ After the human approves the device code:
255
+
256
+ ```sh
257
+ npx odla-ai provision --email <existing-odla-account> --write-dev-vars --push-secrets
258
+ npm run build
259
+ npx wrangler deploy --env dev
260
+ npx odla-ai smoke --env dev
261
+ ```
262
+
263
+ If credential transfer alone must be retried, use the non-rotating
264
+ `npx odla-ai secrets push --env dev`; do not reprovision or rotate by default.
265
+ Verify the deployed Chapter health marker, a database-backed read, and an
266
+ unauthenticated private-route rejection. Record the URL and Cloudflare version
267
+ id. A canary advances matrix rows only to `canary`; the migration-readiness
268
+ route is expected to remain `503` until Phase 3 evidence passes.
269
+
270
+ ## Phase 3 — Adopt data with replay and reconciliation
271
+
272
+ Skip historical backfill only when the human-approved inventory proves the
273
+ site has no operating data. Otherwise:
274
+
275
+ 1. Write a checked-in mapping for every legacy table/collection/file and
276
+ relation: natural key, target namespace, attrs, omissions/nulls, edge
277
+ direction, ownership, tombstone/deletion behavior, and provenance.
278
+ 2. Freeze a source snapshot or bounded high-water mark. Record counts,
279
+ earliest/latest timestamps, and a digest/immutable id.
280
+ 3. Backfill in bounded deterministic chunks using stable mutation ids.
281
+ Persist checkpoints and a rejection ledger; each input is written, skipped
282
+ with reason, or failed/retryable.
283
+ 4. Replay from an earlier checkpoint and prove no duplicate applications,
284
+ people, companies, meetings, email log entries, relations, or side effects.
285
+ 5. Generate a redacted reconciliation report: counts, required attrs, duplicate
286
+ natural keys, dangling relations, identity collisions, status/price/role
287
+ differences, and newest timestamps.
288
+ 6. Preserve human-owned fields. Automated projection omits fields it does not
289
+ own rather than reading and writing stale copies.
290
+
291
+ ### Continuous operation
292
+
293
+ Choose one durable event boundary for production-shaped changes. New legacy
294
+ applications, payments, role changes, meetings, CRM edits, and emails must reach
295
+ the dev Chapter projection through a measured outbox/queue/replay process.
296
+ Record oldest pending age, attempts, quarantine, and source-to-target lag.
297
+
298
+ Backfill alone is not conversion. Advance to `continuously-synced` only after
299
+ normal schedules/webhooks are observed and failures replay from durable state.
300
+
301
+ ### Shadow reads
302
+
303
+ For list/detail/search/admin dashboard/member status/join config, read both
304
+ legacy and Chapter in dev while still serving the legacy response. Compare
305
+ normalized status, ids, ordering, counts, fields, relations, price/policy, and
306
+ freshness. Log bounded diffs, never private row bodies.
307
+
308
+ At separate human checkpoints:
309
+
310
+ 1. switch dev reads to Chapter and make failures explicit;
311
+ 2. switch accepted dev writes to Chapter;
312
+ 3. durably mirror Chapter-primary writes back to the legacy rollback authority;
313
+ 4. simulate ODLA unavailable and rollback-mirror unavailable.
314
+
315
+ Every authenticated/admin mutator must use this authority contract or be
316
+ disabled. A separate Chapter demo route does not satisfy primary-dev.
317
+
318
+ ## Phase 4 — Adopt the Worker and routes in safe order
319
+
320
+ Do not replace schema, worker, routes, and UI in one unreviewable change.
321
+
322
+ 1. Re-assert that the active dev descriptor is the reviewed
323
+ `createChapterIntegration(chapter)` from Phase 1 and that the legacy
324
+ provisioner remains only as a parity fixture.
325
+ 2. Start from the off-route `chapterWorker({ chapter, routes })` canary. Host
326
+ routes run before built-ins and receive Chapter's existing context
327
+ (`verifyUser`, `makeDb`, `roleFor`, `isAdmin`); do not verify JWTs twice.
328
+ 3. Initially keep every bespoke endpoint as a host route. Alias legacy URLs to
329
+ Chapter behavior instead of breaking clients.
330
+ 4. Compare built-in and legacy behavior route by route; only then remove a host
331
+ route that Chapter fully owns.
332
+ 5. Keep static fallback and API handling explicit so unknown APIs cannot fall
333
+ through to a successful SPA document.
334
+
335
+ Test public/private method policies, body limits, idempotency, auth 401 vs 403,
336
+ safe redirects, webhook signatures/replay, scheduling conflict,
337
+ replay-deduplicated and concurrent email sends, and unavailable-provider
338
+ degradation. Chapter's email log prevents a later replay after success; it does
339
+ not serialize two concurrent sends, so preserve a stricter existing outbox or
340
+ provider-idempotency contract.
341
+
342
+ ## Phase 5 — Adopt UI without losing the product
343
+
344
+ ### Preserve first
345
+
346
+ Render the Chapter component beside the current implementation in a dev-only
347
+ fixture. Compare markup/behavior and the Phase 0 screenshots before deletion.
348
+
349
+ - Import `@odla-ai/ui/themes/salt/app.css` before host CSS.
350
+ - Map the legacy design tokens into `brand.palette` and `paletteDark`; do not
351
+ carry an unbounded parallel theme system.
352
+ - Public pages remain site-owned. Preserve their content hierarchy, imagery,
353
+ URLs, SEO, and responsive geometry.
354
+ - Replace join orchestration with `JoinIsland`, member behavior with
355
+ `MembersArea`, and admin with `<ChapterAdmin chapter={chapter}/>` only after
356
+ field/auth/action parity.
357
+ - The host still supplies member CSS and the Clerk wrapper around
358
+ `MembersArea`; Chapter does not ship a whole public stylesheet.
359
+ - Scope host form styles. Bare `input`, `button`, `select`, `textarea`, `label`,
360
+ or global box-model rules must not corrupt Clerk/vendor DOM.
361
+
362
+ ### Required visual acceptance
363
+
364
+ At 390, 768, 1280, and 1440 pixels in every theme, compare:
365
+
366
+ - identical product header/navigation position between public/member/admin;
367
+ - wordmark, text baseline, theme control, active state, and hover/focus;
368
+ - join fields/errors, payment and slot picker, Clerk sign-in containment;
369
+ - member cards/actions and every admin section/table/drawer;
370
+ - no clipped labels, card text escape, accidental document scroll, or
371
+ horizontal overflow;
372
+ - loading, empty, error, unauthorized, 404, and reduced-motion states.
373
+
374
+ The human approves deployed screenshots/browser behavior. Passing CSS tests is
375
+ not visual parity.
376
+
377
+ ## Phase 6 — Provider and authorization parity
378
+
379
+ ### Clerk
380
+
381
+ Use the same Clerk application/workspace. Chapter claim mode requires session
382
+ claims for both email and role; hub table mode uses lowercase `admins` rows.
383
+ Prove existing allowed/forbidden users, role ladder, super-admin behavior,
384
+ malformed/expired sessions, sign-out, and return targets.
385
+
386
+ If the legacy verifier enforces JWT `aud` or `azp`, keep that host verifier in
387
+ front of Chapter. This Chapter release verifies issuer but does not expose or
388
+ enforce the CLI-provisioned Clerk audience. Add wrong-audience and
389
+ wrong-authorized-party fixtures; do not cut over auth until the legacy contract
390
+ still rejects them.
391
+
392
+ When Chapter needs server-side account/role operations, set the named
393
+ app-readable `clerk_secret_key` through:
394
+
395
+ ```sh
396
+ npx odla-ai secrets set clerk_secret_key --env dev --stdin
397
+ ```
398
+
399
+ Do not substitute `secrets set-clerk-key`; it writes the platform-reserved
400
+ `$clerk_secret`, which Chapter cannot read under the named key. The generic
401
+ command does not validate Clerk instance prefixes: independently confirm dev
402
+ uses `sk_test_`. Never put `sk_live_` in dev.
403
+
404
+ ### Stripe
405
+
406
+ Use test resources and an isolated dev webhook. Store `stripe_secret_key` and
407
+ `stripe_webhook_secret` write-only; reconcile group `stripePublishableKey` and
408
+ `stripePriceId`. Read that exact Stripe Price from Stripe and prove its first
409
+ charge, recurring amount, currency, and interval match the rendered and
410
+ approved contract. `paymentsReady: true` proves neither that equality nor
411
+ webhook readiness; Chapter's built-in money formatter is USD/dollar-specific in
412
+ this release. Preserve a host UI/route for non-USD or mismatched pricing. Test
413
+ successful payment, duplicate webhook, failure, refund, and subscription
414
+ cancellation before switching the UI.
415
+
416
+ ### Calendar and email
417
+
418
+ Complete dev Google booking consent, reconnecting any grant from the retired
419
+ read-only mirror, then prove actual slots, booking, rebooking, cancellation,
420
+ and invitation behavior. `schedulingReady: false` with HTTP 200 is a failed
421
+ readiness gate, not a pass.
422
+
423
+ Use a verified dev sender plus `debugEmail`; prove every lifecycle template,
424
+ recipient redirect, replay deduplication, concurrent-send behavior, and the
425
+ selected account invitation effect. Never let dev send to imported members.
426
+
427
+ ## Phase 7 — Full deployed-dev journey
428
+
429
+ Check in an executable acceptance manifest with:
430
+
431
+ - actual dev URLs and Worker version ids;
432
+ - deterministic synthetic member/application ids;
433
+ - expected routes/statuses, schema/counts/freshness, and Chapter backend marker;
434
+ - expected migration-readiness inputs and a `200` only when each is green;
435
+ - expected account, payment, booking, email, CRM, and admin outcomes;
436
+ - expected o11y service/release and safe trace;
437
+ - replay and failure-injection outcomes;
438
+ - visual viewport/theme fixtures.
439
+
440
+ Run the real journey:
441
+
442
+ 1. browse every public route/deep link and verify SEO/404/method behavior;
443
+ 2. submit the configured join form, including consent;
444
+ 3. execute Stripe test payment and authoritative webhook;
445
+ 4. book/rebook the intro call and receive the debug-routed email;
446
+ 5. sign in as provisional/member/admin and use the real member/admin pages;
447
+ 6. perform every admin mutation family and verify Chapter-primary audit plus
448
+ legacy rollback mirror;
449
+ 7. replay the event and prove no duplicate row, charge, booking, account, or
450
+ email;
451
+ 8. inject ODLA/provider/mirror failure, observe explicit failure/retry, recover;
452
+ 9. verify freshness/continuous-sync budget and visual parity;
453
+ 10. require the fail-closed migration-readiness gate to return `200`;
454
+ 11. follow safe o11y evidence across the real Worker.
455
+
456
+ `doctor`, unit tests, build, CLI smoke, a login page, and a human-looking admin
457
+ screen are necessary but not sufficient.
458
+
459
+ ## Phase 8 — Production parallel run and cutover
460
+
461
+ Do not add production until the human approves the full dev report and both
462
+ rollback plans.
463
+
464
+ 1. Add only the literal `prod` environment using the production instance of the
465
+ same Clerk app, Stripe live resources, approved email sender, calendars,
466
+ links, and distinct secret targets. Do not use the CLI-valid alias
467
+ `production`: Chapter would suppress or debug-redirect lifecycle mail.
468
+ 2. Run production build/tests/security scan, `doctor`, and
469
+ `provision --dry-run`; inspect CI/Workers Builds so merging cannot
470
+ auto-deploy the legacy production target unexpectedly.
471
+ 3. After explicit approval, provision with `--yes` and deploy a production
472
+ Chapter candidate off-route. Record its version id.
473
+ 4. Repeat historical backfill, reconciliation, continuous sync, shadow reads,
474
+ provider checks, and a production-safe synthetic journey against production
475
+ source data. Dev data is not a production backfill.
476
+ 5. At one checkpoint, require the fail-closed readiness gate to return `200`,
477
+ then switch production reads. Verify freshness, auth, visual behavior, and
478
+ explicit failure handling while legacy writes continue.
479
+ 6. At a second checkpoint, record a write high-water mark, require readiness
480
+ again, and make Chapter authoritative for writes. Keep the durable legacy
481
+ rollback mirror and continuously reconcile it.
482
+ 7. Abort when write loss, unbounded lag, unexplained diff, auth failure,
483
+ freshness breach, or accepted error budget is crossed.
484
+
485
+ ### Runtime rollback
486
+
487
+ Restore the recorded known-good Worker version plus routes, bindings, triggers,
488
+ and domains. Verify representative routes and health. Keep Chapter resources
489
+ and telemetry for investigation; do not delete them.
490
+
491
+ ### Data rollback
492
+
493
+ Pause/drain new Chapter-primary work; enumerate accepted writes since the
494
+ checkpoint; reverse-project missing writes with stable idempotency keys;
495
+ reconcile queues, counts, timestamps, identities, status, meetings, charges,
496
+ and email; only then report legacy reads healthy. Never reset either store.
497
+
498
+ Keep both rollback capabilities through the approved observation window
499
+ (normally at least 72 hours). Retire each legacy reader, writer, binding,
500
+ webhook, job, and secret only in a separate human-approved change with a fresh
501
+ journey afterward.
502
+
503
+ Close `MIGRATION.md` only when every applicable track is `primary-prod` or
504
+ `retired`, the observation window passes, and the human confirms the existing
505
+ product—not merely a Chapter canary—has been converted.
@@ -0,0 +1,652 @@
1
+ # Greenfield runbook: build a Chapter website
2
+
3
+ Audience: an LLM or coding agent starting from an empty repository.
4
+
5
+ Read this file completely before creating files or running provisioning. This
6
+ runbook ships with the installed `@odla-ai/chapter` version; its `README.md`,
7
+ `package.json` exports, and `dist/*.d.ts` are the version-matched API authority.
8
+ Do not substitute remembered APIs or a reference site's private source.
9
+
10
+ If a site, Worker, database, login, scheduled job, or production route already
11
+ exists, stop and use [`adopt-existing.md`](./adopt-existing.md). A redesign in a
12
+ new directory is not a safe adoption.
13
+
14
+ ## What Chapter supplies—and what the host must build
15
+
16
+ Chapter supplies:
17
+
18
+ - the validated `defineChapter()` engine and generated schema/rules/seed;
19
+ - the Cloudflare Worker API through `chapterWorker()`;
20
+ - the join, payment, booking, member, admin, CRM, and network behavior;
21
+ - member-facing React components and the complete Clerk-gated admin console;
22
+ - the provisioning descriptor consumed by `@odla-ai/cli`.
23
+
24
+ Chapter does **not** generate a website. The host still owns:
25
+
26
+ - `package.json`, Vite/React or Vite/Preact, HTML entries, routing, and CSS;
27
+ - public pages, navigation, copy, imagery, SEO, legal text, and the join form;
28
+ - the member auth wrapper that gives `MembersArea` an authenticated `api`;
29
+ - Wrangler configuration, observability wrapping, provider setup, and tests.
30
+
31
+ Do not reimplement Chapter's worker routes, auth checks, admin shell, CRM,
32
+ payment, booking, account, or lifecycle logic just to achieve a different
33
+ brand. Public presentation is site-owned; application mechanics are
34
+ package-owned.
35
+
36
+ ## Non-negotiable boundaries
37
+
38
+ 1. Pin every ODLA package to an exact tested version. Never use `latest`, `^`,
39
+ `~`, or `--legacy-peer-deps` in a committed Chapter site.
40
+ 2. Build and prove `dev` before adding `prod`. Production provisioning, secret
41
+ transfer, deploy, DNS, billing activation, and real outbound email are human
42
+ checkpoints.
43
+ 3. Never print, paste into chat, or commit a secret. Never read back
44
+ `.dev.vars`, `.odla/credentials.local.json`, or `.odla/dev-token.json`.
45
+ A Clerk `pk_test_...` publishable key is public; Clerk/Stripe secret keys,
46
+ webhook secrets, ODLA credentials, and network share secrets are not.
47
+ 4. Keep database rules default-deny. The Worker uses the admin credential;
48
+ browser code never receives `ODLA_API_KEY`.
49
+ 5. `createChapterIntegration()` seeds the `groups` row only when absent.
50
+ Provisioning again does not overwrite owner-edited runtime copy, prices,
51
+ templates, scheduling, or Stripe fields. Compare config to the live row
52
+ deliberately after the first seed.
53
+ 6. `@odla-ai/cli smoke` proves platform/schema reachability, not the Chapter
54
+ application journey. The route, auth, payment, calendar, email, and visual
55
+ checks below are still required.
56
+ 7. The only live environment name in this runbook is the literal `prod`.
57
+ Although the CLI also recognizes `production` as live, Chapter's email
58
+ fail-safe currently recognizes only `prod`; using `production` suppresses or
59
+ debug-redirects lifecycle mail.
60
+
61
+ ## Phase 0 — Resolve the product and brand brief
62
+
63
+ Write `PRODUCT.md` before code. Record decisions rather than inventing business
64
+ facts. Ask the human only for missing choices that change money, identity,
65
+ outbound communication, authorization, or the public brand.
66
+
67
+ | Decision | Required answer |
68
+ | --- | --- |
69
+ | Profile | `chapter` for a public membership site; `hub` for an admin-only CRM hub |
70
+ | Identity | lowercase app id, public name, approved domains, owner |
71
+ | Audience | who the site serves, why it exists, primary action |
72
+ | Public IA | required pages and exact navigation labels |
73
+ | Brand | wordmark, short badge, voice, approved logo/images, light and dark palettes, typography |
74
+ | Membership | price, currency, interval, refund policy, disclaimer/consent |
75
+ | Application | exact fields, required/optional status, caps, confidential vs Clerk-visible vs CRM-projected |
76
+ | Account side effect | `none`, server-side `create`, or emailed Clerk `invite` |
77
+ | Lifecycle | pipeline, booking eligibility, approval effect, refund rule, email trigger |
78
+ | Operations | notification/reply/debug addresses, scheduling timezone/hours |
79
+ | Providers | intended ODLA account, Clerk app/workspace, Stripe test account, Google calendar, verified email sender |
80
+
81
+ ### Brand procedure
82
+
83
+ 1. Inventory approved logos, marks, photography, illustrations, fonts, and
84
+ usage rights. Do not redraw a logo, scrape a reference site's assets, or
85
+ generate claims/testimonials.
86
+ 2. If no direction exists, present three clearly different one-screen visual
87
+ directions and pause for selection. Do not blend unapproved directions.
88
+ 3. Convert the selected direction into semantic tokens:
89
+ `brand.palette` for light mode and `brand.paletteDark` for dark mode. Define
90
+ background, surface tiers, text tiers, border tiers, accent/soft/strong,
91
+ on-accent, good, warning, and danger—not merely one accent. Use the actual
92
+ `--ui-*` token names from the installed UI theme.
93
+ 4. Use one wordmark, one product header, and one navigation model across public,
94
+ join, member, and admin surfaces. Do not repeat a page/product label in a
95
+ secondary strip when the active navigation already establishes context.
96
+ 5. Build public information architecture before decoration. A Chapter site
97
+ normally needs: home/value proposition, thesis or program, process,
98
+ membership, join/apply, member sign-in/area, admin, privacy, and terms.
99
+ 6. Validate at 390, 768, 1280, and 1440 CSS pixels in light and dark mode.
100
+ Require visible focus, readable contrast, no clipped labels, no horizontal
101
+ document overflow, and contained Clerk cards/forms.
102
+
103
+ The public site must look like the approved organization, not an ODLA demo or a
104
+ renamed reference site.
105
+
106
+ ## Phase 1 — Scaffold the host
107
+
108
+ `@odla-ai/chapter` has no scaffolding binary. The agent creates the host files.
109
+
110
+ Initialize npm, install the exact CLI named by this release, and install its
111
+ agent guidance:
112
+
113
+ ```sh
114
+ npm init -y
115
+ npx @odla-ai/cli@0.17.1 setup
116
+ ```
117
+
118
+ Verify every release target exists, then install the exact known-good React
119
+ host matrix. Do not copy these commands into a pre-release branch where
120
+ `@odla-ai/chapter@0.20.2` is not yet on npm.
121
+
122
+ ```sh
123
+ npm view @odla-ai/chapter@0.20.2 version
124
+ npm install --save-exact \
125
+ @odla-ai/chapter@0.20.2 @odla-ai/ui@0.10.0 \
126
+ @odla-ai/crm@0.1.4 @odla-ai/db@0.6.6 \
127
+ @odla-ai/calendar@0.2.0 @odla-ai/email@0.3.1 \
128
+ @odla-ai/auth-clerk@0.4.0 @odla-ai/o11y@2.2.2 \
129
+ jose@6.2.3 react@19.2.7 react-dom@19.2.7
130
+ npm install --save-dev --save-exact \
131
+ @odla-ai/cli@0.17.1 @odla-ai/security@0.3.1 \
132
+ @cloudflare/workers-types@4.20260702.1 \
133
+ @types/react@19.2.17 @types/react-dom@19.2.3 \
134
+ @vitejs/plugin-react@6.0.3 typescript@6.0.3 \
135
+ vite@8.1.4 vitest@4.1.10 wrangler@4.107.0
136
+ npx odla-ai capabilities --json
137
+ ```
138
+
139
+ The parent `README.md` repeats this version matrix. This runbook standardizes on
140
+ React; a Preact host needs its own exact, tested compatibility matrix. Install
141
+ auth-clerk explicitly: it is deliberately not a Chapter peer because
142
+ worker-only and member-only consumers do not need it.
143
+
144
+ Required host scripts:
145
+
146
+ ```json
147
+ {
148
+ "scripts": {
149
+ "build": "vite build",
150
+ "typecheck": "tsc --noEmit",
151
+ "test": "vitest run",
152
+ "dev": "npm run build && wrangler dev --env dev",
153
+ "deploy:dev": "npm run build && wrangler deploy --env dev"
154
+ }
155
+ }
156
+ ```
157
+
158
+ Recommended minimum tree:
159
+
160
+ ```text
161
+ .
162
+ ├── PRODUCT.md
163
+ ├── package.json
164
+ ├── index.html
165
+ ├── vite.config.mjs
166
+ ├── wrangler.jsonc
167
+ ├── odla.config.mjs
168
+ ├── src/
169
+ │ ├── chapter.config.mjs
170
+ │ ├── worker.ts
171
+ │ └── app/
172
+ │ ├── main.tsx
173
+ │ ├── public.tsx
174
+ │ ├── join.tsx
175
+ │ ├── members.tsx
176
+ │ ├── admin.tsx
177
+ │ └── app.css
178
+ └── test/
179
+ ├── chapter-contract.test.ts
180
+ ├── routes.test.ts
181
+ └── render.test.tsx
182
+ ```
183
+
184
+ Gitignore at least:
185
+
186
+ ```gitignore
187
+ node_modules/
188
+ dist/
189
+ .dev.vars
190
+ .odla/
191
+ ```
192
+
193
+ ## Phase 2 — Write the single Chapter decision record
194
+
195
+ Do not accept safety-sensitive defaults silently. A chapter-mode starting point:
196
+
197
+ ```js
198
+ // src/chapter.config.mjs
199
+ import { defineChapter } from "@odla-ai/chapter";
200
+
201
+ export const chapter = defineChapter({
202
+ id: "example-chapter",
203
+ name: "Example Chapter",
204
+ url: "https://example.org",
205
+ mode: "chapter",
206
+ services: ["db", "calendar", "o11y"],
207
+ brand: {
208
+ badge: "EX",
209
+ wordmark: "Example Chapter",
210
+ tagline: "Approved public positioning goes here.",
211
+ palette: {
212
+ "--ui-bg": "#f6f2e9",
213
+ "--ui-surface": "#fffdf8",
214
+ "--ui-surface-2": "#eee7dc",
215
+ "--ui-text": "#24342b",
216
+ "--ui-text-muted": "#66736b",
217
+ "--ui-text-faint": "#8a958e",
218
+ "--ui-border": "#d8d2c5",
219
+ "--ui-border-strong": "#b8ae9d",
220
+ "--ui-accent": "#b85c38",
221
+ "--ui-accent-strong": "#8c3f24",
222
+ "--ui-accent-soft": "rgba(184, 92, 56, 0.12)",
223
+ "--ui-on-accent": "#ffffff",
224
+ "--ui-good": "#4e765c",
225
+ "--ui-warn": "#9a732b",
226
+ "--ui-danger": "#a9443b",
227
+ },
228
+ paletteDark: {
229
+ "--ui-bg": "#15211b",
230
+ "--ui-surface": "#1d2b24",
231
+ "--ui-surface-2": "#27372e",
232
+ "--ui-text": "#f3eee4",
233
+ "--ui-text-muted": "#aab6ad",
234
+ "--ui-text-faint": "#7d8d83",
235
+ "--ui-border": "#3c4a42",
236
+ "--ui-border-strong": "#58675e",
237
+ "--ui-accent": "#ef8a63",
238
+ "--ui-accent-strong": "#ffad88",
239
+ "--ui-accent-soft": "rgba(239, 138, 99, 0.16)",
240
+ "--ui-on-accent": "#15211b",
241
+ "--ui-good": "#83b58f",
242
+ "--ui-warn": "#d5ad5d",
243
+ "--ui-danger": "#e58278",
244
+ },
245
+ },
246
+ prices: {
247
+ standardCents: 100000,
248
+ foundingDiscountCents: 10000,
249
+ currency: "usd",
250
+ interval: "year",
251
+ },
252
+ policy: {
253
+ disclaimerText: "Replace with approved consent language.",
254
+ refundPolicyText: "Replace with the approved refund policy.",
255
+ trustCopy: "Replace with approved trust and privacy copy.",
256
+ },
257
+ emails: {
258
+ notificationEmail: "owner@example.org",
259
+ replyTo: "hello@example.org",
260
+ debugEmail: "dev-mail@example.org",
261
+ },
262
+ scheduling: {
263
+ slotMinutes: 45,
264
+ days: [1, 2, 3, 4, 5],
265
+ startHour: 9,
266
+ endHour: 17,
267
+ timezone: "America/Los_Angeles",
268
+ minNoticeHours: 24,
269
+ windowDays: 14,
270
+ },
271
+ pipeline: {
272
+ stages: [
273
+ "submitted",
274
+ "paid_pending_vetting",
275
+ "call_scheduled",
276
+ "interviewed",
277
+ "approved",
278
+ "declined",
279
+ "refunded",
280
+ ],
281
+ initial: "submitted",
282
+ bookableFrom: ["submitted", "paid_pending_vetting"],
283
+ approvableFrom: ["interviewed"],
284
+ },
285
+ application: {
286
+ required: ["firstName", "lastName", "email", "whoYouAre", "message"],
287
+ optional: ["linkedin", "phone", "state"],
288
+ defaultMaxLen: 2000,
289
+ maxLen: { firstName: 100, lastName: 100, email: 320, state: 100 },
290
+ bodyCap: 32768,
291
+ requireDisclaimerAck: true,
292
+ profileFields: ["phone", "state"],
293
+ crmFields: ["phone", "state", "linkedin", "whoYouAre", "message"],
294
+ maxArrayLen: 25,
295
+ validateEmail: true,
296
+ },
297
+ auth: {
298
+ source: "claim",
299
+ claim: "role",
300
+ ladder: ["provisional", "member", "admin"],
301
+ superAdmins: true,
302
+ },
303
+ account: "none",
304
+ sends: { adminNotification: "submit" },
305
+ operations: {
306
+ onApprove: { promoteTo: "member", send: "onboardingInvite" },
307
+ refund: { allowedFrom: ["paid_pending_vetting"], cancelSubscription: true },
308
+ },
309
+ });
310
+ ```
311
+
312
+ Replace every example value before provisioning. `account: "invite"` sends a
313
+ real Clerk invitation; `account: "create"` creates an account server-side;
314
+ both require a named app-readable vault secret `clerk_secret_key`. Keep
315
+ `account: "none"` until that side effect is explicitly approved and tested.
316
+
317
+ In this release, `prices.currency` and `prices.interval` record intent but do
318
+ not control the group row, public join config, or Stripe charge. The Stripe
319
+ Price id is authoritative. Phase 6 requires a provider-side amount, currency,
320
+ and recurring-interval equality check before payment is exposed.
321
+
322
+ The built-in application schema is fixed. With stock `/api/applications`, form
323
+ string names are limited to `firstName`, `lastName`, `email`, `referral`,
324
+ `referralName`, `whoYouAre`, `linkedin`, `message`, `phone`, and `state`;
325
+ `focus` and `disclaimerAck` are special fields. Every rendered string input must
326
+ appear in `application.required` or `optional`, and every required field must be
327
+ rendered. `JoinIsland` currently keeps only one `FormData` value per name, so it
328
+ cannot submit an array-valued `focus` control without a tested adapter.
329
+
330
+ Do not put an arbitrary name in `required` or `optional`: config validation does
331
+ not reject it, but the provisioned schema cannot store it. If the approved
332
+ product requires another field, stop and either upgrade Chapter to a release
333
+ with that field or keep a reviewed host-owned schema/submit route; do not claim
334
+ the stock join surface supports it.
335
+
336
+ Snapshot these resolved values in `chapter-contract.test.ts`:
337
+
338
+ - `chapter.services`, `auth`, `pipeline`, `application`, and `operations`;
339
+ - namespace names and default-deny rules;
340
+ - `chapter.groupSeed()` exactly;
341
+ - `createChapterIntegration(chapter, { now: 1_700_000_000_000 })`
342
+ schema/rules/seeds/probes, using the fixed clock because the integration seed
343
+ carries the timestamp.
344
+
345
+ ## Phase 3 — Provisioning and Worker shell
346
+
347
+ ```js
348
+ // odla.config.mjs
349
+ import { createChapterIntegration } from "@odla-ai/chapter";
350
+ import { chapter } from "./src/chapter.config.mjs";
351
+
352
+ export default {
353
+ platformUrl: "https://odla.ai",
354
+ dbEndpoint: "https://db.odla.ai",
355
+ app: { id: chapter.id, name: chapter.name },
356
+ envs: ["dev"],
357
+ services: chapter.services,
358
+ integrations: [createChapterIntegration(chapter)],
359
+ calendar: {
360
+ google: {
361
+ availabilityCalendars: { dev: ["primary"] },
362
+ bookingCalendar: { dev: "primary" },
363
+ },
364
+ },
365
+ o11y: { service: chapter.id },
366
+ auth: { clerk: { dev: "REPLACE_WITH_APPROVED_PK_TEST_KEY" } },
367
+ links: { dev: null },
368
+ local: {
369
+ tokenFile: ".odla/dev-token.json",
370
+ credentialsFile: ".odla/credentials.local.json",
371
+ devVarsFile: ".dev.vars",
372
+ },
373
+ };
374
+ ```
375
+
376
+ ```ts
377
+ // src/worker.ts
378
+ import { chapterWorker } from "@odla-ai/chapter/worker";
379
+ import { withObservability } from "@odla-ai/o11y";
380
+ import { chapter } from "./chapter.config.mjs";
381
+
382
+ export default withObservability(chapterWorker({ chapter }));
383
+ ```
384
+
385
+ Use current Cloudflare agent documentation for syntax, but preserve this
386
+ application contract in `wrangler.jsonc`:
387
+
388
+ - `main` is `src/worker.ts`;
389
+ - `nodejs_compat` is enabled for observability;
390
+ - static assets bind as `ASSETS` from `dist`, never the repository root;
391
+ - dev Worker name and `ODLA_TENANT=<chapter.id>--dev` are explicit;
392
+ - dev vars identify `ODLA_ENDPOINT`, `ODLA_PLATFORM`, `ODLA_APP_ID`,
393
+ `ODLA_ENV=dev`, and a verified `EMAIL_FROM`;
394
+ - the dev `ASSETS` and optional `send_email` bindings are declared inside
395
+ `env.dev` because Wrangler environments do not inherit bindings;
396
+ - no production binding, route, schedule, or credential enters the dev target.
397
+
398
+ Run:
399
+
400
+ ```sh
401
+ npx odla-ai doctor
402
+ npx odla-ai provision --dry-run
403
+ ```
404
+
405
+ Show the resolved tenant, services, schema/rules, auth, links, and Worker target
406
+ to the human. Then start the email-bound device handshake:
407
+
408
+ ```sh
409
+ npx odla-ai provision --email <existing-odla-account> --write-dev-vars
410
+ ```
411
+
412
+ The human reviews the exact device code in ODLA Studio. Google Calendar adds a
413
+ separate browser-owned booking-consent checkpoint for availability plus
414
+ create/reschedule/cancel access. A grant created for the retired read-only
415
+ mirror must be reconnected. No OAuth code or token passes through the agent.
416
+
417
+ ## Phase 4 — Build the four application surfaces
418
+
419
+ Import the application theme once, before host CSS:
420
+
421
+ ```ts
422
+ import "@odla-ai/ui/themes/salt/app.css";
423
+ import "./app.css";
424
+ ```
425
+
426
+ `BrandStyle` adds brand token overrides; it does not provide the theme layer or
427
+ member layout CSS. The host must style `join-form`, fields, payment, slots,
428
+ cards, member rows, responsive navigation, and public content.
429
+
430
+ ### Public pages
431
+
432
+ Build the approved route inventory, not a generic landing page. Use semantic
433
+ ODLA UI components/classes and the same header/footer across routes. Public
434
+ routes must render without waiting for Clerk. Include deliberate loading,
435
+ empty, error, 404, and reduced-motion states.
436
+
437
+ ### Join
438
+
439
+ Fetch `GET /api/join-config`; do not hardcode live price/policy/readiness. Render
440
+ `<JoinIsland config={config}>` with labeled inputs matching the configured field
441
+ names and a required `disclaimerAck` checkbox when configured. Test:
442
+
443
+ - invalid/missing required values;
444
+ - over-cap values and malformed email;
445
+ - missing and accepted disclaimer acknowledgement;
446
+ - concurrent double-click suppression and an ambiguous lost-response retry;
447
+ - `paymentsReady: false` and `true`;
448
+ - `schedulingReady: false`, slot conflict, booking, and completion.
449
+
450
+ `JoinIsland` disables its button while one request is pending, but creates a new
451
+ `submissionId` for every submit invocation. A retry after the server committed
452
+ but the response was lost can therefore create another application. If that is
453
+ unacceptable, stop and fix/upgrade the package or use a reviewed host adapter
454
+ that keeps one stable id across logical retries. Do not label the stock behavior
455
+ fully idempotent.
456
+
457
+ ### Members
458
+
459
+ Use `@odla-ai/auth-clerk` to render signed-out/sign-in and signed-in states. In
460
+ the signed-in child, build one `api(path, init)` function that calls
461
+ `useClerkAuth().getToken()`, adds `Authorization: Bearer <token>`, checks the
462
+ HTTP status, and returns JSON. Pass that function, `signOut`, and
463
+ `applyHref="/join"` to `<MembersArea>`; its package default is `/join.html`.
464
+ Do not put a Clerk secret key or ODLA admin key in browser code.
465
+
466
+ ### Admin
467
+
468
+ ```tsx
469
+ import { ChapterAdmin } from "@odla-ai/chapter/ui/admin";
470
+ import { chapter } from "../chapter.config.mjs";
471
+
472
+ export function AdminPage() {
473
+ return <ChapterAdmin chapter={chapter} basePath="/admin" />;
474
+ }
475
+ ```
476
+
477
+ Keep default query routing (`/admin/?tab=people`) unless the host has a tested
478
+ SPA fallback for every admin subpath. Pass custom sections only when the
479
+ product truly needs additional host-owned operations.
480
+
481
+ The router must serve and reload `/`, every public page, `/join`, `/members/`,
482
+ and `/admin/` without accidentally sending API paths to the SPA.
483
+
484
+ ## Phase 5 — Configure Clerk and bootstrap authority
485
+
486
+ At this step, read Clerk's current agent-facing CLI documentation. Use one
487
+ Clerk application with development and production instances; do not create a
488
+ second application for production.
489
+
490
+ For default chapter claim auth, the session token must include both:
491
+
492
+ ```json
493
+ {
494
+ "email": "{{user.primary_email_address}}",
495
+ "role": "{{user.public_metadata.role}}"
496
+ }
497
+ ```
498
+
499
+ Put only the development publishable key in `auth.clerk.dev`. Set the owner's
500
+ Clerk `public_metadata.role` to `"admin"`. If that owner may grant/change admin
501
+ roles, add their lowercase email to `superAdmins` in ODLA Studio.
502
+
503
+ For `mode: "hub"` with table auth, add the first admin's lowercase email to the
504
+ `admins` namespace instead. A mixed-case row silently fails to match.
505
+
506
+ If account creation/invitation or role writes are enabled, transfer the
507
+ app-readable named secret—not the platform-reserved Clerk sync secret:
508
+
509
+ ```sh
510
+ npx odla-ai secrets set clerk_secret_key --env dev --stdin
511
+ ```
512
+
513
+ Supply the value through stdin or a named environment variable without echoing
514
+ it. `secrets set-clerk-key` writes the reserved `$clerk_secret`; Chapter cannot
515
+ read that key as `clerk_secret_key`. The generic named-secret command does not
516
+ validate Clerk instance prefixes: independently confirm the dev source begins
517
+ with `sk_test_`. Never put `sk_live_` in dev; production requires `sk_live_`,
518
+ the literal `prod` environment, and explicit production approval.
519
+
520
+ Test signed out, provisional, member, admin, super-admin, malformed bearer,
521
+ expired session, forbidden account, sign out, and safe return routing on the
522
+ deployed dev origin.
523
+
524
+ ## Phase 6 — Configure Stripe, calendar, and email
525
+
526
+ ### Stripe test mode
527
+
528
+ 1. Create the approved recurring test product/price.
529
+ 2. Point a Stripe test webhook to
530
+ `https://<dev-origin>/api/webhooks/stripe`.
531
+ 3. Store `stripe_secret_key` and `stripe_webhook_secret` as named dev vault
532
+ secrets through write-only `odla-ai secrets set ... --stdin`.
533
+ 4. In the guarded `groups` row, set `stripePublishableKey` and `stripePriceId`.
534
+ 5. Read that exact Stripe Price from Stripe and compare its first charge,
535
+ recurring amount, currency, and interval with the approved public contract.
536
+ 6. Send and verify a Stripe test event before opening the payment UI.
537
+
538
+ `paymentsReady` checks the publishable key, price id, and Stripe secret key; it
539
+ does not prove the webhook secret works or that the Price matches Chapter's
540
+ displayed cents. The built-in formatter is USD/dollar-specific in this release;
541
+ non-USD or unmatched pricing needs a host UI/route and cannot use the stock
542
+ payment presentation. Never accept money until the provider equality check and
543
+ webhook test prove authoritative paid/refunded state and idempotent replay.
544
+
545
+ ### Google Calendar
546
+
547
+ Complete the human Google booking consent from provisioning, then:
548
+
549
+ ```sh
550
+ npx odla-ai calendar calendars --env dev --json
551
+ npx odla-ai calendar status --env dev --json
552
+ ```
553
+
554
+ Refine the checked-in calendar ids and re-provision. Verify
555
+ `GET /api/schedule/slots` returns `schedulingReady: true` and real bounded test
556
+ slots. It returns HTTP 200 with `schedulingReady: false` when unavailable, so a
557
+ generic HTTP smoke check is insufficient. Book and rebook one synthetic
558
+ applicant and confirm the Google invitation/Meet behavior.
559
+
560
+ ### Email
561
+
562
+ Configure Cloudflare's Email Service binding and an onboarded `EMAIL_FROM`.
563
+ Keep `debugEmail` in every non-production Chapter. Send a test lifecycle email,
564
+ prove it is redirected to the debug address, and confirm the bounded
565
+ `emailLog`. If `account: "invite"` is selected, separately prove a test
566
+ applicant receives a Clerk invitation. Chapter deduplicates a replay after the
567
+ success log exists; it does not serialize two concurrent sends. If true
568
+ exactly-once delivery is required, add a serialized outbox or provider
569
+ idempotency and test the concurrent case.
570
+
571
+ ## Phase 7 — Local and deployed development acceptance
572
+
573
+ Before the first deploy:
574
+
575
+ ```sh
576
+ npm test
577
+ npm run typecheck
578
+ npm run build
579
+ npx odla-security scan . --profile odla --out .odla/security/latest --fail-on high --fail-on-candidates critical
580
+ npx wrangler dev --env dev
581
+ ```
582
+
583
+ Required local contracts:
584
+
585
+ - config/integration snapshots described above;
586
+ - full join form, members gate, admin theme, and public routes render;
587
+ - `/api/health`, `/api/config`, `/api/join-config`, static fallback, and 404;
588
+ - valid/invalid application plus `disclaimerAckAt`;
589
+ - signed-out/member/admin authorization matrix;
590
+ - CRM routes and default-deny behavior;
591
+ - no secret-shaped value in source or client bundle.
592
+
593
+ Review every security candidate; a clean deterministic scan is not proof the
594
+ application is safe. Run the optional hosted ODLA security pass only after the
595
+ human reviews its current disclosure plan and explicitly approves the required
596
+ redacted-source acknowledgement.
597
+
598
+ Deploy only the dev Worker:
599
+
600
+ ```sh
601
+ npm run deploy:dev
602
+ ```
603
+
604
+ Record the actual URL and Cloudflare version id. Put the exact URL in
605
+ `links.dev`, re-run the provision dry run, then deliberately transfer Worker
606
+ secrets:
607
+
608
+ ```sh
609
+ npx odla-ai provision --email <existing-odla-account> --write-dev-vars --push-secrets
610
+ npx odla-ai smoke --env dev
611
+ ```
612
+
613
+ Run a real deployed journey:
614
+
615
+ 1. public home and every navigation/deep link;
616
+ 2. join-config readiness, application submit, test payment, booking;
617
+ 3. Clerk account/invite behavior selected by config;
618
+ 4. provisional and member area;
619
+ 5. admin sign-in, people/CRM, application approval, meeting, billing, email;
620
+ 6. replay/idempotency and one safe error;
621
+ 7. o11y trace with no secret, email, token, or request body;
622
+ 8. visual review at the four target widths in light/dark mode.
623
+
624
+ The human reviews the actual browser result. Source inspection is not visual
625
+ acceptance.
626
+
627
+ ## Phase 8 — Production checkpoint
628
+
629
+ Production is a separate operation. Only after dev acceptance:
630
+
631
+ 1. add `prod` to `envs`, `auth.clerk.prod`,
632
+ `calendar.google.availabilityCalendars.prod`,
633
+ `calendar.google.bookingCalendar.prod`, `links.prod`, and reviewed top-level
634
+ Wrangler prod vars/bindings;
635
+ 2. use the production instance of the same Clerk app and Stripe live-mode
636
+ resources;
637
+ 3. run `doctor`, production build/tests, the installed ODLA security preflight,
638
+ and `provision --dry-run`;
639
+ 4. show the complete production plan, secrets, routes, email, billing, and
640
+ rollback to the human;
641
+ 5. after explicit approval, provision with `--yes`, transfer each prod secret
642
+ through write-only flows, deploy the off-route candidate, and run
643
+ `smoke --env prod`;
644
+ 6. repeat the entire Chapter journey with production-safe synthetic data before
645
+ attaching DNS or accepting real money/mail;
646
+ 7. record the previous Worker version and route configuration as runtime
647
+ rollback. Preserve database rows during rollback; do not reset data.
648
+
649
+ The build is complete only when the checked-in decisions, deployed dev journey,
650
+ provider readiness, responsive visual review, and production rollback evidence
651
+ all exist. A working homepage or passing ODLA smoke alone is not a Chapter
652
+ website.