@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,5 @@
1
+ # Direct authoring
2
+
3
+ This guide moved into the handbook: [`docs/handbook/start/project-and-cli.md`](handbook/start/project-and-cli.md).
4
+
5
+ The handbook ships inside the `@aotter/mantle` package under `node_modules/@aotter/mantle/docs/handbook/` and describes the installed release.
@@ -0,0 +1,37 @@
1
+ # Minimal Worker reference
2
+
3
+ This is a directly authored, API-only application and executable consumer test.
4
+ It is not a Starter, template catalog or CLI generator. The notes Schema/View
5
+ is example business data; `mantle generate` never invents it.
6
+
7
+ For your own project, author package.json, manifests, Worker/provider config
8
+ and TypeScript settings for your requirements. Pin all selected `@aotter/mantle*`
9
+ dependencies to the same intended release. This reference records alpha.17 as
10
+ its last published baseline; Core's test runner substitutes its exact candidate
11
+ in a disposable copy, including during a future release.
12
+
13
+ Outside the SDK workspace, with Node 22+ and pnpm 9+:
14
+
15
+ ```sh
16
+ pnpm install
17
+ pnpm check
18
+ pnpm dev
19
+ ```
20
+
21
+ Commit the resolved lockfile in a real application and use frozen installs
22
+ subsequently. `generate` writes `.mantle/generated/mantle.ts`; `skills` separately
23
+ projects version-matched instructions. Public GET `/api/views/published-notes`
24
+ returns an empty result against fresh local D1. `/` is 404: no visitor frontend
25
+ is installed or rendered. Auth routes fail closed until auth is configured.
26
+ No provider resources or secrets are needed for this local reference.
27
+ The View declares a one-hour shared-cache hint and the Worker supplies the
28
+ stable `minimal-worker-local` cache scope; authenticated requests remain private.
29
+
30
+ `mantle-web` is optional runtime document composition; it does not generate a
31
+ home page. Add application-owned routes/templates/frontend only when needed.
32
+ Configure real D1 identity and the chosen auth mode before any remote deploy;
33
+ never copy test names over an existing project's bindings or commit secrets.
34
+
35
+ Maintainers run `pnpm check:worker-consumer` from the SDK root for exact packed
36
+ packages. The release controller also runs the same check against public npm
37
+ artifacts before promoting any public channel.
@@ -0,0 +1,25 @@
1
+ apiVersion: cms.mantle.aotter.net/v1
2
+ kind: Schema
3
+ metadata:
4
+ name: notes
5
+ spec:
6
+ title: Notes
7
+ schema:
8
+ type: object
9
+ required: [title]
10
+ properties:
11
+ title: { type: string }
12
+ lifecycle: publishing
13
+ ---
14
+ apiVersion: cms.mantle.aotter.net/v1
15
+ kind: View
16
+ metadata:
17
+ name: published-notes
18
+ spec:
19
+ surface: public
20
+ from: notes
21
+ cache: { sharedMaxAge: 3600 }
22
+ fields: [id, title]
23
+ filter:
24
+ eq: { field: status, value: published }
25
+ limit: 20
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "mantle-minimal-consumer",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "generate": "mantle generate",
7
+ "validate": "mantle validate",
8
+ "typecheck": "tsc --noEmit",
9
+ "dev": "wrangler dev --local",
10
+ "check": "mantle generate && mantle generate --check && mantle validate && mantle skills && mantle skills --check && tsc --noEmit && node smoke.mjs"
11
+ },
12
+ "dependencies": {
13
+ "@aotter/mantle": "0.1.0-alpha.17",
14
+ "@aotter/mantle-cloudflare": "0.1.0-alpha.17",
15
+ "better-auth": "1.7.2",
16
+ "hono": "^4.13.3",
17
+ "zod": "^4.5.4",
18
+ "aws4fetch": "^1.0.20"
19
+ },
20
+ "devDependencies": {
21
+ "@cloudflare/workers-types": "^5.20260907.1",
22
+ "typescript": "^6.0.3",
23
+ "wrangler": "^4.125.0"
24
+ },
25
+ "packageManager": "pnpm@9.15.0"
26
+ }
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { unstable_dev } from "wrangler";
4
+
5
+ const worker = await unstable_dev("src/index.ts", {
6
+ config: "wrangler.jsonc", local: true, ip: "127.0.0.1", port: 0,
7
+ inspectorPort: 0, persist: false,
8
+ experimental: { disableExperimentalWarning: true },
9
+ });
10
+ try {
11
+ const response = await worker.fetch("/api/views/published-notes");
12
+ assert.equal(response.status, 200);
13
+ const body = await response.json();
14
+ assert.equal(body.ok, true);
15
+ assert.deepEqual(body.data.rows, []);
16
+ assert.equal((await worker.fetch("/")).status, 404, "No implicit visitor homepage");
17
+ const auth = await worker.fetch("/mcp/staff");
18
+ assert.equal(auth.status, 503, "Auth must fail closed until configured");
19
+ const pkg = JSON.parse(readFileSync("node_modules/@aotter/mantle/package.json", "utf8"));
20
+ console.log(`Mantle ${pkg.version}: public View 200, home 404, unconfigured auth 503`);
21
+ } finally {
22
+ await worker.stop();
23
+ }
@@ -0,0 +1,4 @@
1
+ import { createMantleWorker } from "@aotter/mantle/cloudflare";
2
+ import { plan } from "../.mantle/generated/mantle.js";
3
+
4
+ export default createMantleWorker({ plan, cacheScope: "minimal-worker-local" });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "types": [
10
+ "@cloudflare/workers-types"
11
+ ]
12
+ },
13
+ "include": [
14
+ "src/**/*.ts",
15
+ ".mantle/generated/**/*.ts"
16
+ ]
17
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "mantle-reference",
4
+ "main": "src/index.ts",
5
+ "compatibility_date": "2026-09-08",
6
+ "compatibility_flags": [
7
+ "nodejs_compat",
8
+ "global_fetch_strictly_public"
9
+ ],
10
+ "observability": {
11
+ "enabled": true
12
+ },
13
+ "vars": {
14
+ "MANTLE_AUTH_MODE": "self-managed"
15
+ },
16
+ "d1_databases": [
17
+ {
18
+ "binding": "DB",
19
+ "database_name": "mantle-reference-local"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,167 @@
1
+ ---
2
+ description: Configure MANTLE_AUTH_MODE, secrets, the first owner and staff roles; understand which routes need a session.
3
+ ---
4
+ # Authentication
5
+
6
+ Conventional Auth is chosen by one variable, `MANTLE_AUTH_MODE`, and fails closed when its configuration is incomplete. This page covers the two modes, the secrets each needs, first-owner bootstrap, roles, the routes that require a session, and the curated Better Auth surface.
7
+
8
+ ## Mode matrix
9
+
10
+ | Mode | Non-secret vars | Worker secrets | Must be absent |
11
+ |---|---|---|---|
12
+ | `self-managed` | `MANTLE_AUTH_MODE=self-managed`, `PUBLIC_ORIGIN`, `GITHUB_CLIENT_ID`, `ADMIN_GITHUB_LOGIN` | `GITHUB_CLIENT_SECRET`, `BETTER_AUTH_SECRET` | `MANTLE_HOSTED_AUTH_ISSUER`, `MANTLE_HOSTED_AUTH_CLIENT_ID` |
13
+ | `hosted` | `MANTLE_AUTH_MODE=hosted`, `PUBLIC_ORIGIN`, `MANTLE_HOSTED_AUTH_ISSUER`, `MANTLE_HOSTED_AUTH_CLIENT_ID`, `ADMIN_GITHUB_LOGIN` | `BETTER_AUTH_SECRET` | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` |
14
+
15
+ Validation rules:
16
+
17
+ - `PUBLIC_ORIGIN` is the site's HTTPS origin without a trailing slash. When unset, the adapter falls back to `http://localhost:8787`.
18
+ - Self-managed uses the site's own GitHub OAuth app. Register its callback URL as `<PUBLIC_ORIGIN>/api/auth/callback/github`.
19
+ - Hosted is a public PKCE client with no client secret. `MANTLE_HOSTED_AUTH_ISSUER` must be an HTTPS root origin (no path, query or fragment; `http` only for loopback). `MANTLE_HOSTED_AUTH_CLIENT_ID` must be a URL on that same origin shaped `/clients/<id>`.
20
+ - `ADMIN_GITHUB_LOGIN` must be a valid GitHub login.
21
+
22
+ Any missing, invalid, partial or mixed-mode configuration produces a setup-incomplete Auth instance instead of a working one.
23
+
24
+ ## `503 setup_incomplete`
25
+
26
+ With incomplete Auth, public routes keep working: public Views, public HTTP Triggers, public pages, `.md` mirrors, `llms.txt` and the sitemap. Auth-owned private routes return:
27
+
28
+ ```json
29
+ { "error": "setup_incomplete", "message": "Self-managed Auth configuration errors: BETTER_AUTH_SECRET is not set; ..." }
30
+ ```
31
+
32
+ with status `503` and `private, no-store`. The affected paths are `/admin` and `/admin/*`, `/api/auth` and `/api/auth/*`, `/oauth` and `/oauth/*`, `/.well-known/oauth*`, `/mcp` and `/mcp/*`. The message lists every failing check.
33
+
34
+ ## Secrets and local values
35
+
36
+ ```sh
37
+ wrangler secret put BETTER_AUTH_SECRET
38
+ wrangler secret put GITHUB_CLIENT_SECRET # self-managed only
39
+ ```
40
+
41
+ Non-secret vars go in `wrangler.jsonc` under `vars`. For local development put the same names in `.dev.vars`, which the minimal reference ignores in git (`.dev.vars*`). Never commit a secret.
42
+
43
+ ## First owner
44
+
45
+ Every new user receives the default role `user`, which has no staff access. The first sign-in whose GitHub login matches `ADMIN_GITHUB_LOGIN` is promoted to `owner`. Promotion is blocked once any staff user exists, so the variable only bootstraps an empty site.
46
+
47
+ ## Roles
48
+
49
+ Staff roles are `owner`, `editor` and `contributor`, in descending order. Owners manage them in Admin; the underlying routes are:
50
+
51
+ | Route | Minimum role |
52
+ |---|---|
53
+ | `GET /admin/api/staff`, `PATCH /admin/api/staff/:id/role` (a role or `null` to revoke) | `owner` |
54
+ | `POST /admin/api/staff/invitations`, `DELETE /admin/api/staff/invitations/:id` | `owner` |
55
+ | `GET /admin/api/site-settings`, `PATCH /admin/api/site-settings` | `owner` |
56
+ | `GET /admin/api/members` | `editor` |
57
+ | `POST /admin/api/entries/:id/publish`, `POST /admin/api/entries/:id/unpublish`, `DELETE /admin/api/entries/:id` | `editor` |
58
+
59
+ The staff role is re-read from D1 on every protected REST and MCP call; a revoked role takes effect on the next request. Manifest-level rules such as `requires.auth` are covered in [Authorization](../concepts/authorization.md) and the [authorization reference](../reference/authorization.md).
60
+
61
+ ## What needs a session
62
+
63
+ | Surface | Requirement |
64
+ |---|---|
65
+ | `/admin`, `/admin/api/*` | Staff session; role gates per route |
66
+ | `/api/auth/*`, `/oauth/*`, `/.well-known/oauth*` | Auth-owned; public endpoints of the OAuth flow |
67
+ | `/mcp` | Any authenticated OAuth caller; anonymous requests get `401` with a `WWW-Authenticate` challenge |
68
+ | `/mcp/staff` | Authenticated caller with a staff role |
69
+ | `/<locale>/<segment>/<slug>?preview=1` | Staff session (`401` without a session, `403` without a staff role) |
70
+ | Public Views, public HTTP Triggers, public pages, `.md`, `llms.txt`, sitemap | None, unless the manifest declares `requires` |
71
+
72
+ MCP tokens are session-bound: signing out of Admin ends MCP access. See [MCP and agents](../concepts/mcp-and-agents.md).
73
+
74
+ ## Curated Better Auth surface
75
+
76
+ `createAuth()` exposes curated fields, not a Better Auth passthrough: `database`, `baseURL`, `secret`, `methods`, `bootstrapOwner`, `oauthProvider`, `rateLimit`, and for first-party SSO `trustedOrigins`, `cookiePrefix` and `crossSubDomainCookies`. There is no `betterAuthOptions` or `advanced` escape hatch; a new Better Auth knob appears only when a Mantle use case justifies a first-class field.
77
+
78
+ When several first-party apps share one parent domain that the same party controls, configure shared cookies explicitly. `cookiePrefix` is required whenever more than one Better Auth app writes cookies under that domain; `trustedOrigins` is the auth-flow trust list, not a CORS policy.
79
+
80
+ ```ts
81
+ const auth = createAuth({
82
+ database: env.DB,
83
+ baseURL: "https://platform.example.com",
84
+ secret: env.BETTER_AUTH_SECRET,
85
+ methods,
86
+ trustedOrigins: ["https://example.com", "https://www.example.com"],
87
+ cookiePrefix: "example-platform",
88
+ crossSubDomainCookies: { enabled: true, domain: "example.com" },
89
+ });
90
+ ```
91
+
92
+ Shared cookies do not cross registrable domains. A browser never sends an `example.com` cookie to `customer.com`. For a customer-owned domain, use an OAuth/OIDC broker flow: the customer site redirects to the identity provider's authorize endpoint, receives the callback, verifies the response and creates its own local session. The broker returns identity; the customer site remains the authority for its members and grants.
93
+
94
+ ## Self-hosted and hosted
95
+
96
+ A free self-hosted site runs every method `createAuth()` exposes: Better Auth social providers, email OTP, magic link, and parent-domain SSO. The owner supplies provider credentials, email sending and cookie policy. Hosted auth is an operations convenience: the platform holds provider and email configuration and registers the site as a PKCE client, while the site still owns grants, members, content and `ctx.user`/`ctx.staff` mapping. Neither mode changes the runtime's authorization vocabulary.
97
+
98
+ ## Replacing Auth construction
99
+
100
+ ```ts
101
+ createMantleWorker({
102
+ plan,
103
+ auth: (env) => createAuth({ /* curated, site-specific methods */ }),
104
+ });
105
+ ```
106
+
107
+ With `auth` set, `MANTLE_AUTH_MODE` and the mode variables are not read. Core still owns the Auth routes: the factory's `basePath` joins the reserved paths, the MCP resource defaults to `auth.mcpResource ?? <PUBLIC_ORIGIN>/mcp`, and a rejected `auth.ready` evicts the isolate's assembly. Keep the D1 `DB` binding; Better Auth tables live there.
108
+
109
+ ## OAuth resource primitives
110
+
111
+ When one Mantle site is an OAuth client of another, request a stable RFC 8707
112
+ resource and use standard `offline_access` when refresh is needed:
113
+
114
+ ```ts
115
+ const clientAuth = createAuth({
116
+ // database, baseURL, secret, other methods...
117
+ methods: [{
118
+ kind: "oauth",
119
+ providerId: "mantle-platform",
120
+ clientId: env.PLATFORM_CLIENT_ID,
121
+ discoveryUrl: "https://platform.example.com/api/auth/.well-known/openid-configuration",
122
+ scopes: ["openid", "offline_access", "accounts:read"],
123
+ resource: "https://api.example.com",
124
+ }],
125
+ });
126
+
127
+ const { accessToken, accessTokenExpiresAt, scopes } =
128
+ await clientAuth.getProviderAccessToken(request, "mantle-platform");
129
+ ```
130
+
131
+ The server-side getter is bound to the current local session request and never
132
+ returns a refresh token or account row. On the provider:
133
+
134
+ ```ts
135
+ const providerAuth = createAuth({
136
+ // database, baseURL, secret, methods...
137
+ oauthProvider: {
138
+ loginPage: "/sign-in",
139
+ consentPage: "/consent",
140
+ scopes: ["openid", "offline_access", "accounts:read"],
141
+ resources: ["https://api.example.com"],
142
+ },
143
+ });
144
+
145
+ const verification = await providerAuth.verifyOAuthAccessToken(request, {
146
+ audience: "https://api.example.com",
147
+ scopes: ["accounts:read"],
148
+ });
149
+ ```
150
+
151
+ The verifier accepts JWT access tokens only and checks the configured issuer,
152
+ JWKS/signature, audience, time claims, required scopes, and—when passed the
153
+ request—DPoP proof binding with database-backed replay protection. It returns
154
+ only `userId`, `clientId`, `credentialId`, and scopes. Opaque tokens are
155
+ rejected; there is no introspection fallback.
156
+
157
+ ## Source
158
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
159
+ - [`packages/adapters/cloudflare/src/auth/conventionalAuth.ts`](../../../packages/adapters/cloudflare/src/auth/conventionalAuth.ts)
160
+ - [`packages/adapters/cloudflare/src/auth/createAuth.ts`](../../../packages/adapters/cloudflare/src/auth/createAuth.ts)
161
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
162
+ - [`packages/adapters/cloudflare/src/mount/mountPublicRoutes.ts`](../../../packages/adapters/cloudflare/src/mount/mountPublicRoutes.ts)
163
+ - [`packages/adapters/cloudflare/src/mount/mountMcp.ts`](../../../packages/adapters/cloudflare/src/mount/mountMcp.ts)
164
+ - [`packages/mantle-admin/src/mountMantleAdmin.ts`](../../../packages/mantle-admin/src/mountMantleAdmin.ts)
165
+ - [`docs/auth-hosting-model.md`](../../../docs/auth-hosting-model.md)
166
+ - [`docs/adapter-guide.md`](../../../docs/adapter-guide.md)
167
+ - [`docs/examples/minimal-worker/.gitignore`](../../../docs/examples/minimal-worker/.gitignore)
@@ -0,0 +1,236 @@
1
+ ---
2
+ description: Each Cloudflare primitive a Mantle Worker can bind, what Mantle uses it for, the wrangler snippet, and the Env type.
3
+ ---
4
+ # Bindings and primitives
5
+
6
+ This page lists every Cloudflare binding a Mantle Worker commonly declares: which are required, what Mantle does with each, the `wrangler.jsonc` snippet, and the matching `Env` field. Read it when you write or review a Worker's configuration.
7
+
8
+ ## Compatibility flags
9
+
10
+ Every Mantle Worker keeps both flags:
11
+
12
+ ```jsonc
13
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"]
14
+ ```
15
+
16
+ `nodejs_compat` is required by the adapter and its Auth dependencies. `global_fetch_strictly_public` is required because MCP client metadata (CIMD) is fetched through the public Internet boundary.
17
+
18
+ ## The composition-root rule
19
+
20
+ Bindings appear in two places only: the Worker entry (`createMantleWorker` options and the `bindings` hook) and `wrangler.jsonc`. Procedure handlers receive them through `ctx.env`. An application may own additional tables behind its own repository, but it never queries Mantle-owned tables (`entries`, site settings, media, Auth) outside the runtime. Use `runtime.entries`, generated `bindMantle(runtime)` and Views instead.
21
+
22
+ ## The Env interface
23
+
24
+ ```ts
25
+ import type { DeferredHookEnvelope } from "@aotter/mantle/runtime";
26
+ import type { MantleCloudflareEnv } from "@aotter/mantle/cloudflare";
27
+
28
+ interface EmailBinding {
29
+ send(message: { to: string; from: string; subject: string; text?: string; replyTo?: string }): Promise<unknown>;
30
+ }
31
+
32
+ export interface Env extends MantleCloudflareEnv {
33
+ // MantleCloudflareEnv already declares DB, ASSETS?, MANTLE_KV? and the auth vars.
34
+ readonly DB: D1Database; // required
35
+ readonly ASSETS: Fetcher; // Admin bundle + your frontend
36
+ readonly PUBLIC_ORIGIN: string;
37
+ readonly MANTLE_KV?: KVNamespace; // optional MCP catalog cache
38
+ readonly MEDIA_BUCKET?: R2Bucket; // optional media
39
+ readonly R2_ACCOUNT_ID?: string;
40
+ readonly R2_ACCESS_KEY_ID?: string;
41
+ readonly R2_SECRET_ACCESS_KEY?: string;
42
+ readonly MEDIA_PUBLIC_URL_BASE?: string;
43
+ readonly MANTLE_INTERNAL_QUEUE?: Queue<DeferredHookEnvelope>; // optional deferred hooks
44
+ readonly ORDER_EXPIRY_QUEUE?: Queue<{ type: "expire-order"; orderToken: string }>; // application queue
45
+ readonly INVENTORY_COORDINATOR?: DurableObjectNamespace; // application-owned
46
+ readonly EMAIL?: EmailBinding; // Email Service
47
+ readonly TURNSTILE_SECRET_KEY?: string; // secret
48
+ }
49
+ ```
50
+
51
+ ## D1: `DB` (required)
52
+
53
+ D1 is canonical storage. It holds one native table per Manifest Schema, Mantle's
54
+ `_mantle_*` envelope and migration metadata, site settings, media metadata and
55
+ pending uploads, and the Better Auth tables. `createConventionalBindings`
56
+ throws without `DB`.
57
+
58
+ ```jsonc
59
+ "d1_databases": [
60
+ { "binding": "DB", "database_name": "my-site", "database_id": "<production-id>" }
61
+ ]
62
+ ```
63
+
64
+ Omit `database_id` only for local development. Set the production id before any remote deploy.
65
+
66
+ ## Static Assets: `ASSETS`
67
+
68
+ ```jsonc
69
+ "assets": { "directory": "./public", "binding": "ASSETS" }
70
+ ```
71
+
72
+ `mantle generate` syncs the Admin SPA into `public/_mantle/admin/` when `@aotter/mantle-admin-ui` is installed. The facade wraps `ASSETS` as the Admin asset server and falls back to `/_mantle/admin/index.html` for client-side Admin routes. The same binding serves your own CSS, JS and icons.
73
+
74
+ When Worker routes and static paths overlap, list Worker-owned paths in `run_worker_first` and keep `not_found_handling` at `"none"` so unmatched requests fall through to the Worker. A production configuration lists `/admin`, `/admin/*`, `/mcp`, `/mcp/*`, `/oauth`, `/oauth/*`, `/.well-known/*`, `/api/*`, `/llms.txt`, `/robots.txt`, `/sitemap.xml`, `/*/llms.txt` and every public content prefix there, so a static file can never shadow a Mantle route.
75
+
76
+ ## Workers Cache
77
+
78
+ ```jsonc
79
+ "cache": { "enabled": true }
80
+ ```
81
+
82
+ With a valid `cacheScope`, Mantle marks anonymous `200` `GET`/`HEAD` responses from the public mount (HTML, `.md`, `llms.txt`, sitemap) and eligible Views with a shared lifetime and a deployment-scoped `Cache-Tag`; everything else is `private, no-store`. Supplying a custom `credentialResolver` disables shared responses because Core cannot infer which request headers carry identity. Publishing-content and site-setting writes purge the tag after the canonical write. Purge failure is logged and does not turn a committed write into an error. The local emulator does not simulate the entrypoint cache or its purge API. See [Public web](./public-web.md#cache-contract).
83
+
84
+ ## KV: `MANTLE_KV` (optional)
85
+
86
+ ```jsonc
87
+ "kv_namespaces": [
88
+ { "binding": "MANTLE_KV", "id": "<production-id>", "preview_id": "<development-id>" }
89
+ ]
90
+ ```
91
+
92
+ A deployment-owned namespace that caches the caller-independent MCP catalog projection (brand, description, origin, icons, media-purpose policy) so an authenticated MCP catalog does not read D1 site settings. D1 stays canonical; missing or expired snapshots are repaired from D1 within one hour, and a KV failure never fails a committed write. Never store tokens, sessions or content here. `createMantleWorker` uses this binding only when `cacheScope` is valid, producing keys such as `mantle:my-site-production:site-config:v1:mcp`. KV is eventually consistent across regions, so invalidation is not a global read-after-write guarantee.
93
+
94
+ ## R2: `MEDIA_BUCKET` (optional)
95
+
96
+ ```jsonc
97
+ "r2_buckets": [{ "binding": "MEDIA_BUCKET", "bucket_name": "<project>-media" }]
98
+ ```
99
+
100
+ Staff media uploads through Staff MCP presigned PUT. The binding alone cannot sign URLs; you also need `R2_ACCOUNT_ID`, `MEDIA_PUBLIC_URL_BASE` and the two S3 credential secrets. See [Media uploads with R2](./media-r2.md).
101
+
102
+ ## Queues
103
+
104
+ ```jsonc
105
+ "queues": {
106
+ "producers": [
107
+ { "binding": "MANTLE_INTERNAL_QUEUE", "queue": "mantle-internal" },
108
+ { "binding": "ORDER_EXPIRY_QUEUE", "queue": "my-site-order-expiry" }
109
+ ],
110
+ "consumers": [
111
+ { "queue": "mantle-internal", "max_batch_size": 10, "max_batch_timeout": 5,
112
+ "max_retries": 5, "retry_delay": 60, "dead_letter_queue": "mantle-internal-dlq" },
113
+ { "queue": "my-site-order-expiry", "max_concurrency": 1, "max_batch_size": 10,
114
+ "max_retries": 5, "retry_delay": 30, "dead_letter_queue": "my-site-order-expiry-dlq" }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ `MANTLE_INTERNAL_QUEUE` carries deferred `after_*` lifecycle hooks. Application queues share the same Worker; the `queue()` export switches on `batch.queue`. See [Deferred hooks with Queues](./deferred-hooks-queues.md).
120
+
121
+ ## Cron Triggers
122
+
123
+ ```jsonc
124
+ "triggers": { "crons": ["*/5 * * * *"] }
125
+ ```
126
+
127
+ A `scheduled()` handler reuses the fetch path's runtime and invokes a Procedure that has no Trigger of its own:
128
+
129
+ ```ts
130
+ import { bindMantle } from "../.mantle/generated/mantle.js";
131
+
132
+ const worker = createMantleWorker<Env>({ plan, handlers });
133
+
134
+ export default {
135
+ fetch: worker.fetch,
136
+ async scheduled(_controller, env, ctx) {
137
+ const mantle = bindMantle(await worker.getRuntime(env));
138
+ const result = await mantle.procedures.sweepExpiredOrders(
139
+ { now: Date.now() },
140
+ { user: null, staff: null, env, waitUntil: (p) => ctx.waitUntil(p) },
141
+ );
142
+ if (!result.ok) throw new Error(`sweep failed: ${result.diagnostic.code}`);
143
+ },
144
+ } satisfies ExportedHandler<Env>;
145
+ ```
146
+
147
+ ## Durable Objects (application-owned)
148
+
149
+ Durable Objects are not a Core primitive. Use one as an application-owned coordinator when a decision must be serialized, for example reserving inventory exactly once. Export the class from the Worker entry, then bind and migrate it:
150
+
151
+ ```jsonc
152
+ "durable_objects": { "bindings": [{ "name": "INVENTORY_COORDINATOR", "class_name": "InventoryCoordinator" }] },
153
+ "migrations": [{ "tag": "inventory-v1", "new_sqlite_classes": ["InventoryCoordinator"] }]
154
+ ```
155
+
156
+ ```ts
157
+ export { InventoryCoordinator } from "./commerce/InventoryCoordinator.js";
158
+ ```
159
+
160
+ The coordinator keeps its own state; Mantle entries mirror the result. See [Commerce transaction](../examples/commerce-transaction.md).
161
+
162
+ ## Email Service: `[[send_email]]`
163
+
164
+ ```jsonc
165
+ "send_email": [{ "name": "EMAIL" }]
166
+ ```
167
+
168
+ Send from an `after_create` handler with `errorPolicy: continue`, and fail soft when the binding or addresses are absent so submissions still save on a fresh deployment:
169
+
170
+ ```ts
171
+ export async function notifyIntake(input: { name?: string; email?: string }, ctx: HandlerContext) {
172
+ const env = ctx.env as Env & { INTAKE_NOTIFY_TO?: string; INTAKE_NOTIFY_FROM?: string };
173
+ if (!env.EMAIL || !env.INTAKE_NOTIFY_TO || !env.INTAKE_NOTIFY_FROM) {
174
+ console.info("[intake] notification not configured");
175
+ return { ok: true };
176
+ }
177
+ await env.EMAIL.send({
178
+ to: env.INTAKE_NOTIFY_TO,
179
+ from: env.INTAKE_NOTIFY_FROM,
180
+ subject: `New intake response from ${input.name ?? "website"}`,
181
+ text: `Email: ${input.email ?? ""}`,
182
+ ...(input.email ? { replyTo: input.email } : {}),
183
+ });
184
+ return { ok: true };
185
+ }
186
+ ```
187
+
188
+ ## Turnstile
189
+
190
+ Store the secret with `wrangler secret put TURNSTILE_SECRET_KEY`. Declare the token in the Procedure input, then verify it in a `before_create` lifecycle Trigger that aborts on failure:
191
+
192
+ ```yaml
193
+ apiVersion: cms.mantle.aotter.net/v1
194
+ kind: Trigger
195
+ metadata:
196
+ name: intake-before-create-verify-turnstile
197
+ spec:
198
+ source:
199
+ kind: lifecycle
200
+ schema: intake-submissions
201
+ on: [before_create]
202
+ errorPolicy: abort
203
+ target:
204
+ procedure: verify-intake-turnstile
205
+ ```
206
+
207
+ The adapter exports a handler factory for the `siteverify` call:
208
+
209
+ ```ts
210
+ import { cloudflareTurnstileCheck } from "@aotter/mantle/cloudflare";
211
+
212
+ extend: ({ env }) => ({
213
+ handlers: {
214
+ "verify-intake-turnstile": cloudflareTurnstileCheck({
215
+ secret: env.TURNSTILE_SECRET_KEY ?? "dev-stub",
216
+ tokenField: "turnstileToken",
217
+ }),
218
+ },
219
+ }),
220
+ ```
221
+
222
+ Authenticated callers (`ctx.user` set) bypass the check. The literal secret `"dev-stub"` skips the network and rejects only the token `"fail"`. Rejection throws `AUTH_DENIED`, which maps to HTTP `403`. See [Intake form](../examples/intake-form.md).
223
+
224
+ ## Source
225
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
226
+ - [`packages/adapters/cloudflare/src/bindings/conventionalBindings.ts`](../../../packages/adapters/cloudflare/src/bindings/conventionalBindings.ts)
227
+ - [`packages/adapters/cloudflare/src/mount/cmsConfig.ts`](../../../packages/adapters/cloudflare/src/mount/cmsConfig.ts)
228
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
229
+ - [`packages/adapters/cloudflare/src/handlers/turnstile.ts`](../../../packages/adapters/cloudflare/src/handlers/turnstile.ts)
230
+ - [`packages/mantle/src/cli/generate.ts`](../../../packages/mantle/src/cli/generate.ts)
231
+ - [`docs/deferred-lifecycle-queues.md`](../../../docs/deferred-lifecycle-queues.md)
232
+ - [`docs/media-uploads.md`](../../../docs/media-uploads.md)
233
+ - [`docs/performance-harness.md`](../../../docs/performance-harness.md)
234
+ - [`docs/cloudflare-low-level-composition.md`](../../../docs/cloudflare-low-level-composition.md)
235
+ - [`docs/examples/minimal-worker/wrangler.jsonc`](../../../docs/examples/minimal-worker/wrangler.jsonc)
236
+ - Retired-starter patterns: [`blank/wrangler.toml`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/blank/wrangler.toml), [`overlays/transaction/wrangler.append.toml`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/transaction/wrangler.append.toml), [`overlays/transaction/src/index.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/transaction/src/index.ts), [`overlays/transaction/src/mantle/config.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/transaction/src/mantle/config.ts), [`overlays/intake/src/worker/features/intake/notifyIntake.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/intake/src/worker/features/intake/notifyIntake.ts), [`overlays/intake/manifests/site.yaml`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/intake/manifests/site.yaml)