devflare 1.0.0-next.1 → 1.0.0-next.10

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 (123) hide show
  1. package/LLM.md +775 -637
  2. package/R2.md +200 -0
  3. package/README.md +285 -514
  4. package/bin/devflare.js +8 -8
  5. package/dist/{account-rvrj687w.js → account-8psavtg6.js} +27 -4
  6. package/dist/bridge/miniflare.d.ts +6 -0
  7. package/dist/bridge/miniflare.d.ts.map +1 -1
  8. package/dist/bridge/proxy.d.ts +5 -6
  9. package/dist/bridge/proxy.d.ts.map +1 -1
  10. package/dist/bridge/server.d.ts.map +1 -1
  11. package/dist/browser.d.ts +50 -0
  12. package/dist/browser.d.ts.map +1 -0
  13. package/dist/{build-mnf6v8gd.js → build-k36xrzvy.js} +26 -7
  14. package/dist/bundler/do-bundler.d.ts +7 -0
  15. package/dist/bundler/do-bundler.d.ts.map +1 -1
  16. package/dist/cli/commands/account.d.ts.map +1 -1
  17. package/dist/cli/commands/build.d.ts.map +1 -1
  18. package/dist/cli/commands/deploy.d.ts.map +1 -1
  19. package/dist/cli/commands/dev.d.ts.map +1 -1
  20. package/dist/cli/commands/doctor.d.ts.map +1 -1
  21. package/dist/cli/commands/init.d.ts.map +1 -1
  22. package/dist/cli/commands/types.d.ts.map +1 -1
  23. package/dist/cli/config-path.d.ts +5 -0
  24. package/dist/cli/config-path.d.ts.map +1 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/package-metadata.d.ts +16 -0
  27. package/dist/cli/package-metadata.d.ts.map +1 -0
  28. package/dist/config/compiler.d.ts +7 -0
  29. package/dist/config/compiler.d.ts.map +1 -1
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +2575 -1221
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/{deploy-nhceck39.js → deploy-dbvfq8vq.js} +33 -15
  35. package/dist/{dev-qnxet3j9.js → dev-rk8p6pse.js} +900 -234
  36. package/dist/dev-server/miniflare-log.d.ts +12 -0
  37. package/dist/dev-server/miniflare-log.d.ts.map +1 -0
  38. package/dist/dev-server/runtime-stdio.d.ts +8 -0
  39. package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
  40. package/dist/dev-server/server.d.ts +2 -0
  41. package/dist/dev-server/server.d.ts.map +1 -1
  42. package/dist/dev-server/vite-utils.d.ts +37 -0
  43. package/dist/dev-server/vite-utils.d.ts.map +1 -0
  44. package/dist/{doctor-e8fy6fj5.js → doctor-06y8nxd4.js} +73 -50
  45. package/dist/{durable-object-t4kbb0yt.js → durable-object-yt8v1dyn.js} +1 -1
  46. package/dist/index-05fyzwne.js +195 -0
  47. package/dist/index-1p814k7s.js +227 -0
  48. package/dist/{index-hcex3rgh.js → index-1phx14av.js} +84 -7
  49. package/dist/{index-tk6ej9dj.js → index-2q3pmzrx.js} +12 -16
  50. package/dist/{index-pf5s73n9.js → index-59df49vn.js} +11 -281
  51. package/dist/index-5yxg30va.js +304 -0
  52. package/dist/index-62b3gt2g.js +12 -0
  53. package/dist/index-6h8xbs75.js +44 -0
  54. package/dist/{index-67qcae0f.js → index-6v3wjg1r.js} +16 -1
  55. package/dist/index-8gtqgb3q.js +529 -0
  56. package/dist/{index-gz1gndna.js → index-9wt9x09k.js} +42 -62
  57. package/dist/index-fef08w43.js +231 -0
  58. package/dist/{index-ep3445yc.js → index-jht2j546.js} +393 -170
  59. package/dist/index-k7r18na8.js +0 -0
  60. package/dist/{index-m2q41jwa.js → index-n932ytmq.js} +9 -1
  61. package/dist/index-pwgyy2q9.js +39 -0
  62. package/dist/{index-07q6yxyc.js → index-v8vvsn9x.js} +1 -0
  63. package/dist/index-vky23txa.js +70 -0
  64. package/dist/index-vs49yxn4.js +322 -0
  65. package/dist/{index-z14anrqp.js → index-wfbfz02q.js} +14 -15
  66. package/dist/index-ws68xvq2.js +311 -0
  67. package/dist/index-y1d8za14.js +196 -0
  68. package/dist/{init-f9mgmew3.js → init-na2atvz2.js} +42 -55
  69. package/dist/router/types.d.ts +24 -0
  70. package/dist/router/types.d.ts.map +1 -0
  71. package/dist/runtime/context.d.ts +249 -8
  72. package/dist/runtime/context.d.ts.map +1 -1
  73. package/dist/runtime/exports.d.ts +50 -55
  74. package/dist/runtime/exports.d.ts.map +1 -1
  75. package/dist/runtime/index.d.ts +8 -1
  76. package/dist/runtime/index.d.ts.map +1 -1
  77. package/dist/runtime/middleware.d.ts +77 -60
  78. package/dist/runtime/middleware.d.ts.map +1 -1
  79. package/dist/runtime/router.d.ts +7 -0
  80. package/dist/runtime/router.d.ts.map +1 -0
  81. package/dist/runtime/validation.d.ts +1 -1
  82. package/dist/runtime/validation.d.ts.map +1 -1
  83. package/dist/src/browser.js +150 -0
  84. package/dist/src/cli/index.js +10 -0
  85. package/dist/{cloudflare → src/cloudflare}/index.js +3 -3
  86. package/dist/{decorators → src/decorators}/index.js +2 -2
  87. package/dist/src/index.js +132 -0
  88. package/dist/src/runtime/index.js +111 -0
  89. package/dist/{sveltekit → src/sveltekit}/index.js +14 -6
  90. package/dist/{test → src/test}/index.js +22 -13
  91. package/dist/{vite → src/vite}/index.js +128 -59
  92. package/dist/sveltekit/platform.d.ts.map +1 -1
  93. package/dist/test/bridge-context.d.ts +5 -2
  94. package/dist/test/bridge-context.d.ts.map +1 -1
  95. package/dist/test/cf.d.ts +25 -11
  96. package/dist/test/cf.d.ts.map +1 -1
  97. package/dist/test/email.d.ts +16 -7
  98. package/dist/test/email.d.ts.map +1 -1
  99. package/dist/test/queue.d.ts.map +1 -1
  100. package/dist/test/resolve-service-bindings.d.ts.map +1 -1
  101. package/dist/test/scheduled.d.ts.map +1 -1
  102. package/dist/test/simple-context.d.ts +1 -1
  103. package/dist/test/simple-context.d.ts.map +1 -1
  104. package/dist/test/tail.d.ts +2 -1
  105. package/dist/test/tail.d.ts.map +1 -1
  106. package/dist/test/worker.d.ts +6 -0
  107. package/dist/test/worker.d.ts.map +1 -1
  108. package/dist/transform/durable-object.d.ts.map +1 -1
  109. package/dist/transform/worker-entrypoint.d.ts.map +1 -1
  110. package/dist/{types-5nyrz1sz.js → types-x9q7t491.js} +30 -16
  111. package/dist/utils/entrypoint-discovery.d.ts +6 -3
  112. package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
  113. package/dist/utils/send-email.d.ts +15 -0
  114. package/dist/utils/send-email.d.ts.map +1 -0
  115. package/dist/vite/plugin.d.ts.map +1 -1
  116. package/dist/worker-entry/composed-worker.d.ts +13 -0
  117. package/dist/worker-entry/composed-worker.d.ts.map +1 -0
  118. package/dist/worker-entry/routes.d.ts +22 -0
  119. package/dist/worker-entry/routes.d.ts.map +1 -0
  120. package/dist/{worker-entrypoint-m9th0rg0.js → worker-entrypoint-c259fmfs.js} +1 -1
  121. package/package.json +21 -19
  122. package/dist/index.js +0 -298
  123. package/dist/runtime/index.js +0 -111
package/LLM.md CHANGED
@@ -1,319 +1,416 @@
1
1
  # Devflare
2
2
 
3
- This file is for LLMs and developers **using** the `devflare` library.
3
+ This file is the truth-first contract for `devflare` as implemented today.
4
4
 
5
- It explains the public mental model, the intended project structure, and the practical authoring patterns that make Devflare different from using raw Wrangler + Miniflare directly.
5
+ Use it when you are:
6
6
 
7
- It does **not** document the internal repository layout or example cases. Treat it as package-facing guidance.
7
+ - generating code
8
+ - reviewing code
9
+ - updating docs
10
+
11
+ Use `Quick start` for the safest defaults, `Trust map` when similar layers are getting mixed together, and `Sharp edges` before documenting advanced behavior. If an example and the implementation disagree, trust the implementation and update the docs.
8
12
 
9
13
  ---
10
14
 
11
- ## What Devflare is
15
+ ## Quick start: safest supported path
12
16
 
13
- Devflare is a **developer platform for Cloudflare Workers built on top of Miniflare, Wrangler-compatible config, and framework-aware tooling**.
17
+ Prefer this shape unless you have a concrete reason not to:
14
18
 
15
- The short version:
19
+ - explicit `files.fetch`
20
+ - `src/fetch.ts` as the main HTTP entry
21
+ - a named event-first `fetch(event)` or `handle(event)` export
22
+ - request-wide middleware via `sequence(...)`
23
+ - explicit bindings in config
24
+ - `createTestContext()` for core integration tests
25
+ - `ref()` for cross-worker composition
16
26
 
17
- > Miniflare gives you a local Workers runtime. Devflare turns that runtime into a coherent developer system.
27
+ ```ts
28
+ import { defineConfig } from 'devflare'
18
29
 
19
- Devflare does this by combining:
30
+ export default defineConfig({
31
+ name: 'hello-worker',
32
+ compatibilityDate: '2026-03-17',
33
+ files: {
34
+ fetch: 'src/fetch.ts'
35
+ }
36
+ })
37
+ ```
20
38
 
21
- - typed config via `defineConfig()`
22
- - convention-driven file discovery
23
- - binding compilation to Wrangler-compatible config
24
- - local Miniflare orchestration
25
- - Node ↔ Worker bridging where needed
26
- - dedicated test helpers for each handler type
27
- - framework integration for Vite and SvelteKit
28
- - local development features that Miniflare alone does not provide cleanly, such as **browser rendering support**, multi-surface orchestration, and a consistent file-structured app model
39
+ ```ts
40
+ import type { FetchEvent } from 'devflare/runtime'
29
41
 
30
- If you are building Cloudflare applications and want more structure than a single `worker.ts` plus manual plumbing, Devflare is the layer that supplies that structure.
42
+ export async function fetch(event: FetchEvent): Promise<Response> {
43
+ return Response.json({
44
+ path: new URL(event.request.url).pathname
45
+ })
46
+ }
47
+ ```
31
48
 
32
49
  ---
33
50
 
34
- ## How Devflare extends Miniflare
51
+ ## Trust map
35
52
 
36
- This is the most important concept to understand.
53
+ ### Layers that are easy to confuse
37
54
 
38
- Devflare is **not** trying to replace Miniflare. It **extends** Miniflare so developers can work with a higher-level application model.
55
+ Keep these layers separate until you have a reason to connect them:
39
56
 
40
- ### Raw Miniflare gives you a runtime
57
+ 1. config-time `process.env`
58
+ 2. `devflare.config.ts`
59
+ 3. generated `wrangler.jsonc`
60
+ 4. runtime Worker `env` bindings
61
+ 5. local dev/test helpers
41
62
 
42
- Miniflare is excellent at local execution and local emulation of many Cloudflare bindings.
63
+ The classic mixups are:
43
64
 
44
- But by itself, it does not define:
65
+ - `files.routes` vs top-level `routes`
66
+ - `vars` vs `secrets`
67
+ - Bun `.env` loading vs runtime secret loading
68
+ - Devflare `config.env` overrides vs Wrangler environment blocks
69
+ - main-entry `env` vs runtime `env`
45
70
 
46
- - how your project should be structured
47
- - how `fetch`, `queue`, `scheduled`, `email`, routes, DOs, and entrypoints should relate
48
- - how cross-worker references should stay typed
49
- - how tests should address each handler surface consistently
50
- - how framework output and auxiliary workers should be wired together
51
- - how to simulate browser rendering locally in a coherent workflow
71
+ ### Source of truth vs generated output
52
72
 
53
- ### Devflare adds the missing platform layer
73
+ Treat these as generated output, not authoring input:
54
74
 
55
- Devflare adds those higher-level capabilities on top of Miniflare.
75
+ - `.devflare/`
76
+ - `env.d.ts`
77
+ - generated `wrangler.jsonc`
56
78
 
57
- Concretely, it provides:
79
+ The source of truth is still:
58
80
 
59
- 1. **Convention-first file structure**
60
- - `src/fetch.ts`
61
- - `src/queue.ts`
62
- - `src/scheduled.ts`
63
- - `src/email.ts`
64
- - `src/routes/**`
65
- - `do.*.ts`
66
- - `ep.*.ts`
67
- - `wf.*.ts`
68
- - `src/transport.ts`
81
+ - `devflare.config.ts`
82
+ - your source files under `src/`
83
+ - your tests
69
84
 
70
- 2. **Config compilation**
71
- - Devflare compiles your config into Wrangler-compatible config rather than making you hand-maintain the low-level representation yourself.
85
+ If generated output looks wrong, fix the source and regenerate it. Do not hand-edit generated artifacts.
72
86
 
73
- 3. **Responsibility isolation**
74
- - Instead of one giant worker file containing every concern, Devflare encourages one surface per responsibility: fetch, queue, cron, email, routes, Durable Objects, WorkerEntrypoints, workflows, and transport.
87
+ ---
75
88
 
76
- 4. **Unified testing surface**
77
- - `cf.worker`
78
- - `cf.queue`
79
- - `cf.scheduled`
80
- - `cf.email`
81
- - `cf.tail`
89
+ ## What Devflare is
82
90
 
83
- 5. **Bridge-backed local development**
84
- - Devflare handles Node-side access, env access, transport decoding, and Miniflare orchestration so tests and tooling feel like one system.
91
+ Devflare is a developer-first layer on top of Cloudflare Workers tooling.
85
92
 
86
- 6. **Capabilities beyond raw Miniflare**
87
- - most notably **browser rendering support** through a local browser shim that lets browser-rendering workflows run locally in a way Miniflare does not provide directly
93
+ It composes existing tools instead of replacing them:
88
94
 
89
- 7. **Framework integration**
90
- - Vite and SvelteKit support with config generation, worker-aware transforms, auxiliary Durable Object workers, and websocket-aware local dev workflows
95
+ - **Miniflare** supplies the local Workers runtime
96
+ - **Wrangler** supplies deployment config and deploy workflows
97
+ - **Vite** participates when the current package opts into Vite-backed mode
98
+ - **Bun** is the CLI runtime and affects process-level `.env` loading
99
+ - **Devflare** ties those pieces into one authoring model
91
100
 
92
- So the design goal is not just “run a Worker locally”.
101
+ Core public capabilities today:
93
102
 
94
- The design goal is:
103
+ - typed config via `defineConfig()`
104
+ - compilation from Devflare config into Wrangler-compatible output
105
+ - event-first runtime helpers
106
+ - request-scoped proxies and per-surface getters
107
+ - typed multi-worker references via `ref()`
108
+ - local orchestration around Miniflare, Vite, and test helpers
109
+ - framework-aware Vite and SvelteKit integration
95
110
 
96
- > let developers build Cloudflare systems using coherent, isolated responsibilities while Devflare composes those responsibilities into a working local and testable whole
111
+ Devflare is not a replacement runtime. It is a higher-level developer system that sits on top of the Cloudflare ecosystem.
97
112
 
98
- ---
113
+ ### Authoring model
99
114
 
100
- ## The authoring model Devflare wants you to use
115
+ A **surface** is a distinct handler or entry file that Devflare treats as its own concern. The common surfaces are:
101
116
 
102
- Devflare is opinionated in a useful way.
117
+ - HTTP via `src/fetch.ts`
118
+ - queue consumers via `src/queue.ts`
119
+ - scheduled handlers via `src/scheduled.ts`
120
+ - incoming email via `src/email.ts`
121
+ - Durable Objects via `do.*.ts`
122
+ - WorkerEntrypoints via `ep.*.ts`
123
+ - workflows via `wf.*.ts`
124
+ - custom transport definitions via `src/transport.ts`
103
125
 
104
- It wants you to think in **surfaces** rather than in “one file that does everything”.
126
+ Prefer one responsibility per file unless you have a strong reason to combine surfaces. That keeps runtime behavior, testing, and multi-worker composition easier to reason about.
105
127
 
106
- ### HTTP surface
128
+ ---
107
129
 
108
- Use `src/fetch.ts` for normal request handling.
130
+ ## Package entrypoints
109
131
 
110
- ### Queue surface
132
+ Use the narrowest import path that matches where the code runs.
111
133
 
112
- Use `src/queue.ts` for queue consumers.
134
+ | Import | Use it for | Practical rule |
135
+ |---|---|---|
136
+ | `devflare` | main package entrypoint | config helpers, `ref()`, `workerName`, the main-entry `env`, and selected Node-side helpers |
137
+ | `devflare/runtime` | handler/runtime code | event types, middleware, strict `env` / `ctx` / `event` / `locals`, and per-surface getters |
138
+ | `devflare/test` | tests | `createTestContext()`, `cf.*`, and test helpers |
139
+ | `devflare/vite` | Vite integration | explicit Vite-side helpers |
140
+ | `devflare/sveltekit` | SvelteKit integration | SvelteKit-facing helpers |
141
+ | `devflare/cloudflare` | Cloudflare account and resource helpers | account/resource/usage helpers |
142
+ | `devflare/decorators` | decorators only | `durableObject()` and related decorator utilities |
113
143
 
114
- ### Scheduled surface
144
+ ### `devflare` vs `devflare/runtime`
115
145
 
116
- Use `src/scheduled.ts` for cron triggers.
146
+ Default rule:
117
147
 
118
- ### Email surface
148
+ 1. use handler parameters first
149
+ 2. use `devflare/runtime` in helpers that run inside a live handler trail
150
+ 3. use the main `devflare` entry when you specifically want the fallback-friendly `env`
119
151
 
120
- Use `src/email.ts` for incoming email handling. Use `sendEmail` bindings in config for outgoing email.
152
+ `import { env } from 'devflare/runtime'` is strict request-scoped access. It works only while Devflare has established an active handler context.
121
153
 
122
- ### Routing surface
154
+ `import { env } from 'devflare'` is the main-entry proxy. It prefers active handler context and can also fall back to test or bridge-backed context when no live request is active.
123
155
 
124
- Use `files.routes` with a directory like `src/routes` when you want file-based HTTP routing instead of one monolithic fetch file.
156
+ Practical example:
125
157
 
126
- ### Durable Object surface
158
+ ```ts
159
+ // worker code
160
+ import { env, locals, type FetchEvent } from 'devflare/runtime'
161
+
162
+ export async function fetch(event: FetchEvent<DevflareEnv>): Promise<Response> {
163
+ const pathname = new URL(event.request.url).pathname
164
+ locals.startedAt = Date.now()
165
+
166
+ const cached = await env.CACHE.get(pathname)
167
+ if (cached) {
168
+ return new Response(cached, {
169
+ headers: {
170
+ 'x-cache': 'hit'
171
+ }
172
+ })
173
+ }
127
174
 
128
- Use `do.*.ts` files for Durable Objects.
175
+ return new Response(`miss:${pathname}`)
176
+ }
177
+ ```
178
+
179
+ ```ts
180
+ // test or bridge code
181
+ import { env } from 'devflare'
182
+ import { createTestContext } from 'devflare/test'
129
183
 
130
- ### RPC / service-entry surface
184
+ await createTestContext()
185
+ await env.CACHE.put('health', 'ok')
186
+ ```
131
187
 
132
- Use `ep.*.ts` files for named WorkerEntrypoints.
188
+ ### Worker-safe caveat for the main entry
133
189
 
134
- ### Workflow surface
190
+ `devflare/runtime` is the explicit worker-safe runtime entry and should be the default teaching path for worker code.
135
191
 
136
- Use `wf.*.ts` files for Workflow classes.
192
+ The main `devflare` entry can also resolve to a worker-safe bundle when the resolver selects the package `browser` condition. Treat that as a compatibility detail, not as a signal that the main entry is the best place to import runtime helpers.
137
193
 
138
- ### Transport surface
194
+ In that worker-safe main bundle:
139
195
 
140
- Use `src/transport.ts` when values crossing boundaries need custom serialization.
196
+ - the browser-safe subset of the main entry remains usable
197
+ - Node-side APIs such as config loading, CLI helpers, Miniflare orchestration, and test-context setup are not available and intentionally throw if called
141
198
 
142
- This is one of Devflare’s biggest advantages: it turns Cloudflare development from “single worker blob” into **separated, discoverable, testable responsibilities**.
199
+ If you need `ctx`, `event`, `locals`, middleware, or per-surface getters, import them from `devflare/runtime`.
143
200
 
144
201
  ---
145
202
 
146
- ## Public package entry points
203
+ ## Runtime and HTTP model
147
204
 
148
- Use the package subpaths intentionally.
205
+ ### Event-first handlers are the public story
149
206
 
150
- | Import | Use for |
151
- |---|---|
152
- | `devflare` | config, `ref()`, `env`, bridge/test convenience exports |
153
- | `devflare/runtime` | runtime-safe utilities for Worker code |
154
- | `devflare/test` | test context, mocks, `cf.*` helpers |
155
- | `devflare/vite` | Vite integration |
156
- | `devflare/sveltekit` | SvelteKit integration |
157
- | `devflare/cloudflare` | Cloudflare-specific helpers |
158
- | `devflare/decorators` | decorators such as `durableObject()` |
207
+ Fresh Devflare code should be event-first. Use shapes like:
159
208
 
160
- ---
209
+ - `fetch(event: FetchEvent)`
210
+ - `queue(event: QueueEvent)`
211
+ - `scheduled(event: ScheduledEvent)`
212
+ - `email(event: EmailEvent)`
213
+ - `tail(event: TailEvent)` when you are wiring a tail surface
214
+ - Durable Object handlers with their matching event types
161
215
 
162
- ## Core ideas you should internalize
216
+ These event types augment native Cloudflare inputs rather than replacing them with unrelated wrappers.
163
217
 
164
- ### `defineConfig()` is the center of the project
218
+ | Event type | Also behaves like | Convenience fields |
219
+ |---|---|---|
220
+ | `FetchEvent` | `Request` | `request`, `env`, `ctx`, `params`, `locals`, `type` |
221
+ | `QueueEvent` | `MessageBatch<T>` | `batch`, `env`, `ctx`, `locals`, `type` |
222
+ | `ScheduledEvent` | `ScheduledController` | `controller`, `env`, `ctx`, `locals`, `type` |
223
+ | `EmailEvent` | `ForwardableEmailMessage` | `message`, `env`, `ctx`, `locals`, `type` |
224
+ | `TailEvent` | `TraceItem[]` | `events`, `env`, `ctx`, `locals`, `type` |
225
+ | `DurableObjectFetchEvent` | `Request` | `request`, `env`, `ctx`, `state`, `locals`, `type` |
226
+ | `DurableObjectAlarmEvent` | event object only | `env`, `ctx`, `state`, `locals`, `type` |
227
+ | `DurableObjectWebSocketMessageEvent` | `WebSocket` | `ws`, `message`, `env`, `ctx`, `state`, `locals`, `type` |
228
+ | `DurableObjectWebSocketCloseEvent` | `WebSocket` | `ws`, `code`, `reason`, `wasClean`, `env`, `ctx`, `state`, `locals`, `type` |
229
+ | `DurableObjectWebSocketErrorEvent` | `WebSocket` | `ws`, `error`, `env`, `ctx`, `state`, `locals`, `type` |
165
230
 
166
- Always define your worker through `defineConfig()`.
231
+ On worker surfaces, `event.ctx` is the current `ExecutionContext`.
167
232
 
168
- ```ts
169
- import { defineConfig } from 'devflare'
233
+ On Durable Object surfaces, `event.ctx` is the current `DurableObjectState`, and Devflare also exposes it as `event.state` for clarity.
170
234
 
171
- export default defineConfig({
172
- name: 'my-worker'
173
- })
174
- ```
235
+ ### Runtime access: parameters, getters, and proxies
175
236
 
176
- This is the source of truth Devflare uses to:
237
+ Prefer runtime access in this order:
177
238
 
178
- - understand your bindings
179
- - understand your file layout
180
- - generate Wrangler-compatible config
181
- - drive dev mode
182
- - drive test mode
239
+ 1. handler parameters such as `fetch(event: FetchEvent)`
240
+ 2. per-surface getters when a deeper helper needs the current concrete surface
241
+ 3. generic runtime proxies when surface-agnostic access is enough
183
242
 
184
- ### Convention beats boilerplate
243
+ Devflare carries the active event through the current handler call trail, so deeper helpers can recover the current surface without manually threading arguments.
185
244
 
186
- If your filenames follow the defaults, you do not need to spell everything out.
245
+ Proxy semantics:
187
246
 
188
- Defaults include:
247
+ - `env`, `ctx`, and `event` from `devflare/runtime` are readonly
248
+ - `locals` is the mutable request-scoped storage object
249
+ - `event.locals` and `locals` point at the same underlying object
250
+ - strict runtime helpers throw when there is no active Devflare-managed context
189
251
 
190
- - `src/fetch.ts`
191
- - `src/queue.ts`
192
- - `src/scheduled.ts`
193
- - `src/email.ts`
194
- - `src/transport.ts`
195
- - `**/do.*.{ts,js}`
196
- - `**/ep.*.{ts,js}`
197
- - `**/wf.*.{ts,js}`
252
+ Per-surface getters:
198
253
 
199
- ### `ref()` is how cross-worker systems stay clean
254
+ - worker surfaces: `getFetchEvent()`, `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, `getTailEvent()`
255
+ - Durable Object surfaces: `getDurableObjectEvent()`, `getDurableObjectFetchEvent()`, `getDurableObjectAlarmEvent()`
256
+ - Durable Object WebSocket surfaces: `getDurableObjectWebSocketMessageEvent()`, `getDurableObjectWebSocketCloseEvent()`, `getDurableObjectWebSocketErrorEvent()`
200
257
 
201
- If one worker depends on another worker or on another worker’s Durable Objects, use `ref()` instead of manually duplicating names.
258
+ Every getter also exposes `.safe()`, which returns `null` instead of throwing.
202
259
 
203
- That keeps cross-worker relationships typed and centralized.
260
+ Practical example:
204
261
 
205
- ### `env` is the unified access point
262
+ ```ts
263
+ import { getFetchEvent, locals, type FetchEvent } from 'devflare/runtime'
206
264
 
207
- Use:
265
+ function currentPath(): string {
266
+ return new URL(getFetchEvent().request.url).pathname
267
+ }
208
268
 
209
- ```ts
210
- import { env } from 'devflare'
269
+ export async function fetch(event: FetchEvent): Promise<Response> {
270
+ locals.requestId = crypto.randomUUID()
271
+
272
+ return Response.json({
273
+ path: currentPath(),
274
+ requestId: String(locals.requestId),
275
+ requestUrl: getFetchEvent.safe()?.request.url ?? null,
276
+ method: event.request.method
277
+ })
278
+ }
211
279
  ```
212
280
 
213
- Devflare makes `env` work coherently across request handling, tests, and bridge-backed local flows.
281
+ ### Manual context helpers are advanced/internal
214
282
 
215
- ---
283
+ Normal Devflare application code should **not** need `runWithEventContext()` or `runWithContext()`.
216
284
 
217
- ## Why file structure matters so much in Devflare
285
+ Devflare establishes the active event/context automatically before invoking user code in the flows developers normally use:
218
286
 
219
- In plain Worker setups, it is common to end up with a single file that mixes:
287
+ - generated worker entrypoints for `fetch`, `queue`, `scheduled`, and `email`
288
+ - Durable Object wrappers
289
+ - HTTP middleware and route resolution
290
+ - `createTestContext()` helpers such as `cf.worker`, `cf.queue`, `cf.scheduled`, `cf.email`, and `cf.tail`
220
291
 
221
- - HTTP routing
222
- - queue handling
223
- - scheduled jobs
224
- - email handling
225
- - Durable Object exports
226
- - auxiliary RPC handlers
227
- - test-specific wiring
292
+ That means getters like `getQueueEvent()`, `getScheduledEvent()`, `getEmailEvent()`, and `getDurableObjectFetchEvent()` should work inside ordinary handlers without manual wrapping.
228
293
 
229
- Devflare pushes in the opposite direction.
294
+ Keep these helpers in the "advanced escape hatch" bucket:
230
295
 
231
- It gives each responsibility a natural home and then composes them together.
296
+ - `runWithEventContext(event, fn)` preserves the exact event you provide
297
+ - `runWithContext(env, ctx, request, fn, type)` is a lower-level compatibility helper
232
298
 
233
- That has several benefits:
299
+ Important difference:
234
300
 
235
- - lower mental overhead
236
- - easier code generation
237
- - easier code review
238
- - easier testing
239
- - fewer accidental runtime coupling mistakes
240
- - clearer ownership boundaries within a team
301
+ - `runWithContext()` only synthesizes rich augmented events for `fetch` and `durable-object-fetch` when a `Request` is available
302
+ - if you are manually constructing a non-fetch surface and truly need per-surface getters outside normal Devflare-managed entrypoints, `runWithEventContext()` is the correct low-level helper
241
303
 
242
- For LLM generation specifically, this matters a lot.
304
+ ### HTTP entry, middleware, and method handlers
243
305
 
244
- When generating Devflare code, prefer **separate files with clear responsibilities** over giant all-in-one worker files.
306
+ Think of the built-in HTTP path today as:
245
307
 
246
- ---
308
+ `src/fetch.ts` request-wide entry → same-module method handlers → matched `src/routes/**` leaf module
309
+
310
+ When the HTTP surface matters to build or deploy output, prefer making it explicit in config:
311
+
312
+ ```ts
313
+ files: {
314
+ fetch: 'src/fetch.ts'
315
+ }
316
+ ```
317
+
318
+ The file router is enabled automatically when a `src/routes` directory exists, unless you set `files.routes: false`.
319
+ Use `files.routes` when you want to change the route root or mount it under a prefix.
247
320
 
248
- ## Config example
321
+ Supported primary entry shapes today:
249
322
 
250
- This is a good realistic starting point.
323
+ - `export async function fetch(...) { ... }`
324
+ - `export const fetch = ...`
325
+ - `export const handle = ...`
326
+ - `export default async function (...) { ... }`
327
+ - `export default { fetch(...) { ... } }`
328
+ - `export default { handle(...) { ... } }`
329
+
330
+ `fetch` and `handle` are aliases for the same primary HTTP entry. Export one or the other, not both.
331
+
332
+ When this document says a `handle` export here, it means the primary fetch export name, not the legacy `handle(...handlers)` helper.
333
+
334
+ Request-wide middleware belongs in `src/fetch.ts` and composes with `sequence(...)`.
251
335
 
252
336
  ```ts
253
- import { defineConfig } from 'devflare'
337
+ import { sequence } from 'devflare/runtime'
338
+ import type { FetchEvent, ResolveFetch } from 'devflare/runtime'
254
339
 
255
- export default defineConfig({
256
- name: 'my-app',
257
- files: {
258
- routes: {
259
- dir: 'src/routes',
260
- prefix: '/api'
261
- }
262
- },
263
- bindings: {
264
- kv: {
265
- CACHE: 'cache-kv-id'
266
- },
267
- d1: {
268
- DB: 'db-id'
269
- },
270
- r2: {
271
- FILES: 'files-bucket'
272
- },
273
- durableObjects: {
274
- COUNTER: 'Counter'
275
- },
276
- queues: {
277
- producers: {
278
- TASK_QUEUE: 'task-queue'
279
- },
280
- consumers: [
281
- {
282
- queue: 'task-queue',
283
- maxBatchSize: 10,
284
- maxRetries: 3
285
- }
286
- ]
287
- },
288
- browser: {
289
- binding: 'BROWSER'
290
- }
291
- },
292
- triggers: {
293
- crons: ['0 */6 * * *']
294
- },
295
- vars: {
296
- LOG_LEVEL: 'info'
340
+ async function authHandle(event: FetchEvent, resolve: ResolveFetch): Promise<Response> {
341
+ if (!event.request.headers.get('authorization')) {
342
+ return new Response('Unauthorized', { status: 401 })
297
343
  }
298
- })
344
+
345
+ return resolve(event)
346
+ }
347
+
348
+ async function appFetch({ request }: FetchEvent): Promise<Response> {
349
+ return new Response(new URL(request.url).pathname)
350
+ }
351
+
352
+ export const handle = sequence(authHandle, appFetch)
299
353
  ```
300
354
 
301
- ---
355
+ `sequence(...)` uses the usual nested middleware flow:
302
356
 
303
- ## File-based routing
357
+ 1. outer middleware before `resolve(event)`
358
+ 2. inner middleware before `resolve(event)`
359
+ 3. downstream leaf handler
360
+ 4. inner middleware after `resolve(event)`
361
+ 5. outer middleware after `resolve(event)`
304
362
 
305
- Devflare supports a file-structured routing model through `files.routes`.
363
+ Same-module method exports are real runtime behavior:
306
364
 
307
- That is important because it moves you away from hand-written request dispatch in one file and toward a more maintainable app structure.
365
+ - method handlers such as `GET`, `POST`, and `ALL` are supported in the fetch module
366
+ - if `HEAD` is not exported, it falls back to `GET` with an empty body
367
+ - this is same-module dispatch, not file-system routing
368
+ - if a module exports a primary `fetch` or `handle` entry and also exports method handlers, the method handlers are the downstream leaf handlers reached through `resolve(event)`
369
+ - if no primary `fetch` or `handle` exists, Devflare can still dispatch directly to same-module method handlers
308
370
 
309
- Example:
371
+ ### Routing today
372
+
373
+ `src/routes/**` is now a real built-in router.
374
+
375
+ Default behavior:
376
+
377
+ - if `src/routes` exists, Devflare discovers it automatically
378
+ - `files.routes.dir` changes the route root
379
+ - `files.routes.prefix` mounts the discovered routes under a fixed prefix such as `/api`
380
+ - `files.routes: false` disables file-route discovery
381
+
382
+ Filename conventions:
383
+
384
+ - `index.ts` → directory root
385
+ - `[id].ts` → single dynamic segment
386
+ - `[...slug].ts` → rest segment, one or more path parts
387
+ - `[[...slug]].ts` → optional rest segment, including the directory root
388
+ - files or directories beginning with `_` are ignored so route-local helpers can live beside handlers
389
+
390
+ Dispatch semantics:
391
+
392
+ - route params are populated on `event.params`
393
+ - route modules use the same handler forms as fetch modules: HTTP method exports, primary `fetch`, or primary `handle`
394
+ - if `src/fetch.ts` exports same-module `GET` / `POST` / `ALL` handlers, those run before the file router for matching methods
395
+ - for route-tree apps, keep `src/fetch.ts` focused on request-wide middleware and whole-app concerns
396
+ - `resolve(event)` from the primary fetch module falls through to the matched route module when no same-module method handler responded
397
+
398
+ | Key | What it means today | What it does not do |
399
+ |---|---|---|
400
+ | `files.routes` | built-in file router configuration for `src/routes/**` discovery, custom route roots, and optional prefixes | it does not replace top-level Cloudflare deployment `routes` |
401
+ | top-level `routes` | Cloudflare deployment route patterns such as `example.com/*`, compiled into generated Wrangler config | it does not choose handlers inside your app |
402
+ | `wsRoutes` | dev-only WebSocket proxy rules that forward matching local upgrade requests to Durable Objects | it does not replace deployment `routes` or act as general HTTP app routing |
403
+
404
+ Practical route-tree example:
310
405
 
311
406
  ```ts
407
+ // devflare.config.ts
312
408
  import { defineConfig } from 'devflare'
313
409
 
314
410
  export default defineConfig({
315
- name: 'my-api',
411
+ name: 'api-worker',
316
412
  files: {
413
+ fetch: 'src/fetch.ts',
317
414
  routes: {
318
415
  dir: 'src/routes',
319
416
  prefix: '/api'
@@ -322,655 +419,696 @@ export default defineConfig({
322
419
  })
323
420
  ```
324
421
 
325
- Conceptually, this means:
422
+ ```ts
423
+ // src/fetch.ts
424
+ import { sequence } from 'devflare/runtime'
326
425
 
327
- - route files map onto URL paths
328
- - route handlers can stay focused on their specific endpoint logic
329
- - your HTTP surface can scale without turning `fetch.ts` into a switch-statement graveyard
426
+ export const handle = sequence(async (event, resolve) => {
427
+ const response = await resolve(event)
428
+ const next = new Response(response.body, response)
429
+ next.headers.set('x-user-id', event.params.id ?? 'none')
430
+ return next
431
+ })
432
+ ```
330
433
 
331
- If you only need a small Worker, `src/fetch.ts` is perfect.
434
+ ```ts
435
+ // src/routes/users/[id].ts
436
+ import type { FetchEvent } from 'devflare/runtime'
332
437
 
333
- If your HTTP surface is growing, `files.routes` is usually the better model.
438
+ export async function GET({ params }: FetchEvent): Promise<Response> {
439
+ return Response.json({ id: params.id })
440
+ }
441
+ ```
442
+
443
+ ```ts
444
+ // GET /api/users/123
445
+ // -> { "id": "123" }
446
+ // and x-user-id: 123
447
+ ```
334
448
 
335
449
  ---
336
450
 
337
- ## Queues: produce in one place, consume in another
451
+ ## Configuration and compilation
452
+
453
+ ### Stable config flow
454
+
455
+ This is the stable public flow for Devflare config:
456
+
457
+ 1. write `devflare.config.*` and export it with `defineConfig()`
458
+ 2. Devflare loads the base config
459
+ 3. if you select an environment, Devflare merges `config.env[name]` into the base config
460
+ 4. Devflare compiles the resolved config into Wrangler-compatible output
461
+ 5. commands such as `dev`, `build`, `deploy`, and `types` use that resolved config for their own work
462
+
463
+ Current implementation note: Devflare validates config against its schema before compilation, and some commands write generated `wrangler.jsonc` files as build artifacts. Treat generated Wrangler config as output, not source of truth.
338
464
 
339
- Devflare wants queue production and queue consumption to live in obvious places.
465
+ ### Supported config files and `defineConfig()`
340
466
 
341
- - produce messages from `fetch` or another handler via the bound queue producer
342
- - consume them in `src/queue.ts`
343
- - test them with `cf.queue`
467
+ Supported config filenames:
344
468
 
345
- ### Queue config example
469
+ - `devflare.config.ts`
470
+ - `devflare.config.mts`
471
+ - `devflare.config.js`
472
+ - `devflare.config.mjs`
473
+
474
+ Use a plain object when your config is static. Use a sync or async function when you need to compute values from `process.env`, the selected environment, or project state.
346
475
 
347
476
  ```ts
348
477
  import { defineConfig } from 'devflare'
349
478
 
350
479
  export default defineConfig({
351
- name: 'tasks-app',
352
- bindings: {
353
- queues: {
354
- producers: {
355
- TASK_QUEUE: 'task-queue'
356
- },
357
- consumers: [
358
- {
359
- queue: 'task-queue',
360
- maxBatchSize: 10,
361
- maxRetries: 3
362
- }
363
- ]
364
- },
365
- kv: {
366
- RESULTS: 'results-kv-id'
367
- }
480
+ name: 'api-worker',
481
+ compatibilityDate: '2026-03-17',
482
+ files: {
483
+ fetch: 'src/fetch.ts'
484
+ },
485
+ vars: {
486
+ APP_NAME: 'api-worker'
368
487
  }
369
488
  })
370
489
  ```
371
490
 
372
- ### Producer example from `fetch`
491
+ TypeScript note: `defineConfig<T>()` is an advanced typing helper. It is mainly useful when you want stronger `ref()` and entrypoint inference after `env.d.ts` exists.
373
492
 
374
- ```ts
375
- import { env } from 'devflare'
493
+ ### Top-level keys
376
494
 
377
- export default async function fetch(request: Request): Promise<Response> {
378
- const url = new URL(request.url)
495
+ Most projects only need a small set of config keys at first. Start with `name`, `compatibilityDate`, `files`, and the `bindings`, `vars`, `secrets`, `triggers`, `routes`, or `env` your worker actually needs.
379
496
 
380
- if (url.pathname === '/tasks') {
381
- const task = {
382
- id: crypto.randomUUID(),
383
- type: 'resize-image'
384
- }
497
+ Current defaults worth knowing:
385
498
 
386
- await env.TASK_QUEUE.send(task)
387
- return Response.json({ queued: true, task })
388
- }
499
+ - `compatibilityDate` defaults to the current date when omitted
500
+ - current default compatibility flags include `nodejs_compat` and `nodejs_als`
389
501
 
390
- return new Response('Not found', { status: 404 })
391
- }
392
- ```
502
+ Common keys:
393
503
 
394
- ### Consumer example in `src/queue.ts`
504
+ | Key | Use it for |
505
+ |---|---|
506
+ | `name` | Worker name |
507
+ | `compatibilityDate` | Workers compatibility date |
508
+ | `compatibilityFlags` | Workers compatibility flags |
509
+ | `files` | explicit handler paths and discovery globs |
510
+ | `bindings` | Cloudflare bindings |
511
+ | `triggers` | scheduled trigger config |
512
+ | `vars` | non-secret string bindings |
513
+ | `secrets` | secret binding declarations |
514
+ | `routes` | Cloudflare deployment routes |
515
+ | `env` | environment overrides merged before compile |
516
+
517
+ Secondary keys:
518
+
519
+ | Key | Use it for |
520
+ |---|---|
521
+ | `accountId` | account selection for remote and deploy flows |
522
+ | `observability` | Workers observability settings |
523
+ | `migrations` | Durable Object migrations |
524
+ | `assets` | static asset config |
525
+ | `limits` | worker limits |
526
+ | `wsRoutes` | local WebSocket proxy rules for dev |
527
+ | `vite` | Devflare Vite metadata |
528
+ | `rolldown` | worker-only bundler options |
529
+ | `wrangler.passthrough` | raw Wrangler overrides after compile |
395
530
 
396
- ```ts
397
- import type { MessageBatch } from '@cloudflare/workers-types'
531
+ ### Native config coverage vs `wrangler.passthrough`
398
532
 
399
- type Task = {
400
- id: string
401
- type: string
402
- }
533
+ Devflare natively models the common Worker config it actively composes around. It does **not** try to mirror every Wrangler field one-by-one as a first-class Devflare schema key.
403
534
 
404
- export default async function queue(
405
- batch: MessageBatch<Task>,
406
- env: DevflareEnv
407
- ): Promise<void> {
408
- for (const message of batch.messages) {
409
- try {
410
- await env.RESULTS.put(
411
- `result:${message.body.id}`,
412
- JSON.stringify({ status: 'completed', type: message.body.type })
413
- )
414
- message.ack()
415
- } catch {
416
- message.retry()
417
- }
418
- }
419
- }
420
- ```
535
+ Use `wrangler.passthrough` for unsupported Wrangler options.
421
536
 
422
- Authoring guidance:
537
+ Current merge order is:
423
538
 
424
- - put queue consumers in `src/queue.ts` unless you have a strong reason not to
425
- - keep message bodies plain and serializable
426
- - use `message.ack()` on success and `message.retry()` on failure
427
- - bind persistence or downstream systems in config rather than hiding them in globals
539
+ 1. compile native Devflare config
540
+ 2. shallow-merge `wrangler.passthrough` on top
541
+ 3. if the same key exists in both places, the passthrough value wins
428
542
 
429
- ---
543
+ Practical example:
430
544
 
431
- ## Email: receiving and sending
545
+ ```ts
546
+ import { defineConfig } from 'devflare'
432
547
 
433
- Devflare supports two sides of email: **receiving** incoming messages and **sending** outgoing ones.
548
+ export default defineConfig({
549
+ name: 'advanced-worker',
550
+ files: {
551
+ fetch: 'src/fetch.ts'
552
+ },
553
+ wrangler: {
554
+ passthrough: {
555
+ placement: {
556
+ mode: 'smart'
557
+ }
558
+ }
559
+ }
560
+ })
561
+ ```
434
562
 
435
- ### Receiving email
563
+ Two practical rules:
436
564
 
437
- Export an `email` function from `src/email.ts`. Devflare wires it as the worker's incoming email handler.
565
+ - if you want Devflare-managed entry composition, keep using `files.fetch`, `files.routes`, and related surface config, and do **not** set `wrangler.passthrough.main`
566
+ - if you want total control of the Worker `main` entry, set `wrangler.passthrough.main` and own that entry file yourself
438
567
 
439
- ```ts
440
- // src/email.ts
441
- import { env } from 'devflare'
442
- import type { ForwardableEmailMessage } from '@cloudflare/workers-types'
443
-
444
- export async function email(message: ForwardableEmailMessage): Promise<void> {
445
- const id = crypto.randomUUID()
446
-
447
- // Log the email to KV
448
- await env.EMAIL_LOG.put(
449
- `email:${id}`,
450
- JSON.stringify({
451
- from: message.from,
452
- to: message.to,
453
- receivedAt: new Date().toISOString()
454
- })
455
- )
568
+ ### `files`
456
569
 
457
- // Forward every incoming message to admin
458
- await message.forward('admin@example.com')
459
- }
460
- ```
570
+ `files` tells Devflare where your surfaces live. Use explicit paths for any surface that matters to build or deploy output, especially `files.fetch`.
461
571
 
462
- The `ForwardableEmailMessage` object also supports `message.reply(replyMessage)` for auto-responses.
572
+ | Key | Shape | Default convention | Meaning |
573
+ |---|---|---|---|
574
+ | `fetch` | <code>string &#124; false</code> | `src/fetch.ts` | main HTTP handler |
575
+ | `queue` | <code>string &#124; false</code> | `src/queue.ts` | queue consumer handler |
576
+ | `scheduled` | <code>string &#124; false</code> | `src/scheduled.ts` | scheduled handler |
577
+ | `email` | <code>string &#124; false</code> | `src/email.ts` | incoming email handler |
578
+ | `durableObjects` | <code>string &#124; false</code> | `**/do.*.{ts,js}` | Durable Object discovery glob |
579
+ | `entrypoints` | <code>string &#124; false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
580
+ | `workflows` | <code>string &#124; false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
581
+ | `routes` | <code>{ dir, prefix? } &#124; false</code> | `src/routes` when that directory exists | built-in file router configuration |
582
+ | `transport` | `string` | none | custom transport definition file |
463
583
 
464
- ### Sending email (the `sendEmail` binding)
584
+ Discovery does not behave identically in every subsystem:
465
585
 
466
- To send outgoing email, declare a `sendEmail` binding in config. Each key becomes a typed `env` property of type `SendEmail`.
586
+ - local dev and `createTestContext()` can fall back to conventional handler files when present
587
+ - local dev, `createTestContext()`, and higher-level worker-entry generation also auto-discover `src/routes/**` unless `files.routes` is `false`
588
+ - type generation and discovery use default globs for Durable Objects, entrypoints, and workflows
589
+ - `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicit
590
+ - higher-level `build`, `deploy`, and `devflare/vite` flows may still generate a composed `.devflare/worker-entrypoints/main.ts`, including route trees
591
+ - `wrangler.passthrough.main` disables that composed-entry generation path
467
592
 
468
- The value object accepts an optional `destinationAddress`:
593
+ Safe rule: if a surface matters to build or deploy output, declare it explicitly even if another subsystem can discover it by convention.
469
594
 
470
- - **With `destinationAddress`** — every email sent through that binding is locked to that recipient.
471
- - **Without it (`{}`)** — you choose the recipient at send time.
595
+ Practical example:
472
596
 
473
597
  ```ts
474
598
  import { defineConfig } from 'devflare'
475
599
 
476
600
  export default defineConfig({
477
- name: 'support-mail',
478
- bindings: {
479
- kv: {
480
- EMAIL_LOG: 'email-log-kv-id'
601
+ name: 'multi-surface-worker',
602
+ files: {
603
+ fetch: 'src/fetch.ts',
604
+ queue: 'src/queue.ts',
605
+ scheduled: 'src/scheduled.ts',
606
+ email: 'src/email.ts',
607
+ routes: {
608
+ dir: 'src/routes',
609
+ prefix: '/api'
481
610
  },
482
- sendEmail: {
483
- // Locked destination — always delivers to admin
484
- ADMIN_EMAIL: { destinationAddress: 'admin@example.com' },
485
-
486
- // Open destination — specify per message
487
- EMAIL: {}
488
- }
611
+ durableObjects: 'src/do/**/*.ts',
612
+ entrypoints: 'src/rpc/**/*.ts',
613
+ workflows: 'src/workflows/**/*.ts'
489
614
  }
490
615
  })
491
616
  ```
492
617
 
493
- This produces:
618
+ `files.transport` is opt-in. Devflare only loads it when `files.transport` is explicitly set, and the file must export a named `transport` object.
494
619
 
495
- - `env.ADMIN_EMAIL: SendEmail` hardcoded to `admin@example.com`
496
- - `env.EMAIL: SendEmail` — recipient provided at send time
620
+ There is no public `files.tail` config key today.
497
621
 
498
- ### Testing email
622
+ ### `.env`, `vars`, `secrets`, and `config.env`
499
623
 
500
- Use the `email` helper from `devflare/test`:
624
+ Keep these layers separate:
501
625
 
502
- ```ts
503
- import { email } from 'devflare/test'
626
+ | Layer | Holds values? | Compiled into generated config? | Use it for |
627
+ |---|---|---|---|
628
+ | `.env` / `process.env` | yes | indirectly, only when your config reads from it | local process-time inputs |
629
+ | `vars` | yes | yes | non-secret string bindings |
630
+ | `secrets` | no, declaration only | no | required/optional runtime secret bindings |
631
+ | `config.env` | yes, as config overlays | yes after merge | environment-specific config overrides |
504
632
 
505
- const response = await email.send({
506
- from: 'sender@example.com',
507
- to: 'recipient@example.com',
508
- subject: 'Test message',
509
- body: 'Hello from the test suite.'
510
- })
633
+ #### `.env` and process env
511
634
 
512
- expect(response.ok).toBe(true)
513
- ```
635
+ `loadConfig()` does not do dotenv loading by itself. The Devflare CLI runs under Bun, and Bun may auto-load `.env` files into `process.env`.
514
636
 
515
- Authoring guidance:
637
+ #### `vars`
516
638
 
517
- - use `src/email.ts` for incoming mail handling
518
- - use `message.forward(...)` for routing workflows
519
- - use `message.reply(...)` when implementing auto-replies
520
- - use `sendEmail` bindings with `destinationAddress` when the recipient is always the same
521
- - use `sendEmail` bindings with `{}` when you need flexible per-message recipients
522
- - log metadata to KV or D1 if you need observability or replay/debugging context
639
+ Use `vars` for non-secret runtime values that can safely appear in generated config, such as public URLs, modes, IDs, and feature flags.
523
640
 
524
- ---
641
+ In the current native Devflare schema, `vars` is:
525
642
 
526
- ## Browser rendering is a real example of Devflare extending Miniflare
643
+ ```ts
644
+ Record<string, string>
645
+ ```
527
646
 
528
- This is worth emphasizing.
647
+ #### `secrets`
529
648
 
530
- Devflare supports **browser rendering** locally via a browser shim layer.
649
+ `secrets` is a declaration layer. Use it to say which secret bindings your worker expects and whether they are required. Do not put secret values here.
531
650
 
532
- That matters because browser rendering is not something Miniflare gives you directly as a complete local developer workflow.
651
+ In the current schema, each secret has the shape:
533
652
 
534
- With Devflare, browser rendering becomes part of the same system as the rest of your bindings and local development story:
653
+ ```ts
654
+ { required?: boolean }
655
+ ```
535
656
 
536
- - declare the browser binding in config
537
- - run the app through Devflare’s orchestration
538
- - use the browser capability locally with the rest of your worker system
657
+ `required` defaults to `true`, so this:
539
658
 
540
- This is exactly the kind of feature that demonstrates the Devflare philosophy:
659
+ ```ts
660
+ secrets: {
661
+ API_KEY: {}
662
+ }
663
+ ```
541
664
 
542
- > take a low-level runtime foundation, then add the higher-level orchestration developers actually need
665
+ means “`API_KEY` is a required runtime secret,” not “optional secret with no requirements.”
543
666
 
544
- ---
667
+ #### `devflare types`
545
668
 
546
- ## Durable Objects and multi-worker systems
669
+ `devflare types` generates `env.d.ts` from the resolved config plus discovered surfaces. The stable public result is typed `DevflareEnv` coverage for bindings such as `vars`, `secrets`, services, Durable Objects, and discovered entrypoints.
547
670
 
548
- Devflare is designed to keep multi-worker and DO-heavy systems manageable.
671
+ Import-path behavior stays the same as described earlier: the main-entry `env` is the broader typed access story, while `devflare/runtime` is the strict request-scoped runtime helper.
549
672
 
550
- ### Local Durable Objects
673
+ #### `config.env`
551
674
 
552
- Bind them in config:
675
+ `config.env` is Devflare’s environment override layer. When you pass `--env <name>` or call `compileConfig(config, name)`, Devflare starts from the base config, merges `config.env[name]` into it, and compiles the resolved result.
553
676
 
554
- ```ts
555
- bindings: {
556
- durableObjects: {
557
- COUNTER: 'Counter'
558
- }
559
- }
560
- ```
677
+ Current merge behavior uses deep merge semantics:
561
678
 
562
- Put their classes in `do.*.ts` files.
679
+ - scalar fields override base values
680
+ - nested objects inherit omitted keys
681
+ - arrays append instead of replacing
682
+ - `null` and `undefined` do not delete base values
563
683
 
564
- ### Complete local Durable Object example
684
+ If you need full replacement behavior, compute the final value in `defineConfig()` instead of relying on `config.env`.
565
685
 
566
- If you need to generate a normal local Durable Object and call it from HTTP code, prefer this shape.
686
+ Example:
567
687
 
568
688
  ```ts
569
- // devflare.config.ts
570
689
  import { defineConfig } from 'devflare'
571
690
 
572
691
  export default defineConfig({
573
- name: 'sessions-app',
692
+ name: 'api',
693
+ files: {
694
+ fetch: 'src/fetch.ts'
695
+ },
696
+ vars: {
697
+ APP_ENV: 'development',
698
+ API_ORIGIN: 'http://localhost:3000'
699
+ },
574
700
  bindings: {
575
- durableObjects: {
576
- SESSION: 'SessionStore'
701
+ kv: {
702
+ CACHE: 'api-cache-dev'
703
+ }
704
+ },
705
+ env: {
706
+ production: {
707
+ vars: {
708
+ APP_ENV: 'production',
709
+ API_ORIGIN: 'https://api.example.com'
710
+ },
711
+ bindings: {
712
+ kv: {
713
+ CACHE: 'api-cache'
714
+ }
715
+ }
577
716
  }
578
717
  }
579
718
  })
580
719
  ```
581
720
 
582
- ```ts
583
- // src/do.session.ts
584
- import { DurableObject } from 'cloudflare:workers'
721
+ With `--env production`, the resolved config keeps `files.fetch`, overrides the `vars`, and overrides `bindings.kv.CACHE`.
585
722
 
586
- export class SessionStore extends DurableObject<DevflareEnv> {
587
- private data = new Map<string, string>()
588
-
589
- getValue(key: string): string | null {
590
- return this.data.get(key) ?? null
591
- }
723
+ Current limitation: `accountId` and `wsRoutes` are not supported inside `config.env`. Compute those in the outer `defineConfig()` function when you need environment-specific values.
592
724
 
593
- setValue(key: string, value: string): void {
594
- this.data.set(key, value)
595
- }
725
+ ---
596
726
 
597
- clearAll(): void {
598
- this.data.clear()
599
- }
600
- }
601
- ```
727
+ ## Bindings and multi-worker composition
602
728
 
603
- ```ts
604
- // src/fetch.ts
605
- import { env } from 'devflare'
729
+ ### Binding groups
606
730
 
607
- export default async function fetch(request: Request): Promise<Response> {
608
- const url = new URL(request.url)
609
- const sessionId = url.searchParams.get('session') ?? 'default'
731
+ | Group | Members | Coverage wording | Safe promise |
732
+ |---|---|---|---|
733
+ | Core bindings | `kv`, `d1`, `r2`, `durableObjects`, `queues.producers`, `queues.consumers` | well-covered | safest public binding path today across config, generated types, and local dev/test |
734
+ | Services and `ref()` | `services` | well-covered with a named-entrypoint caveat | worker-to-worker composition is solid; validate generated output when a named entrypoint matters |
735
+ | Remote-oriented bindings | `ai`, `vectorize` | remote-dependent | supported and typed, but not full local equivalents of KV or D1 |
736
+ | Narrower bindings | `hyperdrive`, `analyticsEngine`, `browser` | supported, narrower-scope | real public surfaces, but less central than the core storage/runtime path |
737
+ | Outbound email binding | `sendEmail` | supported public surface | real outbound email binding; keep it separate from inbound email handling |
610
738
 
611
- const id = env.SESSION.idFromName(sessionId)
612
- const session = env.SESSION.get(id)
739
+ ### Core bindings
613
740
 
614
- if (url.pathname === '/set') {
615
- const key = url.searchParams.get('key') ?? 'name'
616
- const value = url.searchParams.get('value') ?? 'Arthur'
617
- await session.setValue(key, value)
618
- return Response.json({ ok: true })
619
- }
741
+ KV, D1, R2, Durable Objects, and queues have the clearest end-to-end story today across config, generated types, and local dev/test flows.
620
742
 
621
- if (url.pathname === '/get') {
622
- const key = url.searchParams.get('key') ?? 'name'
623
- const value = await session.getValue(key)
624
- return Response.json({ value })
625
- }
743
+ ### R2 browser access and public URLs
626
744
 
627
- if (url.pathname === '/clear') {
628
- await session.clearAll()
629
- return Response.json({ ok: true })
630
- }
745
+ `bindings.r2` gives you real R2 binding access in local dev, tests, generated types, and compiled config.
631
746
 
632
- return new Response('Not found', { status: 404 })
633
- }
634
- ```
747
+ What Devflare does **not** currently expose as a public contract is a stable browser-facing local bucket URL.
635
748
 
636
- Important authoring notes:
749
+ If you need browser-visible local asset flows:
637
750
 
638
- - extend `DurableObject` from `cloudflare:workers`
639
- - bind the namespace in `bindings.durableObjects`
640
- - put the class in a `do.*.ts` file so discovery works naturally
641
- - create a stub with `env.SESSION.get(env.SESSION.idFromName(...))`
642
- - keep RPC-style method inputs and outputs simple and serializable
751
+ - serve them through your Worker routes or app endpoints
752
+ - test real public/custom-domain URL behavior against an intentional staging bucket
753
+ - do not hard-code frontend assumptions about a magic local bucket origin
643
754
 
644
- ### Transport layer example: Devflare encodes and decodes for you
755
+ For delivery patterns and production recommendations, see [`R2.md`](./R2.md).
645
756
 
646
- Use `src/transport.ts` when a Worker or Durable Object returns custom classes that should survive supported RPC/test boundaries as real instances.
757
+ Practical example:
647
758
 
648
759
  ```ts
649
- // src/DoubleableNumber.ts
650
- export class DoubleableNumber {
651
- value: number
652
-
653
- constructor(n: number) {
654
- this.value = n
655
- }
760
+ import { defineConfig } from 'devflare'
656
761
 
657
- get double() {
658
- return this.value * 2
762
+ export default defineConfig({
763
+ name: 'orders-worker',
764
+ bindings: {
765
+ kv: {
766
+ CACHE: 'orders-cache'
767
+ },
768
+ d1: {
769
+ DB: 'orders-db'
770
+ },
771
+ durableObjects: {
772
+ COUNTER: 'Counter'
773
+ },
774
+ queues: {
775
+ producers: {
776
+ TASK_QUEUE: 'orders-tasks'
777
+ }
778
+ }
659
779
  }
660
- }
780
+ })
661
781
  ```
662
782
 
663
783
  ```ts
664
- // src/transport.ts
665
- import { DoubleableNumber } from './DoubleableNumber'
784
+ import type { FetchEvent } from 'devflare/runtime'
666
785
 
667
- export const transport = {
668
- DoubleableNumber: {
669
- encode: (v: unknown) => v instanceof DoubleableNumber && v.value,
670
- decode: (v: number) => new DoubleableNumber(v)
671
- }
786
+ export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
787
+ const status = await env.DB.prepare('select 1 as ok').first()
788
+ await env.CACHE.put('health', JSON.stringify(status))
789
+ await env.TASK_QUEUE.send({ type: 'reindex-orders' })
790
+ return Response.json({ ok: true })
672
791
  }
673
792
  ```
674
793
 
675
- ```ts
676
- // src/do.counter.ts
677
- import { DoubleableNumber } from './DoubleableNumber'
678
-
679
- export class Counter {
680
- private count = 0
794
+ ### Services and named entrypoints
681
795
 
682
- getValue(): DoubleableNumber {
683
- return new DoubleableNumber(this.count)
684
- }
685
-
686
- increment(n: number = 1): DoubleableNumber {
687
- this.count += n
688
- return new DoubleableNumber(this.count)
689
- }
690
- }
691
- ```
796
+ Service bindings can be written directly or via `ref()`.
692
797
 
693
798
  ```ts
694
- // tests/counter.test.ts
695
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
696
- import { createTestContext } from 'devflare/test'
697
- import { env } from 'devflare'
698
- import { DoubleableNumber } from '../src/DoubleableNumber'
699
-
700
- beforeAll(() => createTestContext())
701
- afterAll(() => env.dispose())
799
+ import { defineConfig, ref } from 'devflare'
702
800
 
703
- describe('counter transport', () => {
704
- test('result is decoded back into a class instance', async () => {
705
- const id = env.COUNTER.idFromName('main')
706
- const counter = env.COUNTER.get(id)
707
- const result = await counter.increment(2)
801
+ const auth = ref(() => import('../auth/devflare.config'))
708
802
 
709
- expect(result).toBeInstanceOf(DoubleableNumber)
710
- expect(result.double).toBe(4)
711
- })
803
+ export default defineConfig({
804
+ name: 'gateway',
805
+ bindings: {
806
+ services: {
807
+ AUTH: auth.worker,
808
+ ADMIN: auth.worker('AdminEntrypoint')
809
+ }
810
+ }
712
811
  })
713
812
  ```
714
813
 
715
- What Devflare is doing for the developer:
814
+ Direct worker-to-worker service composition is a strong public path. Named service entrypoints are typed and configurable, but if a specific entrypoint matters at deployment time, validate the generated output in your project.
716
815
 
717
- 1. your DO returns `new DoubleableNumber(...)`
718
- 2. Devflare finds a matching encoder in `src/transport.ts`
719
- 3. the value is serialized while crossing the boundary
720
- 4. Devflare applies the decoder on the receiving side
721
- 5. your code gets a real `DoubleableNumber` instance back
722
-
723
- This means the developer writes the codec once and then works with real domain objects rather than manual serialization code.
724
-
725
- ### Cross-worker references
726
-
727
- Use `ref()` when one worker depends on another worker or another worker’s DO bindings.
816
+ Practical example:
728
817
 
729
818
  ```ts
819
+ // gateway/devflare.config.ts
730
820
  import { defineConfig, ref } from 'devflare'
731
821
 
732
- const authWorker = ref(() => import('../auth/devflare.config'))
822
+ const auth = ref(() => import('../auth/devflare.config'))
733
823
 
734
824
  export default defineConfig({
735
825
  name: 'gateway',
736
826
  bindings: {
737
827
  services: {
738
- AUTH: authWorker.worker
828
+ ADMIN: auth.worker('AdminEntrypoint')
739
829
  }
740
830
  }
741
831
  })
742
832
  ```
743
833
 
744
- This is cleaner than scattering worker names and entrypoint names by hand throughout the codebase.
745
-
746
- ---
834
+ ```ts
835
+ // gateway/src/fetch.ts
836
+ import type { FetchEvent } from 'devflare/runtime'
747
837
 
748
- ## Runtime-safe helpers
838
+ export async function GET({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
839
+ const stats = await env.ADMIN.getStats()
840
+ return Response.json(stats)
841
+ }
842
+ ```
749
843
 
750
- Import runtime-only utilities from `devflare/runtime` when the code runs inside Workers.
844
+ ### Remote-oriented and narrower bindings
751
845
 
752
- Important helpers include:
846
+ `ai` and `vectorize` are remote-dependent bindings. Devflare supports their config and typing, but they should not be documented as full local equivalents of KV or D1.
753
847
 
754
- - `sequence()`
755
- - `resolve()`
756
- - `pipe()`
757
- - context-related utilities
848
+ `hyperdrive`, `analyticsEngine`, and `browser` are supported narrower-scope surfaces. `browser` also sits closer to the dev-orchestration layer than to the core fetch/queue path.
758
849
 
759
- Use this path when you want runtime-safe middleware composition without pulling in Node-oriented orchestration code.
850
+ ### `sendEmail` and inbound email
760
851
 
761
- ---
852
+ `sendEmail` is the outbound email binding surface. It is supported across config, generated types, and local development flows.
762
853
 
763
- ## Testing model
854
+ Incoming email is a separate worker surface:
764
855
 
765
- Devflare testing is one of the library’s biggest strengths.
856
+ - `files.email`
857
+ - `src/email.ts`
858
+ - `EmailEvent`
766
859
 
767
- ### Integration-style local tests
860
+ Keep outbound `sendEmail` and inbound email handling separate in docs and examples.
768
861
 
769
- Use `createTestContext()` when you want a real local Devflare environment backed by Miniflare orchestration.
862
+ Practical example:
770
863
 
771
864
  ```ts
772
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
773
- import { createTestContext, cf } from 'devflare/test'
774
- import { env } from 'devflare'
865
+ // devflare.config.ts
866
+ import { defineConfig } from 'devflare'
775
867
 
776
- beforeAll(() => createTestContext())
777
- afterAll(() => env.dispose())
868
+ export default defineConfig({
869
+ name: 'mail-worker',
870
+ files: {
871
+ fetch: 'src/fetch.ts',
872
+ email: 'src/email.ts'
873
+ },
874
+ bindings: {
875
+ sendEmail: {
876
+ EMAIL: {
877
+ destinationAddress: 'team@example.com'
878
+ }
879
+ }
880
+ }
881
+ })
882
+ ```
778
883
 
779
- describe('worker', () => {
780
- test('GET /health', async () => {
781
- const response = await cf.worker.get('/health')
782
- expect(response.status).toBe(200)
884
+ ```ts
885
+ // src/fetch.ts
886
+ import type { FetchEvent } from 'devflare/runtime'
887
+
888
+ export async function POST({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
889
+ await env.EMAIL.send({
890
+ from: 'noreply@example.com',
891
+ to: 'team@example.com',
892
+ subject: 'Welcome',
893
+ text: 'Hello from Devflare'
783
894
  })
784
- })
895
+
896
+ return new Response('sent')
897
+ }
785
898
  ```
786
899
 
787
- ### Mock/unit-style tests
900
+ ```ts
901
+ // src/email.ts
902
+ import type { EmailEvent } from 'devflare/runtime'
788
903
 
789
- Use `createMockTestContext()`, `withTestContext()`, and the mock binding helpers when you want pure logic tests.
904
+ export async function email({ message }: EmailEvent<DevflareEnv>): Promise<void> {
905
+ await message.forward('ops@example.com')
906
+ }
907
+ ```
790
908
 
791
- ### Why this is better than ad hoc Miniflare setup
909
+ ### `ref()` and multi-worker composition
792
910
 
793
- Devflare gives you a single mental model for testing each handler surface:
911
+ Use `ref()` when one worker depends on another worker config.
794
912
 
795
- - HTTP via `cf.worker`
796
- - queues via `cf.queue`
797
- - cron via `cf.scheduled`
798
- - email via `cf.email`
799
- - tail events via `cf.tail`
913
+ Main public story:
800
914
 
801
- That consistency is part of the value. Instead of every project inventing its own test harness, Devflare gives you one coherent interface.
915
+ - `ref(...).worker` for a default service binding backed by `src/worker.ts` or `src/worker.js`
916
+ - `ref(...).worker('EntrypointName')` for a named service entrypoint
917
+ - typed cross-worker Durable Object handles
802
918
 
803
- ---
919
+ ```ts
920
+ const auth = ref(() => import('../auth/devflare.config'))
921
+ ```
804
922
 
805
- ## Vite and SvelteKit integration
923
+ ```ts
924
+ const auth = ref('custom-auth-name', () => import('../auth/devflare.config'))
925
+ ```
806
926
 
807
- Devflare includes dedicated framework-aware entry points.
927
+ Why `ref()` matters:
808
928
 
809
- ### `devflare/vite`
929
+ - worker names stay centralized
930
+ - cross-worker Durable Object bindings stay typed
931
+ - entrypoint names can be typed from generated `Entrypoints`
932
+ - multi-worker systems avoid scattered magic strings
810
933
 
811
- Use this when you want:
934
+ Advanced members such as `.name`, `.config`, `.configPath`, and `.resolve()` are real, but secondary to the main composition story.
812
935
 
813
- - config compilation during dev and build
814
- - worker-aware transforms
815
- - auxiliary Durable Object worker generation
816
- - websocket-aware proxying for local development
936
+ ---
817
937
 
818
- ### `devflare/sveltekit`
938
+ ## Development workflows
819
939
 
820
- Use this when your app needs:
940
+ ### Operational decision rules
821
941
 
822
- - a Devflare-aware platform object
823
- - integration between SvelteKit and Worker bindings
824
- - local dev behavior that still fits the Devflare model
942
+ Use these rules in order:
825
943
 
826
- This is another example of Devflare going beyond raw local runtime emulation. It helps compose framework tooling, worker bindings, auxiliary workers, and local dev behavior into one predictable setup.
944
+ 1. a local `vite.config.*` in the current package decides whether `dev`, `build`, and `deploy` run in Vite-backed mode or worker-only mode
945
+ 2. `devflare/vite` is an explicit helper layer, not a separate CLI mode
946
+ 3. worker-only mode is a supported first-class path; no local `vite.config.*` means Devflare does not start Vite
947
+ 4. raw Vite config belongs in `vite.config.*`; `config.vite` is Devflare metadata, not full Vite config
948
+ 5. treat `.devflare/*`, `env.d.ts`, and generated Wrangler config as outputs, not authoring inputs
949
+ 6. remote mode is mainly for remote-oriented services such as AI and Vectorize, not a blanket “make everything remote” switch
827
950
 
828
- ---
951
+ ### Daily development loop
829
952
 
830
- ## Remote-only services
953
+ Use the same CLI loop for both worker-only and Vite-backed packages. The presence of a local `vite.config.*` changes the mode automatically.
831
954
 
832
- Some Cloudflare services are not meaningfully local.
955
+ ```bash
956
+ bunx --bun devflare dev
957
+ bunx --bun devflare types
958
+ bunx --bun devflare doctor
959
+ bunx --bun devflare build --env staging
960
+ bunx --bun devflare deploy --dry-run --env staging
961
+ ```
833
962
 
834
- Most importantly:
963
+ Use `--env <name>` on build and deploy when you want Devflare to resolve a named config environment before compilation.
835
964
 
836
- - Workers AI
837
- - Vectorize
965
+ ### CLI reference
838
966
 
839
- Devflare treats those as explicit remote-mode concerns rather than pretending they are fully local.
967
+ | Command | What it does | Important note |
968
+ |---|---|---|
969
+ | `devflare init` | scaffold a project | current templates generate `src/fetch.ts` and explicit `files.fetch` |
970
+ | `devflare dev` | start local development | worker-only by default, Vite-backed only when the current package has a local `vite.config.*` |
971
+ | `devflare build` | build from resolved config | skips Vite for worker-only packages |
972
+ | `devflare deploy` | build and deploy | supports `--env` and `--dry-run` |
973
+ | `devflare types` | generate `env.d.ts` | supports `--output` |
974
+ | `devflare doctor` | check project configuration | supports `--config` |
975
+ | `devflare account` | inspect accounts, resources, usage, and limits | `global` and `workspace` are account-selection flows, not resource-reporting subcommands |
976
+ | `devflare ai` | show Workers AI model pricing | informational command |
977
+ | `devflare remote` | manage remote test mode | helper flow for remote-oriented services |
978
+ | `devflare help` | show help | direct command and flag form both work |
979
+ | `devflare version` | show package version | sourced from installed package metadata |
840
980
 
841
- That is a good thing.
981
+ ### `doctor`
842
982
 
843
- It keeps the local story honest while still giving you an intentional path to test remote capabilities when needed.
983
+ Current `doctor` behavior:
844
984
 
845
- Use the `devflare remote` workflow and guard remote-only tests appropriately.
985
+ - it checks for supported Devflare config filenames
986
+ - it can inspect an explicit config path via `--config`
987
+ - it accepts project-root `wrangler.jsonc`
988
+ - Vite-backed flows may instead generate `.devflare/wrangler.jsonc`
846
989
 
847
990
  ---
848
991
 
849
- ## Generated artifacts
992
+ ## Testing model
850
993
 
851
- Devflare uses `.devflare/` for generated local artifacts.
994
+ Use `devflare/test`.
852
995
 
853
- Treat that directory as generated output rather than hand-authored source.
996
+ The default pairing is:
854
997
 
855
- ---
998
+ ```ts
999
+ import { beforeAll, afterAll } from 'bun:test'
1000
+ import { createTestContext } from 'devflare/test'
1001
+ import { env } from 'devflare'
856
1002
 
857
- ## Recommended project structure
858
-
859
- Here is a strong default layout for most projects.
860
-
861
- ```text
862
- .
863
- ├── devflare.config.ts
864
- ├── env.d.ts
865
- ├── src/
866
- │ ├── fetch.ts
867
- │ ├── queue.ts
868
- │ ├── scheduled.ts
869
- │ ├── email.ts
870
- │ ├── transport.ts
871
- │ ├── routes/
872
- │ ├── do.counter.ts
873
- │ ├── ep.admin.ts
874
- │ └── wf.sync.ts
875
- └── tests/
876
- └── worker.test.ts
1003
+ beforeAll(() => createTestContext())
1004
+ afterAll(() => env.dispose())
877
1005
  ```
878
1006
 
879
- You will not always need every file.
1007
+ `createTestContext()` is the recommended default for most integration-style tests.
880
1008
 
881
- But this shape captures the Devflare philosophy well:
1009
+ Current behavior:
882
1010
 
883
- - each capability gets an isolated home
884
- - config stays declarative
885
- - tests target named surfaces
886
- - local orchestration is handled by Devflare instead of bespoke glue
1011
+ - it can auto-discover the nearest supported config file by walking upward from the calling test file
1012
+ - it sets up the main-entry test env used by `import { env } from 'devflare'`
1013
+ - it resolves service bindings and cross-worker Durable Object references
1014
+ - it can infer conventional fetch, queue, scheduled, and email handler files when present
1015
+ - it also auto-detects `src/tail.ts` when present, even though there is no public `files.tail` config key
1016
+ - `files.transport` is opt-in rather than auto-loaded by filename convention
887
1017
 
888
- ---
1018
+ Practical example:
889
1019
 
890
- ## Guidance for LLMs generating Devflare code
1020
+ ```ts
1021
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
1022
+ import { cf, createTestContext } from 'devflare/test'
1023
+ import { env } from 'devflare'
891
1024
 
892
- When generating Devflare projects or edits, follow these rules.
1025
+ beforeAll(() => createTestContext())
1026
+ afterAll(() => env.dispose())
893
1027
 
894
- ### Prefer conventions first
1028
+ describe('users routes', () => {
1029
+ test('GET /users/123 uses the built-in file router', async () => {
1030
+ const response = await cf.worker.get('/users/123')
1031
+ expect(response.status).toBe(200)
1032
+ expect(await response.json()).toEqual({ id: '123' })
1033
+ })
895
1034
 
896
- Only override paths when there is a real reason.
1035
+ test('queue side effects can be asserted with real bindings', async () => {
1036
+ await cf.queue.trigger([
1037
+ { type: 'reindex', id: 'job-1' }
1038
+ ])
897
1039
 
898
- ### Prefer separation of responsibilities
1040
+ expect(await env.RESULTS.get('job-1')).toBe('done')
1041
+ })
1042
+ })
1043
+ ```
899
1044
 
900
- Do **not** collapse fetch, queue, scheduled, email, DOs, and entrypoints into a single file unless the task explicitly demands that shape.
1045
+ ### Helper behavior
901
1046
 
902
- ### Prefer `defineConfig()`
1047
+ Do not assume every `cf.*` helper behaves the same way.
903
1048
 
904
- Never emit untyped ad hoc config objects when a normal Devflare config is appropriate.
1049
+ | Helper | What it does | Waits for `waitUntil()`? | Important nuance |
1050
+ |---|---|---|---|
1051
+ | `cf.worker.fetch()` | invokes the configured HTTP surface, including built-in file routes | No | returns when the handler resolves |
1052
+ | `cf.queue.trigger()` | invokes the configured queue consumer | Yes | drains queued background work before returning |
1053
+ | `cf.scheduled.trigger()` | invokes the configured scheduled handler | Yes | drains queued background work before returning |
1054
+ | `cf.email.send()` | sends a raw email through the helper | Yes on the direct-handler path; fallback endpoint behavior is runtime-driven | when `createTestContext()` has wired an email handler, it imports and invokes that handler directly; otherwise it falls back to the local email endpoint |
1055
+ | `cf.tail.trigger()` | directly invokes a tail handler | Yes | `createTestContext()` auto-detects `src/tail.ts` when present, but there is still no public `files.tail` config key |
905
1056
 
906
- ### Prefer `ref()` for cross-worker relationships
1057
+ Two important consequences:
907
1058
 
908
- Avoid manually duplicating worker names and binding metadata.
1059
+ - `cf.worker.fetch()` is not a “wait for all background work” helper
1060
+ - helper availability does not mean every helper replays the full Cloudflare dispatch path in the same way
909
1061
 
910
- ### Prefer `devflare/test` for tests
1062
+ ### Email testing
911
1063
 
912
- Use `createTestContext()` for integration-style tests and mock helpers for unit tests.
1064
+ Email testing has two directions:
913
1065
 
914
- ### Prefer `env` for ergonomic binding access
1066
+ - incoming email helper dispatch via `cf.email.send()` / `email.send()`
1067
+ - outgoing email observation via helper state
915
1068
 
916
- Use `import { env } from 'devflare'` when it improves clarity.
1069
+ When `createTestContext()` has wired an email handler, the helper imports that handler directly, creates an `EmailEvent`, and waits for queued `waitUntil()` work.
917
1070
 
918
- ### Respect remote-only boundaries
1071
+ If no email handler has been wired, the helper falls back to posting the raw email to the local email endpoint.
919
1072
 
920
- Do not fake fully local AI or Vectorize support.
1073
+ That makes the helper useful for handler-level tests, but ingress-fidelity-sensitive flows should still use higher-level integration testing.
921
1074
 
922
- ### Use routing when HTTP complexity grows
1075
+ ### Tail testing
923
1076
 
924
- If the app has many endpoints, prefer `files.routes` and route files over a giant `fetch.ts` dispatcher.
1077
+ Tail helpers are exported publicly, and `createTestContext()` auto-detects `src/tail.ts` when present.
925
1078
 
926
- ---
1079
+ So the truthful public statement is:
927
1080
 
928
- ## Minimal example
1081
+ - `cf.tail.trigger()` is exported
1082
+ - `createTestContext()` wires it automatically when `src/tail.ts` exists
1083
+ - `cf.tail.trigger()` directly imports and invokes that handler and waits for `waitUntil()`
1084
+ - there is no public `files.tail` config key to advertise today
929
1085
 
930
- ```ts
931
- // devflare.config.ts
932
- import { defineConfig } from 'devflare'
1086
+ ### Remote mode in tests
933
1087
 
934
- export default defineConfig({
935
- name: 'hello-worker'
936
- })
937
- ```
1088
+ Remote mode is mainly about AI and Vectorize. Use `shouldSkip` for cost-sensitive or remote-only test gating. Do not treat remote mode as a blanket “make every binding remote” switch.
938
1089
 
939
- ```ts
940
- // src/fetch.ts
941
- export default async function fetch(): Promise<Response> {
942
- return new Response('Hello from Devflare')
943
- }
944
- ```
1090
+ ---
945
1091
 
946
- ```ts
947
- // tests/worker.test.ts
948
- import { beforeAll, afterAll, describe, expect, test } from 'bun:test'
949
- import { createTestContext, cf } from 'devflare/test'
950
- import { env } from 'devflare'
1092
+ ## Sharp edges
951
1093
 
952
- beforeAll(() => createTestContext())
953
- afterAll(() => env.dispose())
1094
+ Keep these caveats explicit:
954
1095
 
955
- describe('hello-worker', () => {
956
- test('GET / returns text', async () => {
957
- const response = await cf.worker.get('/')
958
- expect(response.status).toBe(200)
959
- expect(await response.text()).toBe('Hello from Devflare')
960
- })
961
- })
962
- ```
1096
+ - `files.routes` configures the built-in file router; it is not the same thing as Cloudflare deployment `routes`
1097
+ - route trees are real now, but `src/fetch.ts` same-module method handlers still take precedence over file routes for matching methods
1098
+ - route files that normalize to the same pattern are rejected as conflicts
1099
+ - `_`-prefixed files and directories inside the route tree are ignored
1100
+ - `vars` values are strings in the current native schema
1101
+ - `secrets` declares runtime secret bindings; it does not store secret values
1102
+ - `wrangler.passthrough` is the escape hatch for unsupported Wrangler keys and is merged after native compilation
1103
+ - named service entrypoints need deployment-time validation if they are critical to your app
1104
+ - local R2 binding support is real, but there is still no stable public/browser local bucket URL contract to document as public API
1105
+ - `sendEmail` is a supported outbound binding, while inbound email is a separate worker surface
1106
+ - email and tail helpers have real, useful test paths, but they should not be described as identical to full Cloudflare ingress or tail replay
1107
+ - Vite and Rolldown are different systems and should not be blurred together
1108
+ - higher-level flows may generate composed main worker entries more aggressively than older docs implied
963
1109
 
964
1110
  ---
965
1111
 
966
- ## What to remember
967
-
968
- If you remember only seven things, remember these:
1112
+ ## Summary
969
1113
 
970
- 1. `defineConfig()` is the source of truth
971
- 2. Devflare extends Miniflare into a full developer workflow
972
- 3. file structure is a feature, not a side detail
973
- 4. responsibilities should live in separate surfaces and files
974
- 5. browser rendering is a concrete example of Devflare adding capabilities above raw Miniflare
975
- 6. `devflare/test` gives you a coherent test API across handler types
976
- 7. use routing, refs, and framework integrations when the app grows beyond a tiny single-worker shape
1114
+ Prefer explicit config over discovery, separated surfaces over a monolithic Worker file, and generated output as output rather than source. The safest public path today is explicit `files.fetch`, event-first handlers, explicit bindings, `createTestContext()` for core tests, and `ref()` for cross-worker composition.