Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export default extendConfig(
collapsed: false,
items: [
{ text: 'AI', link: '/guide/ai' },
{ text: 'Email', link: '/guide/email' },
{ text: 'Sandboxes', link: '/guide/sandboxes' },
{ text: 'Authentication', link: '/guide/auth' },
{ text: 'WebSockets', link: '/guide/websockets' },
Expand Down
396 changes: 396 additions & 0 deletions docs/guide/email.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/integrations/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ Without `--provision`, deploy is a **drift check**: if a binding your source nee
- **WebSocket / Durable Object routes (`*.ws.ts`) are not supported here (yet).** A WebSocket route makes the build emit a key-value-backed Durable Object namespace (a `new_classes` migration), which a fresh Cloudflare account -- and every Workers Free account -- refuses to create. Deploy those with `void deploy`. SQLite-backed WebSocket support for this backend is a follow-up.
- **Custom `migrations_pattern` is not supported here.** v1 deploys only Void's default migration convention -- `db/migrations/*.sql` with wrangler's default pattern (leave `migrations_pattern` unset). Remove a custom `migrations_pattern` from the D1 binding, or deploy with `void deploy`. Supporting custom patterns is a follow-up.
- **Non-Cloudflare targets (`node` / `bun` / `deno`) are not supported here** -- this backend deploys Cloudflare Workers only.
- **Email (`void/email`) is not supported here (yet).** The deploy does not fail, but `sendEmail` returns `BINDING_MISSING` at runtime and `email/` handlers receive nothing -- no Email Routing rule, no `send_email` binding, and a handler's actions are not applied even if you add a rule by hand. Deploy with `void deploy` for email. Own-account email setup (one prompt on the first deploy, through your `wrangler login` session) is a follow-up.
- **`--skip-build` is not supported here.** Every check on this path validates the artifact the build emits -- the worker `vars` in `dist/ssr/wrangler.json` and the generated auth schema -- so skipping the build would leave them reading a stale or missing artifact instead of the worker being uploaded. Drop `--skip-build`, or use `void deploy` (the managed platform), which supports it.
- **PostgreSQL apps with auth enabled are not supported here.** This backend verifies + applies auth tables through remote D1 (SQLite) migrations only, so it cannot provision or verify the Better Auth schema on Postgres/Hyperdrive. Deploy with `void deploy`, or use D1 for the auth app.
- **PostgreSQL apps with checked-in migrations are not supported here.** This backend applies only remote D1 (SQLite) migrations. A PostgreSQL app's `db/migrations/*.sql` are applied over Hyperdrive by the managed platform (`void deploy`) at deploy time; this self-host backend never runs them, so it would deploy against an unmigrated database. Deploy with `void deploy`, or apply the migrations yourself against your Postgres and remove them from `db/migrations/`. (A pg app _without_ auth **and** with an externally-managed schema — no checked-in migrations — still deploys.)
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Use this page as a command reference. If you are setting up a project for the fi
| `void project cancel` | Cancel an active deployment |
| `void project purge-cache` | Purge all cached pages |
| `void build logs` | Stream, tail, or download build logs |
| `void email usage` | Show monthly email send/receive counts and quota |
| `void email logs` | Show recent email delivery activity |
| `void email destinations` | List verified recipient addresses |
| `void email allow <address>` | Add a recipient and send a verification email |
| `void email disallow <address>` | Remove a recipient from the allowlist |
| `void mcp` | Start the Void MCP server |
| `void init` | Setup wizard for new or existing projects |

Expand Down Expand Up @@ -881,6 +886,54 @@ Pass `--verbose` to additionally print the raw multi-line status breakdown (DB s

Project resolution for domain commands follows the same order as deploy (`--project`, `VOID_PROJECT`, linked project).

## Email

Inspect email usage and manage the recipients a project is allowed to send to. See [Email](../guide/email.md) for the runtime API.

Project resolution for email commands follows the same order as deploy (`--project`, `VOID_PROJECT`, linked project).

### `void email usage`

```
void email usage [--project <name>]
```

Show the current month's outbound and inbound counts, the monthly outbound limit, and how much of it is left. A suspended project is flagged in the output.

### `void email logs`

```
void email logs [--limit <n>] [--project <name>]
```

Show recent email activity — timestamp, direction, sender, recipient, status, and subject. `--limit` takes a positive integer. While persistent log storage is still being wired up, the command says so and points you at `wrangler tail` for real-time activity.

### `void email destinations`

```
void email destinations [--project <name>]
```

List the project's recipient addresses and their state (`verified`, `pending`, or `failed`). Outbound mail is only delivered to verified addresses.

### `void email allow`

```
void email allow <address> [--project <name>]
```

Add one recipient to the project's destination list. Cloudflare emails that address a verification link — the recipient clicks it, with no Void or Cloudflare account required — and the project can send to them from then on. Re-run `void email destinations` to check whether they have verified yet.

The project owner's email is added automatically when the project is created, so sending to yourself works without this step.

### `void email disallow`

```
void email disallow <address> [--project <name>]
```

Remove one recipient from the project's destination list. Outbound mail to that address is rejected from the next deploy onward.

## Agent

### `void init --agents`
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,12 @@ Each binding accepts `true` (use default name), `false` (disable), or a string (
| `kv` | `KV` | `KVNamespace` | `"kv": "MY_KV"` |
| `storage` | `STORAGE` | `R2Bucket` | `"storage": "MY_BUCKET"` |
| `ai` | `AI` | `Ai` | Boolean only |
| `email` | — | — | Boolean only |

Custom names are used in the deploy manifest, Wrangler config generation/sync, remote binding proxying, internal migrations, and generated runtime helpers such as `void/db`, `void/kv`, and `void/storage`.

`email` is a feature switch rather than a Cloudflare binding, so it has no binding name or type. Outbound mail is sent through the Void platform proxy, which owns the `send_email` binding — none is written into your worker. Set it to `true` to turn the [email integration](../guide/email.md) on — which registers the `void dev` inbox — even when no scanned file imports `void/email`, or `false` to turn it off.

#### `inference.build`

Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type. It applies to every app type, including `"void"` (where the default is `vite build`).
Expand Down
4 changes: 2 additions & 2 deletions packages/void/AGENT_PROMPT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Void

This project uses [Void](https://void.cloud) — a fullstack Vite plugin + deployment platform for Cloudflare. `voidPlugin()` in `vite.config.ts` gives you file-based API routing on Hono (`routes/`), Inertia-inspired server-rendered pages with co-located loaders/actions (`pages/` + `@void/vue` or `@void/react`), auto-provisioned D1/KV/R2 bindings, first-class Drizzle ORM integration (schema in `db/schema.ts` -> `void/db` Drizzle instance -> typed routes -> typed fetch client), built-in auth, queues, cron jobs, edge caching (ISR), and one-command deploys via `npx void deploy`. For first-time setup, prefer `npx void init`; in an empty directory, install `void` first and let the interactive flow scaffold the starter with Vite+ by default, add the matching framework adapter, configure project files, handle auth, and link or create the deploy project before the first deploy. In an existing app, `void init` configures Void in place by adding missing Vite scripts and creating or patching `vite.config.*` with `voidPlugin()`. Use `void` and `@void/*` package names in source code and package manifests.
This project uses [Void](https://void.cloud) — a fullstack Vite plugin + deployment platform for Cloudflare. `voidPlugin()` in `vite.config.ts` gives you file-based API routing on Hono (`routes/`), Inertia-inspired server-rendered pages with co-located loaders/actions (`pages/` + `@void/vue` or `@void/react`), auto-provisioned D1/KV/R2 bindings, first-class Drizzle ORM integration (schema in `db/schema.ts` -> `void/db` Drizzle instance -> typed routes -> typed fetch client), built-in auth, queues, cron jobs, email (`void/email` for outbound in every project; an `email/` directory for inbound handlers in Void apps only — under a third-party framework that directory is a build error; platform deploys only — not on `--backend cloudflare` yet), edge caching (ISR), and one-command deploys via `npx void deploy`. For first-time setup, prefer `npx void init`; in an empty directory, install `void` first and let the interactive flow scaffold the starter with Vite+ by default, add the matching framework adapter, configure project files, handle auth, and link or create the deploy project before the first deploy. In an existing app, `void init` configures Void in place by adding missing Vite scripts and creating or patching `vite.config.*` with `voidPlugin()`. Use `void` and `@void/*` package names in source code and package manifests.

Database: define Drizzle tables in `db/schema.ts`, import `db` from `void/db` and tables from `@schema`. Use `void db push` for prototyping, `void db generate` for production migrations. `drizzle-orm` and `drizzle-kit` ship with void (no extra install). Migrations live in `db/migrations/`.

Expand All @@ -16,4 +16,4 @@ Requests: `void project logs` only has rows when a worker actually ran. 5xx gene

Self-host deploy: `void deploy` targets the Void platform; to deploy to the user's OWN Cloudflare account instead, use `void deploy --backend cloudflare` (add `--provision` on the first deploy to create the D1/KV/R2/Queues/Hyperdrive resources the source needs). It uses local wrangler auth, so pin an account (`account_id` in `wrangler.jsonc` or `CLOUDFLARE_ACCOUNT_ID`) and keep real secrets in `wrangler secret put` — every `.env*` file this backend loads (`.env`, `.env.local`, `.env.production`, `.env.production.local`) ships as plaintext worker vars, `.local` included. Provisioning a Hyperdrive config reads `DATABASE_URL` from the shell, not from `.env*`. v1 supports full Void apps on the Cloudflare Workers target only (D1/KV/R2/Queues/Hyperdrive; auth and ISR on D1/SQLite); framework SSR of every kind (TanStack Start, React Router, vinext, SvelteKit, Nuxt, Analog, Astro), static/SPA/SSG apps (including `output: "static"`), WebSocket/Durable Object apps (`*.ws.ts`), a custom `migrations_pattern`, node/bun/deno targets, and PostgreSQL apps with auth all fail closed with guidance. Auth apps must ship checked-in migrations that produce the Better Auth schema. `--provision` is a single-operator, dev-machine step and is disabled in CI.

Full docs are in `node_modules/void/docs/`. If you have the `void` skill available, use it for a complete API reference covering project structure, routing, pages mode, database, auth, typed fetch, KV, storage, queues, cron jobs, CLI, configuration, and deployment.
Full docs are in `node_modules/void/docs/`. If you have the `void` skill available, use it for a complete API reference covering project structure, routing, pages mode, database, auth, typed fetch, KV, storage, queues, cron jobs, email, CLI, configuration, and deployment.
2 changes: 2 additions & 0 deletions packages/void/getting-started-prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ To deploy to your OWN Cloudflare account instead of the Void platform, use `void

Database workflow: define Drizzle tables in `db/schema.ts`, import `db` from `void/db` and tables from `@schema`. Use `void db push` to prototype (applies schema directly, no migration files). Use `void db generate` when ready to create production migration files in `db/migrations/`. `drizzle-orm` and `drizzle-kit` ship with void — no extra install needed. Scaffold new tables with `void gen model <name> [columns...]`.

Email: send transactional mail with `import { sendEmail } from "void/email"`. Platform deploys only for now — on `void deploy --backend cloudflare`, `sendEmail` returns `BINDING_MISSING` and `email/` handlers are not routed. Every project gets a platform sender — `<project-slug>+noreply@mail.void.cloud` — which is used when you omit `from`; a custom sender domain is not supported yet. Add recipients with `void email allow <address>` (Cloudflare emails them a verification link) and list them with `void email destinations`. Receive inbound mail by dropping `defineEmail()` handlers in the top-level `email/` directory (e.g. `email/support+[ticket].ts`) — Void apps only; under TanStack Start, React Router, SvelteKit, Nuxt, Analog or Astro an `email/` directory is a build error, while `sendEmail` keeps working. Check quota and activity with `void email usage` and `void email logs`; during `void dev` sends are captured by the local inbox at `/__void/inbox` instead of being delivered — in Void apps and Class A framework apps (TanStack Start, React Router). Class B/C integrations (SvelteKit, Nuxt, Analog, Astro) run their own dev server, so the inbox is not installed there and sends return `BINDING_MISSING`; use `createEmailTestHarness` from `void/email/testing`, which captures in-process everywhere.

Routing extras: declare static redirects/rewrites/fallbacks in `void.json` under `routing.*`, or in a `public/_redirects` file. For dynamic rewrites, call `c.rewrite(path)` in a `defineMiddleware`.

Logs: surface app-level errors via `import { logger } from "void/log"` and `logger.error(msg, fields?)` (also `.warn` / `.info`) so they appear under `void project logs --level error`. Errors only persisted to your own DB are invisible to Cloudflare Tail. Logs only exist where a worker ran — for 5xx generated by the edge router or on static/SPA projects, use `void project requests --status 5xx` instead.
Expand Down
12 changes: 12 additions & 0 deletions packages/void/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"imports": {
"#self": "./dist/index.mjs",
"#self/_env": "./dist/runtime/env.mjs",
"#self/email": "./dist/runtime/email.mjs",
"#self/env": "./dist/runtime/env-public.mjs",
"#self/client": "./dist/runtime/client.mjs",
"#self/handler": "./dist/runtime/handler.mjs",
Expand Down Expand Up @@ -203,6 +204,16 @@
"import": "./dist/runtime/ai.mjs",
"require": "./dist/runtime/ai.mjs"
},
"./email": {
"types": "./dist/runtime/email.d.mts",
"import": "./dist/runtime/email.mjs",
"require": "./dist/runtime/email.mjs"
Comment thread
Brooooooklyn marked this conversation as resolved.
},
"./email/testing": {
"types": "./dist/runtime/email/testing.d.mts",
"import": "./dist/runtime/email/testing.mjs",
"require": "./dist/runtime/email/testing.mjs"
},
"./sandbox": {
"types": "./dist/runtime/sandbox.d.mts",
"import": "./dist/runtime/sandbox.mjs",
Expand Down Expand Up @@ -332,6 +343,7 @@
"ignore": "^7.0.5",
"jsonc-parser": "3.3.1",
"pg": "8.22.0",
"postal-mime": "^2.7.4",
"proper-lockfile": "^4.1.2",
"wrangler": "catalog:"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/void/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@
"ai": {
"type": "boolean",
"description": "Enable Workers AI binding"
},
"email": {
"type": "boolean",
"description": "Enable email support (used by void/email)."
}
},
"additionalProperties": false
Expand Down
14 changes: 14 additions & 0 deletions packages/void/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { authSubcommands, parseAuthArgs } from './auth-args.ts';
import { buildSubcommands, parseBuildArgs } from './build-args.ts';
import { dbSubcommands, parseDbArgs } from './db-args.ts';
import { domainSubcommands, parseDomainArgs } from './domain-args.ts';
import { emailSubcommands, parseEmailArgs } from './email-args.ts';
import { envSubcommands, parseEnvArgs } from './env-args.ts';
import { genSubcommands, parseGenArgs } from './gen-args.ts';
import { githubSubcommands, parseGithubArgs } from './github-args.ts';
Expand Down Expand Up @@ -124,6 +125,10 @@ if (helpPath) {
runSubcommandGroup('domain', domainSubcommands, parseDomainArgs, (root, parsed) =>
import('./domain.ts').then(({ runDomainCommand }) => runDomainCommand(root, parsed)),
);
} else if (command === 'email') {
runSubcommandGroup('email', emailSubcommands, parseEmailArgs, (root, parsed) =>
import('./email.ts').then(({ runEmailCommand }) => runEmailCommand(root, parsed)),
);
} else if (command === 'db') {
runSubcommandGroup('db', dbSubcommands, parseDbArgs, (root, parsed) =>
import('./db.ts').then(({ runDbCommand }) => runDbCommand(root, parsed)),
Expand Down Expand Up @@ -206,6 +211,11 @@ if (helpPath) {
hint: 'add | delete | list | status',
description: 'Configure custom domains',
},
{
name: 'email',
hint: 'usage | logs | destinations | allow | disallow',
description: 'Inspect email activity and manage verified recipients',
},
{
name: 'github',
hint: 'install | installations | connect | update | status | disconnect | link | join',
Expand Down Expand Up @@ -291,6 +301,10 @@ if (helpPath) {
return runSubcommandGroup('build', buildSubcommands, parseBuildArgs, (r, p) =>
import('./build-cmd.ts').then(({ runBuildCommand }) => runBuildCommand(r, p)),
);
case 'email':
return runSubcommandGroup('email', emailSubcommands, parseEmailArgs, (r, p) =>
import('./email.ts').then(({ runEmailCommand }) => runEmailCommand(r, p)),
);
case 'db':
return runSubcommandGroup('db', dbSubcommands, parseDbArgs, (r, p) =>
import('./db.ts').then(({ runDbCommand }) => runDbCommand(r, p)),
Expand Down
Loading