void 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{auth-cmd-MkBG2u1f.mjs → auth-cmd-g_ymMkUe.mjs} +2 -2
  2. package/dist/{auth-link-Devmv96E.mjs → auth-link-DFvxIyZi.mjs} +2 -2
  3. package/dist/{build-cmd-DsBGwtfs.mjs → build-cmd-wJAv808h.mjs} +1 -1
  4. package/dist/{cache-BMw8eMyF.mjs → cache-Be0Fu5Ka.mjs} +1 -1
  5. package/dist/{cancel-deploy-B3_s9NIN.mjs → cancel-deploy-BgmyOwhe.mjs} +1 -1
  6. package/dist/cli/cf-compat.mjs +13 -3
  7. package/dist/cli/cli.mjs +35 -35
  8. package/dist/{connect-BvC9kolB.mjs → connect-LnvW4HKx.mjs} +2 -2
  9. package/dist/{create-project-C9lQhRzj.mjs → create-project-BZBl_gQ0.mjs} +1 -1
  10. package/dist/{create-project-CI-17RVJ.mjs → create-project-JV9-6ICF.mjs} +1 -1
  11. package/dist/{db-R7IQgOv5.mjs → db-Mzv88E1C.mjs} +3 -3
  12. package/dist/{delete-CghI_NXn.mjs → delete-Ri6JemSU.mjs} +1 -1
  13. package/dist/{deploy-CYPymbtG.mjs → deploy-DAH2-m_-.mjs} +1 -1
  14. package/dist/{deploy-H968Lo4T.mjs → deploy-DEsMVrCY.mjs} +83 -46
  15. package/dist/{domain-CITt8lC-.mjs → domain-BA37Ye7T.mjs} +1 -1
  16. package/dist/{email-_8VmyX7V.mjs → email-C5NsxrQG.mjs} +2 -2
  17. package/dist/{env-DV4r3nHz.mjs → env-U_ohASB0.mjs} +1 -1
  18. package/dist/gen-B87rlalt.mjs +2 -0
  19. package/dist/{gen-CFOEEc-t.mjs → gen-pg-Ojg8Y.mjs} +1 -1
  20. package/dist/{github-cmd-jYjha2LO.mjs → github-cmd-DsrkweIl.mjs} +1 -1
  21. package/dist/help-B403BLBB.mjs +2 -0
  22. package/dist/{help-C46mnlUS.mjs → help-sBhFH6pi.mjs} +1 -1
  23. package/dist/index.mjs +3 -3
  24. package/dist/{init-CSXmvxKu.mjs → init-C--Yyj1b.mjs} +5 -5
  25. package/dist/{link-DxMOALXk.mjs → link-BC-mNG9-.mjs} +2 -2
  26. package/dist/{list-0wQs0oYg.mjs → list-DHU1Wj6c.mjs} +1 -1
  27. package/dist/login-BwiopVdg.mjs +2 -0
  28. package/dist/{login-CKk5NX4d.mjs → login-jKe_0QZL.mjs} +2 -2
  29. package/dist/{logs-hxWBSoej.mjs → logs-BG11f5PR.mjs} +1 -1
  30. package/dist/{migrate-DcW8F2W8.mjs → migrate-BV8qHiCb.mjs} +1 -1
  31. package/dist/migrate-BXY3B3m7.mjs +2 -0
  32. package/dist/{operator-cmd-BEUYhaHB.mjs → operator-cmd-BwAS2XZt.mjs} +3 -3
  33. package/dist/{output-urU86XeT.mjs → output-CCH48AMM.mjs} +4 -4
  34. package/dist/{platform-auth-config-Df6yVw-e.mjs → platform-auth-config-C-hv-_Ok.mjs} +2 -2
  35. package/dist/{platform-auth-protection-Jb0yftay.mjs → platform-auth-protection-DjTE5Hj-.mjs} +2 -2
  36. package/dist/{platform-auth-recovery-DmRyIfW0.mjs → platform-auth-recovery-Cr0r8TEb.mjs} +2 -2
  37. package/dist/{platform-cmd-DRxCOTwy.mjs → platform-cmd-0tEh2ZtA.mjs} +4 -4
  38. package/dist/{platform-cmd-C9Vt7m-d.mjs → platform-cmd-DE6dw45v.mjs} +1 -1
  39. package/dist/{platform-domain-IjiXgrXJ.mjs → platform-domain-B_7x6Iqx.mjs} +2 -2
  40. package/dist/{platform-lifecycle-CuJIZNvA.mjs → platform-lifecycle-Dhx9Pnub.mjs} +1 -1
  41. package/dist/{platform-lifecycle-BquAaU-5.mjs → platform-lifecycle-Di0LbhPM.mjs} +7 -7
  42. package/dist/{platform-management-Cgyed0WY.mjs → platform-management-B9LUtMt9.mjs} +1 -1
  43. package/dist/{platform-management-CCQKGsq4.mjs → platform-management-eaiaU3uM.mjs} +2 -2
  44. package/dist/{platform-recovery-DS8ih1SW.mjs → platform-recovery-BxfA8E5C.mjs} +2 -2
  45. package/dist/{prepare-CaUxODOU.mjs → prepare-_T-Haxrr.mjs} +1 -1
  46. package/dist/{project-cmd-2lN--YAX.mjs → project-cmd-DrnRJdep.mjs} +13 -13
  47. package/dist/{project-team-Dapn9HZn.mjs → project-team-CzRbO_kA.mjs} +1 -1
  48. package/dist/{project-token-C90v9SJK.mjs → project-token-Bwxjf4y6.mjs} +1 -1
  49. package/dist/{requests-CzX46I0i.mjs → requests-DwrqUQZ8.mjs} +1 -1
  50. package/dist/{rollback-BEeyc73H.mjs → rollback-B4tFWmkN.mjs} +1 -1
  51. package/dist/{secret-wnTel5Yw.mjs → secret-UtrRjhEO.mjs} +1 -1
  52. package/dist/{subcommand-prompt-Gj3VzLIh.mjs → subcommand-prompt-BuGYkAkC.mjs} +1 -1
  53. package/package.json +7 -7
  54. package/skills/void/docs/guide/deployment.md +6 -13
  55. package/skills/void/docs/guide/email.md +53 -59
  56. package/skills/void/docs/guide/index.md +15 -41
  57. package/skills/void/docs/guide/quickstart.md +11 -78
  58. package/skills/void/docs/index.md +17 -17
  59. package/skills/void/docs/integrations/cloudflare.md +16 -23
  60. package/skills/void/docs/reference/cli.md +13 -7
  61. package/skills/void/docs/reference/config.md +6 -6
  62. package/dist/gen-BBiIZw6g.mjs +0 -2
  63. package/dist/help-CS_nAsWu.mjs +0 -2
  64. package/dist/login-Ch9cgWRF.mjs +0 -2
  65. package/dist/migrate-CLty4mZR.mjs +0 -2
@@ -30,24 +30,18 @@ if (!result.ok) {
30
30
  }
31
31
  ```
32
32
 
33
- Both branches are real. `result.error` exists only on the first, so reading it
34
- unconditionally throws on the second — and the second is the one a new project
35
- hits first, because a recipient you have not verified yet fails per-recipient.
33
+ Check both failure shapes: `result.error` reports a request failure, while `result.deliveries` reports failures for individual recipients. An unverified recipient fails in `deliveries`.
36
34
 
37
35
  ## Setup
38
36
 
39
- On a platform with email enabled, your app needs no email configuration before
40
- `void deploy`. Ask your administrator for the platform's shared mail domain. A
41
- self-hosted administrator [enables email during installation or upgrade](/guide/platform/installation/credentials#runtime-token-permissions); installations without it do not offer platform email. Void Cloud uses `mail.void.cloud`.
37
+ On a platform with email enabled, deploy without additional email configuration. Ask your administrator for its shared mail domain. Administrators [enable email during installation or upgrade](/guide/platform/installation/credentials#runtime-token-permissions); Void Cloud uses `mail.void.cloud`.
42
38
 
43
39
  Each project on an email-enabled platform has:
44
40
 
45
- - **Sender** — `<your-slug>+noreply@<mail-domain>`. Used as the default `from` if you omit it. The platform administrator configures the mail zone and its Email Routing, DKIM, SPF, and DMARC records. Project slugs are capped at 56 characters so this local part fits RFC 5321's 64 octets; a project created before the cap with a longer slug must pass `from` explicitly.
46
- - **No worker binding to add** — outbound mail is sent by the Void proxy, which holds the
47
- platform `send_email` binding. Your worker never gets one, so there is nothing to configure.
48
- - **Your own address as a recipient** — the email on your Void account is registered as a recipient when the project is created. It is verified at once when Cloudflare already holds it verified for the platform (you clicked its link for an earlier project of yours); otherwise Cloudflare mails it a verification link, and until you click that link and run `void email destinations` — the listing is what records the click — a send to yourself comes back `ok: false` with a per-recipient `UNVERIFIED_DESTINATION` in `result.deliveries`.
41
+ - **Default sender:** `<your-slug>+noreply@<mail-domain>`. Older projects with slugs longer than 56 characters must pass `from` explicitly.
42
+ - **Your account email as a recipient:** it is registered when the project is created. If Cloudflare has not verified it for the platform, follow the emailed verification link and run `void email destinations` before sending to it.
49
43
 
50
- That's it for configuration. Delivery is gated separately: outbound mail only reaches verified recipients, so `sendEmail({ to: '...', subject: '...', text: '...' })` works on first deploy for your own address once it is verified, and for anyone else after `void email allow` — see [Adding recipients](#adding-recipients).
44
+ The shared sender can send only to verified recipients. Verify your own address or [add another recipient](#adding-recipients) before sending.
51
45
 
52
46
  Deploying to your own Cloudflare account instead (`void deploy --platform cloudflare`) takes one line of `void.config.ts` and one Enter on the first deploy — see [Your own Cloudflare account](#your-own-cloudflare-account).
53
47
 
@@ -68,9 +62,9 @@ void email destinations
68
62
  The project owner's account email is added automatically when the project is created on an email-enabled platform, so it skips `void email allow` — not the verification. See [Setup](#setup) for when it is verified at once and when there is a link to click.
69
63
 
70
64
  ::: warning When this is the right fit
71
- The shared sender is great for: ops alerts to the team, notifications to the project owner, reply-by-email flows on top of inbound, internal/app-internal mail.
65
+ The shared sender suits team alerts, project notifications, and replies to inbound mail.
72
66
 
73
- For SaaS sending to arbitrary end-users (every signup gets a welcome email), the per-recipient verification model doesn't fit. On the platform, [registering your own domain](#your-own-domain-on-the-platform) lifts that gate for sends from that domain. On [your own Cloudflare account](#your-own-cloudflare-account) with Workers Paid, Void onboards your mail domain for Email Sending, which lifts the verified-recipient gate. Otherwise use [Resend](https://resend.com), [Postmark](https://postmarkapp.com), or [SES](https://aws.amazon.com/ses/) directly — install their SDK and call it from your handler. We may formalize this with a provider abstraction later if there is demand; until then, calling the SDK directly is simple enough that the wrapper would not earn its keep.
67
+ For mail to arbitrary users, [register your own domain](#your-own-domain-on-the-platform), use [Workers Paid on your own Cloudflare account](#your-own-cloudflare-account), or call another email provider from your handler.
74
68
  :::
75
69
 
76
70
  ## Your own domain on the platform
@@ -120,7 +114,7 @@ On an administrator-managed platform, `add` prints the administrator command for
120
114
  | `attachments` | `Attachment[]` | See [Attachments](#attachments). |
121
115
  | `idempotencyKey` | `string` | Optional on the Void Platform: 1–128 printable, non-space ASCII characters. Reuse for retries of the same send. |
122
116
 
123
- At most 50 recipients across `to`, `cc` and `bcc` per call. Each address is checked with [`email-validator`](https://www.npmjs.com/package/email-validator): an ASCII dot-atom local part (before the `@`) of at most 64 characters — no whitespace, control character or RFC 5322 special, no leading, trailing or doubled dot, and no quoted local part — and a dotted domain of ASCII labels (at most 63 characters each) whose TLD starts with a letter and is at least 2 characters, so `user@localhost` is refused and an IDN domain must be given as punycode (`xn--…`); at most 254 characters in total (RFC 5321: a 256-octet forward-path `<local@domain>` and a 64-octet local part are the longest every receiver must accept). The address itself carries no display-name syntax; a display name goes around it (`"Name <addr>"` or `{ email, name }`). All of these are checked before anything is built and return `INVALID_TO` (`INVALID_FROM` for `from`; `MIME_ERROR` for `cc`, `bcc` and `replyTo`). The platform's inbound router applies the same package to `forward()` and `reply()` addresses, so nothing a handler records is dropped there for its shape. Custom header names must be RFC 5322 field names (printable ASCII, no colon); a name too long to fit a 998-octet line — a field name cannot be folded — is rejected with `MIME_ERROR` before anything is built.
117
+ You can send to at most 50 recipients across `to`, `cc`, and `bcc`. Addresses must have an ASCII local part of at most 64 characters and a dotted domain; the whole address can be at most 254 characters. Quoted local parts, `user@localhost`, and malformed dots are rejected. Use punycode for internationalized domains. Void returns `INVALID_TO` for an invalid `to`, `INVALID_FROM` for `from`, and `MIME_ERROR` for `cc`, `bcc`, `replyTo`, or invalid custom header names.
124
118
 
125
119
  `Address` accepts either a string (`"hello@acme.dev"` or `"Name <hello@acme.dev>"`) or an object (`{ email, name? }`). Display names with non-ASCII characters are RFC 2047 encoded automatically.
126
120
 
@@ -164,7 +158,7 @@ await sendEmail({
164
158
  });
165
159
  ```
166
160
 
167
- `contentType` is inferred from the filename extension when omitted; when given, it must be a valid media type (`type/subtype`, optionally followed by `; attribute=value` parameters — no `name`, which is set from `filename`), or `sendEmail` returns `MIME_ERROR`. `contentId` is the identifier the HTML references as `cid:<id>`: letters, digits, the RFC 5322 `atext` symbols and dots, optionally with an `@domain` part and optionally in one pair of angle brackets (`logo`, `logo@acme.dev` and `<logo@acme.dev>` all render as `Content-ID: <…>`); anything else — whitespace, quotes, parentheses, a stray `<` or `>`, or an empty string — returns `MIME_ERROR`. Total encoded message size is capped at 5 MiB, and custom headers at 16 KiB; oversize payloads return `MIME_ERROR` instead of failing upstream.
161
+ Void infers `contentType` from the filename if you omit it. An explicit value must be a valid media type. `contentId` names the image referenced by `cid:<id>` in HTML; use an ID such as `logo` or `logo@acme.dev`. The encoded message is limited to 5 MiB and custom headers to 16 KiB. Invalid attachment fields or oversized messages return `MIME_ERROR`.
168
162
 
169
163
  ## Result and errors
170
164
 
@@ -207,7 +201,7 @@ For 30 days, repeating a key with the same payload returns its recorded outcome
207
201
  | `OUTCOME_UNKNOWN` | The send may have reached the provider; do not blindly resend. |
208
202
  | `UPSTREAM_ERROR` | The provider or platform refused the request. |
209
203
 
210
- The default platform allowance is 200 recipient submissions per UTC calendar month and 10 in a rolling 60-second window. Reserved submissions count toward the limits. Hourly cleanup cancels reservations older than 15 minutes that never started and releases their quota. Once an attempt starts, it stays charged even if the provider fails or the outcome is unknown. Administrators can change these limits.
204
+ The default platform allowance is 200 recipient submissions per UTC month and 10 per rolling minute. Reserved and started submissions count toward the limits; a started attempt stays charged even if it fails or its outcome is unknown. Administrators can change these limits.
211
205
 
212
206
  `void email usage` shows monthly recipient attempts, inbound receipts, and the remaining allowance. `void email logs --limit 50` shows up to 100 recent metadata entries retained for 30 days: operation, recipient, direction, state, provider reference, and error code. It does not store subjects, bodies, or attachments. Receiving a message and sending a reply are separate events.
213
207
 
@@ -233,13 +227,11 @@ curl -X DELETE http://localhost:5173/__void/inbox \
233
227
  -H "x-void-dev-trigger: <printed-token>"
234
228
  ```
235
229
 
236
- The buffer holds the most recent 100 messages and survives HMR but not a full server restart. No disk persistence.
230
+ The inbox keeps the most recent 100 messages through HMR and clears on a full server restart.
237
231
 
238
- Under the hood, your code runs in workerd (a separate process from the Vite dev server), so `sendEmail` hands each captured message to the dev server over the worker's `assets` binding, which Vite wires back into its own middleware. Void apps always have that binding, and so do apps built on a Class A framework (TanStack Start, React Router) once their wrangler config declares one.
232
+ The dev inbox works in native Void apps, TanStack Start, and React Router. It is unavailable in SvelteKit, Nuxt, Analog, and Astro; sends from those apps return `BINDING_MISSING`. Use `createEmailTestHarness` from `void/email/testing` to capture sends in tests. If a configured inbox cannot be reached, `sendEmail` returns `UPSTREAM_ERROR` without sending.
239
233
 
240
- The dev inbox is **not** available under a Class B or C framework — SvelteKit, Nuxt, Analog, Astro. Those adapters own their own dev server and worker build, so Void never installs the Cloudflare Vite plugin for them and has nowhere to register the inbox. Declaring an `assets` binding does not help: SvelteKit, Nuxt and Analog do not run your code in a workerd instance fronted by Vite at all, so there is no loopback to bind to. Sends from those apps return `BINDING_MISSING`. Use `createEmailTestHarness` from `void/email/testing` instead, which captures in-process and works everywhere. When the inbox is configured but unreachable, `sendEmail` returns `UPSTREAM_ERROR` — nothing is captured and nothing is sent.
241
-
242
- `sendInDev: true` bypasses the dev inbox for a single send. `void dev` binds no send transport at all — the platform's `__VOID_PROXY` service binding is added only on a deployed worker, and the own-account `SEND_EMAIL` binding is stripped under `serve` so miniflare cannot write stray `.eml` files or send real mail (only that entry: a `send_email` binding of your own under another name is left exactly as `cloudflare.send_email` in `void.config.ts` declares it). With the inbox skipped there is nothing left to fall through to, so the call returns `BINDING_MISSING`: it proves the inbox was bypassed, it does not deliver. To verify real delivery, deploy and send from the deployed worker.
234
+ `sendInDev: true` skips the inbox for one call. It returns `BINDING_MISSING` in `void dev` because no live send transport is bound. Deploy the app to test real delivery.
243
235
 
244
236
  ```ts
245
237
  const result = await sendEmail({
@@ -362,7 +354,7 @@ export default defineEmail(async (message, env, ctx, info) => {
362
354
 
363
355
  `replyEmail` builds the reply MIME with `Subject: Re: <original>` (no double-prefix), `In-Reply-To: <Message-ID>`, and a continued `References` chain, then dispatches through the message's `reply()`. Defaults: `to = message.from`, `subject = "Re: <original>"`.
364
356
 
365
- Everything taken from the inbound message is best-effort, because the remote sender controls it. A `Message-ID` or `References` value too long for a header line is dropped rather than threaded, and a `References` chain over 8 KiB keeps only its newest ids — the parent's `Message-ID` always ends it. Control characters in the subject become a space, and a subject over 4096 characters, or an ASCII one too long to fold, falls back to a bare `Re:`. None of these fallbacks suppresses the reply; pass `subject` to control it exactly.
357
+ Void uses the inbound message's `Message-ID` and `References` when they fit in reply headers. It drops unusable values and falls back to a bare `Re:` for a subject it cannot safely reuse. Pass `subject` to set it explicitly.
366
358
 
367
359
  On the platform, shared-domain replies default to
368
360
  `<slug>+noreply@<mail domain>`. For mail received at a registered custom domain,
@@ -474,27 +466,27 @@ The harness uses the same precedence rules as the production dispatcher. Reserve
474
466
 
475
467
  ## Your own Cloudflare account
476
468
 
477
- `void deploy --platform cloudflare` sets email up on a zone **you** own, through your Cloudflare sign-in (`void cloudflare login`). You never handle a Cloudflare object — no zone id, no routing rule, no token scope. You type one address, read one checklist, and press Enter once.
469
+ For direct Cloudflare deployment, set a sender address on a zone you own. Void checks the zone and asks before changing its email settings.
470
+
471
+ ### Setup
478
472
 
479
- ### Two things to type, once
473
+ 1. Set the default sender and mail domain in `void.config.ts`:
480
474
 
481
- 1. **`email.from` in `void.config.ts`** — the default sender, and the domain Void sets up:
475
+ ```ts
476
+ import { defineConfig } from 'void/config';
482
477
 
483
- ```json
484
- {
485
- "email": {
486
- "from": "Acme <support@mail.acme.com>"
487
- }
488
- }
478
+ export default defineConfig({
479
+ email: { from: 'Acme <support@mail.acme.com>' },
480
+ });
489
481
  ```
490
482
 
491
- The host (`mail.acme.com`) must be a zone in the Cloudflare account your deploy is pinned to, or sit under one. Without `email.from`, a deploy that uses email asks you to set it in `void.config.ts` and deploys without email. Void does not pick a zone for you or write the setting automatically.
483
+ The host (`mail.acme.com`) must be a zone in your selected Cloudflare account or a subdomain of one. If you omit `email.from`, Void deploys without email and tells you to set it. It does not choose a zone for you.
492
484
 
493
- 2. **`void cloudflare login`**, or **`CLOUDFLARE_API_TOKEN`** set to a token with **Email Routing Edit** and **Email Sending Edit** (zone and account) alongside the deploy permissions. A browser session created by older Cloudflare tooling lacks the two email scopes; the deploy tells you to run `void cloudflare logout`, then `void cloudflare login` (one browser Allow) and skips the email step. A token's permissions cannot be listed up front, so a token that lacks one shows up as a row Void could not read (`unknown`), with the permission named. A Global API Key pair (`CLOUDFLARE_API_KEY`) is refused: it has no single bearer for the two calls wrangler has no command for.
485
+ 2. Run `void cloudflare login`, or set `CLOUDFLARE_API_TOKEN` with **Email Routing Edit** and **Email Sending Edit** permissions in addition to deploy permissions. If an older browser session lacks email scopes, log out and sign in again. Void names missing token permissions in its checklist. Global API Keys are not supported for email setup.
494
486
 
495
487
  ### The first deploy
496
488
 
497
- Before any of your project code runs, the deploy **reads** — the session, the zone, public DNS (MX on the mail domain and on the apex, `_dmarc`, `cf-bounce`), Email Routing status, the zone's subaddressing setting, the routing rules on your addresses, Email Sending, and what the resolved Cloudflare config already holds — and prints what it found and what it would change:
489
+ Before building, Void checks the zone, DNS, routing rules, and sending status. It shows a checklist of the changes it would make:
498
490
 
499
491
  ```
500
492
  Email in use email/support+[ticket].ts · email/_default.ts · sendEmail() in 2 files
@@ -518,7 +510,7 @@ Before any of your project code runs, the deploy **reads** — the session, the
518
510
  ◆ Set up email on mail.acme.com? ● Yes / ○ No
519
511
  ```
520
512
 
521
- Enter (Yes is the default) applies only the `+` rows that are not ready, then the deploy continues as usual: the build, the version upload and activation, wrangler's trigger synchronization — which creates the routing rules from the `addresses` array wrangler now finds in your config and prints its own `Email Routing plan:` — and finally the address map:
513
+ Accepting the prompt applies the missing setup, builds and deploys your app, then shows its email address map:
522
514
 
523
515
  ```
524
516
  ✔ deployed acme-support
@@ -526,13 +518,13 @@ Enter (Yes is the default) applies only the `+` rows that are not ready, then th
526
518
  outbound sendEmail() from support@mail.acme.com
527
519
  ```
528
520
 
529
- On the second deploy every row reads ready: no prompt, no account call, and wrangler reports `Email Routing rules are up to date.` Answer No before any setup has been committed, and the deploy continues without email.
521
+ Later deploys skip the prompt when setup is ready. Declining before any setup is saved deploys without email.
530
522
 
531
523
  DNS can take a few minutes to become visible to the resolver running the deploy. If `void email setup` has already written the exact `addresses` plan but that resolver still sees no subdomain MX, Void preserves the plan and stops before the build or upload. Run `void email status --platform cloudflare`, then retry the deploy after it sees Cloudflare's MX records.
532
524
 
533
- Two rows live in the generated Cloudflare config rather than in your account, and a deploy that finds every account row ready reconciles them with a plain file write, no prompt: the `addresses` array is rewritten whenever it is not the current derivation (an entry pruned since, a worker rename, a new handler), and `vars.__VOID_EMAIL_FROM` follows a changed `email.from`. Routing rules are `wrangler deploy`'s own work, so a committed `addresses` entry whose rule does not exist yet — the state right after `void email setup` — still reads ready; the address map marks it `(rule created by this deploy)`.
525
+ Void updates the configured addresses when handlers change and keeps the default sender in sync with `email.from`. `void email setup` records the setup; the next deploy creates routing rules and may mark them as `(rule created by this deploy)` in the address map.
534
526
 
535
- A subdomain is added to a zone that already routes. When Email Routing is **off** on the apex (`Enabled: false`), the subdomain step is not attempted at all — Void never enables routing on the apex from the subdomain path, since that would lock MX records over the apex's live mail — and the checklist prints the dashboard step (`Email → Settings → Subdomains → add mail.acme.com`) instead; `addresses` is withheld until routing on the subdomain reads ready.
527
+ If Email Routing is off on the apex, Void does not enable it while setting up a subdomain; doing so could replace the apex's live mail records. The checklist directs you to **Email → Settings → Subdomains** in the Cloudflare dashboard. Deploy does not add addresses until the subdomain is ready.
536
528
 
537
529
  ### Subdomain or apex
538
530
 
@@ -548,24 +540,28 @@ The apex path (`email.from` on `acme.com` itself) is allowed with the same one E
548
540
 
549
541
  ### What Void records in `void.lock.json`
550
542
 
543
+ The relevant fields appear under `resolved`:
544
+
551
545
  ```jsonc
552
546
  {
553
- "send_email": [{ "name": "SEND_EMAIL" }],
554
- "vars": { "__VOID_EMAIL_FROM": "Acme <support@mail.acme.com>" },
555
- "addresses": ["support@mail.acme.com"],
547
+ "resolved": {
548
+ "send_email": [{ "name": "SEND_EMAIL" }],
549
+ "vars": { "__VOID_EMAIL_FROM": "Acme <support@mail.acme.com>" },
550
+ "addresses": ["support@mail.acme.com"],
551
+ },
556
552
  }
557
553
  ```
558
554
 
559
- - **`send_email`** — the binding `sendEmail()` delivers through, one `EmailMessage` per recipient. It is written once routing is enabled or sending is onboarded, never on a bare account. An existing `send_email` entry under another name is refused with the rename — a second binding would be silent.
560
- - **`__VOID_EMAIL_FROM`** — your `email.from`, so the worker knows its default sender.
561
- - **`addresses`** — derived from your `email/` directory. `wrangler deploy` turns each entry into an Email Routing rule pointing at this worker (a literal address → this worker; `*@acme.com` → the zone's catch-all). Rules and the catch-all are wrangler's job; Void only derives the list.
555
+ - **`send_email`** — the binding used by `sendEmail()`. An existing binding under another name must be renamed before Void can use it.
556
+ - **`__VOID_EMAIL_FROM`** — the default sender from `email.from`.
557
+ - **`addresses`** — addresses derived from your `email/` handlers. Deploy creates the corresponding Email Routing rules for this Worker.
562
558
 
563
- **Void owns the `addresses` array.** It is rewritten on every deploy from the current `email/` scan while every existing entry is still one Void derives, and deleted — never emptied to `[]`, which would remove every rule wrangler owns — when a later preflight finds routing not ready, with the reason printed (on every branch, including CI and a declined prompt; a stale array left in place would make wrangler's plan fail after the upload, on every deploy). Two consequences:
559
+ Void derives `addresses` from your handlers on each deploy. If routing is not ready, it withholds the array and explains why. It also protects rules it does not own:
564
560
 
565
- - An address already routed to another worker or to a forwarding rule is **pruned** from the array and reported (`! support@mail.acme.com already routed to …; left alone, not in addresses`). wrangler's plan is never destructive on your account because of something Void derived.
566
- - If the array holds entries Void did not derive, the deploy prints which ones and **skips the email step for that deploy** — the deploy itself continues and the array is left untouched. Remove them, or manage `addresses` by hand and leave `email.from` unset.
561
+ - An address routed to another Worker or a forwarding rule is excluded and reported; Void leaves that rule alone.
562
+ - If the array contains addresses Void did not derive, deploy skips email setup and lists them. Remove them, or manage `addresses` yourself and leave `email.from` unset.
567
563
 
568
- Deleting a handler leaves its address in `addresses`: the next deploy names it in a `✘` row as an entry Void no longer derives, and skips the email step until you override `cloudflare.addresses` in `void.config.ts` with the desired list (the row says which). Once removed, Cloudflare's plan drops the rule with its own y/n (default No) in a terminal, an error in CI. Removing the last handler and every `sendEmail()` call leaves the whole setup in `void.lock.json`; the deploy warns which addresses are still routed to a worker with no `email()` export and how to detach them, and deploys as-is. To detach fully, remove `addresses`, `send_email`, and `vars.__VOID_EMAIL_FROM` from `resolved` in `void.lock.json` and remove any authored overrides in `void.config.ts`, then delete the routing rules in Cloudflare.
564
+ If you delete a handler, the next deploy reports its stale address and skips email setup until you set the desired `cloudflare.addresses` in `void.config.ts`. Removing a routing rule requires confirmation in a terminal and fails in CI without it. If you remove all email use, deploy keeps the existing setup and warns about remaining routes. To detach it, remove `addresses`, `send_email`, and `vars.__VOID_EMAIL_FROM` from `resolved` in `void.lock.json`, remove any authored overrides in `void.config.ts`, then delete the routing rules in Cloudflare.
569
565
 
570
566
  How handlers become addresses, with `email.from` on `mail.acme.com` under the zone `acme.com`:
571
567
 
@@ -581,9 +577,9 @@ Inside the worker, a message reaches your handlers exactly as addressed — `sup
581
577
 
582
578
  ### Sending
583
579
 
584
- `sendEmail()` uses the worker's own `SEND_EMAIL` binding; there is no proxy hop and no platform quota — Cloudflare's own limits apply, reported as `QUOTA_EXCEEDED`. The `void email usage`, `logs`, `destinations`, `allow` and `disallow` commands are platform commands and do not apply here.
580
+ `sendEmail()` uses the Worker's `SEND_EMAIL` binding. Cloudflare's limits apply, with `QUOTA_EXCEEDED` on failure. The `void email usage`, `logs`, `destinations`, `allow`, and `disallow` commands are for Void platforms.
585
581
 
586
- Setup is per mail domain, not per direction: any use of email — an `email/` handler or a `void/email` import — sets the domain up for routing and sending together, so an app that only receives is still onboarded for Email Sending and still gets the binding; Cloudflare meters outbound per message, so a domain that never sends costs nothing, and `replyEmail` goes through Email Routing's own `message.reply()`, which needs no onboarding either way.
582
+ Email setup covers both inbound and outbound mail for the domain, even if your app uses only one. Cloudflare meters outbound messages; `replyEmail` uses Email Routing and does not need Email Sending onboarding.
587
583
 
588
584
  Who you can send to depends on your Workers plan. Onboarding the mail domain for Email Sending is what allows **arbitrary recipients**, and it needs **Workers Paid** — billing is dashboard-only, so Void cannot do that for you. On Workers Free the onboarding row fails and the deploy prints:
589
585
 
@@ -593,35 +589,33 @@ Workers Paid needed for arbitrary recipients. Inbound works; sendEmail() to veri
593
589
 
594
590
  Everything else — routing, rules, the binding — still goes through, and the address map ends with `outbound sendEmail() from support@mail.acme.com (verified destinations only)`. Verified destinations are the addresses under **Email Routing → Destination addresses** in your Cloudflare dashboard; a handler that `forward()`s to a new address needs the same verification click.
595
591
 
596
- The refusal is remembered by the `send_email` binding that same run records in `void.lock.json`: Void records the binding only after it has attempted the onboarding, so a committed binding next to a domain that is still not onboarded means "tried, refused". Later deploys ask nothing about it, `--require-email` passes, `void email status` reads the domain as set up (its sending row says `not onboarded — verified destinations only` and names the retry), and the address map keeps ending with `(verified destinations only)`. The deploy never retries the onboarding on its own. After upgrading to Workers Paid, run `void email setup --platform cloudflare` once: it asks `Onboard mail.acme.com for Email Sending?` and, on Yes, onboards the domain — from then on the map ends without the marker.
592
+ Void remembers when Email Sending onboarding was refused. Later deploys keep inbound mail and sends to verified destinations working; this state also satisfies `--require-email`. Void does not retry onboarding automatically. After upgrading to Workers Paid, run `void email setup --platform cloudflare` to enable arbitrary recipients. `void email status --platform cloudflare` shows whether sending is still limited to verified destinations.
597
593
 
598
594
  If `_dmarc.mail.acme.com` or `cf-bounce.mail.acme.com` already has a TXT record, sending onboarding is refused — it writes its own `_dmarc` (`p=reject`) and DKIM records and Cloudflare would answer with a conflict. Inbound is unaffected; remove the records or keep sending off.
599
595
 
600
596
  ### CI
601
597
 
602
- The email prompt follows wrangler's own interactivity rule: a CI environment as wrangler detects it, or stdin or stdout not a terminal, means non-interactive. A non-interactive deploy that finds something not ready prints the checklist and
598
+ In CI or when input or output is redirected, Void cannot prompt. If email is not ready, it prints the checklist and:
603
599
 
604
600
  ```
605
601
  deploy: email on mail.acme.com is not set up, and this shell cannot ask.
606
602
  Run `void email setup --platform cloudflare` once locally, commit void.lock.json, then redeploy — deploying without email.
607
603
  ```
608
604
 
609
- then deploys **without** email. Pass `--require-email` to fail instead. When setup has already committed the exact subdomain `addresses` plan and only its MX records are not visible yet, every deploy stops and preserves that plan until DNS can be verified. Automatic resource provisioning is not an escape hatch: it creates D1/KV/R2/Queue/Hyperdrive resources, never a mail setup. To read the rows without deploying or being asked anything, run `void email status --platform cloudflare`.
605
+ Void then deploys without email. Pass `--require-email` to fail instead. If setup has recorded the subdomain addresses but its MX records are not visible yet, deploy stops until DNS can be verified. Check progress with `void email status --platform cloudflare`.
610
606
 
611
- So the CI story is: run `void email setup --platform cloudflare` once on your machine (it runs the same preflight, checklist and prompt as the first deploy, then updates `void.lock.json`, without deploying), commit `void.lock.json`, and let CI run `void deploy --platform cloudflare --require-email`. Once the MX records are visible, the committed binding and `addresses` make every row read ready and nothing is asked — on Workers Free too, where the committed binding is what remembers the refused sending onboarding (see [Sending](#sending)).
607
+ For CI, run `void email setup --platform cloudflare` once locally and commit `void.lock.json`. Then run `void deploy --platform cloudflare --require-email` in CI. After DNS is ready, later deploys need no prompt. Workers Free can still send to verified destinations; see [Sending](#sending).
612
608
 
613
609
  ### If the subdomain step is refused
614
610
 
615
- Enabling routing on a subdomain and switching subaddressing on have no wrangler command, and neither does reading the subaddressing flag or telling a zone in another account from one not on Cloudflare. For those calls Void borrows your session's bearer through `wrangler auth token --json`, uses it inside one function, and drops it — nothing is stored, refreshed or written to disk (the child runs with `WRANGLER_WRITE_LOGS=false`, because wrangler would otherwise log the token). Every other read and write goes through Void's own pinned wrangler.
616
-
617
- When either call is refused, or the token cannot be read, the deploy does not fail. It prints what Cloudflare answered and the one-time dashboard step:
611
+ If Cloudflare refuses subdomain routing or subaddressing, Void prints the response and the dashboard step to complete:
618
612
 
619
613
  ```
620
614
  ✘ routing mail.acme.com: Cloudflare answered 403 …
621
615
  Cloudflare dashboard → your zone → Email → Settings → Subdomains → add the subdomain, then redeploy
622
616
  ```
623
617
 
624
- and **withholds `addresses` for that run** — no routing rule is created, so nothing points at a worker on a subdomain that does not yet receive mail. Do the dashboard step once and redeploy; the routing row then reads ready and `addresses` is written.
618
+ Void withholds the addresses until routing is ready. Complete the dashboard step and deploy again.
625
619
 
626
620
  ### What stays manual
627
621
 
@@ -631,4 +625,4 @@ and **withholds `addresses` for that run** — no routing rule is created, so no
631
625
  4. Workers Paid, if you need `sendEmail()` to arbitrary recipients.
632
626
  5. A verification click when a handler `forward()`s to a new destination.
633
627
  6. The Subdomains form in the dashboard, only if the subdomain step above is refused.
634
- 7. A y/n when a deploy would delete a routing rule — wrangler's own semantics.
628
+ 7. Confirmation when a deploy would delete a routing rule.
@@ -4,52 +4,32 @@ outline: deep
4
4
 
5
5
  # What is Void?
6
6
 
7
- Void is a fullstack SDK for Vite apps. It connects your server code, database, and frontend types, and deploys your app to Cloudflare Workers. To try it, start with the [Quickstart](./quickstart).
7
+ Void adds server routes, typed data access, and deployment to Vite apps. Native Void apps run on Cloudflare Workers. You can also use Void with a [supported framework](../integrations/frameworks/overview).
8
8
 
9
- Add the Vite plugin to your app. As you use features such as a database, key-value storage, or queues, Void detects what you need and sets up the corresponding resources.
9
+ Start with the [Quickstart](./quickstart) to create an app, run it locally, and deploy it.
10
10
 
11
11
  The CLI and build tools require Node.js 24.21.0 or later.
12
12
 
13
- ```sh
14
- npm install -D vite void
15
- ```
16
-
17
- ```ts
18
- import { defineConfig } from 'vite';
19
- import { voidPlugin } from 'void';
20
-
21
- export default defineConfig({
22
- plugins: [voidPlugin()],
23
- });
24
- ```
25
-
26
- ```sh
27
- void init # choose your framework and deployment target
28
- void deploy
29
- ```
13
+ ## Resources from your code
30
14
 
31
- ## The idea
15
+ Import `db` from `void/db`, and Void provides a local database and provisions one for deployment. The same pattern applies to KV, object storage, queues, and AI. Configure resources explicitly when you need to use existing infrastructure.
32
16
 
33
- Your code often already describes the resources it needs. Import `db` from `void/db`, for example, and Void provides a database for local development and provisions its production resource when you deploy. The same pattern applies to `kv`, `storage`, queues, and AI.
17
+ Your Drizzle schema defines database types. Route handlers infer return types, and the [typed fetch client](./typed-fetch.md) checks calls from the frontend. Changing a field shows you which callers need to change.
34
18
 
35
- Types follow your data through the app: from a Drizzle schema to a route handler, then to the frontend calling that route. You can change a field in one place and let TypeScript show you the code that needs updating.
19
+ Void connects these parts of your app:
36
20
 
37
- Void brings these pieces together:
21
+ - [Server routes](./server-routing.md) and [pages](./pages-routing/overview.md) for your app's backend and UI
22
+ - [Database](./database.md), [KV](./kv.md), and [storage](./storage.md) that work locally and in production
23
+ - [Standard Schema](https://standardschema.dev/) validation for runtime values and TypeScript types
24
+ - `void deploy` to build, apply migrations, provision resources, and deploy
38
25
 
39
- - **Resources from your code:** imports tell Void which supported resources to provision. Use `void.config.ts` and its `cloudflare` field when you need to customize them.
40
- - **Types from database to frontend:** your Drizzle schema defines DB types, route handlers infer return types, and the [typed fetch client](./typed-fetch.md) checks calls at the usage site. One [Standard Schema](https://standardschema.dev/) validator can drive both runtime validation and compile-time types.
41
- - **Local Cloudflare development:** native Void apps run server code in `workerd`, with local database, KV, and storage.
42
- - **Deploy that understands the app:** `void deploy` reads your migrations, provisions the resources you actually use, and ships the result to the edge.
26
+ ## Deployment targets
43
27
 
44
- ## The platform
45
-
46
- Void deploys to [Cloudflare Workers](https://developers.cloudflare.com/workers/). You can use your own Cloudflare account or connect to a Void platform managed by your team. Both use the same SDK and CLI.
28
+ Deploy to [your own Cloudflare account](../integrations/cloudflare.md#deploy-to-your-own-cloudflare-account) or to a [Void platform](./self-hosted-platform.md) run by your team. Both use the same SDK and CLI.
47
29
 
48
30
  [Static assets](./edge/static-assets) are cached at the edge. [Prerendering](./edge/prerendering) builds pages ahead of time, while [incremental revalidation](./edge/revalidation) caches pages rendered on demand. Database, storage, secrets, and deployment commands are available through Void.
49
31
 
50
- For a single app, [deploy to your own account](../integrations/cloudflare#deploy-to-your-own-cloudflare-account). To give a team a shared deployment service, [install a Void platform](./self-hosted-platform.md), then let developers connect to its URL.
51
-
52
- ## How it works
32
+ ## How resource detection works
53
33
 
54
34
  ```
55
35
  vite.config.ts → voidPlugin() (works with any Vite app)
@@ -58,17 +38,11 @@ import { kv } → KV namespace (auto-provisioned)
58
38
  import { storage } → R2 bucket (auto-provisioned)
59
39
  import { ai } → Workers AI inference (metered)
60
40
  db/schema.ts → Drizzle schema (source of truth for DB types)
61
- db/migrations/*.sql → Applied to D1 on deploy
41
+ db/migrations/*.sql → Applied to the selected database on deploy
62
42
  void deploy → Deploy to the saved Cloudflare or Void target
63
43
  ```
64
44
 
65
- The plugin scans your source code at build time, detects which imports you use, and provisions the corresponding Cloudflare bindings on deploy.
66
-
67
- Void also works with existing frameworks. [TanStack Start](/integrations/frameworks/tanstack-start), [React Router](/integrations/frameworks/react-router), [SvelteKit](/integrations/frameworks/sveltekit), [Nuxt](/integrations/frameworks/nuxt), and [Astro](/integrations/frameworks/astro) can all deploy with the same `voidPlugin()`.
68
-
69
- If you are building a full-stack app without a meta-framework, Void also gives you [file-based server routing](./server-routing) with method exports, dynamic params, middleware, and validation. It also includes [pages routing](./pages-routing/overview) for server-rendered UI, SPA navigation, co-located data loading, and typed forms across React, Vue, Svelte, and Solid.
70
-
71
- Void detects your [app type](./app-types) and chooses the appropriate build and deployment flow.
45
+ The Vite plugin detects supported imports and adds the corresponding bindings. Void also detects your [app type](./app-types) to choose the build and deployment flow. Use `void.config.ts` when you need to control either choice.
72
46
 
73
47
  ## Next steps
74
48
 
@@ -4,15 +4,11 @@ outline: deep
4
4
 
5
5
  # Quickstart
6
6
 
7
- Let's create a Void app, run it locally, and deploy it. You can start in an empty directory or [add Void to an existing Vite app](#adding-to-an-existing-vite-app).
8
-
9
- Use Node.js 24.21.0 or later. New projects pin the SDK's tested Workers
10
- compatibility date, so the bundled local runtime can start them. An existing
11
- compatibility date in your project is preserved.
7
+ Create a Void app, run it locally, and deploy it. You can also [add Void to an existing Vite app](#adding-to-an-existing-vite-app). Use Node.js 24.21.0 or later.
12
8
 
13
9
  ## Start in an Empty Directory
14
10
 
15
- Install Void in your project directory:
11
+ Install Void in an empty project directory:
16
12
 
17
13
  ::: code-group
18
14
 
@@ -34,17 +30,7 @@ bun add -D void
34
30
 
35
31
  :::
36
32
 
37
- Then run the setup command:
38
-
39
- With pnpm, you can also start with `pnpm create void my-app`; the scaffolder sets
40
- up the required native build permissions before installing Void. If a manual
41
- installation reports blocked build scripts, approve `esbuild`, `sharp`, and
42
- `workerd` with `pnpm approve-builds`. Set `better-sqlite3: false` in
43
- `pnpm-workspace.yaml`'s `allowBuilds`: Void uses version 13's bundled binaries,
44
- so it does not need a native rebuild.
45
-
46
- The setup install updates the pnpm lockfile to match the generated dependencies,
47
- including when setup runs in CI. Later builds can use `pnpm install --frozen-lockfile`.
33
+ Run setup:
48
34
 
49
35
  ::: code-group
50
36
 
@@ -66,58 +52,25 @@ bunx void init
66
52
 
67
53
  :::
68
54
 
69
- Void asks you to choose Vite+ or plain Vite, a UI framework, and a starter. Vite+ is the default. For a database app, D1 needs no local database server; PostgreSQL and MySQL are available if you want to use an external database. Static Pages starts with pages only.
70
-
71
- Setup also asks where you want to deploy. Choose Cloudflare to use your own account, or Void to connect to your team's platform. You can skip this and decide later.
72
-
73
- <details>
74
- <summary style="cursor:pointer">
75
- 💡 <b>Notes on <code>void</code> binary usage</b>
76
- </summary>
77
-
78
- The docs use `void` for brevity. Because it's installed in your project, run it through your package manager outside package scripts: `npx void`, `pnpm void`, `yarn void`, or `bunx void`.
55
+ Void asks you to choose Vite+ or Vite, a UI framework, a starter, and a deployment target. Vite+ is the default. D1 needs no local database server; choose PostgreSQL or MySQL if you use an external database. You can skip deployment setup and decide later.
79
56
 
80
- Alternatively, you can add `./node_modules/.bin` to your `PATH` so that you can invoke `void` directly when you are in the root directory of your app.
57
+ With pnpm, you can start with `pnpm create void my-app`. It configures native build permissions before installing Void.
81
58
 
82
- :::warning ⚠️ Prefer local install
83
- Install `void` locally so the CLI and your app use the same version.
84
- :::
59
+ If a manual pnpm install reports blocked build scripts, run `pnpm approve-builds` for `esbuild`, `sharp`, and `workerd`. Set `better-sqlite3: false` in `pnpm-workspace.yaml`'s `allowBuilds`; Void uses its bundled binaries. Setup updates the pnpm lockfile, including in CI. Later installs can use `pnpm install --frozen-lockfile`.
85
60
 
86
- </details>
61
+ The examples below use `void` for brevity. Outside package scripts, run the local binary with `npx void`, `pnpm void`, `yarn void`, or `bunx void`. Keep Void installed in the project so the CLI and app use the same version.
87
62
 
88
63
  ## Using with Coding Agents
89
64
 
90
- `void init` detects your coding agent and sets up the matching instructions and skills.
91
-
92
- If auto-detection fails, `void init` asks you to choose from a short list (Claude, Cursor, Codex, Gemini CLI, Generic).
93
-
94
- In agents that support it, use the `/void` skill to load the relevant guidance, then describe the app you want to build. See [Coding Agents](../integrations/agents) for setup details.
65
+ `void init` detects your coding agent and installs its instructions and skills. If detection fails, choose an agent when prompted. In agents that support it, load the `/void` skill and describe the app you want to build. See [Coding Agents](../integrations/agents) for setup details.
95
66
 
96
67
  ## Meta Frameworks
97
68
 
98
- You can build pages directly with Void's [Pages routing](./pages-routing/overview), or keep an existing framework such as TanStack Start, React Router, or SvelteKit. Follow the [framework integration guides](../integrations/frameworks/overview) for framework-specific setup.
69
+ Use Void's [Pages routing](./pages-routing/overview) or keep a framework such as TanStack Start, React Router, or SvelteKit. Follow the [framework guides](../integrations/frameworks/overview) for setup.
99
70
 
100
71
  ## Adding to an Existing Vite App
101
72
 
102
- ::: code-group
103
-
104
- ```sh [npm]
105
- npm install -D void
106
- ```
107
-
108
- ```sh [pnpm]
109
- pnpm add -D void
110
- ```
111
-
112
- ```sh [yarn]
113
- yarn add -D void
114
- ```
115
-
116
- ```sh [bun]
117
- bun add -D void
118
- ```
119
-
120
- :::
73
+ Install Void using the package manager command [above](#start-in-an-empty-directory).
121
74
 
122
75
  Enable the plugin in `vite.config.ts`:
123
76
 
@@ -130,27 +83,7 @@ export default defineConfig({
130
83
  });
131
84
  ```
132
85
 
133
- Run setup to configure the remaining project files:
134
-
135
- ::: code-group
136
-
137
- ```sh [npm]
138
- npx void init
139
- ```
140
-
141
- ```sh [pnpm]
142
- pnpm void init
143
- ```
144
-
145
- ```sh [yarn]
146
- yarn void init
147
- ```
148
-
149
- ```sh [bun]
150
- bunx void init
151
- ```
152
-
153
- :::
86
+ Then run `void init` with your package manager to configure the remaining project files. Existing compatibility dates are preserved; new projects use Void's tested Workers compatibility date.
154
87
 
155
88
  ## Once You Have a Working App
156
89
 
@@ -3,9 +3,9 @@ layout: home
3
3
  theme: dark
4
4
 
5
5
  hero:
6
- name: Void.
7
- text: Ship Vite apps at warp speed
8
- tagline: A deployment platform designed for Vite. A powerful backend SDK to make your Vite apps truly full-stack.
6
+ name: Void
7
+ text: Full-stack apps with Vite
8
+ tagline: Add server routes, data, and Cloudflare resources to your app. Deploy with one command.
9
9
  actions:
10
10
  - theme: brand
11
11
  text: Get Started
@@ -16,26 +16,26 @@ hero:
16
16
 
17
17
  features:
18
18
  - iconify: lucide:terminal
19
- title: One Command to Production
20
- details: '`void deploy` builds your app, runs migrations, provisions resources, and deploys it.'
19
+ title: Deploy with One Command
20
+ details: '`void deploy` builds your app, provisions resources, applies migrations, and deploys it.'
21
21
  - iconify: lucide:layers
22
- title: Truly Full-Stack
23
- details: Database, KV storage, object storage, AI inference, authentication, queues, and cron jobs. All built-in. Import what you need, skip what you don't.
22
+ title: Full-Stack Features
23
+ details: Use a database, KV, object storage, AI, authentication, queues, and cron jobs as your app needs them.
24
24
  - iconify: lucide:wand-sparkles
25
- title: Your Code is Your Infra
26
- details: Void detects supported resources from your code and provisions them when you deploy. Use explicit configuration for existing infrastructure and services that need additional setup.
25
+ title: Resources from Your Code
26
+ details: Void detects supported resources from your imports and provisions them on deploy. Configure existing resources explicitly.
27
27
  - iconify: lucide:shield-check
28
- title: Performant and Reliable
29
- details: Build on Cloudflare Workers with checked migrations, versioned deployments, and tools to inspect and roll back application releases.
28
+ title: Deploy with Confidence
29
+ details: Run on Cloudflare Workers with checked migrations, versioned deployments, logs, and rollback.
30
30
  - iconify: lucide:blocks
31
- title: Your Framework, Your Rendering
32
- details: React, Vue, Svelte, Solid, Vite-based meta-frameworks. SSR, SSG, ISR, islands with partial hydration, and markdown.
31
+ title: Choose Your Framework
32
+ details: Use React, Vue, Svelte, Solid, or a supported Vite framework with SSR, SSG, ISR, and islands.
33
33
  - iconify: lucide:bot
34
- title: AI-Native
35
- details: Built-in skills and reference prompts let coding agents scaffold and ship full-stack apps in a single prompt.
34
+ title: Work with Coding Agents
35
+ details: Void provides project instructions and skills to help coding agents build and deploy your app.
36
36
 
37
- footer_heading: Deploy at Warp Speed
38
- footer_subheading: Vite. Optimized. Isomorphic. Deploy.
37
+ footer_heading: Build with Vite. Deploy with Void.
38
+ footer_subheading: Server code, resources, and deployment in one workflow.
39
39
  ---
40
40
 
41
41
  <script setup>