@aotter/mantle 0.1.0-alpha.9 → 0.1.2-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.
Files changed (109) hide show
  1. package/README.md +38 -38
  2. package/dist/cli/generate.d.ts +10 -1
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +49 -4
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.js +0 -8
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/codegen/emitMantleModule.js +8 -0
  9. package/dist/codegen/emitMantleModule.js.map +1 -1
  10. package/docs/adapter-guide.md +23 -26
  11. package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
  12. package/docs/adr/0010-locale-and-translates.md +14 -16
  13. package/docs/adr/0011-adapter-port-spec.md +2 -1
  14. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
  15. package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
  16. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +38 -2
  17. package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
  18. package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
  19. package/docs/adr/0022-caller-observed-version-occ.md +212 -0
  20. package/docs/adr/0023-port-failure-contract.md +69 -0
  21. package/docs/adr/0024-manifest-native-schema-tables.md +260 -0
  22. package/docs/adr/README.md +19 -3
  23. package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
  24. package/docs/adr/adr-lite-808-route-readiness.md +47 -0
  25. package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
  26. package/docs/adr/adr-lite-812-native-parity.md +150 -0
  27. package/docs/adr/adr-lite-823-home-statistics.md +64 -0
  28. package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
  29. package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
  30. package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
  31. package/docs/api-mcp-authorization.md +3 -664
  32. package/docs/auth-hosting-model.md +4 -4
  33. package/docs/cloudflare-low-level-composition.md +3 -104
  34. package/docs/deferred-lifecycle-queues.md +2 -243
  35. package/docs/design-atoms.md +16 -796
  36. package/docs/direct-authoring.md +5 -0
  37. package/docs/examples/minimal-worker/README.md +37 -0
  38. package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
  39. package/docs/examples/minimal-worker/package.json +26 -0
  40. package/docs/examples/minimal-worker/smoke.mjs +23 -0
  41. package/docs/examples/minimal-worker/src/index.ts +4 -0
  42. package/docs/examples/minimal-worker/tsconfig.json +17 -0
  43. package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
  44. package/docs/handbook/cloudflare/authentication.md +167 -0
  45. package/docs/handbook/cloudflare/bindings.md +236 -0
  46. package/docs/handbook/cloudflare/conventional-worker.md +162 -0
  47. package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
  48. package/docs/handbook/cloudflare/deploy-and-operate.md +105 -0
  49. package/docs/handbook/cloudflare/low-level-composition.md +169 -0
  50. package/docs/handbook/cloudflare/media-r2.md +216 -0
  51. package/docs/handbook/cloudflare/public-web.md +144 -0
  52. package/docs/handbook/concepts/authorization.md +116 -0
  53. package/docs/handbook/concepts/four-atoms.md +173 -0
  54. package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
  55. package/docs/handbook/concepts/mcp-and-agents.md +109 -0
  56. package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
  57. package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
  58. package/docs/handbook/concepts/views.md +191 -0
  59. package/docs/handbook/examples/commerce-transaction.md +813 -0
  60. package/docs/handbook/examples/guarded-api.md +427 -0
  61. package/docs/handbook/examples/intake-form.md +319 -0
  62. package/docs/handbook/examples/legal-documents.md +218 -0
  63. package/docs/handbook/examples/procurement-approvals.md +240 -0
  64. package/docs/handbook/examples/publication.md +240 -0
  65. package/docs/handbook/examples/reservation.md +220 -0
  66. package/docs/handbook/navigation.json +63 -0
  67. package/docs/handbook/reference/authorization.md +295 -0
  68. package/docs/handbook/reference/diagnostics.md +183 -0
  69. package/docs/handbook/reference/manifest.md +132 -0
  70. package/docs/handbook/reference/procedure.md +263 -0
  71. package/docs/handbook/reference/schema.md +250 -0
  72. package/docs/handbook/reference/site-config.md +233 -0
  73. package/docs/handbook/reference/surface.md +207 -0
  74. package/docs/handbook/reference/trigger.md +193 -0
  75. package/docs/handbook/reference/view.md +256 -0
  76. package/docs/handbook/start/project-and-cli.md +122 -0
  77. package/docs/handbook/start/quickstart-worker.md +193 -0
  78. package/docs/labels.md +3 -1
  79. package/docs/media-uploads.md +3 -184
  80. package/docs/migration-0.1.2.md +60 -3
  81. package/docs/performance-harness.md +127 -10
  82. package/docs/release-process.md +85 -231
  83. package/docs/schema-indexes.md +3 -180
  84. package/docs/sealed-pipeline-ownership.md +6 -5
  85. package/docs/spec-only-host-adoption.md +158 -0
  86. package/docs/transaction-patterns.md +5 -0
  87. package/package.json +21 -25
  88. package/skills/README.md +4 -4
  89. package/skills/develop/SKILL.md +23 -28
  90. package/skills/install/SKILL.md +55 -147
  91. package/skills/provision/SKILL.md +4 -3
  92. package/skills/theme/SKILL.md +6 -6
  93. package/skills/update/SKILL.md +32 -68
  94. package/dist/cli/create.d.ts +0 -2
  95. package/dist/cli/create.d.ts.map +0 -1
  96. package/dist/cli/create.js +0 -243
  97. package/dist/cli/create.js.map +0 -1
  98. package/dist/cli/update.d.ts +0 -2
  99. package/dist/cli/update.d.ts.map +0 -1
  100. package/dist/cli/update.js +0 -413
  101. package/dist/cli/update.js.map +0 -1
  102. package/dist/provision/renderProvisionBundle.d.ts +0 -70
  103. package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
  104. package/dist/provision/renderProvisionBundle.js +0 -367
  105. package/dist/provision/renderProvisionBundle.js.map +0 -1
  106. package/dist/provision.d.ts +0 -2
  107. package/dist/provision.d.ts.map +0 -1
  108. package/dist/provision.js +0 -2
  109. package/dist/provision.js.map +0 -1
@@ -0,0 +1,162 @@
1
+ ---
2
+ description: Assemble a Mantle Worker with createMantleWorker, add handlers and routes through extend, and respect Core-owned paths.
3
+ ---
4
+ # The conventional Worker
5
+
6
+ `createMantleWorker` is the normal Cloudflare entry for a Mantle application. This page covers its options, what the facade owns, how to add application handlers and routes, and the readiness and reserved-path rules that keep those extensions safe.
7
+
8
+ ## Minimal entry
9
+
10
+ ```ts
11
+ import { createMantleWorker } from "@aotter/mantle/cloudflare";
12
+ import { plan } from "../.mantle/generated/mantle.js";
13
+
14
+ export default createMantleWorker({ plan, cacheScope: "my-site-production" });
15
+ ```
16
+
17
+ `plan` is the sealed plan that `mantle generate` writes to `.mantle/generated/mantle.ts`. With only `plan`, the Worker serves public View REST, HTTP Triggers, Admin, Auth, OAuth and MCP. It renders no public pages and `/` is a 404; see [Public web](./public-web.md).
18
+
19
+ ## Options
20
+
21
+ | Option | Type | Purpose |
22
+ |---|---|---|
23
+ | `plan` | `RuntimePlan` | Required. Generated plan; a fingerprint or version mismatch fails immediately and asks you to regenerate. |
24
+ | `handlers` | `Record<string, AnyHandler>` | Implementations for `handler.kind: ref` Procedures. Merged with `extend().handlers`; a name registered twice throws. |
25
+ | `siteDefaults` | `SiteDefaults \| (env) => SiteDefaults` | Brand, title, description, origin, locales, icons, media purposes. Use the function form to read `env.PUBLIC_ORIGIN`. See [Site config](../reference/site-config.md). |
26
+ | `cacheScope` | `string \| (env) => string` | Stable deployment/site identifier for public cache tags and optional `MANTLE_KV` keys. Lowercase letters, digits, `_` and `-`, up to 64 characters. Shared caching is disabled when absent or invalid. |
27
+ | `templates` | `TemplateRegistry` | Entry and list templates for public HTML. |
28
+ | `publicPathResolver` | `PublicPathResolver` | Collection-to-URL mapping used for canonical URLs, sitemap and hreflang. |
29
+ | `mediaAllowSvg` | `boolean \| (env) => boolean` | Accept SVG uploads. Default `false`. |
30
+ | `auth` | `(env) => Auth` | Replace Auth construction only. Core still owns the Auth routes. |
31
+ | `bindings` | `(env, conventional) => MantleWorkerBindings` | Augment the conventional adapters, for example `mediaStorage` or `deferredHookDispatcher`. |
32
+ | `extend` | `(ctx) => MantleWorkerExtension \| void` | The one seam for handlers, credential resolution, JWT bearer verification and new Hono routes. |
33
+
34
+ `extend` receives `{ env, auth, bindings, getRuntime }` and may return:
35
+
36
+ ```ts
37
+ {
38
+ handlers?: Record<string, AnyHandler>;
39
+ credentialResolver?: ConsumerCredentialResolver; // site-owned API keys and personal tokens
40
+ jwtBearer?: { audience: string; scopes?: readonly string[] };
41
+ mount?: ({ app, ref, env, auth, bindings, getRuntime }) => void;
42
+ }
43
+ ```
44
+
45
+ `extend` may run again after a failed initialization. Keep external side effects out of it.
46
+
47
+ ## What the facade owns
48
+
49
+ Once per isolate, `createMantleWorker` assembles and memoizes:
50
+
51
+ - Conventional bindings: `DB` becomes the D1 driver, `ASSETS` serves the Admin bundle, `MANTLE_KV` (when bound) becomes the MCP catalog projection. See [Bindings](./bindings.md).
52
+ - Conventional Auth chosen by `MANTLE_AUTH_MODE`, or your `auth` factory. See [Authentication](./authentication.md).
53
+ - Runtime endpoints: manifest HTTP Triggers, `GET /api/views` and `GET /api/views/<name>` for public Views.
54
+ - Admin at `/admin` when Admin assets are present, OAuth consent and discovery, and MCP at `/mcp` and `/mcp/staff`.
55
+ - A `/favicon.ico` route derived from `siteDefaults.icons`. This is a convention, not a reserved path; an existing host route wins.
56
+ - The final cache policy on every response, and best-effort purge of the deployment-scoped public tag after publishing-content and site-setting writes.
57
+ - A redacted error boundary: an unexpected failure returns `500` with `{ "ok": false, "error": "internal_error" }` and `private, no-store`.
58
+
59
+ If `auth.ready` rejects, the memoized assembly is evicted so the next request rebuilds instead of reusing a poisoned isolate.
60
+
61
+ ## The returned handler
62
+
63
+ ```ts
64
+ const worker = createMantleWorker<Env>({ plan });
65
+ worker.fetch(request, env, ctx); // HTTP entry
66
+ worker.getRuntime(env); // the runtime fetch uses, resolved after auth.ready
67
+ ```
68
+
69
+ Queue and scheduled handlers call `worker.getRuntime(env)` and then generated `bindMantle(runtime)` so they reuse the assembled runtime and never write Mantle tables directly. See [Bindings](./bindings.md#cron-triggers) and [Deferred hooks](./deferred-hooks-queues.md).
70
+
71
+ ## Readiness rule
72
+
73
+ Standard protected routes establish readiness themselves: the facade awaits `getRuntime()` before Auth, `/oauth`, `/mcp`, `/admin/api` and `/.well-known/oauth*` requests. Extension routes do not. Before an extension route reads or writes Mantle data, or relies on database-backed Auth, it must `await ref.get()` (inside `mount`) or `await getRuntime()`.
74
+
75
+ `getRuntime` rejects when called synchronously inside `extend` before it returns. Retain the function and call it later, for example inside a request handler.
76
+
77
+ ## Reserved paths
78
+
79
+ Extensions add routes; they never replace Core surfaces. These registrations are rejected:
80
+
81
+ - `/admin`, `/_mantle`, `/api/auth`, `/api/views`, `/oauth`, `/mcp`, and anything beneath them
82
+ - `/.well-known/oauth*`
83
+ - the custom Auth factory's `basePath`
84
+ - global `*` and `/*` handlers
85
+ - any exact method and path pair that a manifest HTTP Trigger already owns
86
+
87
+ Static literals fail TypeScript during your build (`MantleExtensionPath`). Computed paths cannot be proven statically, so after `mount` returns the facade inspects Hono's assembled route table and throws before serving any request. There is no override option. The `app` passed to `mount` exposes `get`, `post`, `put`, `patch`, `delete`, `options`, `all`, `on`, `use` and `route`; it omits global error and not-found hooks.
88
+
89
+ ## Handlers and routes
90
+
91
+ ```ts
92
+ import { createMantleWorker, type MantleCloudflareEnv } from "@aotter/mantle/cloudflare";
93
+ import { plan } from "../.mantle/generated/mantle.js";
94
+ import { notifyIntake } from "./handlers/notifyIntake.js";
95
+
96
+ interface Env extends MantleCloudflareEnv {
97
+ readonly PUBLIC_ORIGIN: string;
98
+ }
99
+
100
+ export default createMantleWorker<Env>({
101
+ plan,
102
+ siteDefaults: (env) => ({
103
+ brand: "Example",
104
+ title: "Example",
105
+ description: "Example site.",
106
+ origin: env.PUBLIC_ORIGIN,
107
+ locales: ["en"],
108
+ icons: [
109
+ { src: "/site-icon.png", mimeType: "image/png", sizes: ["64x64"] },
110
+ { src: "/site-icon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
111
+ ],
112
+ }),
113
+ extend: () => ({
114
+ handlers: { "notify-intake": notifyIntake },
115
+ mount({ app, ref }) {
116
+ app.get("/api/health", (c) => c.json({ ok: true }));
117
+ app.get("/api/locales", async (c) => {
118
+ const runtime = await ref.get(); // readiness before data
119
+ const site = await runtime.siteConfig.load();
120
+ return c.json({ locales: site.locales });
121
+ });
122
+ },
123
+ }),
124
+ });
125
+ ```
126
+
127
+ Handler keys match `spec.handler.ref` in Procedure manifests; see [Procedures and Triggers](../concepts/procedures-and-triggers.md).
128
+
129
+ ## A `bindings` hook
130
+
131
+ `bindings` receives the conventional set and returns the set the runtime uses. Spread the conventional bindings, then add capability adapters:
132
+
133
+ ```ts
134
+ import {
135
+ createMantleWorker,
136
+ WorkersQueueHookDispatcher,
137
+ } from "@aotter/mantle/cloudflare";
138
+
139
+ export default createMantleWorker<Env>({
140
+ plan,
141
+ bindings: (env, conventional) => ({
142
+ ...conventional,
143
+ mediaStorage: buildMediaStorage(env), // see media-r2.md
144
+ deferredHookDispatcher: new WorkersQueueHookDispatcher(env.MANTLE_INTERNAL_QUEUE),
145
+ }),
146
+ });
147
+ ```
148
+
149
+ The conventional set is `{ db, adminAssets, mcpCatalogKv? }`. Never drop `db`.
150
+
151
+ ## Site identity
152
+
153
+ `siteDefaults.icons` is one identity reused by browser favicons, Admin chrome and MCP `serverInfo.icons`. Keep the SVG as the editable source and add a PNG rendition for MCP clients that need raster formats. Both files live in `public/`.
154
+
155
+ ## Source
156
+ - [`packages/mantle/README.md`](../../../packages/mantle/README.md)
157
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
158
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
159
+ - [`packages/adapters/cloudflare/src/bindings/conventionalBindings.ts`](../../../packages/adapters/cloudflare/src/bindings/conventionalBindings.ts)
160
+ - [`packages/adapters/cloudflare/src/mount/cmsConfig.ts`](../../../packages/adapters/cloudflare/src/mount/cmsConfig.ts)
161
+ - [`docs/examples/minimal-worker/src/index.ts`](../../../docs/examples/minimal-worker/src/index.ts)
162
+ - [`docs/migration-0.1.2.md`](../../../docs/migration-0.1.2.md)
@@ -0,0 +1,171 @@
1
+ ---
2
+ description: Move after_* lifecycle Triggers onto Cloudflare Queues with at-least-once delivery, idempotent handlers and a DLQ.
3
+ ---
4
+ # Deferred hooks with Queues
5
+
6
+ `after_create`, `after_update`, `after_delete` and `after_publish` lifecycle Triggers run inline by default. Binding a Cloudflare Queue moves them off the request path. This page covers the guarantees, the setup, the Worker wiring and how to write handlers that survive replay.
7
+
8
+ ## When to defer
9
+
10
+ Defer hooks that must not add request latency and that tolerate a short delay: notifications, projections, upstream API calls. Keep synchronous `before_*` hooks for validation and abuse prevention; they may read request-only input such as a CAPTCHA token, which deferred envelopes never retain.
11
+
12
+ ## Guarantees
13
+
14
+ - The entry write commits first. D1 and the Queue do not share a transaction, so a Worker failure between them can lose the enqueue.
15
+ - Delivery is at-least-once. A message can run more than once.
16
+ - A rejected or ambiguous `Queue.send()` falls back to `ctx.waitUntil` (or inline when unavailable) with the same event identity. That fallback is best-effort.
17
+ - A consumer runs every captured Trigger before acknowledging; a later failure can replay an earlier success.
18
+ - Malformed, oversized, removed-Trigger and persistently failing messages are retried and then land in the dead-letter queue. Without a DLQ, Cloudflare discards them after `max_retries`.
19
+
20
+ Handlers must therefore be idempotent. Mantle does not promise exactly-once execution.
21
+
22
+ ## The v1 envelope
23
+
24
+ ```ts
25
+ interface DeferredHookEnvelope {
26
+ version: 1;
27
+ eventId: string;
28
+ triggerNames: readonly string[];
29
+ hook: "after_create" | "after_update" | "after_delete" | "after_publish";
30
+ schema: string;
31
+ entry: EntryRow;
32
+ ctxSnapshot: CtxSnapshot | null;
33
+ }
34
+ ```
35
+
36
+ Each handler receives the persisted `entry.data` as input and `ctx.event = { id, trigger, hook, schema, entry }`. Use `${ctx.event.id}:${ctx.event.trigger}` as the idempotency key; it is stable across fallback, retries and replay. `ctxSnapshot` holds normalized actor and credential metadata, never cookies, tokens or API keys.
37
+
38
+ Cloudflare's 128 KB message limit is decimal and includes platform metadata. The dispatcher rejects non-JSON-safe envelopes and any encoded envelope of 127,000 bytes or more before calling `Queue.send`. Keep entry payloads well below that.
39
+
40
+ ## Setup
41
+
42
+ ```sh
43
+ pnpm wrangler queues create mantle-internal
44
+ pnpm wrangler queues create mantle-internal-dlq
45
+ ```
46
+
47
+ ```jsonc
48
+ "queues": {
49
+ "producers": [{ "binding": "MANTLE_INTERNAL_QUEUE", "queue": "mantle-internal" }],
50
+ "consumers": [{
51
+ "queue": "mantle-internal",
52
+ "max_batch_size": 10,
53
+ "max_batch_timeout": 5,
54
+ "max_retries": 5,
55
+ "retry_delay": 60,
56
+ "dead_letter_queue": "mantle-internal-dlq"
57
+ }]
58
+ }
59
+ ```
60
+
61
+ These are adapter settings, not manifest grammar. See [Trigger](../reference/trigger.md) for the `lifecycle` source shape.
62
+
63
+ ## Worker wiring
64
+
65
+ ```ts
66
+ import type { DeferredHookEnvelope } from "@aotter/mantle/runtime";
67
+ import {
68
+ WorkersQueueHookDispatcher,
69
+ createMantleWorker,
70
+ createQueueHandler,
71
+ type MantleCloudflareEnv,
72
+ } from "@aotter/mantle/cloudflare";
73
+ import { plan } from "../.mantle/generated/mantle.js";
74
+
75
+ interface Env extends MantleCloudflareEnv {
76
+ readonly MANTLE_INTERNAL_QUEUE: Queue<DeferredHookEnvelope>;
77
+ }
78
+
79
+ const worker = createMantleWorker<Env>({
80
+ plan,
81
+ handlers,
82
+ bindings: (env, conventional) => ({
83
+ ...conventional,
84
+ deferredHookDispatcher: new WorkersQueueHookDispatcher(env.MANTLE_INTERNAL_QUEUE),
85
+ }),
86
+ });
87
+
88
+ export default {
89
+ fetch: worker.fetch,
90
+ queue(batch, env) {
91
+ return createQueueHandler<Env>({ get: () => worker.getRuntime(env) })(batch, env);
92
+ },
93
+ } satisfies ExportedHandler<Env>;
94
+ ```
95
+
96
+ The same Worker is producer and consumer. Opt-in adds only the dispatcher binding and the `queue` export; Auth, MCP, cache and runtime assembly stay on the standard path.
97
+
98
+ ## Multiplexing application queues
99
+
100
+ Route on `batch.queue` when the Worker also consumes its own queues:
101
+
102
+ ```ts
103
+ queue(batch, env, ctx) {
104
+ const mantleQueue = createQueueHandler<Env>({ get: () => worker.getRuntime(env) });
105
+ switch (batch.queue) {
106
+ case "mantle-internal": return mantleQueue(batch, env);
107
+ case "billing-jobs": return consumeBilling(batch, env, ctx);
108
+ default:
109
+ batch.retryAll();
110
+ console.error(`No consumer for queue '${batch.queue}'`);
111
+ }
112
+ }
113
+ ```
114
+
115
+ Application consumers own their own acknowledgement. A common pattern is to `bindMantle(await worker.getRuntime(env))` and call a Procedure, then `ack()` on success, `retry()` on `INTERNAL_ERROR` or `CONFLICT`, and `ack()` on other diagnostics so a poison message does not loop. See [Bindings](./bindings.md#queues).
116
+
117
+ ## Consumer semantics
118
+
119
+ - At most five messages run concurrently within a delivered batch, even if `max_batch_size` is higher.
120
+ - Runtime boot failure calls `batch.retryAll()` so per-message attempt counters advance and `max_retries` and the DLQ apply.
121
+ - Each message is acknowledged exactly once after every captured Trigger succeeds; any failure calls `retry()` on that message only.
122
+
123
+ ## An idempotent D1 handler
124
+
125
+ Make the event key a unique database key:
126
+
127
+ ```sql
128
+ CREATE TABLE notification_jobs (
129
+ idempotency_key TEXT PRIMARY KEY,
130
+ entry_id TEXT NOT NULL,
131
+ payload TEXT NOT NULL,
132
+ created_at INTEGER NOT NULL
133
+ );
134
+ ```
135
+
136
+ ```ts
137
+ import type { HandlerContext } from "@aotter/mantle/runtime";
138
+
139
+ export async function enqueueNotification(input: Record<string, unknown>, ctx: HandlerContext) {
140
+ if (!ctx.event?.entry) throw new Error("lifecycle entry event required");
141
+ const key = `${ctx.event.id}:${ctx.event.trigger}`;
142
+ await (ctx.env as Env).DB.prepare(
143
+ `INSERT OR IGNORE INTO notification_jobs (idempotency_key, entry_id, payload, created_at)
144
+ VALUES (?, ?, ?, ?)`,
145
+ ).bind(key, ctx.event.entry.id, JSON.stringify(input), Date.now()).run();
146
+ return { ok: true };
147
+ }
148
+ ```
149
+
150
+ For an upstream API, send the same key as `Idempotency-Key` and throw on a non-OK response so the Queue retries. Core adds no outbox, job registry or provider retry wrapper; those remain application-owned.
151
+
152
+ ## The removed-Trigger footgun
153
+
154
+ `triggerNames` is captured when the mutation runs. Renaming or removing a captured Trigger makes every older message fail validation at dispatch and eventually reach the DLQ; it never silently changes meaning. Drain the queue before removing a deferred Trigger, and inspect the DLQ before raising `max_retries`.
155
+
156
+ ## Verify
157
+
158
+ In the SDK checkout the contract test is:
159
+
160
+ ```sh
161
+ pnpm --filter @aotter/mantle-cloudflare test -- mantle-internal-queue.test.ts
162
+ ```
163
+
164
+ For a site, run `wrangler dev`, submit a mutation that has an `after_*` Trigger, and read the consumer log. Make one handler fail once and confirm the replay carries the same `ctx.event.id` and `ctx.event.trigger`.
165
+
166
+ ## Source
167
+ - [`docs/deferred-lifecycle-queues.md`](../../../docs/deferred-lifecycle-queues.md)
168
+ - [`packages/adapters/cloudflare/src/bindings/WorkersQueueHookDispatcher.ts`](../../../packages/adapters/cloudflare/src/bindings/WorkersQueueHookDispatcher.ts)
169
+ - [`packages/adapters/cloudflare/src/mount/cmsConfig.ts`](../../../packages/adapters/cloudflare/src/mount/cmsConfig.ts)
170
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
171
+ - Retired-starter pattern: [`overlays/transaction/src/index.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/transaction/src/index.ts)
@@ -0,0 +1,105 @@
1
+ ---
2
+ description: Production checklist, the check loop, deploy and post-deploy probes, day-to-day content operations, and upgrades.
3
+ ---
4
+ # Deploy and operate
5
+
6
+ This page is the production checklist for a Mantle Worker on Cloudflare: what to pin and configure, which checks to run before `wrangler deploy`, how to verify a deployment, how content is operated afterwards, and how to upgrade.
7
+
8
+ ## Before the first deploy
9
+
10
+ - Pin every `@aotter/mantle*` package to one exact version and commit the lockfile. Install with `pnpm install --frozen-lockfile` (or `npm ci`) from then on. See [Project and CLI](../start/project-and-cli.md).
11
+ - Set `PUBLIC_ORIGIN` to the real HTTPS origin, without a trailing slash. It drives canonical URLs, `.md` mirrors, `llms.txt`, the MCP resource and the OAuth callback. If a static documentation build also emits absolute URLs, give it the same value.
12
+ - Set the production D1 `database_id` (and `account_id` if your deployment needs it) in `wrangler.jsonc`. A local `database_name` is not a production identifier, and local data is not migrated.
13
+ - Choose `MANTLE_AUTH_MODE` and store the secrets with `wrangler secret put`. See [Authentication](./authentication.md).
14
+ - Keep `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
15
+ - Enable observability:
16
+
17
+ ```jsonc
18
+ "observability": { "enabled": true, "logs": { "head_sampling_rate": 1 } },
19
+ "upload_source_maps": true
20
+ ```
21
+
22
+ ## The check loop
23
+
24
+ Run the project's `check` script before every deploy. The minimal reference chains:
25
+
26
+ ```sh
27
+ mantle generate && mantle generate --check && mantle validate \
28
+ && mantle skills && mantle skills --check && tsc --noEmit && node smoke.mjs
29
+ ```
30
+
31
+ Add tests and the frontend build where the project has them, then confirm index coverage for every public View:
32
+
33
+ ```sh
34
+ pnpm exec mantle-harness indexes --require-public --format text
35
+ ```
36
+
37
+ A required path fails on a required Schema-table scan, a temporary sort or an unindexed data-field predicate. Then dry-run and deploy:
38
+
39
+ ```sh
40
+ wrangler deploy --dry-run
41
+ wrangler deploy
42
+ ```
43
+
44
+ ## Post-deploy verification
45
+
46
+ Probe the deployed origin, not `wrangler dev`:
47
+
48
+ | Probe | Expect |
49
+ |---|---|
50
+ | `GET /api/views/<public-view>` | `200`, `{ "ok": true, "data": { "rows": [...] } }` |
51
+ | `GET /<locale>/<segment>/<slug>` and `GET /<locale>/<segment>/<slug>.md` | `200` HTML and Markdown for a published entry |
52
+ | `GET /llms.txt`, `GET /sitemap.xml`, `GET /robots.txt` | `200` |
53
+ | `GET /<locale>/<segment>/does-not-exist` | `404` from your `notFoundRenderer` |
54
+ | `GET /mcp` without credentials | `401` with `WWW-Authenticate` |
55
+ | `GET /admin` | Sign-in page; sign in with the `ADMIN_GITHUB_LOGIN` account |
56
+
57
+ Then check the cache: a second anonymous `GET` of a public page should show `cf-cache-status: HIT`; publish a change in Admin and the next request should be a `MISS`. Sample latency with:
58
+
59
+ ```sh
60
+ pnpm exec mantle-harness http --base-url https://example.com \
61
+ --route recent=/api/views/recent-posts --route page=/en/posts/hello --rounds 20 --warmup 2
62
+ ```
63
+
64
+ ## Operating content
65
+
66
+ Sign in to Admin with a staff account. Publishing collections (`lifecycle: publishing`) follow draft, publish, verify:
67
+
68
+ 1. Create a draft with its title, slug, locale and body.
69
+ 2. Publish (`editor` or above). The write purges the deployment-scoped public cache tag.
70
+ 3. Open the public URL and its `.md` mirror. Drafts never appear on pages, mirrors, `llms.txt` or the sitemap; use `?preview=1` with a staff session to see one.
71
+ 4. Unpublish removes the entry from every public surface; the Admin delete action and the Staff MCP `archive_entry` tool retire it.
72
+
73
+ Operational collections (`lifecycle: operational`) have no publish step; records are edited in place and do not purge the public cache. The same operations are available to agents through Staff MCP; see [MCP and agents](../concepts/mcp-and-agents.md).
74
+
75
+ Site settings split by owner. Brand, title and description seed once from `siteDefaults` and are then edited in Admin (`owner`); each edit purges the public cache. Origin, icons, locales and media purposes are code-owned and re-sync from `siteDefaults` on every boot, so change them in the Worker and redeploy. See [Site config](../reference/site-config.md).
76
+
77
+ ## Upgrading
78
+
79
+ Read the migration notes shipped with the target release before changing versions; docs on the development branch do not describe your installed version. The 0.1.2 line removes `mantle create`, the bundle `mantle update` and `@aotter/mantle/provision`; `generate`, `validate`, `emit-openapi` and `skills` remain. To upgrade:
80
+
81
+ 1. Pin the new exact release for every selected package and update the lockfile through the package manager; review peer upgrades.
82
+ 2. Remove scripts that call retired commands. Keep application source, Worker/D1/KV identity, origins, auth mode and secrets.
83
+ 3. Run `mantle generate`, `generate --check`, `skills`, `skills --check`, `validate`, typecheck and tests.
84
+ 4. Test local routes and authorization, then deploy.
85
+
86
+ The native-table storage contract is a pre-beta breaking change. Existing
87
+ generic-`entries` databases are unsupported: rebuild the instance and move any
88
+ required data manually outside Mantle. New native-table instances deploy safe
89
+ additive changes online; destructive changes are rejected and require another
90
+ manual rebuild. Mantle does not ship an in-product migration workflow for this
91
+ unreleased storage format.
92
+
93
+ ## Source
94
+ - [`packages/mantle/README.md`](../../../packages/mantle/README.md)
95
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
96
+ - [`docs/direct-authoring.md`](../../../docs/direct-authoring.md)
97
+ - [`docs/migration-0.1.2.md`](../../../docs/migration-0.1.2.md)
98
+ - [`docs/performance-harness.md`](../../../docs/performance-harness.md)
99
+ - [`docs/examples/minimal-worker/README.md`](../../../docs/examples/minimal-worker/README.md)
100
+ - [`docs/examples/minimal-worker/package.json`](../../../docs/examples/minimal-worker/package.json)
101
+ - [`docs/examples/minimal-worker/wrangler.jsonc`](../../../docs/examples/minimal-worker/wrangler.jsonc)
102
+ - [`docs/examples/minimal-worker/smoke.mjs`](../../../docs/examples/minimal-worker/smoke.mjs)
103
+ - [`packages/mantle-spec/src/domain/model/SiteConfig.ts`](../../../packages/mantle-spec/src/domain/model/SiteConfig.ts)
104
+ - [`packages/mantle-admin/src/mountMantleAdmin.ts`](../../../packages/mantle-admin/src/mountMantleAdmin.ts)
105
+ - [`packages/adapters/cloudflare/src/oauth/cachePolicy.ts`](../../../packages/adapters/cloudflare/src/oauth/cachePolicy.ts)
@@ -0,0 +1,169 @@
1
+ ---
2
+ description: Own the top-level Hono assembly with the same public primitives createMantleWorker uses, and the invariants you must keep.
3
+ ---
4
+ # Low-level composition
5
+
6
+ Use `createMantleWorker` unless the deployment must own its top-level Worker lifecycle. This page covers when that is true, what it costs, the exported building blocks, the assembly order, and the invariants a hand-assembled Worker must preserve.
7
+
8
+ ## When you need it
9
+
10
+ You need low-level composition when the Worker entry must do something the facade's single `extend` seam cannot: run logic around every response (an audit queue, custom telemetry), select bindings or Auth per request, or interleave Mantle with another framework's router. Custom handlers, routes, credential resolvers and capability bindings do not require it; see [The conventional Worker](./conventional-worker.md).
11
+
12
+ The cost is ownership. The Worker entry, error boundary, readiness handling and cache boundary become application code, and there is no automatic merge path back to `createMantleWorker`. Mantle still owns the imported adapters and standard route behavior; update them through the package version and never copy their source.
13
+
14
+ ## Building blocks
15
+
16
+ All of these are exported from `@aotter/mantle/cloudflare` unless noted:
17
+
18
+ | Export | Role |
19
+ |---|---|
20
+ | `createConventionalBindings(env, cacheScope?)` | `DB` to D1 driver, `ASSETS` to Admin assets, optional deployment-scoped `MANTLE_KV` |
21
+ | `createConventionalAuth(env)`, `createAuth(config)` | Mode-driven Auth, or a curated custom factory |
22
+ | `conventionalMcpResource(env)`, `conventionalAuthBaseURL(env)` | `<PUBLIC_ORIGIN>/mcp` and the canonical base URL |
23
+ | `setupIncompleteAuthResponse(request, auth)` | The `503 setup_incomplete` guard for Auth-owned paths |
24
+ | `createMantleRuntimeRef(config)` | Per-isolate runtime singleton with `get()`, `web()`, `auth`, `plan` |
25
+ | `mountRuntimeEndpoints(app, ref)` | HTTP Triggers, `GET /api/views`, `GET /api/views/<name>` |
26
+ | `mountAdmin(app, ref, assets)` | Admin routes and SPA |
27
+ | `mountMantleOAuth(app, { auth, assets })` | OAuth consent and discovery, from `@aotter/mantle/admin` |
28
+ | `createMcpApiHandler({ ref, surface, resource })` | `/mcp` (`"public"`) and `/mcp/staff` (`"staff"`) handlers |
29
+ | `mountPublicRoutes(app, ref, options)` | Public HTML, `.md`, `llms.txt`, sitemap |
30
+ | `applyCachePolicy(request, response, cacheTag?)` | The final cache decision |
31
+ | `runMantleWorkerRequest(fn)` | Redacted `500 internal_error` boundary |
32
+ | `D1DatabaseDriver`, `AssetsAssetServer`, `R2MediaStorage`, `WorkersQueueHookDispatcher`, `createQueueHandler`, `KvSiteConfigRepository`, `cloudflareTurnstileCheck`, `resolveCaller` | Individual adapters and helpers |
33
+ | `MANTLE_RESERVED_PATH_PREFIXES`, `MANTLE_RESERVED_WELL_KNOWN_PREFIX`, `MANTLE_RESERVED_EXACT_PATHS` | Core-owned path constants |
34
+
35
+ ## Assembly
36
+
37
+ The order is bindings, Auth, runtime ref, Hono app, runtime endpoints, Admin, OAuth, MCP handlers, then the cache policy on every response. This example adds one application-owned audit message after each request:
38
+
39
+ ```ts
40
+ import { Hono } from "hono";
41
+ import {
42
+ applyCachePolicy,
43
+ conventionalMcpResource,
44
+ createMantleRuntimeRef,
45
+ createConventionalAuth,
46
+ createConventionalBindings,
47
+ createMcpApiHandler,
48
+ mountAdmin,
49
+ mountRuntimeEndpoints,
50
+ runMantleWorkerRequest,
51
+ scopedPublicCacheTag,
52
+ setupIncompleteAuthResponse,
53
+ type MantleCloudflareEnv,
54
+ } from "@aotter/mantle/cloudflare";
55
+ import { mountMantleOAuth } from "@aotter/mantle/admin";
56
+ import { plan } from "../.mantle/generated/mantle.js";
57
+
58
+ interface Env extends MantleCloudflareEnv {
59
+ readonly AUDIT_QUEUE: Queue<{ kind: "request-complete"; path: string; status: number }>;
60
+ }
61
+
62
+ let assembled: ReturnType<typeof assemble> | undefined;
63
+ const CACHE_SCOPE = "my-site-production";
64
+ const CACHE_TAG = scopedPublicCacheTag(CACHE_SCOPE)!;
65
+
66
+ export default {
67
+ fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
68
+ return runMantleWorkerRequest(async () => {
69
+ const worker = assembled ??= assemble(env);
70
+ if (worker.auth.ready) ctx.waitUntil(worker.auth.ready.catch((error) => {
71
+ if (assembled === worker) assembled = undefined; // evict a failed assembly
72
+ throw error;
73
+ }));
74
+ const incomplete = await setupIncompleteAuthResponse(request, worker.auth);
75
+ const response = incomplete ?? await worker.fetch(request, env, ctx);
76
+ ctx.waitUntil(env.AUDIT_QUEUE.send({
77
+ kind: "request-complete",
78
+ path: new URL(request.url).pathname,
79
+ status: response.status,
80
+ }));
81
+ return response;
82
+ });
83
+ },
84
+ } satisfies ExportedHandler<Env>;
85
+
86
+ function assemble(env: Env) {
87
+ const bindings = createConventionalBindings(env, CACHE_SCOPE);
88
+ const auth = createConventionalAuth(env);
89
+ const ref = createMantleRuntimeRef({
90
+ plan,
91
+ bindings,
92
+ auth,
93
+ cacheScope: CACHE_SCOPE,
94
+ onPublicChange: purgePublicCache,
95
+ });
96
+ const app = new Hono<{ Bindings: Env }>();
97
+
98
+ mountRuntimeEndpoints(app, ref);
99
+ if (bindings.adminAssets) mountAdmin(app, ref, bindings.adminAssets);
100
+ mountMantleOAuth(app, { auth, assets: bindings.adminAssets });
101
+ app.get("/cache-probe", () => new Response("public", {
102
+ headers: { "cache-control": "public, s-maxage=60" },
103
+ }));
104
+
105
+ const resource = conventionalMcpResource(env);
106
+ const mcp = new Map([
107
+ ["/mcp/staff", createMcpApiHandler<Env>({ ref, surface: "staff", resource })],
108
+ ["/mcp", createMcpApiHandler<Env>({ ref, surface: "public", resource })],
109
+ ]);
110
+
111
+ return {
112
+ auth,
113
+ async fetch(request: Request, workerEnv: Env, ctx: ExecutionContext) {
114
+ // Prepare the canonical D1 schema before Better Auth handles a
115
+ // token, client, consent or CIMD request.
116
+ await ref.get();
117
+ const handler = mcp.get(new URL(request.url).pathname);
118
+ const response = handler?.fetch
119
+ ? await handler.fetch(request, workerEnv, ctx)
120
+ : await app.fetch(request, workerEnv, ctx);
121
+ return applyCachePolicy(request, response, CACHE_TAG);
122
+ },
123
+ };
124
+ }
125
+
126
+ async function purgePublicCache() {
127
+ try {
128
+ const { cache } = await import("cloudflare:workers");
129
+ const result = await cache.purge({ tags: [CACHE_TAG] });
130
+ if (!result.success) console.error("public cache purge failed", result.errors);
131
+ } catch (error) {
132
+ console.error("public cache purge failed", error);
133
+ }
134
+ }
135
+ ```
136
+
137
+ ```jsonc
138
+ {
139
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
140
+ "queues": { "producers": [{ "binding": "AUDIT_QUEUE", "queue": "my-site-audit" }] }
141
+ }
142
+ ```
143
+
144
+ ## Invariants to keep
145
+
146
+ - Await `ref.get()` before Better Auth handles a token, client, consent or CIMD request. The facade does this for Auth, `/oauth`, `/mcp`, `/admin/api` and `/.well-known/oauth*`; the example awaits before every request.
147
+ - Evict the memoized assembly when `auth.ready` rejects, so a transient D1 failure during boot does not poison the isolate.
148
+ - Pass every response through `applyCachePolicy`, after Admin, Auth, API, OAuth, MCP, application routes, redirects and errors. It marks private surfaces `private, no-store`, strips CDN override headers, and keeps only anonymous `200` `GET`/`HEAD` responses with explicit `public` freshness.
149
+ - Return `setupIncompleteAuthResponse` before dispatch so misconfigured Auth fails closed on Auth-owned paths only.
150
+ - Keep the conventional `DB` binding, both compatibility flags, and the optional `MANTLE_KV` binding. Low-level bindings may instead set `mcpCatalogKv: { namespace, scope }` with a stable deployment-owned scope, never one derived from a request.
151
+ - Pass `reservedHttpPathPrefixes` (the exported constants plus `auth.basePath`) to `createMantleRuntimeRef` so a manifest HTTP Trigger cannot claim a Core path.
152
+ - If you mount public routes, provide a stable `cacheScope`, pass `onPublicChange` to `createMantleRuntimeRef`, and purge the resulting scoped tag there with the Workers cache API. The facade wires this purge itself; a bare `createMantleRuntimeRef` does not.
153
+ - Do not replace Auth, MCP or cache handling with local copies.
154
+
155
+ ## Adding an application queue producer
156
+
157
+ The audit queue above is the pattern: declare the binding in `Env` and `wrangler.jsonc`, send from `ctx.waitUntil` after the response is decided, and keep the message contract application-owned. Consume it in the same Worker by switching on `batch.queue`; see [Deferred hooks with Queues](./deferred-hooks-queues.md#multiplexing-application-queues). For the runtime pipeline these pieces sit on, see [Runtime and adapters](../concepts/runtime-and-adapters.md).
158
+
159
+ ## Source
160
+ - [`docs/cloudflare-low-level-composition.md`](../../../docs/cloudflare-low-level-composition.md)
161
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
162
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
163
+ - [`packages/adapters/cloudflare/src/mount/bootRuntimeOnce.ts`](../../../packages/adapters/cloudflare/src/mount/bootRuntimeOnce.ts)
164
+ - [`packages/adapters/cloudflare/src/mount/cmsConfig.ts`](../../../packages/adapters/cloudflare/src/mount/cmsConfig.ts)
165
+ - [`packages/adapters/cloudflare/src/oauth/cachePolicy.ts`](../../../packages/adapters/cloudflare/src/oauth/cachePolicy.ts)
166
+ - [`packages/adapters/cloudflare/src/index.ts`](../../../packages/adapters/cloudflare/src/index.ts)
167
+ - [`packages/adapters/cloudflare/src/bindings/index.ts`](../../../packages/adapters/cloudflare/src/bindings/index.ts)
168
+ - [`packages/adapters/cloudflare/src/mount/index.ts`](../../../packages/adapters/cloudflare/src/mount/index.ts)
169
+ - [`packages/adapters/cloudflare/src/worker/index.ts`](../../../packages/adapters/cloudflare/src/worker/index.ts)