@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.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.
@@ -96,26 +96,19 @@ The vocabulary is exactly:
96
96
 
97
97
  - `ctx.user` — caller is signed in as an end-user
98
98
  - `ctx.staff: [<role>, ...]` — caller is signed in as staff in one of these roles
99
-
100
- Anything beyond (`any:` disjunction; `owns:`, `withinMinutes:`,
101
- `contains:`, quota predicates) is DRAFT — see
102
- [ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
103
- discipline.
99
+ - `ctx.auth` — caller supplied an adapter-verified credential
100
+ - `ctx.auth.scope: <scope>` credential carries the exact scope
104
101
 
105
102
  ### Adding a new entry is an explicit grammar-revise round
106
103
 
107
- New `x-mantle-bind` values or `ctx.*` predicates do not get added
108
- ad-hoc. They go through the discipline gate documented in
109
- [ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
110
- discipline:
104
+ New `x-mantle-bind` values or `ctx.*` predicates do not get added ad hoc:
111
105
 
112
106
  1. A documented use case showing the existing closed set cannot
113
107
  express the requirement.
114
108
  2. A design pass on what the new value's runtime semantics are
115
109
  (where does it come from? when is it null? what happens at
116
110
  the storage layer?).
117
- 3. A spec doc revision, including the v0.1-vs-DRAFT
118
- classification.
111
+ 3. A spec doc revision.
119
112
  4. Code that updates the validator to accept the new value.
120
113
 
121
114
  This treats the closed set as load-bearing infrastructure, not
@@ -208,10 +201,8 @@ attributes.
208
201
  - New manifest using a value not in the enum: parse error,
209
202
  exact diagnostic shape with `candidates` populated. AI authors
210
203
  fix in one turn.
211
- - New use case wanting an entry not in the enum: open a
212
- grammar-revise discussion per ADR-0001 § Future grammar
213
- discipline. Document the use case in the spec PR; do not
214
- fast-track.
204
+ - New use case wanting an entry not in the enum: document the concrete
205
+ semantics and add validation, runtime behavior, and docs together.
215
206
  - Lookups (e.g. "stamp the team_id"): handler-side TS, not
216
207
  binding metadata. The Procedure handler has the lookup
217
208
  context anyway.
@@ -100,6 +100,8 @@ What it catches:
100
100
  `View.filter` refer only to declared properties of that Schema
101
101
  - `Schema.uniqueIndexes` and `Schema.indexes` contain valid ordered
102
102
  tuples of scalar properties declared in `spec.schema.properties`
103
+ - `Schema.searchableFields` contains unique top-level string properties
104
+ - `Schema.uiSchema.list.filterField` names an indexed operational string enum
103
105
  - `x-mantle-bind: <value>` is in the closed enum
104
106
  - `requires.auth.all` predicates are in the v0.1 vocabulary
105
107
  - `Trigger.source.path` does not collide with another Trigger
@@ -264,7 +266,7 @@ Concrete artifacts today:
264
266
  admin: write a post, update a draft, request publish, list
265
267
  submissions.
266
268
  - Future ops-role verbs that don't exist yet but have an obvious
267
- operator use case ("translate this draft", "schedule for Friday").
269
+ operator use case ("translate this draft", "publish this entry").
268
270
  When added, they land as **new SDK builtin tools** — declared in
269
271
  the MCP catalog source under the closed Day-1 set, not
270
272
  auto-generated from consumer Procedures.
@@ -126,7 +126,7 @@ code string. That was retired because:
126
126
  ### `path` format
127
127
 
128
128
  - For static validation: filesystem path + JSON Pointer fragment,
129
- e.g. `manifests/recent-published.view.yaml#/spec/from`.
129
+ e.g. `manifests/site.yaml#/3/spec/from`.
130
130
  - For a consumer test diagnostic: test file path + assertion location when
131
131
  available, e.g. `tests/handlers/contact.test.ts:42`.
132
132
  - For boot-time: manifest pointer (no on-disk path because boot
@@ -250,7 +250,7 @@ Diagnostic candidate (alternative (d) below) applies to zod —
250
250
  and AI authors see the same structure.
251
251
  - Single source of truth. The interface, code constants, and
252
252
  formatter all live in `@aotter/mantle-spec`; the runtime
253
- package and adapters import them. A future `mantle-netlify`
253
+ package and adapters import them. A future adapter
254
254
  inherits the shape for free.
255
255
 
256
256
  ### Costs
@@ -22,9 +22,10 @@ YAML machinery into the runtime bundle and coupled consumers to Wrangler
22
22
 
23
23
  ## Decision
24
24
 
25
- Consumers own YAML under `manifests/`. The installed `mantle` CLI parses and
26
- validates that YAML, then writes the machine-owned runtime module and handler
27
- types:
25
+ Consumers own exactly `manifests/site.yaml`. The installed `mantle` CLI parses
26
+ and validates its multi-document YAML, then writes the machine-owned runtime
27
+ module and handler types. A missing or differently named file fails before
28
+ generation:
28
29
 
29
30
  ```bash
30
31
  pnpm exec mantle generate
@@ -72,7 +73,7 @@ runtime boot validation covers cross-manifest and registered-handler facts.
72
73
 
73
74
  The external [`aotter/mantle-starters`](https://github.com/aotter/mantle-starters)
74
75
  repository owns the blank project and typed overlays. Materialized projects
75
- carry their own `manifests/`, generated module, handlers, and package scripts;
76
+ carry their own `manifests/site.yaml`, generated module, handlers, and package scripts;
76
77
  they consume the exact packed Core artifact rather than a workspace link.
77
78
 
78
79
  ## Consequences
@@ -128,7 +129,7 @@ runtime.
128
129
 
129
130
  When authoring or reviewing a generated project:
130
131
 
131
- 1. Edit YAML only under `manifests/`.
132
+ 1. Edit only `manifests/site.yaml`; do not create feature-named manifest files.
132
133
  2. Run the installed `pnpm exec mantle generate`.
133
134
  3. Import `manifest` from `.mantle/generated/site.js` into the conventional
134
135
  Worker, or pass it as `CmsConfig.manifests` in low-level composition.
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Deciders**: phsu
8
8
 
9
- **Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends; §"Future grammar discipline" covers the v0.1-vs-DRAFT window this lands in).
9
+ **Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends).
10
10
 
11
11
  ---
12
12
 
@@ -70,7 +70,7 @@ matches the principle that locale is opt-in.
70
70
 
71
71
  The boot validator only inspects the manifest at this layer. It
72
72
  checks shape (every `localized: true` Schema is well-formed, every
73
- `translates:` block resolves) and rejects DRAFT keys; it does **not**
73
+ `translates:` block resolves) and rejects unsupported keys; it does **not**
74
74
  read D1 to confirm that the site actually has any locales configured.
75
75
  That cross-check is deferred to runtime (Layer 3).
76
76
 
@@ -206,29 +206,35 @@ apiVersion: cms.mantle.aotter.net/v1
206
206
  kind: Schema
207
207
  metadata: { name: products }
208
208
  spec:
209
+ title: Products
210
+ localized: false
209
211
  schema:
212
+ type: object
210
213
  properties:
211
214
  slug: { type: string }
212
215
  sku: { type: string }
213
216
  price: { type: number }
214
217
  required: [slug, sku, price]
215
- unique: [slug]
218
+ uniqueIndexes: [[slug]]
216
219
  ---
217
220
  apiVersion: cms.mantle.aotter.net/v1
218
221
  kind: Schema
219
222
  metadata: { name: product-translations }
220
223
  spec:
224
+ title: Product translations
221
225
  localized: true
222
226
  translates:
223
227
  parent: products
224
228
  on: slug
225
229
  schema:
230
+ type: object
226
231
  properties:
227
232
  slug: { type: string }
233
+ locale: { type: string }
228
234
  title: { type: string }
229
235
  description: { type: string }
230
- required: [slug, title]
231
- unique: [[slug, locale]]
236
+ required: [slug, locale, title]
237
+ uniqueIndexes: [[slug, locale]]
232
238
  ```
233
239
 
234
240
  `Schema.spec.translates` declares the parent/child relationship as
@@ -245,9 +251,6 @@ all treat the relation as known structure rather than convention:
245
251
  - Admin UI groups parent + per-locale translation entries together.
246
252
  - Boot validate enforces parent existence and join-field presence in
247
253
  both parent and child JSON Schemas (manifest shape, no D1 reads).
248
- - View executor (when `View.join` lands per the future-grammar
249
- appendix) can auto-join parent + child without per-View
250
- configuration.
251
254
  - AI authoring an entry against the child knows from the manifest
252
255
  that there's a parent it must reference by `slug`.
253
256
 
@@ -262,6 +265,8 @@ Validation rules introduced:
262
265
  - `TRANSLATES_REQUIRES_LOCALIZED` — `translates: ...` declared on a
263
266
  Schema where `localized` isn't `true`. (A non-localized translation
264
267
  table makes no sense.)
268
+ - `TRANSLATES_REQUIRES_CONTENT_FIELD` — the child declares only its join
269
+ field and `locale`, with no locale-specific payload to translate.
265
270
 
266
271
  ## Consequences
267
272
 
@@ -1,8 +1,8 @@
1
1
  # ADR-0011: Adapter port spec
2
2
 
3
- **Status:** Accepted for v0.1.0. Amended 2026-08-11 to remove the rendered-artifact `KvCache` port.
3
+ **Status:** Accepted for v0.1.0. Amended 2026-08-13 to remove the unimplemented adapter stub.
4
4
 
5
- **Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, and 2026-08-11).
5
+ **Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, 2026-08-11, and 2026-08-13).
6
6
 
7
7
  ## Context
8
8
 
@@ -10,8 +10,6 @@
10
10
 
11
11
  `@aotter/mantle-cloudflare` is the only adapter shipping in v0.1.0. It binds the runtime's interfaces against Cloudflare Workers' D1 and ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in + MCP bearer validation. OAuth grant KV remains adapter-owned infrastructure and is not a runtime port.
12
12
 
13
- `@aotter/mantle-netlify` is a v0.2 stub — README only. It exists in the package layout as an engineering forcing function: with N=1 adapter, "adapter-agnostic" silently rots in PR review (a `D1Database` import slips into runtime, then a second, then five). With a second adapter visible in the workspace (even if its impl is a TODO), reviewers have somewhere to point when blocking the slip.
14
-
15
13
  This ADR fixes the contract so:
16
14
  - Future adapter authors have a stable target.
17
15
  - PR reviewers can mechanically check "does this commit add a CF-specific type to runtime?"
@@ -25,7 +23,7 @@ The POC accumulated multiple half-decisions about this seam (POC ADR-0015 docume
25
23
 
26
24
  | Port | Surface |
27
25
  |---|---|
28
- | `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, `approvals`, plus migrations. |
26
+ | `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, plus migrations. |
29
27
  | `AssetServer` | Static-asset serving for the admin SPA. The runtime hands the adapter an asset path + `Request`; the adapter returns a `Response` with the right MIME and caching. |
30
28
 
31
29
  Rendered public artifacts are not a second storage model. D1 stays canonical;
@@ -123,7 +121,7 @@ export interface DatabaseDriver {
123
121
 
124
122
  The runtime never sees `D1Database`, `Pool` (postgres), or any concrete driver. The `prepare` / `batch` shape is intentionally close to D1's surface (which is itself close to the SQLite C API) — that's the smallest common denominator. Adapters wrap their native driver to this shape.
125
123
 
126
- The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres-via-Hyperdrive adapter wraps `pg` to the same shape; a Netlify adapter could wrap Neon, Supabase, or PlanetScale.
124
+ The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres adapter can wrap its driver to the same shape.
127
125
 
128
126
  ### `AssetServer`
129
127
 
@@ -136,7 +134,7 @@ export interface AssetServer {
136
134
  }
137
135
  ```
138
136
 
139
- CF adapter: wraps `env.ASSETS.fetch(req)`. Future: filesystem read, S3+CDN, Netlify static-publish dir.
137
+ CF adapter: wraps `env.ASSETS.fetch(req)`. Other adapters can use a filesystem or object storage.
140
138
 
141
139
  The admin SPA itself lives in `@aotter/mantle-admin-ui` as a pre-built `dist/`. The adapter binds `AssetServer` to whatever serves that `dist/`; the runtime knows nothing about static asset serving except "ask the port and pass through the response."
142
140
 
@@ -198,7 +196,7 @@ export default createOAuthProvider({
198
196
  });
199
197
  ```
200
198
 
201
- The runtime gets three required adapter ports (`db`, `kv`, `assets`)
199
+ The runtime gets two required adapter ports (`db`, `assets`)
202
200
  alongside manifests, handlers, templates, and site defaults. Auth is
203
201
  owned by the adapter layer that mounts HTTP/MCP surfaces; the runtime
204
202
  receives authenticated context when the adapter dispatches requests.
@@ -208,19 +206,16 @@ There's no module-global state holding adapter-specific bindings.
208
206
 
209
207
  **Hard-enforced boundaries**:
210
208
  - `@aotter/mantle-runtime` MUST NOT import `D1Database`, `KVNamespace`, `Fetcher` (CF Workers ASSETS), `@cloudflare/*`, or any other adapter-specific type. CI will lint for this; PR reviewers can grep.
211
- - A new required port can be added only by amending this ADR and updating ALL adapters (CF + Netlify stub) in the same change. Optional feature ports must be documented here and must state when adapters are required to implement them.
209
+ - A new required port can be added only by amending this ADR and updating every shipping adapter in the same change. Optional feature ports must be documented here and must state when adapters are required to implement them.
212
210
  - Removing a port is also possible (if a port is found to overlap or be unnecessary), again by amending this ADR.
213
211
 
214
212
  **Discoverability for adapter authors**:
215
- - A future Bun/Deno/Vercel/Netlify port author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the two required ports, then wires boot and HTTP/MCP surfaces. That's the contract. No hidden state, no implicit assumptions about the HTTP framework.
213
+ - A future adapter author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the two required ports, then wires boot and HTTP/MCP surfaces. That's the contract. No hidden state, no implicit assumptions about the HTTP framework.
216
214
 
217
215
  **Test ergonomics**:
218
216
  - Each port is small and isolated. Tests can mock individual ports without spinning up D1 or an OAuth provider.
219
217
  - The runtime's test suite exercises against in-memory port impls; the adapter's test suite exercises the binding against real CF resources via `wrangler dev` or live deploy.
220
218
 
221
- **The Netlify stub's job**:
222
- - The `@aotter/mantle-netlify` package's README declares a public commitment to an N>=2 adapter world. If a PR adds CF-specific code to runtime, reviewers point at the stub README and reject. The stub doesn't have to ship code to perform its function — its existence is the constraint.
223
-
224
219
  ## Alternatives considered
225
220
 
226
221
  **(a) Single mega-port** — One `RuntimePorts` interface containing every method (db.prepare, assets.fetch, media.createUpload, …). **Rejected**: leaks the entire surface onto every adapter. Discrete ports keep change blast radius per port.
@@ -229,7 +224,7 @@ There's no module-global state holding adapter-specific bindings.
229
224
 
230
225
  **(c) Function-injection (no interfaces, just functions)** — Runtime accepts a record of functions such as `{ dbPrepare, assetFetch, sessionRead, … }`. **Rejected**: TypeScript interfaces are more discoverable and document grouping.
231
226
 
232
- **(d) Plugin pattern (each port is a separate package)** — `@aotter/mantle-port-database`, `@aotter/mantle-port-kv`, etc., and runtime depends on one package per port. **Rejected**: the port set is too small to warrant per-port packages. The current 5-package structure (spec / runtime / admin-ui / cloudflare / netlify) is already at the boundary of "too many"; splitting further increases the maintenance tax without useful benefit. Ports are TS interfaces in `mantle-runtime`'s `src/domain/port/` directory — that's enough.
227
+ **(d) Plugin pattern (each port is a separate package)** — `@aotter/mantle-port-database`, `@aotter/mantle-port-kv`, etc., and runtime depends on one package per port. **Rejected**: the port set is too small to warrant per-port packages. Ports are TypeScript interfaces in `mantle-runtime`'s `src/domain/port/` directory — that's enough.
233
228
 
234
229
  **(e) gRPC / wire-protocol seam** — Make ports a network protocol so adapters can be in any language. **Rejected**: the runtime is not an external service, it's a TypeScript library that adapters compose into a single Worker / Function. Network seam adds latency, deployment complexity, and operational surface for zero authoring benefit. The ports are in-process; they always will be.
235
230
 
@@ -241,27 +236,26 @@ When you're authoring `@aotter/mantle-runtime` code:
241
236
  2. If a port is missing the method you need, **amend this ADR first** in the same PR, then add the method. Adapters in the same PR.
242
237
  3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 from runtime tests.
243
238
 
244
- When you're authoring an adapter (`@aotter/mantle-cloudflare` for v0.1.0; future `mantle-netlify`, `mantle-bun`, …):
239
+ When you're authoring an adapter:
245
240
 
246
241
  1. Read `mantle-runtime/src/domain/port/`. Implement each required port against your runtime's primitives.
247
242
  2. Compose the runtime via `createCmsRuntime({ db, assets, manifests, handlers, templates, siteDefaults, ... })`.
248
243
  3. Call `runtime.bootInit()` once before serving CMS traffic.
249
- 4. Bind to your HTTP framework — Hono on CF, Netlify Functions handler, raw `fetch` Worker, …
244
+ 4. Bind to your HTTP framework.
250
245
  5. Provide adapter-owned auth and map sessions/scopes/roles into runtime handler context.
251
246
  6. Bundle `@aotter/mantle-admin-ui`'s `dist/` via your runtime's static-asset surface and bind `AssetServer` to it.
252
247
 
253
248
  When you're reviewing a PR:
254
249
 
255
250
  1. Grep the diff for `@cloudflare`, `D1Database`, `KVNamespace`, `Fetcher` — flag any occurrence in `mantle-runtime/`.
256
- 2. If a new port method shows up, check it's also reflected in this ADR + the Netlify stub README.
257
- 3. If a port shape changed, all 2 adapters (CF real, Netlify stub) get updated in the same PR.
251
+ 2. If a new port method shows up, check it is also reflected in this ADR.
252
+ 3. If a port shape changed, every shipping adapter gets updated in the same PR.
258
253
 
259
254
  ## Implementation status
260
255
 
261
256
  - [x] Required port interface files live in `packages/mantle-runtime/src/domain/port/*.ts`.
262
257
  - [x] Cloudflare required port implementations live in `packages/adapters/cloudflare/src/bindings/*.ts`.
263
258
  - [x] Optional feature port `MediaStorage` (public bucket) is declared but not required by first-run adapters. `PrivateMediaStorage` is v0.2.
264
- - [x] Netlify stub README references this ADR.
265
259
  - [ ] CI lint: forbid `@cloudflare/*` / `D1Database` / `KVNamespace` imports in `mantle-runtime/` (post-v0.1.0; manual review until then)
266
260
 
267
261
  ## See also
@@ -96,8 +96,6 @@ Boot validator gates:
96
96
  - Every `{ $param: <name> }` ref MUST resolve to a declared param (`VIEW_FILTER_PARAM_REF_UNKNOWN`).
97
97
  - Every `{ $param: <name> }` ref MUST appear in `params.required` (`VIEW_FILTER_PARAM_REF_NOT_REQUIRED`).
98
98
 
99
- The required-only rule is a v0.1.0 simplification. v0.1.x will promote optional-with-skip semantics (filter clauses referencing missing optional params evaluate to TRUE / no-op) — the runtime compiler already implements drop semantics for forward compatibility, but the parser rejects it today so authors get a clear "not yet" diagnostic.
100
-
101
99
  ### 6. Response envelope is `{ rows, page, show, hasMore }`
102
100
 
103
101
  ```json
@@ -130,12 +128,11 @@ Query strings arrive as strings; `View.spec.params` declares the JSON Schema typ
130
128
 
131
129
  Required params not present → `400 INPUT_VALIDATION_FAILED`. Coercion failure → `400 INPUT_VALIDATION_FAILED`. Unknown query-string keys are silently ignored (lenient v0.1.0; strict mode is a candidate v0.1.x flag).
132
130
 
133
- ## Out of scope (deferred)
131
+ ## Current limits
134
132
 
135
- - **`Trigger.target.view`** (lifecycle/projection triggers fired by Views). Tracked separately as a v0.2 grammar move.
136
- - **`spec.output.kind`** (declaring scalar / tree / tabular result shape per View). Lands with join + group-by support in v0.1.x.
137
- - **Optional param-ref drop semantics in the parser.** Runtime is already implemented; parser promotes when v0.1.x lands.
138
- - **DRAFT filter operators** (`contains` / `in` / `like` / `not`). v0.1 keeps comparison operators closed to `eq` / `gt` / `gte` / `lt` / `lte`; field-to-field comparisons remain out of scope.
133
+ - Param refs must be required.
134
+ - Filter operators are closed to `eq` / `gt` / `gte` / `lt` / `lte`;
135
+ field-to-field comparisons are unsupported.
139
136
  - **Row-level policy rewriting.** `requires` authorizes the whole View; it does
140
137
  not inject per-row visibility predicates. Consumer-specific membership,
141
138
  payment, or entitlement checks belong in the optional guard Procedure.
@@ -149,13 +146,12 @@ Required params not present → `400 INPUT_VALIDATION_FAILED`. Coercion failure
149
146
  - Cheap pagination + dynamic filters without hand-writing handlers.
150
147
 
151
148
  **Authors lose:**
152
- - A View per filter combination (until DRAFT operators land). `posts-by-locale` plus `posts-by-tag` plus `posts-by-locale-and-tag` would be three Views in v0.1.0.
149
+ - Each named query shape remains an explicit View.
153
150
  - No internal-only View surface; choose public/staff or keep the query in a TS
154
151
  helper.
155
152
 
156
153
  **Runtime gains:**
157
154
  - One executor and response shape cover public/staff REST and MCP reads.
158
- - Forward-compat for join / group-by / aggregation: envelope generalises by Views declaring `output.kind` later.
159
155
 
160
156
  **Reviewers / future contributors should:**
161
157
  - Reject any PR adding `Schema.spec.expose.rest` or a similar Schema-level public-read flag.
@@ -44,7 +44,7 @@ A 2026 Workers-friendly auth library — [Better Auth](https://better-auth.com)
44
44
  - **MCP plugin (`mcp`)** — purpose-built on top of the OAuth 2.1 provider for MCP DCR; auto-mounts `.well-known/oauth-authorization-server` + `.well-known/oauth-protected-resource`, exposes `auth.api.getMcpSession()` for protected-resource validation
45
45
  - Account linking with policies (verified-email match + reauth requirement)
46
46
 
47
- Better Auth depends on a Kysely / Drizzle / Prisma adapter for the database, not on any Cloudflare-specific service. The auth machinery becomes platform-agnostic — porting to Netlify / Bun / Deno is config-only.
47
+ Better Auth depends on a Kysely / Drizzle / Prisma adapter for the database, not on any Cloudflare-specific service. The auth machinery remains platform-agnostic.
48
48
 
49
49
  ## Decision (historical baseline; amended below)
50
50
 
@@ -64,7 +64,7 @@ Adopt Better Auth as the SDK's full auth surface. It owns:
64
64
 
65
65
  The auth runtime stops being an adapter port. `OAuthVerifier` port + `WorkersOAuthVerifier` adapter are deleted. Validating bearer tokens at `/mcp` and `/staff/mcp` becomes `auth.api.getMcpSession(req.raw)` — a direct Better Auth API call, no port indirection.
66
66
 
67
- This makes the runtime more platform-agnostic, not less: Better Auth runs on Workers (D1 via Kysely), Bun (sqlite), Node (postgres) without code changes. Future Netlify / partner adapters get the auth surface for free.
67
+ This makes the runtime more platform-agnostic, not less: Better Auth runs on Workers (D1 via Kysely), Bun (sqlite), and Node (postgres) without runtime changes.
68
68
 
69
69
  ### 2. `staff` table → `user.role` via Better Auth admin plugin
70
70
 
@@ -81,23 +81,18 @@ admin({
81
81
 
82
82
  The manifest grammar predicate `requires.auth.all: [{ "ctx.staff": ["editor"] }]` evaluates against `session.user.role` at runtime. Closed enum membership unchanged.
83
83
 
84
- What we lose: `grantedBy` / `grantedAt` audit trail. v0.1.0 doesn't need this; v0.1.x can re-add via `additionalFields` on user, or via a separate append-only `staff_audit_log` table.
84
+ ### 3. Two explicit MCP surfaces
85
85
 
86
- ### 3. Two MCP routes, surface-derived from manifest predicate
87
-
88
- `/mcp` and `/staff/mcp` are mounted side-by-side from boot. v0.1.0 ships the conservative partition: `/staff/mcp` exposes all staff authoring/lifecycle tools and requires `mcp:staff` plus an admin role; `/mcp` exposes only read-only `query_view_<name>` tools and requires `mcp:read`. The v0.2+ extension point is **automatic** surface partition derived from each Procedure's `requires.auth.all` predicate:
89
-
90
- ```
91
- predicate contains ctx.staff: [...] → tool exposed on /staff/mcp only
92
- predicate only ctx.user / no predicate → tool exposed on /mcp only
93
- ```
86
+ `/mcp` and `/staff/mcp` are mounted side-by-side from boot. `/staff/mcp`
87
+ exposes staff authoring/lifecycle tools and `/mcp` exposes declared public
88
+ Views and Procedures. An MCP Trigger explicitly chooses `surface: public |
89
+ staff`; the target's `requires.auth` still gates every call.
94
90
 
95
91
  Tool partition rules:
96
92
 
97
93
  - Per-collection auto-emitted authoring tools (`create_draft_<schema>`, `update_draft_<schema>`) — predicate baked-in to require `ctx.staff: [contributor+]`; route to `/staff/mcp`
98
94
  - `list_entries` / `get_entry` / `request_publish` / `archive_entry` / `unpublish_entry` — staff-only (return drafts, mutate state); `/staff/mcp` only
99
- - `query_view_<name>` (auto-emitted from each parsed View, mirroring the existing `/api/views/<name>` REST shape) — public; `/mcp` only
100
- - v0.2 community / v0.2.x fan-club user-facing writes (comment, reaction, subscribe, ...) — predicate `ctx.user` or `ctx.user.subscription`; `/mcp`
95
+ - `query_view_<name>` follows `View.spec.surface`.
101
96
 
102
97
  ### 4. Scope-aware DCR via Better Auth `oauthProvider`
103
98
 
@@ -140,7 +135,7 @@ The token can carry `role` via `customAccessTokenClaims` for caller convenience,
140
135
 
141
136
  ### 6. Single auth surface, no port indirection
142
137
 
143
- Auth is no longer an adapter port. `mantle-runtime` does NOT define an auth port and `createCmsRuntime()` does not accept auth. Adapter packages (`mantle-cloudflare`, future `mantle-netlify`) construct the Better Auth instance with the right database adapter for their platform and keep it in the adapter-owned HTTP/MCP mount layer.
138
+ Auth is no longer an adapter port. `mantle-runtime` does NOT define an auth port and `createCmsRuntime()` does not accept auth. Adapter packages construct the Better Auth instance with the right database adapter for their platform and keep it in the adapter-owned HTTP/MCP mount layer.
144
139
 
145
140
  The adapter uses Better Auth to validate sessions, MCP bearer tokens, scopes, and roles, then passes authenticated user/staff context into runtime dispatchers. Better Auth remains platform-agnostic, but it is not a runtime dependency.
146
141
 
@@ -188,7 +183,7 @@ This makes the implicit explicit. The SDK's auth surface is committee-curated; u
188
183
 
189
184
  ### 8. Path to `@aotter/mantle-better-auth` separate package (deferred)
190
185
 
191
- When `mantle-netlify` lands, the Better Auth wiring moves to its own package. Today the seam is in place:
186
+ Each adapter owns its Better Auth wiring. Today the seam is:
192
187
 
193
188
  - `Auth` interface lives in the adapter (could move to runtime or a separate package without breaking the contract — adapters consume the type, not the implementation).
194
189
  - `createAuth.ts` is the only file with `import { betterAuth }` (~290 LOC, no Cloudflare-binding-specific code outside `config.database: D1Database`).
@@ -201,10 +196,10 @@ The future split looks like:
201
196
  @aotter/mantle-runtime ← ports + use cases (today)
202
197
  @aotter/mantle-better-auth ← createAuth + EmailSender impls + appleClientSecret (new, when needed)
203
198
  @aotter/mantle-cloudflare ← Workers adapter; depends on (or accepts) Auth-shape (today)
204
- @aotter/mantle-netlify ← Netlify adapter; same shape (v0.2)
199
+ future adapter same contract, implemented when needed
205
200
  ```
206
201
 
207
- The pivot point — when to extract — is when the second adapter (`mantle-netlify`) needs the same wiring. Until then, in-place co-location is cheaper than a new package boundary.
202
+ The pivot point — when to extract — is when a second adapter needs the same wiring. Until then, in-place co-location is cheaper than a new package boundary.
208
203
 
209
204
  ## Consequences
210
205
 
@@ -241,7 +236,6 @@ The pivot point — when to extract — is when the second adapter (`mantle-netl
241
236
  - `databaseHooks.user.create.after` for `ensureBootstrapOwner` semantics
242
237
  - Two `/.well-known/oauth-protected-resource/*` metadata endpoints (Better Auth helpers)
243
238
  - Public View MCP tools: dispatcher emits `query_view_<name>` on `/mcp`.
244
- - Future manifest grammar tools: dispatcher will read `Procedure.requires.auth.all` to route user-facing tools to `/mcp` or `/staff/mcp`.
245
239
  - Skills + docs updates for the dual MCP URL handoff
246
240
 
247
241
  ### Backward compatibility
@@ -263,20 +257,9 @@ User MCP URL: https://<worker>.workers.dev/mcp (give to visitors / t
263
257
  The publication starter repo's production smoke recipe uses `/mcp/staff`
264
258
  for the MCP operator smoke step.
265
259
 
266
- ### Future-proof for v0.2
267
-
268
- The end-user MCP via DCR + role-gated content (community / fan-club) requires no architectural change — just:
269
-
270
- - Enable Better Auth `socialProviders.google` / `.apple` (config-only)
271
- - Enable `magicLink` and `emailOTP` plugins (config + `EmailSender` wiring already in place)
272
- - Promote DRAFT manifest grammar from POC ADR-0005 — `Schema.spec.policies.readable: ctx.user` and `requires.auth.all: [{ ctx.user.subscription: [premium] }]`
273
- - Add `additionalFields: { subscriptionTier: ... }` on user when Stripe entitlement lands
274
-
275
- No config flag flips, no surface migration. The dispatcher partition rule (predicate → surface) handles new tool emission automatically.
276
-
277
260
  ### Platform agnosticism
278
261
 
279
- By removing `@cloudflare/workers-oauth-provider` and routing auth through Better Auth, the SDK no longer depends on any CF-specific auth service. A future Netlify adapter constructs a Better Auth instance backed by a Netlify-compatible D1 / postgres / sqlite database; the rest of the runtime + dispatcher + skills + prompts work unchanged. ADR-0011 (adapter port spec) is amended: the `OAuthVerifier` port disappears; auth becomes a direct constructor argument with platform-agnostic Better Auth as the type.
262
+ By removing `@cloudflare/workers-oauth-provider` and routing auth through Better Auth, the SDK no longer depends on any CF-specific auth service. A future adapter can construct Better Auth against its database while the runtime + dispatcher + skills + prompts stay unchanged. ADR-0011 (adapter port spec) is amended: the `OAuthVerifier` port disappears; auth becomes adapter-owned, with platform-agnostic Better Auth as the type.
280
263
 
281
264
  ## Alternatives considered
282
265
 
@@ -346,13 +329,6 @@ Phase 2 (v0.1.x):
346
329
  - Magic-link + email-OTP plugins enabled (need `ResendEmailSender` wired)
347
330
  - Account-linking with reauth UI in publication starter
348
331
 
349
- Phase 3 (v0.2+, with community / fan-club):
350
-
351
- - POC ADR-0005 DRAFT grammar promotion: `Schema.spec.policies.readable`, `requires.auth.all: ctx.user.subscription[*]`
352
- - Subscription tier on user (`additionalFields`)
353
- - Stripe webhook → entitlement updater
354
- - Community / fan-club starter manifests
355
-
356
332
  ## How to apply
357
333
 
358
334
  When reviewing or implementing a change that touches auth, MCP routing, or roles:
@@ -12,7 +12,7 @@ Records of *why* mantle ended up shaped this way. The numbering preserves POC AD
12
12
  | [0008](0008-structured-diagnostic-shape.md) | Diagnostic shape for validate/boot/runtime failures, with a reserved consumer-test phase; measured harnesses keep purpose-shaped reports. | Accepted + amended |
13
13
  | [0009](0009-consumer-supplied-manifests.md) | Consumers own manifest YAML; the installed CLI emits the parser-free runtime module and handler types. Core ships no application manifests. | Accepted + amended |
14
14
  | [0010](0010-locale-and-translates.md) | Locale 3-layer (manifest / D1 site_config / data field) + translates pattern. Boot decoupled from `site_config` (issue #60 fix). | Accepted (refreshed) |
15
- | [0011](0011-adapter-port-spec.md) | Adapter port spec. Required runtime ports plus optional feature ports. CF impl + Netlify stub. | Accepted (new) |
15
+ | [0011](0011-adapter-port-spec.md) | Adapter port spec. Required runtime ports plus optional feature ports. | Accepted (new) |
16
16
  | [0012](0012-views-as-public-rest.md) | Views auto-expose matching REST and `query_view_*` MCP reads on their declared `public` or `staff` surface. Schemas never get a public REST endpoint. | Accepted + amended |
17
17
  | [0013](0013-agent-provisioned-consumer-projects.md) | Historical agent-provisioned consumer projects path. Superseded for first launch by landing provision bundles. | Superseded |
18
18
  | [0014](0014-auth-better-auth-and-multi-tenant-mcp.md) | The Cloudflare adapter owns the curated Better Auth identity/session facade and top-level `@cloudflare/workers-oauth-provider` MCP transport. Both normalize verified callers into runtime context; mutable staff role and target authorization are re-evaluated per call. | Accepted + amended |
@@ -39,12 +39,12 @@ for new v0.1.0 boundaries, and folds / drops the rest:
39
39
 
40
40
  - **POC ADR-0003** OpenAPI emission → folded into `mantle-spec` README (the *what* is implementation; the *why* was already captured by ADR-0001's grammar lock).
41
41
  - **POC ADR-0004** D1 today, Hyperdrive PG tomorrow → folded into `mantle-cloudflare` README (now a v0.2 roadmap item, not an architectural decision).
42
- - **POC ADR-0005** v0.1 minimum vs DRAFT discipline → folded into ADR-0001 §"Future grammar discipline."
42
+ - **POC ADR-0005** v0.1 minimum grammar → folded into ADR-0001's fail-closed grammar policy.
43
43
  - **POC ADR-0006** multi-doc YAML → folded into ADR-0001 §"Authoring shape: multi-doc YAML."
44
- - **POC ADR-0011** lifecycle binary opt-in → distilled to a §"Lifecycle" subsection in `docs/design-atoms.md`. v0.1.0 ships `simple` only; `editorial` is a v0.1.x feature.
44
+ - **POC ADR-0011** lifecycle binary opt-in → distilled to a §"Lifecycle" subsection in `docs/design-atoms.md`. v0.1.0 ships `publishing` and `operational`.
45
45
  - **POC ADR-0012** strategic posture vs adjacent CMS designs → strategic / marketing material, lives in `README.md` if anywhere.
46
46
  - **POC ADR-0013** role-split surfaces (coder agent vs operator agent) → folded into ADR-0007 (Part B).
47
- - **POC ADR-0014** builtin handlers and lifecycle Triggers → promoted to v0.1.0 and implemented in the rebuild via `LifecycleHookingEntryRepository` and `InvokeBuiltinUseCase`. Editorial lifecycle remains v0.1.x-gated. Full shape spec lives in `docs/design-atoms.md`.
47
+ - **POC ADR-0014** builtin handlers and lifecycle Triggers → promoted to v0.1.0 and implemented in the rebuild via `LifecycleHookingEntryRepository` and `InvokeBuiltinUseCase`. Full shape spec lives in `docs/design-atoms.md`.
48
48
  - **POC ADR-0015** cms-astro internal seam discipline → POC-specific to a package that no longer exists; replaced by ADR-0011 (adapter port spec).
49
49
  - **POC ADR-0029** drop Astro from cms-cloudflare → POC-specific historical record; the rebuild starts post-Astro.
50
50
 
@@ -128,6 +128,13 @@ Mantle Platform user id, Hosted Auth upstream subject, email, or provider id.
128
128
  Hosted Auth may establish the site session, but Platform is not part of the
129
129
  View query path.
130
130
 
131
+ ## Site OAuth symmetry
132
+
133
+ A site-issued OAuth access token represents the same caller on public MCP and
134
+ manifest HTTP routes. Both surfaces populate `ctx.user` and `ctx.auth` from the
135
+ same token grant; expiry, revocation, scope, client, and resource audience are
136
+ enforced before the Procedure or View runs.
137
+
131
138
  ## Cloudflare consumer wiring
132
139
 
133
140
  Pass one site-owned resolver to `createCmsRef`. Return `not-handled` when the