@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 +77 -38
- package/package.json +3 -2
- package/runbooks/adopt-existing.md +505 -0
- package/runbooks/greenfield.md +652 -0
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @odla-ai/chapter
|
|
2
2
|
|
|
3
|
-
A foundation for **membership sites**. One `defineChapter({...})` config
|
|
4
|
-
|
|
5
|
-
member area — plus an admin console and
|
|
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"`
|
|
24
|
-
"hub"` is admin-only and CRM-focused. The mode
|
|
25
|
-
route surface; everything else (auth, CRM,
|
|
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`)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.**
|
|
50
|
-
(`sendTemplated`/`isAlreadySent
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
##
|
|
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.
|
|
157
|
-
|
|
158
|
-
components
|
|
159
|
-
|
|
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
|
-
- **
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
438
|
-
`@odla-ai/
|
|
439
|
-
`@odla-ai/
|
|
440
|
-
|
|
441
|
-
|
|
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.
|
|
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.
|