@fabrika/app 0.8.2 → 0.9.0-cloudflare.1

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 (194) hide show
  1. package/package.json +40 -24
  2. package/skill/SKILL.md +27 -3
  3. package/skill/references/app-setup.md +8 -8
  4. package/skill/references/dev.md +2 -1
  5. package/skill/references/endpoint.md +7 -3
  6. package/skill/references/errors.md +39 -35
  7. package/skill/references/files.md +4 -2
  8. package/skill/references/table-and-migration.md +19 -2
  9. package/skill/references/tests.md +11 -0
  10. package/skill/references/webhooks-links-connectors.md +1 -1
  11. package/src/build-recipe.ts +198 -0
  12. package/src/build.ts +55 -4
  13. package/src/bun.ts +23 -8
  14. package/src/cloudflare/application.ts +352 -0
  15. package/src/cloudflare/bindings.ts +138 -0
  16. package/src/cloudflare/names.ts +10 -0
  17. package/src/cloudflare/ops.ts +246 -0
  18. package/src/cloudflare/r2-files.ts +172 -0
  19. package/src/cloudflare.ts +99 -146
  20. package/src/contract/app-id.ts +5 -0
  21. package/src/contract/artifact-schema.ts +2 -0
  22. package/src/contract/build.ts +35 -1
  23. package/src/contract/compile.ts +1 -0
  24. package/src/contract/context.ts +9 -0
  25. package/src/contract/declaration.ts +2 -0
  26. package/src/contract/define.ts +28 -2
  27. package/src/contract/kit.ts +4 -1
  28. package/src/contract/platform.ts +3 -2
  29. package/src/contract/vocabulary.ts +26 -8
  30. package/src/dev/changes.ts +35 -0
  31. package/src/dev/contract-digest.ts +17 -0
  32. package/src/dev/index.ts +11 -0
  33. package/src/dev/launcher.ts +713 -0
  34. package/src/dev/output.ts +72 -0
  35. package/src/dev/wire.ts +136 -0
  36. package/src/files/archive.ts +2 -2
  37. package/src/files/job-names.ts +12 -0
  38. package/src/files/kinds.ts +19 -7
  39. package/src/files/scan.ts +3 -4
  40. package/src/files/streams.ts +3 -1
  41. package/src/implement/anonymous.ts +11 -1
  42. package/src/implement/boot.ts +61 -4
  43. package/src/implement/bounded.ts +21 -2
  44. package/src/implement/capability.ts +15 -3
  45. package/src/implement/dispatch.ts +249 -145
  46. package/src/implement/failures.ts +17 -1
  47. package/src/implement/faults.ts +1 -0
  48. package/src/implement/index.ts +1 -0
  49. package/src/implement/invocation.ts +8 -0
  50. package/src/implement/principal.ts +25 -2
  51. package/src/implement/terminals.ts +93 -0
  52. package/src/implement/workload.ts +24 -13
  53. package/src/jobs/report.ts +5 -1
  54. package/src/jobs/runner.ts +33 -17
  55. package/src/jobs/schedule.ts +7 -1
  56. package/src/jobs/scrub.ts +14 -1
  57. package/src/testing/endpoint-access.ts +180 -137
  58. package/src/testing/kit.ts +20 -2
  59. package/src/testing/proxy.ts +5 -5
  60. package/types/src/agent-skill.d.ts +29 -0
  61. package/types/src/build-recipe.d.ts +70 -0
  62. package/types/src/build.d.ts +29 -0
  63. package/types/src/bun.d.ts +66 -0
  64. package/types/src/client/caller.d.ts +19 -0
  65. package/types/src/client/contract.d.ts +131 -0
  66. package/types/src/client/error-reporting.d.ts +66 -0
  67. package/types/src/client/index.d.ts +4 -0
  68. package/types/src/client/rpc.d.ts +14 -0
  69. package/types/src/client/share.d.ts +2 -0
  70. package/types/src/client/transport.d.ts +42 -0
  71. package/types/src/cloudflare/application.d.ts +42 -0
  72. package/types/src/cloudflare/bindings.d.ts +27 -0
  73. package/types/src/cloudflare/names.d.ts +6 -0
  74. package/types/src/cloudflare/ops.d.ts +46 -0
  75. package/types/src/cloudflare/r2-files.d.ts +9 -0
  76. package/types/src/cloudflare.d.ts +42 -0
  77. package/types/src/contract/app-id.d.ts +2 -0
  78. package/types/src/contract/artifact-schema.d.ts +8 -0
  79. package/types/src/contract/attachment.d.ts +41 -0
  80. package/types/src/contract/build.d.ts +12 -0
  81. package/types/src/contract/compile.d.ts +11 -0
  82. package/types/src/contract/connect.d.ts +51 -0
  83. package/types/src/contract/context.d.ts +510 -0
  84. package/types/src/contract/declaration.d.ts +307 -0
  85. package/types/src/contract/defaults.d.ts +208 -0
  86. package/types/src/contract/define.d.ts +376 -0
  87. package/types/src/contract/file.d.ts +44 -0
  88. package/types/src/contract/group.d.ts +274 -0
  89. package/types/src/contract/handled.d.ts +18 -0
  90. package/types/src/contract/index.d.ts +19 -0
  91. package/types/src/contract/kit.d.ts +211 -0
  92. package/types/src/contract/platform.d.ts +9 -0
  93. package/types/src/contract/problems.d.ts +14 -0
  94. package/types/src/contract/redirect.d.ts +25 -0
  95. package/types/src/contract/schema-from-shape.d.ts +10 -0
  96. package/types/src/contract/schema-shape.d.ts +5 -0
  97. package/types/src/contract/snapshot-source.d.ts +15 -0
  98. package/types/src/contract/token.d.ts +58 -0
  99. package/types/src/contract/vocabulary.d.ts +32 -0
  100. package/types/src/contract/webhook-binding.d.ts +24 -0
  101. package/types/src/contract/webhook-scheme.d.ts +23 -0
  102. package/types/src/dev/changes.d.ts +7 -0
  103. package/types/src/dev/contract-digest.d.ts +1 -0
  104. package/types/src/dev/index.d.ts +2 -0
  105. package/types/src/dev/launcher.d.ts +32 -0
  106. package/types/src/dev/output.d.ts +28 -0
  107. package/types/src/dev/wire.d.ts +126 -0
  108. package/types/src/disk-assets.d.ts +3 -0
  109. package/types/src/errors.d.ts +46 -0
  110. package/types/src/fetch.d.ts +7 -0
  111. package/types/src/files/archive.d.ts +26 -0
  112. package/types/src/files/clamscan.d.ts +32 -0
  113. package/types/src/files/handle.d.ts +106 -0
  114. package/types/src/files/job-names.d.ts +9 -0
  115. package/types/src/files/kinds.d.ts +101 -0
  116. package/types/src/files/scan.d.ts +78 -0
  117. package/types/src/files/sniff.d.ts +7 -0
  118. package/types/src/files/streams.d.ts +17 -0
  119. package/types/src/files/treatment.d.ts +18 -0
  120. package/types/src/from-row.d.ts +20 -0
  121. package/types/src/honeypot.d.ts +2 -0
  122. package/types/src/implement/anonymous.d.ts +28 -0
  123. package/types/src/implement/attachment.d.ts +14 -0
  124. package/types/src/implement/boot.d.ts +114 -0
  125. package/types/src/implement/bounded.d.ts +32 -0
  126. package/types/src/implement/browser-config.d.ts +19 -0
  127. package/types/src/implement/capability.d.ts +142 -0
  128. package/types/src/implement/challenge.d.ts +75 -0
  129. package/types/src/implement/client-address.d.ts +11 -0
  130. package/types/src/implement/connect.d.ts +67 -0
  131. package/types/src/implement/crashes.d.ts +31 -0
  132. package/types/src/implement/dispatch.d.ts +220 -0
  133. package/types/src/implement/egress.d.ts +14 -0
  134. package/types/src/implement/environment.d.ts +14 -0
  135. package/types/src/implement/failures.d.ts +39 -0
  136. package/types/src/implement/faults.d.ts +45 -0
  137. package/types/src/implement/headers.d.ts +7 -0
  138. package/types/src/implement/implement.d.ts +58 -0
  139. package/types/src/implement/index.d.ts +25 -0
  140. package/types/src/implement/invocation.d.ts +125 -0
  141. package/types/src/implement/membership.d.ts +201 -0
  142. package/types/src/implement/multipart.d.ts +36 -0
  143. package/types/src/implement/params.d.ts +7 -0
  144. package/types/src/implement/principal.d.ts +57 -0
  145. package/types/src/implement/project.d.ts +11 -0
  146. package/types/src/implement/public.d.ts +61 -0
  147. package/types/src/implement/serve.d.ts +25 -0
  148. package/types/src/implement/terminals.d.ts +33 -0
  149. package/types/src/implement/tokens.d.ts +49 -0
  150. package/types/src/implement/types.d.ts +243 -0
  151. package/types/src/implement/urls.d.ts +15 -0
  152. package/types/src/implement/verifiers.d.ts +19 -0
  153. package/types/src/implement/webhook.d.ts +55 -0
  154. package/types/src/implement/workload.d.ts +56 -0
  155. package/types/src/implement/xlsx.d.ts +6 -0
  156. package/types/src/index.d.ts +12 -0
  157. package/types/src/jobs/exception.d.ts +40 -0
  158. package/types/src/jobs/mail.d.ts +67 -0
  159. package/types/src/jobs/report.d.ts +56 -0
  160. package/types/src/jobs/runner.d.ts +151 -0
  161. package/types/src/jobs/schedule.d.ts +17 -0
  162. package/types/src/jobs/scrub.d.ts +12 -0
  163. package/types/src/jobs/types.d.ts +98 -0
  164. package/types/src/react/caller.d.ts +30 -0
  165. package/types/src/react/errors.d.ts +40 -0
  166. package/types/src/react/hooks.d.ts +110 -0
  167. package/types/src/react/index.d.ts +5 -0
  168. package/types/src/react/provider.d.ts +58 -0
  169. package/types/src/react/store.d.ts +68 -0
  170. package/types/src/react/testing.d.ts +28 -0
  171. package/types/src/react/types.d.ts +33 -0
  172. package/types/src/rpc/builder.d.ts +52 -0
  173. package/types/src/rpc/contract.d.ts +23 -0
  174. package/types/src/rpc/dispatcher.d.ts +11 -0
  175. package/types/src/rpc/types.d.ts +62 -0
  176. package/types/src/service/define.d.ts +42 -0
  177. package/types/src/service/index.d.ts +3 -0
  178. package/types/src/service/serve.d.ts +28 -0
  179. package/types/src/standard-schema.d.ts +55 -0
  180. package/types/src/testing/caller.d.ts +95 -0
  181. package/types/src/testing/challenge.d.ts +11 -0
  182. package/types/src/testing/dev.d.ts +97 -0
  183. package/types/src/testing/endpoint-access.d.ts +107 -0
  184. package/types/src/testing/index.d.ts +12 -0
  185. package/types/src/testing/isolation.d.ts +3 -0
  186. package/types/src/testing/kit.d.ts +187 -0
  187. package/types/src/testing/membership.d.ts +132 -0
  188. package/types/src/testing/policy-isolation.d.ts +113 -0
  189. package/types/src/testing/preload.d.ts +1 -0
  190. package/types/src/testing/proxy.d.ts +40 -0
  191. package/types/src/testing/runner.d.ts +8 -0
  192. package/types/src/testing/sample-file.d.ts +5 -0
  193. package/types/src/testing/status.d.ts +4 -0
  194. package/types/src/undumpable.d.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/app",
3
- "version": "0.8.2",
3
+ "version": "0.9.0-cloudflare.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,77 +14,93 @@
14
14
  "files": [
15
15
  "src",
16
16
  "skill",
17
- "!src/**/__tests__/**"
17
+ "!src/**/__tests__/**",
18
+ "types"
18
19
  ],
19
20
  "exports": {
20
21
  ".": {
21
22
  "bun": "./src/index.ts",
22
- "types": "./src/index.ts",
23
+ "types": "./types/src/index.d.ts",
23
24
  "default": "./src/index.ts"
24
25
  },
25
26
  "./bun": {
26
27
  "bun": "./src/bun.ts",
27
- "types": "./src/bun.ts",
28
+ "types": "./types/src/bun.d.ts",
28
29
  "default": "./src/bun.ts"
29
30
  },
30
31
  "./cloudflare": {
31
32
  "bun": "./src/cloudflare.ts",
32
- "types": "./src/cloudflare.ts",
33
+ "types": "./types/src/cloudflare.d.ts",
33
34
  "default": "./src/cloudflare.ts"
34
35
  },
35
36
  "./contract": {
36
37
  "bun": "./src/contract/index.ts",
37
- "types": "./src/contract/index.ts",
38
+ "types": "./types/src/contract/index.d.ts",
38
39
  "default": "./src/contract/index.ts"
39
40
  },
40
41
  "./client": {
41
42
  "bun": "./src/client/index.ts",
42
- "types": "./src/client/index.ts",
43
+ "types": "./types/src/client/index.d.ts",
43
44
  "default": "./src/client/index.ts"
44
45
  },
45
46
  "./react": {
46
47
  "bun": "./src/react/index.ts",
47
- "types": "./src/react/index.ts",
48
+ "types": "./types/src/react/index.d.ts",
48
49
  "default": "./src/react/index.ts"
49
50
  },
50
51
  "./react/testing": {
51
52
  "bun": "./src/react/testing.ts",
52
- "types": "./src/react/testing.ts",
53
+ "types": "./types/src/react/testing.d.ts",
53
54
  "default": "./src/react/testing.ts"
54
55
  },
55
56
  "./implement": {
56
57
  "bun": "./src/implement/index.ts",
57
- "types": "./src/implement/index.ts",
58
+ "types": "./types/src/implement/index.d.ts",
58
59
  "default": "./src/implement/index.ts"
59
60
  },
60
61
  "./service": {
61
62
  "bun": "./src/service/index.ts",
62
- "types": "./src/service/index.ts",
63
+ "types": "./types/src/service/index.d.ts",
63
64
  "default": "./src/service/index.ts"
64
65
  },
65
66
  "./build": {
66
67
  "bun": "./src/build.ts",
67
- "types": "./src/build.ts",
68
+ "types": "./types/src/build.d.ts",
68
69
  "default": "./src/build.ts"
69
70
  },
70
71
  "./testing": {
71
72
  "bun": "./src/testing/index.ts",
72
- "types": "./src/testing/index.ts",
73
+ "types": "./types/src/testing/index.d.ts",
73
74
  "default": "./src/testing/index.ts"
74
75
  },
75
76
  "./testing/isolation": {
76
77
  "bun": "./src/testing/isolation.ts",
77
- "types": "./src/testing/isolation.ts",
78
+ "types": "./types/src/testing/isolation.d.ts",
78
79
  "default": "./src/testing/isolation.ts"
79
80
  },
80
81
  "./testing/preload": {
81
82
  "bun": "./src/testing/preload.ts",
82
- "types": "./src/testing/preload.ts",
83
+ "types": "./types/src/testing/preload.d.ts",
83
84
  "default": "./src/testing/preload.ts"
84
85
  },
86
+ "./dev": {
87
+ "bun": "./src/dev/index.ts",
88
+ "types": "./types/src/dev/index.d.ts",
89
+ "default": "./src/dev/index.ts"
90
+ },
91
+ "./dev/wire": {
92
+ "bun": "./src/dev/wire.ts",
93
+ "types": "./types/src/dev/wire.d.ts",
94
+ "default": "./src/dev/wire.ts"
95
+ },
96
+ "./dev/changes": {
97
+ "bun": "./src/dev/changes.ts",
98
+ "types": "./types/src/dev/changes.d.ts",
99
+ "default": "./src/dev/changes.ts"
100
+ },
85
101
  "./agent-skill": {
86
102
  "bun": "./src/agent-skill.ts",
87
- "types": "./src/agent-skill.ts",
103
+ "types": "./types/src/agent-skill.d.ts",
88
104
  "default": "./src/agent-skill.ts"
89
105
  }
90
106
  },
@@ -93,14 +109,14 @@
93
109
  "test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
94
110
  },
95
111
  "dependencies": {
96
- "@fabrika/auth": "0.8.2",
97
- "@fabrika/auth-core": "0.8.2",
98
- "@fabrika/db": "0.8.2",
99
- "@fabrika/email": "0.8.2",
100
- "@fabrika/jwks": "0.8.2",
101
- "@fabrika/platform": "0.8.2",
102
- "@fabrika/platform-node": "0.8.2",
103
- "@fabrika/proxy-core": "0.8.2",
112
+ "@fabrika/auth": "0.9.0-cloudflare.1",
113
+ "@fabrika/auth-core": "0.9.0-cloudflare.1",
114
+ "@fabrika/db": "0.9.0-cloudflare.1",
115
+ "@fabrika/email": "0.9.0-cloudflare.1",
116
+ "@fabrika/jwks": "0.9.0-cloudflare.1",
117
+ "@fabrika/platform": "0.9.0-cloudflare.1",
118
+ "@fabrika/platform-node": "0.9.0-cloudflare.1",
119
+ "@fabrika/proxy-core": "0.9.0-cloudflare.1",
104
120
  "altcha-lib": "2.6.0",
105
121
  "jose": "^5.9.0"
106
122
  },
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fabrika-app
3
- version: 0.8.2
3
+ version: 0.9.0-cloudflare.1
4
4
  description: >
5
5
  How to write and change a Fabrika application: src/vocabulary.ts, db/migrations/, src/db/tables.ts, the
6
6
  slices' endpoints.ts with their handlers, fabrika.contract.ts, jobs, files, webhooks, share links, connectors
@@ -258,7 +258,7 @@ Which promise the types, the checker, boot, the runtime or the approver keeps, a
258
258
  - No `as` (except `as const`), `any`, `@ts-ignore`, `@ts-expect-error`, `process.env` or `Bun.env`.
259
259
  - Never log an error object, a request, headers or the environment.
260
260
  - Never edit a generated file (`src/db/schema.generated.ts`, the generated tests, `fabrika/entry.generated.ts`,
261
- `fabrika/connect/`) or a committed migration. Regenerate the file; write a new migration.
261
+ `fabrika/migrations.generated.ts`, `fabrika/connect/`) or a committed migration. Regenerate the file; write a new migration.
262
262
 
263
263
  ## Ask the human first
264
264
 
@@ -295,6 +295,29 @@ bunx tsc --noEmit # once, at the end
295
295
  bunx fabrika-check . # once, after tsc; refusals fail; hints print below them
296
296
  ```
297
297
 
298
+ **On Cloudflare** (`fabrika.config.ts` imports `defineApp` from `@fabrika/provider-cloudflare`) the application runs in
299
+ one Durable Object over SQLite. Its `db/migrations/*.sql` are SQLite (`references/table-and-migration.md`), and the
300
+ object applies them when it boots, so there is no `migrate` and no command needs a database URL or `FABRIKA_APP_ID`.
301
+ The tests run on `@fabrika/db/testing-sqlite` (`references/tests.md`), in memory, and never skip. A handler holds the
302
+ object's transaction, so its `limits.timeoutMs` is at most 25 s. No `sql` (`ctx.sql`) and no `scan: true`: the
303
+ checker, the deploy and the boot refuse both.
304
+
305
+ The object runs one transaction at a time, so every `await` in a handler that may write holds the whole environment:
306
+ IAM, R2, a `fetch`, a `ctx.connect` call, a binding. One 2 s call caps the environment at half a request a second. A
307
+ query holds no transaction: each of its statements runs on its own, so two of its reads may see different committed
308
+ states. Inside a
309
+ mutation keep I/O to the framework's own: IAM, and the file it writes. A slow outside call goes into a row, the
310
+ mutation calls `ctx.kick('<job>')`, and the job makes the call outside `ctx.transaction` (`references/jobs.md`). The
311
+ checker hints on a mutation that declares `egress` or `connect` (`cloudflare-held-call`), and refuses
312
+ `DEFAULT CURRENT_TIMESTAMP` in a migration (`cloudflare-current-timestamp`).
313
+
314
+ ```bash
315
+ bunx fabrika db new <slug> # a new SQLite migration file
316
+ bunx fabrika db codegen # applies the migrations in memory; writes src/db/schema.generated.ts
317
+ bunx fabrika db test-scaffold # the generated tests, on @fabrika/db/testing-sqlite
318
+ bun test --isolate # no database URL; then tsc and fabrika-check as above
319
+ ```
320
+
298
321
  A generated file changes only through its command (`codegen`, `test-scaffold`); running it is the loop, not an edit.
299
322
  Format and lint only the files you wrote, with the repository's own tools: in the fabrika-platform repository
300
323
  `bunx dprint fmt <files>` and `bunx biome lint <files>`, never `biome check` (its formatter fights dprint), and never a
@@ -326,7 +349,8 @@ e-mails them, a link holder's files, HR's review with its history.
326
349
 
327
350
  ## Not built yet
328
351
 
329
- - The data layer runs on Bun and Postgres only (Zerops). On Cloudflare an application has no tables, jobs or files.
352
+ - On Cloudflare, where the application runs in one Durable Object over its SQLite and its `db/migrations/*.sql` are
353
+ SQLite, `sql` (`ctx.sql`) and a file kind with `scan: true` are refused. Everything else on the road runs there.
330
354
  - Jobs are cron only and app-wide: no `ctx.jobs.enqueue`, no job per scope. A `ctx.kick` starts a run of its job; it
331
355
  carries nothing, and the job finds its work in a queue table with `claim`. A list write is off the road (`sql`).
332
356
  - A read across tables (a join, a filter over a related table, an aggregate over several) is off the road (`sql`).
@@ -19,20 +19,20 @@ the SPA is added with it.
19
19
  "test": "bun test --isolate"
20
20
  },
21
21
  "dependencies": {
22
- "@fabrika/app": "0.8.2",
23
- "@fabrika/db": "0.8.2",
24
- "@fabrika/provider-zerops": "0.8.2",
25
- "@fabrika/proxy-core": "0.8.2",
26
- "@fabrika/ui": "0.8.2",
22
+ "@fabrika/app": "0.9.0-cloudflare.1",
23
+ "@fabrika/db": "0.9.0-cloudflare.1",
24
+ "@fabrika/provider-zerops": "0.9.0-cloudflare.1",
25
+ "@fabrika/proxy-core": "0.9.0-cloudflare.1",
26
+ "@fabrika/ui": "0.9.0-cloudflare.1",
27
27
  "kysely": "0.29.6",
28
28
  "react": "^19.0.0",
29
29
  "react-dom": "^19.0.0",
30
30
  "zod": "^3.25.76"
31
31
  },
32
32
  "devDependencies": {
33
- "@fabrika/check": "0.8.2",
34
- "@fabrika/cli": "0.8.2",
35
- "@fabrika/platform": "0.8.2",
33
+ "@fabrika/check": "0.9.0-cloudflare.1",
34
+ "@fabrika/cli": "0.9.0-cloudflare.1",
35
+ "@fabrika/platform": "0.9.0-cloudflare.1",
36
36
  "@happy-dom/global-registrator": "^20.11.2",
37
37
  "@types/bun": "^1.4.3",
38
38
  "@types/react": "^19.0.0",
@@ -1,7 +1,8 @@
1
1
  # references/dev.md — previewing the application with `fabrika app dev`
2
2
 
3
3
  `bunx fabrika app dev` serves the application on `http://127.0.0.1:3000/` through its test kit: the proxy decision,
4
- the stand-in IAM, a throwaway schema of `FABRIKA_TEST_POSTGRES_URL`, and each SPA from its build (run
4
+ the stand-in IAM, a throwaway schema of `FABRIKA_TEST_POSTGRES_URL` (on Cloudflare an in-memory SQLite database,
5
+ without a URL), and each SPA from its build (run
5
6
  `bun run build:web` first). Use it to look at the screens, never in place of a test.
6
7
 
7
8
  ## The seed
@@ -421,7 +421,7 @@ export const workloadRows = (
421
421
  ## Raw SQL: `sql: 'read'`
422
422
 
423
423
  Raw SQL is legitimate for a join, a report, an `OR` or `IS NULL` filter, or a bulk write. Declare it on the endpoint;
424
- `ctx.sql` exists only there.
424
+ `ctx.sql` exists only there. It runs on Postgres only: on Cloudflare a site that declares `sql` is refused.
425
425
 
426
426
  ```ts src/reports/endpoints.ts
427
427
  import { attachment } from '@fabrika/app/contract'
@@ -564,7 +564,7 @@ Every procedure takes `admission` (who may send it, how often) and `limits` (how
564
564
  | `admission.origins` | `'same-origin'` (default) or `'listed'` | `'same-origin'`, `'listed'` or `'any'` |
565
565
  | `admission.challenge` | none | `{ provider: 'altcha', secret, action?, cost?, maxAgeMs? }`, or `{ provider: 'turnstile', secret, siteKey, action? }` |
566
566
  | `limits.maxBytes` | 64 KiB, at most 1 MiB; with file fields 64 KiB plus each file at its kind's `maxBytes`, at most 48 MiB | the same |
567
- | `limits.timeoutMs` | 10 s, at most 60 s | the same |
567
+ | `limits.timeoutMs` | 10 s, at most 60 s; at most 25 s on Cloudflare | the same |
568
568
 
569
569
  - A declared `rate` merges into the default; `null` turns one bucket off. Past a bucket the answer is 429.
570
570
  - Every bucket is charged before the body is read and stays charged whatever the handler does. A bucket is a database
@@ -642,7 +642,7 @@ export const app = defineApp(vocabulary, tables, {
642
642
  - A public procedure updates or removes a row only by an id a verified token of its request carries, with
643
643
  `expectVersion`.
644
644
  - Single use and revocation are your state (a row the token names). The keys are the platform's: Control generates
645
- and rotates them, and an old key verifies until its last token expires. Cloudflare has no signed tokens.
645
+ and rotates them, and an old key verifies until its last token expires.
646
646
 
647
647
  ## Outside calls: `egress`
648
648
 
@@ -652,6 +652,10 @@ refused while the site runs (`capability_refused: undeclared_egress`). A public,
652
652
  none. The checker's `cross-app-fetch` accepts an absolute URL only under an origin some site declares. The approver
653
653
  sees every origin. The guard stops mistakes; it is no sandbox.
654
654
 
655
+ On Cloudflare a procedure waits for its outside call inside the object's transaction, so the whole environment waits
656
+ with it (`SKILL.md`). A mutation records a row and kicks a job, which declares the `egress` and calls outside
657
+ `ctx.transaction` (`references/jobs.md`).
658
+
655
659
  ## Links to your own procedures: `ctx.url`
656
660
 
657
661
  `ctx.url('<endpoint id>', { input?, query? })` builds an absolute URL of one of the application's own procedures, on
@@ -8,39 +8,43 @@ architecture, and a refusal's allowed form is the one fix that passes. There is
8
8
 
9
9
  The refusals agents meet most, and their fix:
10
10
 
11
- | Rule | Means | Fix |
12
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
13
- | `literal` | the shell (contract, vocabulary, tables, groups, schemas) holds a computed value or call | literal objects, `z.*` calls and imported literal consts; move logic into `.handle(…)` |
14
- | `contract` | the contract does not compile (a reserved role, an unknown action, a wrong field) | the message names the field |
15
- | `package-replacement` | `package.json` names the app under `@fabrika/*`, or a dependency is `workspace:*` | another name; a registry range such as `^0.0.4` |
16
- | `generated-test-hash` | a generated test is missing, edited or stale | `bunx fabrika db test-scaffold`, and commit what it writes |
17
- | `endpoint-fixture` | an endpoint's input cannot be sampled, a fixture or seed names no endpoint, or a member, invitation or link field no endpoint that reads, cancels or revokes one | a literal in `src/__tests__/endpoint-access.fixtures.ts` (`references/tests.md`) |
18
- | `raw-sql` | a driver's `sql` (imported from `bun`, Kysely, `postgres`) | the handles, or `sql: 'read'` and `ctx.sql` |
19
- | `forbidden-import` | a driver, an S3 client, `jose`, `node:http`, `child_process` | the handles, a file column with `ctx.files`, a connector |
20
- | `forbidden-global` | `Bun.sql`, `Bun.serve`, `Bun.spawn`; `globalThis`, `window`, `self`, `global` or `Bun` in a server module | the handles; state lives in a table, not in a global; `crypto.randomUUID()` for an id |
21
- | `env-read` | `process.env`, `Bun.env` | `defineDeps(build)`, which receives the application's variables |
22
- | `client-identity` | an input field `userId`, `ownerId`, `createdBy`, `tenantId`, `workspaceId` | the access form's `from`, `ctx.principal.id`, or `createdBy` in the policy |
23
- | `type-assertion` | `as T` (other than `as const`), `<T>x` | fix the type; `fromRow` parses a row into a schema's type |
24
- | `sensitive-log` | a log of an error object, a request, headers or the environment | log a short fixed message |
25
- | `migration-name` | a migration not named `YYYYMMDDHHMMSS_slug.sql` (UTC) | `bunx fabrika db new <slug>` |
26
- | `test-only-import` | `@fabrika/app/testing`, `@fabrika/app/react/testing` or `@fabrika/db/testing` outside a test file | import them only from `*.test.ts` or `__tests__/` |
27
- | `spa-server-import` | the SPA loads a server module | the SPA imports `@fabrika/app/client`, the contract, `src/schemas.ts` and `src/shared/` |
28
- | `shared-import` | a module under `src/shared/` loads a slice, `src/db/`, `@fabrika/*` or a builtin | a shared module imports `zod/v4` and other shared modules only |
29
- | `skipped-test` | `.skip`, `.only`, `.todo` | run the test |
30
- | `file-column-migration` | a file column without `REFERENCES fabrika_files (id)` or without an index it leads | both, in a new migration (`references/table-and-migration.md`) |
31
- | `on-conflict-index` | `insert(…, { onConflict: { columns } })` whose columns are no unique index the migrations create | a `UNIQUE` constraint or a `CREATE UNIQUE INDEX` without `WHERE` on exactly those columns |
32
- | `cross-app-fetch` | a `fetch` of an absolute URL under no origin a site declares in `egress` | `egress: ['https://…']` on the site that calls it (`references/endpoint.md`) |
33
- | `url-endpoint` | `ctx.url('<id>')` names no procedure of the contract | the endpoint id `<mount>.<group>.<procedure>` |
11
+ | Rule | Means | Fix |
12
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
13
+ | `literal` | the shell (contract, vocabulary, tables, groups, schemas) holds a computed value or call | literal objects, `z.*` calls and imported literal consts; move logic into `.handle(…)` |
14
+ | `contract` | the contract does not compile (a reserved role, an unknown action, a wrong field) | the message names the field |
15
+ | `package-replacement` | `package.json` names the app under `@fabrika/*`, or a dependency is `workspace:*` | another name; a registry range such as `^0.0.4` |
16
+ | `generated-test-hash` | a generated test is missing, edited or stale | `bunx fabrika db test-scaffold`, and commit what it writes |
17
+ | `endpoint-fixture` | an endpoint's input cannot be sampled, a fixture or seed names no endpoint, or a member, invitation or link field no endpoint that reads, cancels or revokes one | a literal in `src/__tests__/endpoint-access.fixtures.ts` (`references/tests.md`) |
18
+ | `raw-sql` | a driver's `sql` (imported from `bun`, Kysely, `postgres`) | the handles, or `sql: 'read'` and `ctx.sql` |
19
+ | `forbidden-import` | a driver, an S3 client, `jose`, `node:http`, `child_process` | the handles, a file column with `ctx.files`, a connector |
20
+ | `forbidden-global` | `Bun.sql`, `Bun.serve`, `Bun.spawn`; `globalThis`, `window`, `self`, `global` or `Bun` in a server module | the handles; state lives in a table, not in a global; `crypto.randomUUID()` for an id |
21
+ | `env-read` | `process.env`, `Bun.env` | `defineDeps(build)`, which receives the application's variables |
22
+ | `client-identity` | an input field `userId`, `ownerId`, `createdBy`, `tenantId`, `workspaceId` | the access form's `from`, `ctx.principal.id`, or `createdBy` in the policy |
23
+ | `type-assertion` | `as T` (other than `as const`), `<T>x` | fix the type; `fromRow` parses a row into a schema's type |
24
+ | `sensitive-log` | a log of an error object, a request, headers or the environment | log a short fixed message |
25
+ | `migration-name` | a migration not named `YYYYMMDDHHMMSS_slug.sql` (UTC) | `bunx fabrika db new <slug>` |
26
+ | `test-only-import` | `@fabrika/app/testing`, `@fabrika/app/react/testing`, `@fabrika/db/testing` or `@fabrika/db/testing-sqlite` outside a test file | import them only from `*.test.ts` or `__tests__/` |
27
+ | `spa-server-import` | the SPA loads a server module | the SPA imports `@fabrika/app/client`, the contract, `src/schemas.ts` and `src/shared/` |
28
+ | `shared-import` | a module under `src/shared/` loads a slice, `src/db/`, `@fabrika/*` or a builtin | a shared module imports `zod/v4` and other shared modules only |
29
+ | `skipped-test` | `.skip`, `.only`, `.todo` | run the test |
30
+ | `file-column-migration` | a file column without `REFERENCES fabrika_files (id)` or without an index it leads | both, in a new migration (`references/table-and-migration.md`) |
31
+ | `on-conflict-index` | `insert(…, { onConflict: { columns } })` whose columns are no unique index the migrations create | a `UNIQUE` constraint or a `CREATE UNIQUE INDEX` without `WHERE` on exactly those columns |
32
+ | `cross-app-fetch` | a `fetch` of an absolute URL under no origin a site declares in `egress` | `egress: ['https://…']` on the site that calls it (`references/endpoint.md`) |
33
+ | `url-endpoint` | `ctx.url('<id>')` names no procedure of the contract | the endpoint id `<mount>.<group>.<procedure>` |
34
+ | `cloudflare-sql` | on Cloudflare, a site that declares `sql` | the handles (`ctx.db`), without `sql` |
35
+ | `cloudflare-scan` | on Cloudflare, a file kind with `scan: true` | the kind without `scan`, and no public procedure that takes its files |
36
+ | `cloudflare-current-timestamp` | on Cloudflare, `DEFAULT CURRENT_TIMESTAMP` in a migration: SQLite stores `YYYY-MM-DD HH:MM:SS`, which every read of the table refuses | `DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`, in a new migration |
34
37
 
35
38
  The hints:
36
39
 
37
- | Hint | Means | Fix |
38
- | ---------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- |
39
- | `handler-location` | a `.handle(…)` outside a slice's `endpoints.ts` | move the endpoint into `src/<slice>/endpoints.ts` |
40
- | `data-access-location` | `ctx.db` or `ctx.sql` outside `endpoints.ts` and `queries.ts` | pass the handle into a function of the slice's `queries.ts` |
41
- | `logic-data-access` | `logic.ts` reaches data | keep `logic.ts` pure; pass it plain values |
42
- | `cross-slice-queries` | a slice loads another slice's `queries.ts` | move the function, or call that slice's endpoint |
43
- | `sql-undeclared` | `ctx.sql` in a handler whose endpoint declares no `sql` | add `sql: 'read'` (or `'write'`) to that endpoint |
40
+ | Hint | Means | Fix |
41
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
42
+ | `handler-location` | a `.handle(…)` outside a slice's `endpoints.ts` | move the endpoint into `src/<slice>/endpoints.ts` |
43
+ | `data-access-location` | `ctx.db` or `ctx.sql` outside `endpoints.ts` and `queries.ts` | pass the handle into a function of the slice's `queries.ts` |
44
+ | `logic-data-access` | `logic.ts` reaches data | keep `logic.ts` pure; pass it plain values |
45
+ | `cross-slice-queries` | a slice loads another slice's `queries.ts` | move the function, or call that slice's endpoint |
46
+ | `sql-undeclared` | `ctx.sql` in a handler whose endpoint declares no `sql` | add `sql: 'read'` (or `'write'`) to that endpoint |
47
+ | `cloudflare-held-call` | on Cloudflare, a mutation that declares `egress` or `connect` holds the whole environment while it waits | record a row and `ctx.kick('<job>')`; the job makes the call outside `ctx.transaction` |
44
48
 
45
49
  The first run may report only the contract's refusals: the generated-test findings appear once the contract compiles.
46
50
 
@@ -99,11 +103,11 @@ A 500 answers a fixed message; the server log has one line, `fabrika: <endpoint>
99
103
  fixed failure code and, for a framework refusal, its reason and the reason's fixed text, never a value or your error's
100
104
  message. The codes you meet most:
101
105
 
102
- | Code | Means |
103
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
- | `query_refused` | the data layer refused a statement or an operation: `undeclared_reach`, `sql_read_only`, `unknown_column`, `read_only`, `sql_outside_transaction` …; the reason's text names the rule |
105
- | `capability_refused` | a capability of another kind, dimension or action than the call takes (`wrong_capability`, `wrong_action`, `out_of_reach`), or one kept past its request (`ended`, `foreign_work`) |
106
- | `handler_failed` | your handler threw something else; the log has no more: the Operations issue has the stack, or test the handler |
106
+ | Code | Means |
107
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108
+ | `query_refused` | the data layer refused a statement or an operation: `undeclared_reach`, `sql_read_only`, `unknown_column`, `read_only`, `sql_outside_transaction`, `sql_dialect` (`ctx.sql` on SQLite, Cloudflare) …; the reason's text names the rule |
109
+ | `capability_refused` | a capability of another kind, dimension or action than the call takes (`wrong_capability`, `wrong_action`, `out_of_reach`), or one kept past its request (`ended`, `foreign_work`) |
110
+ | `handler_failed` | your handler threw something else; the log has no more: the Operations issue has the stack, or test the handler |
107
111
 
108
112
  With an Operations DSN the same 500 is a `RequestFailed` issue in Operations. Its event also carries what your handler
109
113
  threw: the error's type, its message, its stack and its `cause` chain. Read the stack there, not in the log. A crash
@@ -363,7 +363,8 @@ export const erase = app.cron('30 2 * * *', {
363
363
  ## Malware scan
364
364
 
365
365
  `scan: true` on a kind holds every file of it `quarantined` until ClamAV answers clean; on Zerops the platform
366
- installs ClamAV and runs the scan job. A file answer is 409 `file_not_ready` until then and 409 `file_rejected` once
366
+ installs ClamAV and runs the scan job. No scanner runs on Cloudflare: the deploy and the boot refuse a kind with
367
+ `scan: true` there, and so a public procedure with a file field. A file answer is 409 `file_not_ready` until then and 409 `file_rejected` once
367
368
  the file is `rejected`; a list shows each file's `status`, so show it rather than a broken link. In a test, the scan runs with
368
369
  `kit.runJob(FILE_SCAN_JOB)` and a `scanner` passed to `testKit` (`references/tests.md`); the generated tests pass their
369
370
  own seeded files.
@@ -401,7 +402,8 @@ the transaction that deletes the row. The file is never copied; the sweep delete
401
402
  ## Storage
402
403
 
403
404
  On Zerops, declare one `object-storage` service in `fabrika.config.ts`; the platform writes `FABRIKA_APP_STORAGE_*`
404
- from it. Never read those variables, build a storage URL or import an S3 client.
405
+ from it. Never read those variables, build a storage URL or import an S3 client. On Cloudflare the platform binds the
406
+ environment's R2 bucket itself; declare nothing.
405
407
 
406
408
  ```text
407
409
  // in the services of fabrika.config.ts
@@ -1,7 +1,7 @@
1
1
  # references/table-and-migration.md — the vocabulary, migrations and table policies
2
2
 
3
- The order is fixed: the vocabulary, then the migration, then `fabrika db migrate` and `fabrika db codegen`, then the
4
- table policies, which take the generated `columns`.
3
+ The order is fixed: the vocabulary, then the migration, then `fabrika db migrate` and `fabrika db codegen` (on
4
+ Cloudflare `codegen` only), then the table policies, which take the generated `columns`.
5
5
 
6
6
  ## The vocabulary
7
7
 
@@ -155,6 +155,23 @@ INSERT INTO priorities (code, label, rank) VALUES ('low', 'Low', 1), ('normal',
155
155
  - Never edit, rename or delete a committed migration; add a new one. Change a schema by expand, switch, contract, and
156
156
  ask before you drop or rename anything.
157
157
 
158
+ ## On Cloudflare: SQLite migrations
159
+
160
+ An application on Cloudflare (its `fabrika.config.ts` imports `defineApp` from `@fabrika/provider-cloudflare`) writes
161
+ its migrations in SQLite. Its Durable Object applies them when it boots, each file once in name order, in a transaction
162
+ of its own, so there is no `fabrika db migrate`; `fabrika db codegen` applies them to an in-memory database and needs
163
+ no URL. The rules above for keys, chains, composite foreign keys, `version` and `created_by` hold unchanged, and the
164
+ database enforces foreign keys.
165
+
166
+ - The declared types codegen reads: `TEXT` (a UUID too), `INTEGER`, `REAL`, `BLOB`; `BOOLEAN`, stored as 0 or 1 and
167
+ read as a boolean; `TIMESTAMP`, stored as UTC ISO text and read as a `Date`; `DATE`, `YYYY-MM-DD` text. Any other
168
+ declared type is refused. A `STRICT` table admits only `INTEGER`, `REAL`, `TEXT` and `BLOB`, so a table with a
169
+ `BOOLEAN`, `TIMESTAMP` or `DATE` column is not `STRICT`.
170
+ - SQLite has no `ENUM`: a column with a fixed set of values is `TEXT` with `CHECK (<column> IN ('a', 'b'))`. The
171
+ generated tests write its first value.
172
+ - No transaction control (`BEGIN`, `COMMIT`, `SAVEPOINT`) and no Postgres syntax (`timestamptz`, `::` casts,
173
+ `GENERATED ALWAYS AS IDENTITY`, `CREATE TYPE`).
174
+
158
175
  ## Database code is a declared deviation
159
176
 
160
177
  A function, trigger, view, rule, policy or `DO` block in a migration is allowed. The migration file declares it, and
@@ -59,6 +59,14 @@ export const members = { 'api.tasks.assign': ['assignee'] }
59
59
  table of the table that owns it, or a table it contains; the checker refuses a row further down
60
60
  (`endpoint-fixture`). Without it they send a sample, the handler
61
61
  finds no row (404), and the case fails.
62
+ - `appRows['<endpoint id>']` gives the rows of application tables (`scope: { app }`) that the endpoint's handler reads
63
+ for the caller's own scope, by table, with the columns they need: `export const appRows = { 'portal.guide': { guides:
64
+ { active: true } } }`. Each of its leak and own-object cases seeds one row of each before it asks and removes them
65
+ once it is over, with every row that refers to them (a download the handler logged), so a handler that reads the
66
+ newest guide answers 200 instead of 404, and no other endpoint's case finds the row. A foreign key from one given row
67
+ to another holds that row's key. A text column may be given; the key, the version, the creator and a file column may
68
+ not (a file column holds a live, scanned file). Only application tables, and only for an endpoint with a leak or an
69
+ own-object case: the checker refuses a table in a scope, which `rows` and `seeds` address (`endpoint-fixture`).
62
70
  - The endpoint id is `<mount>.<group>.<endpoint>` (`api.tasks.close`).
63
71
  - The seed of the tests fills every text column with a marker and every other required column with a sample of its
64
72
  type: an enum's first label, `1`, `false`, today. A column with a `DEFAULT` keeps it (`status` is `open`). A column
@@ -196,6 +204,9 @@ if (postgres !== null) {
196
204
  `<label> skipped: set FABRIKA_TEST_POSTGRES_URL to run the Postgres-backed tests`, and the file registers no test.
197
205
  A skip proves nothing. To confirm a file ran, run it alone (`bun test --isolate src/tasks`) and check that the pass
198
206
  count is its number of tests, or set `FABRIKA_TEST_POSTGRES_REQUIRED=1`: a missing URL then fails the file.
207
+ - On Cloudflare import `testDatabase` from `@fabrika/db/testing-sqlite` instead, with the same options: it migrates a
208
+ fresh in-memory SQLite database as the Durable Object does, needs no URL and never answers `null`. The generated tests
209
+ already use it.
199
210
  - `statusOf(call)` answers 200 when the call resolves and the status of the application's refusal otherwise; any other
200
211
  failure (no answer, a bug in the test) is rethrown. Import it beside `testKit`; do not write your own.
201
212
  - `kit.testCaller(grants, { kind })` makes a caller: a grant is a role or an action, in one scope or global. A
@@ -89,7 +89,7 @@ export const integrations = app.group({ callers: ['human'] }, (r) => ({
89
89
  that binds it takes an `app` capability: `access: { action, app: true }`, `binds`, and
90
90
  `ctx.webhooks('<webhook>').bind(ctx.app, account)`.
91
91
  - `limits: { maxBytes?, timeoutMs? }`: the body is 1 MiB by default and at most 48 MiB; the handler runs 10 s by
92
- default, at most 60 s. A webhook may declare `kicks`, `signs`, `egress`, `sql` and `unscoped`, and `ctx.files.put`
92
+ default, at most 60 s (25 s on Cloudflare). A webhook may declare `kicks`, `signs`, `egress`, `sql` and `unscoped`, and `ctx.files.put`
93
93
  stores a file a provider sends (an inbound e-mail's attachment) for a file column: raise `limits.maxBytes` to the
94
94
  mail you accept.
95
95
  - Not covered: a provider that validates its URL with a `GET`, or that needs a custom answer body.