Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e6222f3
fix(void): run MySQL void/db and migrations in workerd
Brooooooklyn Oct 13, 2026
40dc6e8
fix(void): prebundle void's CommonJS deps wherever workerd runs them
Brooooooklyn Oct 13, 2026
5671fad
fix(void): bundle the hosted MySQL migration handler for workerd
Brooooooklyn Oct 13, 2026
02a13a1
fix(void): run MySQL migrations atomically and name failed files
Brooooooklyn Oct 13, 2026
bb568b4
fix(void): give framework requests a pg/MySQL request scope
Brooooooklyn Oct 13, 2026
a30f942
fix(void): load the real void/db in SvelteKit dev with installed void
Brooooooklyn Oct 13, 2026
cc87476
fix(void): bundle every migration handler dependency for hoisted inst…
Brooooooklyn Oct 13, 2026
7853e5e
fix(void): serialize MySQL migrations per database across deploys and…
Brooooooklyn Oct 13, 2026
d149219
fix(void): share the request env across void copies and guard install…
Brooooooklyn Oct 13, 2026
e7c9226
fix(void): run pg/MySQL queries on the current request's pool, and sc…
Brooooooklyn Oct 13, 2026
439d2c4
fix(void): close idle MySQL connections without a pool timer
Brooooooklyn Oct 13, 2026
f59bcc8
fix(void): build SvelteKit pg/MySQL apps for workerd with a request s…
Brooooooklyn Oct 13, 2026
c75284a
fix(void): give Nuxt, Analog and deploy cron bundles the app's void/db
Brooooooklyn Oct 13, 2026
84c8140
fix(void): split MySQL migration files and hold the migration lock th…
Brooooooklyn Oct 14, 2026
afe1470
fix(void): bundle installed void packages for the Node target and Sve…
Brooooooklyn Oct 14, 2026
ee0267b
fix(void): give framework dev crons the handler exports of bare void,…
Brooooooklyn Oct 14, 2026
0f43659
fix(void): build Nitro pg/MySQL Workers with Node compatibility and e…
Brooooooklyn Oct 14, 2026
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
54 changes: 53 additions & 1 deletion .github/scripts/packed-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
Expand Down Expand Up @@ -389,8 +390,59 @@ if (runtime.manifest.platformVersion !== ${JSON.stringify(version)}) throw new E
await task.stop();
}
}
if (framework === 'react') {
// The Node target bundles the installed void into its build, so a ValidationError from
// void/handler is the class that page actions check, as in dev.
await run(
manager,
manager === 'npm' ? ['install', '@hono/node-server'] : ['add', '@hono/node-server'],
fixture,
);
writeFileSync(
join(fixture, 'void.config.ts'),
"import { defineConfig } from 'void/config';\nexport default defineConfig({ target: 'node' });\n",
);
// KV and email are Cloudflare bindings.
rmSync(join(fixture, 'routes'), { recursive: true });
writeFileSync(
join(fixture, 'pages/form.tsx'),
'export default function Form() {\n return <h1>Form</h1>;\n}\n',
);
writeFileSync(
join(fixture, 'pages/form.server.ts'),
"import { defineHandler, ValidationError } from 'void/handler';\nexport const action = defineHandler(() => {\n throw new ValidationError({ email: 'Email already taken' });\n});\n",
);
await run(process.execPath, [vite, 'build'], fixture);
const task = launch(
process.execPath,
[vite, 'preview', '--host', '127.0.0.1', '--port', '0', '--strictPort'],
fixture,
);
try {
const deadline = Date.now() + 60_000;
let base;
while (!base && Date.now() < deadline) {
if (task.child.exitCode !== null) {
throw new Error(task.failure());
}
base = packedServerUrl(task.output());
await new Promise((resolve) => setTimeout(resolve, 200));
}
assert(base, task.failure());
const response = await fetch(`${base}/form`, {
method: 'POST',
headers: { accept: 'text/html' },
body: new URLSearchParams({ email: 'taken@example.com' }),
});
const body = await response.text();
assert.equal(response.status, 200, `${body}\n${task.failure()}`);
assert(body.includes('Email already taken'), body);
} finally {
await task.stop();
}
}
console.log(
`Passed packed ${label} (${suite}): init, frozen install, public Vite build/typecheck, assets, KV, email, runtime integrity${full ? ', dev/preview, hydration/navigation' : ', dev HTTP'}`,
`Passed packed ${label} (${suite}): init, frozen install, public Vite build/typecheck, assets, KV, email, runtime integrity${full ? ', dev/preview, hydration/navigation' : ', dev HTTP'}${framework === 'react' ? ', Node target' : ''}`,
);
} catch (error) {
failures.push({ fixture: label, error: String(error.stack ?? error) });
Expand Down
6 changes: 1 addition & 5 deletions docs/guide/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,6 @@ const usersWithPosts = await db.query.users.findMany({
});
```

::: warning ⚠️ Nuxt and Analog limitations
In Nuxt and Analog, use the query builder (`db.select().from(table)`). The `db.query.*` relational API is not available in these frameworks.
:::

## Seeding

Use `void db seed` to reset your local database, re-apply migrations, and then run a seed file.
Expand Down Expand Up @@ -376,7 +372,7 @@ To keep an existing migration history:

Review and commit the metadata, then run `void deploy` again. If no database has applied the SQL yet, you can instead replace it with migrations from `void db generate`.

For D1 migrations containing triggers, add a `--> statement-breakpoint` line after each complete SQL statement, including the trigger's closing `END;`. Keep the trigger body together.
For D1 migrations containing triggers, and MySQL migrations containing triggers, procedures, functions or events, add a `--> statement-breakpoint` line after each complete SQL statement, including the trigger's closing `END;`. Keep the trigger body together.

## Scaffolding

Expand Down
17 changes: 14 additions & 3 deletions docs/guide/database/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,20 @@ void deploy

For direct Cloudflare deploys, export the production `DATABASE_URL` in the shell running `void deploy --platform cloudflare`. Void uses it to provision Hyperdrive and apply pending checked-in migrations before uploading the Worker; it is not written to the Worker config.

::: warning MySQL DDL is not transactional
MySQL may implicitly commit schema changes. If a multi-statement migration fails partway through, earlier statements can remain applied even though Void does not record the migration as complete. Keep migrations small, review them before deploy, and make failed migrations safe to rerun after repairing the database.
:::
Only one migration run changes a database at a time, whether it comes from `void db migrate`, `void db reset`, the reset that `void db seed` does before it loads data, `void deploy --platform cloudflare`, or a `void deploy` of any project that uses the database. Better Auth's table changes run inside the same run. Another run waits for it, up to about 20 seconds for a command that connects with `DATABASE_URL` and about 2 minutes for a `void deploy` to the Void platform, then stops with an error that asks you to try again.

### Failed migrations {#failed-migrations}

Each migration file runs in a transaction, so a failed file that only changes data leaves the database unchanged. On a sharded Vitess keyspace, such as a sharded PlanetScale database, a commit that fails on one shard does not undo the shards that already committed. MySQL commits schema changes such as `CREATE TABLE` and `ALTER TABLE` immediately, so when a file with schema changes fails partway through, its earlier statements stay applied. The error names the file that failed.

To recover, undo the applied statements, fix the file, then run `void db migrate` or `void deploy` again:

```sh
void db execute "DROP TABLE comments"
void db execute --remote "DROP TABLE comments" # deployed database
```

MySQL rejects `IF NOT EXISTS` on `ADD COLUMN` and `CREATE INDEX`, so undoing a statement is usually simpler than making it safe to rerun. Small migrations leave less to undo.

## Database commands

Expand Down
2 changes: 1 addition & 1 deletion docs/integrations/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Void uses Access credentials for readiness checks without saving them. If checks

### Databases and secrets

For PostgreSQL and MySQL, supply `DATABASE_URL` to the deploy process. Void uses it for Hyperdrive and migrations without writing it to config. MySQL schema changes can partially apply before an error.
For PostgreSQL and MySQL, supply `DATABASE_URL` to the deploy process. Void uses it for Hyperdrive and migrations without writing it to config. MySQL schema changes can partially apply before an error; see [failed MySQL migrations](../guide/database/mysql.md#failed-migrations).

Creating Hyperdrive requires an API token with Hyperdrive edit permission. Alternatively, create it in Cloudflare and add its ID to `cloudflare.hyperdrive`; an existing binding works with browser login.

Expand Down
14 changes: 3 additions & 11 deletions docs/integrations/frameworks/analog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Void provides the local Cloudflare binding runtime. `nitro-cloudflare-dev` creat
### 3. Configure `vite.config.ts`

```ts
import { resolve } from 'node:path';
import analog from '@analogjs/platform';
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
Expand All @@ -41,19 +40,16 @@ export default defineConfig({
nitro: {
preset: 'cloudflare-module',
cloudflareDev: { configPath: './.void-wrangler.jsonc' },
modules: ['nitro-cloudflare-dev'],
alias: {
// Nitro has its own bundler that doesn't use Vite aliases.
// Duplicate @schema here so Nitro can resolve it.
'@schema': resolve(__dirname, 'db/schema.ts'),
},
modules: ['nitro-cloudflare-dev', 'void/nitro'],
},
}),
voidPlugin(),
],
});
```

The `void/nitro` module gives Analog API routes and page server loads your app's `void/db` and `@schema`.

### 4. Create `void.config.ts`

Void generates the Cloudflare config used by Analog's development runtime:
Expand Down Expand Up @@ -98,10 +94,6 @@ export const load = async ({ event }: PageServerLoad) => {
};
```

::: warning ⚠️ Analog limitation
Use `db.select().from(table)` for database queries in Analog. The `db.query.*` relational API is not available.
:::

### Other resources

Use [KV](../../guide/kv.md), [object storage](../../guide/storage.md), and [AI](../../guide/ai.md) from server-side code with the same imports as a Void app. Add [cron jobs](../../guide/jobs.md) in `crons/` and [queue consumers](../../guide/queues.md) in `queues/`.
Expand Down
17 changes: 7 additions & 10 deletions docs/integrations/frameworks/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,24 @@ import { resolve } from 'node:path';
import { voidPlugin } from 'void';

export default defineNuxtConfig({
alias: {
'@schema': resolve(import.meta.dirname, 'db/schema.ts'),
},
nitro: {
preset: 'cloudflare-module',
cloudflareDev: { configPath: './.void-wrangler.jsonc' },
modules: ['void/nitro'],
cloudflare: { dev: { configPath: './.void-wrangler.jsonc' } },
// Required for Nuxt 4.6.0 on Windows.
externals: {
inline: [/[\\/]node_modules[\\/]nuxt[\\/]dist[\\/]/],
},
alias: {
// Nitro has its own bundler that doesn't use Vite aliases.
// Duplicate @schema here so Nitro can resolve it.
'@schema': resolve(import.meta.dirname, 'db/schema.ts'),
},
},
vite: { plugins: [voidPlugin()] },
});
```

The `void/nitro` module gives Nuxt server routes your app's `void/db` and `@schema`. The top-level `alias` lets `nuxi typecheck` resolve `@schema`, because Nuxt also checks server routes with the app's types. `cloudflare.dev` gives the development server the bindings from Void's generated Cloudflare config; Nuxt runs it in development when `compatibilityDate` is `2025-07-15` or later.

### 4. Create `void.config.ts`

Void generates the Cloudflare config used by Nuxt's development runtime:
Expand Down Expand Up @@ -88,10 +89,6 @@ export default defineEventHandler(async () => {
});
```

::: warning ⚠️ Nuxt limitation
Use `db.select().from(table)` for database queries in Nuxt. The `db.query.*` relational API is not available.
:::

### Other resources

Use [KV](../../guide/kv.md), [object storage](../../guide/storage.md), and [AI](../../guide/ai.md) from server-side code with the same imports as a Void app. Add [cron jobs](../../guide/jobs.md) in `crons/` and [queue consumers](../../guide/queues.md) in `queues/`.
Expand Down
2 changes: 2 additions & 0 deletions docs/integrations/nodejs-bun-deno.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ dist/

`vite preview` also uses `app.js` to serve your built app locally.

Installed packages that depend on `void` are bundled into `dist/ssr`. If one of their dependencies uses a native addon or reads files next to its own code, add that dependency, in the version the package uses, to your app's `dependencies`. The build then leaves it to Node.

## What Works

These Void features work identically across all targets:
Expand Down
1 change: 1 addition & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ Import Void's app-facing APIs from these paths:
| `void/queues` | `queues` proxy for sending messages to typed queues |
| `void/sandbox` | `getSandbox`, `sandbox`, `SandboxLimitError`, `SandboxUnavailableError`, and the SDK 1.0 file, backup, and mount helpers. `getSandbox(id)` resolves a client. Commands, files, port requests, and process completion return promises; limits reject with `SandboxLimitError`, which extends `UsageLimit`. Container state lasts until inactivity (default ten minutes), a crash, or a restart. See [Sandboxes](../guide/sandboxes.md) for the API and deployment requirements. |
| `void/env` | `defineEnv`, `env`, schema helpers (`string`, `number`, `boolean`, `url`, `email`, `oneOf`, `json`), and global Cloudflare env types |
| `void/nitro` | Nitro module for Nuxt and Analog. Add `'void/nitro'` to the Nitro `modules` so server routes get the app's `void/db` and `@schema` |
| `void/sveltekit` | SvelteKit integration helpers: `withVoidTSConfig()` and `mergeVoidSvelteKitTsconfig()` |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"test": "node test/affected.mjs",
"test:full": "vp test run",
"test:integration": "vp test run --project parallel-integration",
"test:metaframeworks": "vp test run --project parallel-integration analog.test.ts astro.test.ts nuxt.test.ts react-router.test.ts sveltekit.test.ts tanstack-start.test.ts vinext-app.test.ts vinext-pages.test.ts",
"test:metaframeworks": "vp test run --project parallel-integration analog.test.ts astro.test.ts nitro-worker.test.ts nuxt.test.ts react-router.test.ts sveltekit.test.ts tanstack-start.test.ts vinext-app.test.ts vinext-pages.test.ts",
"test:pages:react-vue": "vp test run --project parallel-integration packages/react packages/vue",
"test:pages:svelte-solid": "vp test run --project parallel-integration packages/svelte packages/solid",
"test:platform": "vp test run --project 'platform-*'",
Expand Down
Loading
Loading