devflare 1.0.0-next.10 → 1.0.0-next.11

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.
package/LLM.md CHANGED
@@ -64,7 +64,8 @@ The classic mixups are:
64
64
 
65
65
  - `files.routes` vs top-level `routes`
66
66
  - `vars` vs `secrets`
67
- - Bun `.env` loading vs runtime secret loading
67
+ - Bun or host `.env*` loading vs runtime secret loading
68
+ - config-time `.env*` files vs local runtime `.dev.vars*` files
68
69
  - Devflare `config.env` overrides vs Wrangler environment blocks
69
70
  - main-entry `env` vs runtime `env`
70
71
 
@@ -110,6 +111,11 @@ Core public capabilities today:
110
111
 
111
112
  Devflare is not a replacement runtime. It is a higher-level developer system that sits on top of the Cloudflare ecosystem.
112
113
 
114
+ The shortest truthful mental model is:
115
+
116
+ - **Vite** is the optional outer app/framework host. Devflare plugs into it only when the current package has a local `vite.config.*`.
117
+ - **Rolldown** is the inner builder Devflare uses when Devflare itself needs to transform Worker source into runnable Worker modules. Today that shows up most clearly in the Durable Object bundler and its watch/rebuild loop.
118
+
113
119
  ### Authoring model
114
120
 
115
121
  A **surface** is a distinct handler or entry file that Devflare treats as its own concern. The common surfaces are:
@@ -525,9 +531,234 @@ Secondary keys:
525
531
  | `limits` | worker limits |
526
532
  | `wsRoutes` | local WebSocket proxy rules for dev |
527
533
  | `vite` | Devflare Vite metadata |
528
- | `rolldown` | worker-only bundler options |
534
+ | `rolldown` | Durable Object bundler options |
529
535
  | `wrangler.passthrough` | raw Wrangler overrides after compile |
530
536
 
537
+ ### Complete config property reference
538
+
539
+ Treat this as the exhaustive property checklist for `defineConfig({...})`. The later sections explain behavior in narrative form; this section answers “what keys exist right now, what shape do they accept, and what do they control?”
540
+
541
+ #### Root properties
542
+
543
+ | Property | Shape | Required | Current behavior |
544
+ |---|---|---|---|
545
+ | `name` | `string` | yes | Worker name compiled to Wrangler `name`. It is also the base name used for generated auxiliary DO workers (`${name}-do`) when Devflare creates one. |
546
+ | `accountId` | `string` | no | Compiled to Wrangler `account_id`. Most relevant for deploy flows and remote-oriented bindings such as AI and Vectorize. Not currently supported inside `config.env`. |
547
+ | `compatibilityDate` | `string` (`YYYY-MM-DD`) | no | Compiled to Wrangler `compatibility_date`. Defaults to the current date when omitted. |
548
+ | `compatibilityFlags` | `string[]` | no | Additional Workers compatibility flags. Devflare also forces `nodejs_compat` and `nodejs_als`. |
549
+ | `files` | object | no | Explicit surface file paths and discovery globs. Use this when a surface matters to generated output. |
550
+ | `bindings` | object | no | Cloudflare binding declarations. These compile to Wrangler binding sections and also drive generated typing. |
551
+ | `triggers` | object | no | Scheduled trigger configuration such as cron expressions. |
552
+ | `vars` | `Record<string, string>` | no | Non-secret runtime bindings compiled into Wrangler `vars`. |
553
+ | `secrets` | `Record<string, { required?: boolean }>` | no | Secret declarations only. Values do not live here. |
554
+ | `routes` | `Array<{ pattern; zone_name?; zone_id?; custom_domain? }>` | no | Cloudflare deployment routes. This is separate from the built-in file router. |
555
+ | `wsRoutes` | `Array<{ pattern; doNamespace; idParam?; forwardPath? }>` | no | Dev-only WebSocket proxy rules that forward matching upgrade requests to Durable Objects. Not compiled into Wrangler config. |
556
+ | `assets` | `{ directory: string; binding?: string }` | no | Static asset directory config compiled into Wrangler `assets`. |
557
+ | `limits` | `{ cpu_ms?: number }` | no | Worker execution limits compiled into Wrangler `limits`. |
558
+ | `observability` | `{ enabled?: boolean; head_sampling_rate?: number }` | no | Workers observability and sampling config compiled into Wrangler `observability`. |
559
+ | `migrations` | `Migration[]` | no | Durable Object migration history compiled into Wrangler `migrations`. |
560
+ | `rolldown` | object | no | Rolldown builder configuration for Devflare's own code-transformation path, currently focused on Durable Object workers. This is not a replacement for `vite.config.*`. |
561
+ | `vite` | object | no | Devflare-owned Vite metadata namespace. Raw Vite server/build/resolve config still belongs in local `vite.config.*`. |
562
+ | `env` | `Record<string, EnvOverride>` | no | Named environment overlays merged into the base config before compile/build/deploy. |
563
+ | `wrangler` | `{ passthrough?: Record<string, unknown> }` | no | Escape hatch for unsupported Wrangler keys, merged after native Devflare compile. |
564
+ | `build` | object | legacy | Deprecated alias normalized into `rolldown`. Keep accepting it for compatibility; teach `rolldown` in new docs. |
565
+ | `plugins` | `unknown[]` | legacy | Deprecated alias normalized into `vite.plugins`. Raw Vite plugin wiring still belongs in `vite.config.*`. |
566
+
567
+ #### `files`
568
+
569
+ `files` is where Devflare discovers or pins the files that make up your worker surfaces.
570
+
571
+ | Key | Shape | Default or convention | Meaning |
572
+ |---|---|---|---|
573
+ | `fetch` | `string \| false` | `src/fetch.ts` when present | Main HTTP entry. Keep this explicit when build or deploy output depends on it. |
574
+ | `queue` | `string \| false` | `src/queue.ts` when present | Queue consumer surface. |
575
+ | `scheduled` | `string \| false` | `src/scheduled.ts` when present | Scheduled/cron surface. |
576
+ | `email` | `string \| false` | `src/email.ts` when present | Inbound email surface. |
577
+ | `durableObjects` | `string \| false` | `**/do.*.{ts,js}` | Discovery glob for Durable Object classes. Respects `.gitignore`. |
578
+ | `entrypoints` | `string \| false` | `**/ep.*.{ts,js}` | Discovery glob for `WorkerEntrypoint` classes. Respects `.gitignore`. |
579
+ | `workflows` | `string \| false` | `**/wf.*.{ts,js}` | Discovery glob for workflow classes. Respects `.gitignore`. |
580
+ | `routes` | `{ dir: string; prefix?: string } \| false` | `src/routes` when that directory exists | Built-in route-tree config. `dir` changes the route root; `prefix` mounts it under a static pathname prefix such as `/api`; `false` disables route discovery. |
581
+ | `transport` | `string \| null` | `src/transport.{ts,js,mts,mjs}` when present | Custom serialization transport file. The file must export a named `transport` object. Set `null` to disable autodiscovery explicitly. |
582
+
583
+ Current `files` rules worth keeping explicit:
584
+
585
+ - `compileConfig()` only writes Wrangler `main` when `files.fetch` is explicit
586
+ - higher-level `build`, `deploy`, and `devflare/vite` flows may still generate a composed `.devflare/worker-entrypoints/main.ts` when multiple surfaces must be stitched together
587
+ - `wrangler.passthrough.main` opts out of that composed-entry generation path
588
+ - `createTestContext()` and local dev can still auto-discover conventional files even when you omit them from config
589
+
590
+ #### `bindings`
591
+
592
+ `bindings` groups Cloudflare service bindings by kind.
593
+
594
+ | Key | Shape | Meaning |
595
+ |---|---|---|
596
+ | `kv` | `Record<string, string>` | KV namespace binding name → namespace id |
597
+ | `d1` | `Record<string, string>` | D1 binding name → database id |
598
+ | `r2` | `Record<string, string>` | R2 binding name → bucket name |
599
+ | `durableObjects` | `Record<string, string \| { className: string; scriptName?: string }>` | Durable Object namespace binding. String form is shorthand for `{ className }`. Object form also covers cross-worker DOs and `ref()`-driven bindings. |
600
+ | `queues` | `{ producers?: Record<string, string>; consumers?: QueueConsumer[] }` | Queue producer bindings plus consumer settings |
601
+ | `services` | `Record<string, { service: string; environment?: string; entrypoint?: string }>` | Worker service bindings. `ref().worker` and `ref().worker('Entrypoint')` normalize here. |
602
+ | `ai` | `{ binding: string }` | Workers AI binding |
603
+ | `vectorize` | `Record<string, { indexName: string }>` | Vectorize index bindings |
604
+ | `hyperdrive` | `Record<string, { id: string }>` | Hyperdrive bindings |
605
+ | `browser` | `{ binding: string }` | Browser Rendering binding |
606
+ | `analyticsEngine` | `Record<string, { dataset: string }>` | Analytics Engine dataset bindings |
607
+ | `sendEmail` | `Record<string, { destinationAddress?: string; allowedDestinationAddresses?: string[]; allowedSenderAddresses?: string[] }>` | Outbound email bindings |
608
+
609
+ Queue consumer objects currently support:
610
+
611
+ | Field | Shape | Meaning |
612
+ |---|---|---|
613
+ | `queue` | `string` | Queue name to consume |
614
+ | `maxBatchSize` | `number` | Max messages per batch |
615
+ | `maxBatchTimeout` | `number` | Max seconds to wait for a batch |
616
+ | `maxRetries` | `number` | Max retry attempts |
617
+ | `deadLetterQueue` | `string` | Queue for permanently failed messages |
618
+ | `maxConcurrency` | `number` | Max concurrent batch invocations |
619
+ | `retryDelay` | `number` | Delay between retries in seconds |
620
+
621
+ Two `bindings` details that matter in practice:
622
+
623
+ - `bindings.sendEmail` must use either `destinationAddress` or `allowedDestinationAddresses`, not both
624
+ - `bindings.durableObjects.*.scriptName` is how you point a binding at another worker when the class does not live in the main worker bundle
625
+
626
+ #### `triggers`, `routes`, and `wsRoutes`
627
+
628
+ | Property | Shape | Current behavior |
629
+ |---|---|---|
630
+ | `triggers.crons` | `string[]` | Cloudflare cron expressions compiled into Wrangler `triggers.crons` |
631
+ | `routes[].pattern` | `string` | Deployment route pattern such as `example.com/*` |
632
+ | `routes[].zone_name` | `string` | Optional zone association |
633
+ | `routes[].zone_id` | `string` | Optional zone association alternative to `zone_name` |
634
+ | `routes[].custom_domain` | `boolean` | Mark route as a custom domain |
635
+ | `wsRoutes[].pattern` | `string` | Local URL pattern to intercept for WebSocket upgrades |
636
+ | `wsRoutes[].doNamespace` | `string` | Target Durable Object namespace binding name |
637
+ | `wsRoutes[].idParam` | `string` | Query parameter used to pick the DO instance. Defaults to `'id'`. |
638
+ | `wsRoutes[].forwardPath` | `string` | Path forwarded inside the DO. Defaults to `'/websocket'`. |
639
+
640
+ Remember the split:
641
+
642
+ - `files.routes` is app routing
643
+ - top-level `routes` is Cloudflare deployment routing
644
+ - `wsRoutes` is local dev-time WebSocket proxy routing for Durable Objects
645
+
646
+ #### `vars` and `secrets`
647
+
648
+ | Property | Shape | Current behavior |
649
+ |---|---|---|
650
+ | `vars` | `Record<string, string>` | Non-secret runtime bindings compiled into Wrangler `vars` |
651
+ | `secrets` | `Record<string, { required?: boolean }>` | Secret declarations only. `required` defaults to `true`. Values must come from Cloudflare secrets, tests, or upstream local tooling. |
652
+
653
+ #### `assets`, `limits`, and `observability`
654
+
655
+ | Property | Shape | Current behavior |
656
+ |---|---|---|
657
+ | `assets.directory` | `string` | Required asset directory path |
658
+ | `assets.binding` | `string` | Optional asset binding name for programmatic access |
659
+ | `limits.cpu_ms` | `number` | Optional CPU limit for unbound workers |
660
+ | `observability.enabled` | `boolean` | Enable Worker Logs |
661
+ | `observability.head_sampling_rate` | `number` | Log sampling rate from `0` to `1` |
662
+
663
+ #### `migrations`
664
+
665
+ Each migration object has this shape:
666
+
667
+ | Field | Shape | Meaning |
668
+ |---|---|---|
669
+ | `tag` | `string` | Required migration version label |
670
+ | `new_classes` | `string[]` | Newly introduced DO classes |
671
+ | `renamed_classes` | `Array<{ from: string; to: string }>` | DO class renames with state preservation |
672
+ | `deleted_classes` | `string[]` | Deleted DO classes |
673
+ | `new_sqlite_classes` | `string[]` | DO classes migrating to SQLite storage |
674
+
675
+ #### `rolldown`
676
+
677
+ `rolldown` config applies to Devflare's Durable Object bundler.
678
+
679
+ | Key | Shape | Current behavior |
680
+ |---|---|---|
681
+ | `target` | `string` | Bundle target for emitted DO bundles |
682
+ | `minify` | `boolean` | Minify DO bundles |
683
+ | `sourcemap` | `boolean` | Emit source maps for DO bundles |
684
+ | `options` | `DevflareRolldownOptions` | Additional Rolldown input/output options and plugins, minus Devflare-owned fields |
685
+
686
+ Current `rolldown.options` ownership rules:
687
+
688
+ - Devflare owns `cwd`, `input`, `platform`, and `watch`
689
+ - Devflare also owns output `codeSplitting`, `dir`, `file`, `format`, and `inlineDynamicImports`
690
+ - output stays single-file ESM so the local DO bundling story remains worker-friendly
691
+ - `rolldown.options.plugins` is the intended extension point for custom transforms and Rollup-compatible plugins
692
+
693
+ #### `vite`
694
+
695
+ `vite` is the Devflare-side Vite metadata namespace, not the place for raw Vite app config.
696
+
697
+ | Key | Shape | Current behavior |
698
+ |---|---|---|
699
+ | `plugins` | `unknown[]` | Accepted by the schema and normalized from the legacy top-level `plugins` alias |
700
+ | any other key | `unknown` | Preserved by the schema as Devflare-level Vite metadata, but raw Vite server/build/resolve/plugin wiring still belongs in local `vite.config.*` |
701
+
702
+ That distinction is intentional:
703
+
704
+ - put real Vite config in `vite.config.ts`
705
+ - use `devflare/vite` helpers when Devflare needs to participate in the Vite pipeline
706
+ - treat `config.vite` as Devflare-owned metadata, not as a drop-in replacement for Vite's own config file
707
+
708
+ #### `env`
709
+
710
+ `env` is `Record<string, EnvOverride>`, where each environment can currently override these keys:
711
+
712
+ - `name`
713
+ - `compatibilityDate`
714
+ - `compatibilityFlags`
715
+ - `files`
716
+ - `bindings`
717
+ - `triggers`
718
+ - `vars`
719
+ - `secrets`
720
+ - `routes`
721
+ - `assets`
722
+ - `limits`
723
+ - `observability`
724
+ - `migrations`
725
+ - `rolldown`
726
+ - `vite`
727
+ - `wrangler`
728
+ - deprecated `build`
729
+ - deprecated `plugins`
730
+
731
+ Current exclusions still matter:
732
+
733
+ - `accountId` is not supported inside `env`
734
+ - `wsRoutes` is not supported inside `env`
735
+ - nested `env` blocks are not part of the override shape
736
+
737
+ Merge behavior is also part of the contract:
738
+
739
+ - scalars override base values
740
+ - nested objects merge
741
+ - arrays append instead of replacing
742
+ - `null` and `undefined` do not delete inherited values
743
+
744
+ #### `wrangler`
745
+
746
+ `wrangler` currently exposes one native child key:
747
+
748
+ | Key | Shape | Current behavior |
749
+ |---|---|---|
750
+ | `passthrough` | `Record<string, unknown>` | Shallow-merged on top of the compiled Wrangler config. Use this for unsupported Wrangler keys or to take full ownership of `main`. |
751
+
752
+ #### Deprecated aliases
753
+
754
+ | Legacy key | Current canonical key | Notes |
755
+ |---|---|---|
756
+ | `build.target` | `rolldown.target` | Deprecated but still normalized |
757
+ | `build.minify` | `rolldown.minify` | Deprecated but still normalized |
758
+ | `build.sourcemap` | `rolldown.sourcemap` | Deprecated but still normalized |
759
+ | `build.rolldownOptions` | `rolldown.options` | Deprecated but still normalized |
760
+ | `plugins` | `vite.plugins` | Deprecated top-level alias; raw Vite plugin wiring still belongs in `vite.config.*` |
761
+
531
762
  ### Native config coverage vs `wrangler.passthrough`
532
763
 
533
764
  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.
@@ -579,7 +810,7 @@ Two practical rules:
579
810
  | `entrypoints` | <code>string &#124; false</code> | `**/ep.*.{ts,js}` | WorkerEntrypoint discovery glob |
580
811
  | `workflows` | <code>string &#124; false</code> | `**/wf.*.{ts,js}` | workflow discovery glob |
581
812
  | `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 |
813
+ | `transport` | <code>string &#124; null</code> | `src/transport.{ts,js,mts,mjs}` when one of those files exists | custom transport definition file |
583
814
 
584
815
  Discovery does not behave identically in every subsystem:
585
816
 
@@ -615,25 +846,48 @@ export default defineConfig({
615
846
  })
616
847
  ```
617
848
 
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.
849
+ `files.transport` is convention-first. `createTestContext()` auto-loads `src/transport.{ts,js,mts,mjs}` when present, a string value points at a different transport file, and `files.transport: null` disables transport loading explicitly. The file must export a named `transport` object.
619
850
 
620
851
  There is no public `files.tail` config key today.
621
852
 
622
- ### `.env`, `vars`, `secrets`, and `config.env`
853
+ ### `.env`, `.dev.vars`, `vars`, `secrets`, and `config.env`
623
854
 
624
855
  Keep these layers separate:
625
856
 
626
857
  | Layer | Holds values? | Compiled into generated config? | Use it for |
627
858
  |---|---|---|---|
628
859
  | `.env` / `process.env` | yes | indirectly, only when your config reads from it | local process-time inputs |
860
+ | `.env.dev` / `.env.<name>` | tool/runtime-dependent | indirectly at most, only if the surrounding tool has already populated `process.env` | local process-time variants, not a Devflare-native contract |
861
+ | `.dev.vars` / `.dev.vars.<name>` | yes | no | local runtime secret/value files in upstream Cloudflare tooling when applicable |
629
862
  | `vars` | yes | yes | non-secret string bindings |
630
863
  | `secrets` | no, declaration only | no | required/optional runtime secret bindings |
631
864
  | `config.env` | yes, as config overlays | yes after merge | environment-specific config overrides |
632
865
 
633
- #### `.env` and process env
866
+ #### `.env`, `.env.dev`, and process env
634
867
 
635
868
  `loadConfig()` does not do dotenv loading by itself. The Devflare CLI runs under Bun, and Bun may auto-load `.env` files into `process.env`.
636
869
 
870
+ Important boundary:
871
+
872
+ - Devflare sets `dotenv: false` in its config loader
873
+ - Devflare does **not** define special first-class semantics for `.env.dev` or `.env.<name>`
874
+ - if those files affect `process.env`, that comes from the surrounding host tool/runtime rather than a Devflare-native loader
875
+ - config-time/build-time code can still read `process.env` inside `defineConfig()` or other Node-side tooling
876
+
877
+ Treat `.env*` files as **config/build-time inputs**, not as Devflare's runtime secret system.
878
+
879
+ #### `.dev.vars` and local runtime secrets
880
+
881
+ Devflare does **not** currently implement its own first-class `.dev.vars` / `.dev.vars.<name>` loader for worker-only dev mode or `createTestContext()`.
882
+
883
+ That means:
884
+
885
+ - do not document `.dev.vars*` as a guaranteed Devflare-native feature across all modes
886
+ - `secrets` declares the names of expected runtime secrets, but does not provide values
887
+ - worker-only dev and `createTestContext()` should not be described as automatically materializing secret values from `.dev.vars*`
888
+
889
+ In Vite-backed flows, some local runtime variable behavior may come from upstream Cloudflare/Vite tooling rather than from Devflare itself. Document that as inherited upstream behavior, not as a unified Devflare contract.
890
+
637
891
  #### `vars`
638
892
 
639
893
  Use `vars` for non-secret runtime values that can safely appear in generated config, such as public URLs, modes, IDs, and feature flags.
@@ -664,6 +918,135 @@ secrets: {
664
918
 
665
919
  means “`API_KEY` is a required runtime secret,” not “optional secret with no requirements.”
666
920
 
921
+ In practice, secret **values** come from outside Devflare config:
922
+
923
+ - Cloudflare-stored runtime secrets in deployed environments
924
+ - explicit test injection or lower-level mocks in tests
925
+ - upstream local-dev tooling when you intentionally rely on it
926
+
927
+ Do not describe `secrets` as a place that stores values.
928
+
929
+ #### Example files such as `.env.example` and `.dev.vars.example`
930
+
931
+ Example files are a **team convention**, not a Devflare feature.
932
+
933
+ Current truthful guidance:
934
+
935
+ - use `.env.example` to document required config-time/build-time variables that your config or Node-side tooling reads from `process.env`
936
+ - use `.dev.vars.example` to document expected local runtime secret names **if your project chooses to rely on upstream `.dev.vars` workflows**
937
+ - keep example files committed with placeholder or fake values only
938
+ - do not claim that Devflare auto-generates, auto-loads, or validates these example files today
939
+
940
+ #### Canonical env and secrets layout
941
+
942
+ If you want the lowest-confusion setup, use this split:
943
+
944
+ - `.env.example` documents config-time/build-time inputs
945
+ - `.dev.vars.example` documents local runtime secret names **only if your project intentionally relies on upstream `.dev.vars` workflows**
946
+ - `devflare.config.ts` reads config-time values from `process.env`, puts safe runtime values in `vars`, and declares required runtime secret names in `secrets`
947
+ - deployed secret values live in Cloudflare, not in your repo
948
+
949
+ Recommended project shape:
950
+
951
+ ```text
952
+ my-worker/
953
+ ├─ .env.example
954
+ ├─ .dev.vars.example # optional; only if you intentionally use upstream .dev.vars flows
955
+ ├─ .gitignore
956
+ ├─ devflare.config.ts
957
+ └─ src/
958
+ └─ fetch.ts
959
+ ```
960
+
961
+ Example `.env.example`:
962
+
963
+ ```dotenv
964
+ WORKER_NAME=my-worker
965
+ API_ORIGIN=http://localhost:3000
966
+ ```
967
+
968
+ Example `.dev.vars.example`:
969
+
970
+ ```dotenv
971
+ API_KEY=replace-me
972
+ SESSION_SECRET=replace-me
973
+ ```
974
+
975
+ Typical git ignore pattern for user projects:
976
+
977
+ ```gitignore
978
+ .env
979
+ .env.*
980
+ !.env.example
981
+ .dev.vars
982
+ .dev.vars.*
983
+ !.dev.vars.example
984
+ ```
985
+
986
+ Example `devflare.config.ts`:
987
+
988
+ ```ts
989
+ import { defineConfig } from 'devflare'
990
+
991
+ export default defineConfig({
992
+ name: process.env.WORKER_NAME ?? 'my-worker',
993
+ compatibilityDate: '2026-03-17',
994
+ files: {
995
+ fetch: 'src/fetch.ts'
996
+ },
997
+ vars: {
998
+ API_ORIGIN: process.env.API_ORIGIN ?? 'http://localhost:3000'
999
+ },
1000
+ secrets: {
1001
+ API_KEY: {},
1002
+ SESSION_SECRET: {}
1003
+ },
1004
+ env: {
1005
+ production: {
1006
+ vars: {
1007
+ API_ORIGIN: 'https://api.example.com'
1008
+ }
1009
+ }
1010
+ }
1011
+ })
1012
+ ```
1013
+
1014
+ Example `src/fetch.ts`:
1015
+
1016
+ ```ts
1017
+ import type { FetchEvent } from 'devflare/runtime'
1018
+
1019
+ export async function GET({ env }: FetchEvent<DevflareEnv>): Promise<Response> {
1020
+ return Response.json({
1021
+ origin: env.API_ORIGIN,
1022
+ hasApiKey: Boolean(env.API_KEY),
1023
+ hasSessionSecret: Boolean(env.SESSION_SECRET)
1024
+ })
1025
+ }
1026
+ ```
1027
+
1028
+ Deployed runtime secrets should be created with Cloudflare/Wrangler tooling, not committed to config files or example files.
1029
+
1030
+ Typical deployed secret flow:
1031
+
1032
+ ```bash
1033
+ bunx --bun wrangler secret put API_KEY
1034
+ bunx --bun wrangler secret put SESSION_SECRET
1035
+ ```
1036
+
1037
+ If you use named Cloudflare environments, set the secret in that environment explicitly:
1038
+
1039
+ ```bash
1040
+ bunx --bun wrangler secret put API_KEY --env production
1041
+ bunx --bun wrangler secret put SESSION_SECRET --env production
1042
+ ```
1043
+
1044
+ Practical rule of thumb:
1045
+
1046
+ - if a value is needed while evaluating config, put it in the `.env*` / `process.env` bucket
1047
+ - if a value should exist as a runtime binding but must not be committed, declare it in `secrets`
1048
+ - if a project wants local runtime secret files, treat `.dev.vars*` as an upstream convention and document it explicitly per project
1049
+
667
1050
  #### `devflare types`
668
1051
 
669
1052
  `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.
@@ -937,6 +1320,21 @@ Advanced members such as `.name`, `.config`, `.configPath`, and `.resolve()` are
937
1320
 
938
1321
  ## Development workflows
939
1322
 
1323
+ ### Vite vs Rolldown: the truthful mental model
1324
+
1325
+ They are both important, but they are not two names for the same job.
1326
+
1327
+ | Tool | Role inside Devflare | When it matters most | What it is not |
1328
+ |---|---|---|---|
1329
+ | `Vite` | the optional outer dev/build host for packages that are already Vite apps or frameworks; Devflare plugs generated Worker config, config watching, auxiliary DO workers, and bridge behavior into that pipeline | packages with a local `vite.config.*`, SvelteKit, frontend HMR | not Devflare's own Worker bundler |
1330
+ | `Rolldown` | the inner code-transforming builder Devflare uses when Devflare itself bundles Worker code | Durable Object bundles, watch/rebuild, worker-side plugin transforms such as `.svelte` imported by a DO module | not the main app's Vite build |
1331
+
1332
+ Three practical consequences fall straight out of the implementation:
1333
+
1334
+ - remove `vite.config.*`, and Devflare drops back to worker-only mode instead of starting Vite
1335
+ - import `.svelte` from a Durable Object, and the compilation belongs to `rolldown.options.plugins`, not to the main Vite plugin chain
1336
+ - generated `.devflare/worker-entrypoints/main.ts` is separate glue code produced by Devflare when it needs to compose fetch, queue, scheduled, email, or route-tree surfaces into one Worker entry
1337
+
940
1338
  ### Operational decision rules
941
1339
 
942
1340
  Use these rules in order:
@@ -948,6 +1346,278 @@ Use these rules in order:
948
1346
  5. treat `.devflare/*`, `env.d.ts`, and generated Wrangler config as outputs, not authoring inputs
949
1347
  6. remote mode is mainly for remote-oriented services such as AI and Vectorize, not a blanket “make everything remote” switch
950
1348
 
1349
+ ### Vite-backed workflows
1350
+
1351
+ A package enters Vite-backed mode when it has a local `vite.config.*`. In that mode, Vite is the outer application pipeline: it owns the package's dev server and app build, while Devflare injects Worker-aware config, generated Wrangler output, auxiliary DO worker config, and bridge behavior into that Vite stack.
1352
+
1353
+ Current Vite-backed flow:
1354
+
1355
+ 1. Devflare loads and validates `devflare.config.*`
1356
+ 2. `devflarePlugin()` compiles that config into a generated `.devflare/wrangler.jsonc`
1357
+ 3. Devflare may generate `.devflare/worker-entrypoints/main.ts` when multiple surfaces must be composed into one Worker entry
1358
+ 4. if Durable Object files are discovered, Devflare builds an auxiliary DO worker config for Vite / Cloudflare interop
1359
+ 5. in serve mode, Devflare watches the resolved Devflare config file and triggers a full reload when it changes
1360
+ 6. if `wsRoutes` are configured, Devflare can proxy matching WebSocket upgrade paths to the Miniflare bridge
1361
+ 7. on build, Devflare runs `bunx vite build` only after it has prepared the Worker config for that package
1362
+
1363
+ Two ownership rules matter here:
1364
+
1365
+ - setting `wrangler.passthrough.main` tells Devflare to preserve your explicit Worker `main` instead of generating a composed one
1366
+ - no local `vite.config.*` means none of this Vite-specific behavior runs; the package stays in worker-only mode
1367
+
1368
+ #### `devflare/vite` helpers
1369
+
1370
+ | Helper | Use it for | Timing |
1371
+ |---|---|---|
1372
+ | `devflarePlugin(options)` | generated `.devflare/wrangler.jsonc`, config watching, DO discovery, DO transforms, and WebSocket proxy wiring | include it in `vite.config.*` plugins |
1373
+ | `getCloudflareConfig(options)` | compiled programmatic config for `cloudflare({ config })` | call during Vite config creation |
1374
+ | `getDevflareConfigs(options)` | compiled config plus `auxiliaryWorkers` array for DO workers | call during Vite config creation |
1375
+ | `getPluginContext()` | read resolved plugin state such as `wranglerConfig`, `cloudflareConfig`, discovered DOs, and `projectRoot` | advanced use only, after Vite has resolved config |
1376
+
1377
+ `devflarePlugin(options)` currently supports these options:
1378
+
1379
+ | Option | Default | What it changes |
1380
+ |---|---|---|
1381
+ | `configPath` | auto-resolve local supported config | point Vite at a specific `devflare.config.*` file |
1382
+ | `environment` | no explicit override | resolve `config.env[name]` before compilation |
1383
+ | `doTransforms` | `true` | enable or disable Devflare's DO code transforms |
1384
+ | `watchConfig` | `true` | watch the resolved config file and full-reload on change |
1385
+ | `bridgePort` | `process.env.DEVFLARE_BRIDGE_PORT`, then `8787` when proxying | choose the Miniflare bridge port for WebSocket proxying |
1386
+ | `wsProxyPatterns` | `[]` | add extra WebSocket proxy patterns beyond configured `wsRoutes` |
1387
+
1388
+ Timing rule of thumb:
1389
+
1390
+ - if you need config while building the Vite config object, use `getCloudflareConfig()` or `getDevflareConfigs()`
1391
+ - if another Vite plugin needs to inspect the already-resolved Devflare state, `getPluginContext()` is the advanced hook
1392
+
1393
+ #### Minimal Vite wiring
1394
+
1395
+ ```ts
1396
+ import { defineConfig } from 'vite'
1397
+ import { devflarePlugin } from 'devflare/vite'
1398
+
1399
+ export default defineConfig({
1400
+ plugins: [devflarePlugin()]
1401
+ })
1402
+ ```
1403
+
1404
+ #### Explicit `@cloudflare/vite-plugin` wiring
1405
+
1406
+ ```ts
1407
+ import { defineConfig } from 'vite'
1408
+ import { cloudflare } from '@cloudflare/vite-plugin'
1409
+ import { devflarePlugin, getDevflareConfigs } from 'devflare/vite'
1410
+
1411
+ export default defineConfig(async () => {
1412
+ const { cloudflareConfig, auxiliaryWorkers } = await getDevflareConfigs()
1413
+
1414
+ return {
1415
+ plugins: [
1416
+ devflarePlugin(),
1417
+ cloudflare({
1418
+ config: cloudflareConfig,
1419
+ auxiliaryWorkers: auxiliaryWorkers.length > 0 ? auxiliaryWorkers : undefined
1420
+ })
1421
+ ]
1422
+ }
1423
+ })
1424
+ ```
1425
+
1426
+ That is the current high-signal pattern when you want Vite to stay the package's app/build host while Devflare owns Worker config compilation and Durable Object discovery.
1427
+
1428
+ #### SvelteKit-backed Worker example
1429
+
1430
+ ```ts
1431
+ // devflare.config.ts
1432
+ import { defineConfig } from 'devflare'
1433
+
1434
+ export default defineConfig({
1435
+ name: 'notes-app',
1436
+ files: {
1437
+ fetch: '.svelte-kit/cloudflare/_worker.js',
1438
+ durableObjects: 'src/do/**/*.ts',
1439
+ transport: 'src/transport.ts'
1440
+ },
1441
+ bindings: {
1442
+ durableObjects: {
1443
+ CHAT_ROOM: 'ChatRoom'
1444
+ }
1445
+ }
1446
+ })
1447
+ ```
1448
+
1449
+ ```ts
1450
+ // vite.config.ts
1451
+ import { defineConfig } from 'vite'
1452
+ import { sveltekit } from '@sveltejs/kit/vite'
1453
+ import { devflarePlugin } from 'devflare/vite'
1454
+
1455
+ export default defineConfig({
1456
+ plugins: [
1457
+ devflarePlugin(),
1458
+ sveltekit()
1459
+ ]
1460
+ })
1461
+ ```
1462
+
1463
+ ```ts
1464
+ // src/hooks.server.ts
1465
+ export { handle } from 'devflare/sveltekit'
1466
+ ```
1467
+
1468
+ Use `createHandle({...})` from `devflare/sveltekit` when you need custom binding hints or want to compose Devflare with other SvelteKit handles via `sequence(...)`.
1469
+
1470
+ ### Rolldown bundling and plugin workflows
1471
+
1472
+ `rolldown` is not just a namespace of knobs. Rolldown is the builder Devflare uses for the code Devflare actively bundles itself. Today that means the Durable Object path: Devflare discovers DO source files, applies its own transforms, lets user plugins transform imports, and emits runnable single-file ESM Worker modules that Miniflare can execute.
1473
+
1474
+ That is why `rolldown` is important but different from Vite:
1475
+
1476
+ - Vite may host the outer app or framework pipeline
1477
+ - Rolldown is the inner code-transform step that turns DO source into actual runnable worker code
1478
+ - if a Durable Object imports `.svelte`, that compilation belongs to the Rolldown plugin pipeline, not to the main Vite app plugin chain
1479
+
1480
+ It is still not Vite config, not a replacement for your app's `vite.config.*`, and not the place to configure the main fetch build.
1481
+
1482
+ Current DO bundler behavior:
1483
+
1484
+ - Devflare discovers DO files from `files.durableObjects`
1485
+ - discovered DO entries are bundled to worker-compatible ESM
1486
+ - code splitting is disabled so Devflare can emit a worker-friendly single-file bundle
1487
+ - user `rolldown.options.plugins` are merged into the bundle pipeline
1488
+ - internal externals cover `cloudflare:*`, `node:*`, and other worker/runtime modules that should stay external
1489
+ - Devflare also injects a `debug` alias shim so worker bundles do not accidentally drag in a Node-only debug dependency
1490
+ - this same DO bundling path still matters in unified Vite dev; Vite can host the app while Rolldown rebuilds DO worker code underneath it
1491
+
1492
+ Rolldown's plugin API is almost fully compatible with Rollup's, which is why Rollup-style plugins can often be passed through in `rolldown.options.plugins`. That said, compatibility is high, not magical: keep integration tests around nontrivial plugin stacks.
1493
+
1494
+ #### Minimal custom transform example
1495
+
1496
+ ```ts
1497
+ import { defineConfig } from 'devflare'
1498
+ import type { Plugin as RolldownPlugin } from 'rolldown'
1499
+
1500
+ const inlineSvelteFixturePlugin: RolldownPlugin = {
1501
+ name: 'inline-svelte-fixture',
1502
+ transform(_code, id) {
1503
+ if (!id.endsWith('.svelte')) {
1504
+ return null
1505
+ }
1506
+
1507
+ return {
1508
+ code: 'export default { render() { return { html: "<h1>Hello from Svelte</h1>" } } }',
1509
+ map: null
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ export default defineConfig({
1515
+ name: 'do-worker',
1516
+ files: {
1517
+ durableObjects: 'src/do/**/*.ts'
1518
+ },
1519
+ bindings: {
1520
+ durableObjects: {
1521
+ GREETER: 'Greeter'
1522
+ }
1523
+ },
1524
+ rolldown: {
1525
+ options: {
1526
+ plugins: [inlineSvelteFixturePlugin]
1527
+ }
1528
+ }
1529
+ })
1530
+ ```
1531
+
1532
+ That mirrors the kind of `.svelte` transform path the repo's own DO bundler tests exercise.
1533
+
1534
+ #### Svelte plugin example for Rolldown
1535
+
1536
+ This example is intentionally about a `.svelte` import inside a Durable Object module. In that situation, Rolldown — not the main Vite app build — is the plugin pipeline doing the compilation.
1537
+
1538
+ For this pattern you typically install `svelte`, `rollup-plugin-svelte`, and `@rollup/plugin-node-resolve` in the package that owns the Durable Object code.
1539
+
1540
+ ```ts
1541
+ import { defineConfig } from 'devflare'
1542
+ import resolve from '@rollup/plugin-node-resolve'
1543
+ import type { Plugin as RolldownPlugin } from 'rolldown'
1544
+ import svelte from 'rollup-plugin-svelte'
1545
+
1546
+ export default defineConfig({
1547
+ name: 'chat-worker',
1548
+ files: {
1549
+ durableObjects: 'src/do/**/*.ts'
1550
+ },
1551
+ bindings: {
1552
+ durableObjects: {
1553
+ CHAT_ROOM: 'ChatRoom'
1554
+ }
1555
+ },
1556
+ rolldown: {
1557
+ target: 'es2022',
1558
+ sourcemap: true,
1559
+ options: {
1560
+ plugins: [
1561
+ svelte({
1562
+ emitCss: false,
1563
+ compilerOptions: {
1564
+ generate: 'ssr'
1565
+ }
1566
+ }) as unknown as RolldownPlugin,
1567
+ resolve({
1568
+ browser: true,
1569
+ exportConditions: ['svelte'],
1570
+ extensions: ['.svelte']
1571
+ }) as unknown as RolldownPlugin
1572
+ ]
1573
+ }
1574
+ }
1575
+ })
1576
+ ```
1577
+
1578
+ ```svelte
1579
+ <!-- src/do/Greeting.svelte -->
1580
+ <script lang='ts'>
1581
+ export let name: string
1582
+ </script>
1583
+
1584
+ <h1>Hello {name} from Svelte</h1>
1585
+ ```
1586
+
1587
+ ```ts
1588
+ // src/do/chat-room.ts
1589
+ import { DurableObject } from 'cloudflare:workers'
1590
+ import Greeting from './Greeting.svelte'
1591
+
1592
+ export class ChatRoom extends DurableObject {
1593
+ async fetch(): Promise<Response> {
1594
+ return new Response(Greeting.render({ name: 'Devflare' }).html, {
1595
+ headers: {
1596
+ 'content-type': 'text/html; charset=utf-8'
1597
+ }
1598
+ })
1599
+ }
1600
+ }
1601
+ ```
1602
+
1603
+ What happens in this flow:
1604
+
1605
+ 1. Devflare discovers `src/do/**/*.ts` from `files.durableObjects`
1606
+ 2. the DO module imports `Greeting.svelte`
1607
+ 3. Rolldown runs the configured plugin pipeline, including `rollup-plugin-svelte`
1608
+ 4. the Svelte component becomes JavaScript before the DO bundle is written
1609
+ 5. Devflare writes a runnable single-file Worker bundle for that DO
1610
+
1611
+ Why this example is shaped that way:
1612
+
1613
+ - `emitCss: false` keeps the DO bundle single-file instead of emitting a separate CSS asset pipeline
1614
+ - `generate: 'ssr'` fits the Worker-side rendering story better than a browser DOM target
1615
+ - `@rollup/plugin-node-resolve` helps `.svelte` files and `exports.svelte` packages resolve cleanly
1616
+ - some Rollup plugins need a type cast to satisfy Rolldown's TypeScript types even when the runtime hooks work fine
1617
+ - this example is specifically about worker-side component compilation inside a DO; if your Svelte code lives in the main app or SvelteKit shell, that outer build is still Vite's job
1618
+
1619
+ If a plugin relies on Rollup-only hooks that Rolldown does not support yet, keep that plugin in your main Vite build instead of the DO bundler.
1620
+
951
1621
  ### Daily development loop
952
1622
 
953
1623
  Use the same CLI loop for both worker-only and Vite-backed packages. The presence of a local `vite.config.*` changes the mode automatically.
@@ -1013,7 +1683,8 @@ Current behavior:
1013
1683
  - it resolves service bindings and cross-worker Durable Object references
1014
1684
  - it can infer conventional fetch, queue, scheduled, and email handler files when present
1015
1685
  - 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
1686
+ - it auto-detects `src/transport.{ts,js,mts,mjs}` when present unless `files.transport` is `null`
1687
+ - it does not have a first-class `.dev.vars*` loader for populating declared secret values
1017
1688
 
1018
1689
  Practical example:
1019
1690
 
@@ -1099,12 +1770,17 @@ Keep these caveats explicit:
1099
1770
  - `_`-prefixed files and directories inside the route tree are ignored
1100
1771
  - `vars` values are strings in the current native schema
1101
1772
  - `secrets` declares runtime secret bindings; it does not store secret values
1773
+ - `.env*` and `.dev.vars*` are not a unified Devflare-native loading/validation system; any effect they have comes from Bun or upstream Cloudflare tooling, depending on the mode
1102
1774
  - `wrangler.passthrough` is the escape hatch for unsupported Wrangler keys and is merged after native compilation
1103
1775
  - named service entrypoints need deployment-time validation if they are critical to your app
1104
1776
  - local R2 binding support is real, but there is still no stable public/browser local bucket URL contract to document as public API
1105
1777
  - `sendEmail` is a supported outbound binding, while inbound email is a separate worker surface
1106
1778
  - 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
1779
  - Vite and Rolldown are different systems and should not be blurred together
1780
+ - `rolldown` config affects Durable Object bundling, not the main Vite app build
1781
+ - `wrangler.passthrough.main` suppresses Devflare's composed main-entry generation in higher-level build and Vite-backed flows
1782
+ - `getCloudflareConfig()` and `getDevflareConfigs()` are the safe config-time Vite helpers; `getPluginContext()` is advanced post-resolution state
1783
+ - Rollup-compatible plugins often work in `rolldown.options.plugins`, but compatibility is high-not-total and plugin-specific validation still matters
1108
1784
  - higher-level flows may generate composed main worker entries more aggressively than older docs implied
1109
1785
 
1110
1786
  ---