Skip to content
Draft
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
2 changes: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const integrationItems = [
{ text: 'SvelteKit', link: '/integrations/frameworks/sveltekit' },
{ text: 'Nuxt', link: '/integrations/frameworks/nuxt' },
{ text: 'Analog', link: '/integrations/frameworks/analog' },
{ text: 'pracht', link: '/integrations/frameworks/pracht' },
],
},
{ text: 'Coding Agents', link: '/integrations/agents' },
Expand Down Expand Up @@ -333,6 +334,7 @@ export default extendConfig(
SvelteKit: 'logos:svelte-icon',
Nuxt: 'logos:nuxt-icon',
Analog: 'logos:analog',
pracht: 'logos:preact',
};
const itemIcons: Record<string, IconOption> = {
'Coding Agents': 'lucide:bot',
Expand Down
17 changes: 9 additions & 8 deletions docs/guide/app-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ Void apps can also use [`output: 'static'`](./ssg.md) to pre-render all pages at

Void supports deploying Vite-based meta-framework apps with `void deploy`. The framework owns routing and SSR. Void handles [binding inference](../reference/resource-inference.md), [typed DB queries](./database.md), migrations, and deployment.

| Framework | Detected from package | Setup guide |
| --------------------------------------------------- | ----------------------- | ----------------------------------------------------- |
| [TanStack Start](https://tanstack.com/start/latest) | `@tanstack/react-start` | [Guide](../integrations/frameworks/tanstack-start.md) |
| [React Router v7](https://reactrouter.com/) | `@react-router/dev` | [Guide](../integrations/frameworks/react-router.md) |
| [SvelteKit](https://svelte.dev/docs/kit) | `@sveltejs/kit` | [Guide](../integrations/frameworks/sveltekit.md) |
| [Nuxt](https://nuxt.com/) | `nuxt` | [Guide](../integrations/frameworks/nuxt.md) |
| [Analog](https://analogjs.org/) | `@analogjs/platform` | [Guide](../integrations/frameworks/analog.md) |
| [Astro](https://astro.build/) | `astro` | [Guide](../integrations/frameworks/astro.md) |
| Framework | Detected from package | Setup guide |
| --------------------------------------------------- | ---------------------------- | ----------------------------------------------------- |
| [TanStack Start](https://tanstack.com/start/latest) | `@tanstack/react-start` | [Guide](../integrations/frameworks/tanstack-start.md) |
| [React Router v7](https://reactrouter.com/) | `@react-router/dev` | [Guide](../integrations/frameworks/react-router.md) |
| [SvelteKit](https://svelte.dev/docs/kit) | `@sveltejs/kit` | [Guide](../integrations/frameworks/sveltekit.md) |
| [Nuxt](https://nuxt.com/) | `nuxt` | [Guide](../integrations/frameworks/nuxt.md) |
| [Analog](https://analogjs.org/) | `@analogjs/platform` | [Guide](../integrations/frameworks/analog.md) |
| [Astro](https://astro.build/) | `astro` | [Guide](../integrations/frameworks/astro.md) |
| [pracht](https://pracht.resynapse.dev/) | `@pracht/adapter-cloudflare` | [Guide](../integrations/frameworks/pracht.md) |

Add `voidPlugin()` to the framework's Vite config to get binding inference, typed DB generation, migration management, cron jobs, queues, and caching. Void-managed auth is not supported in framework mode; use Better Auth's official integration for your framework. See the [Meta Frameworks Integration](../integrations/frameworks/overview.md) for supported features and setup guides.

Expand Down
21 changes: 11 additions & 10 deletions docs/integrations/frameworks/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ Keep your framework's routes and rendering, and add Void for databases, storage,

Add `voidPlugin()` to the framework's Vite config. It detects resource usage, generates database types, manages migrations, and prepares the app for `void deploy`.

TanStack Start and React Router use the Cloudflare Vite plugin provided by Void. SvelteKit, Nuxt, Analog, and Astro use their own Cloudflare adapters. The setup differs, but your framework continues to handle routing and rendering.
TanStack Start and React Router use the Cloudflare Vite plugin provided by Void. SvelteKit, Nuxt, Analog, Astro, and pracht use their own Cloudflare adapters. The setup differs, but your framework continues to handle routing and rendering.

![void relationships with meta frameworks and deployment targets](./void-relationships.svg)

## Supported Frameworks

| Framework | Package | Integration |
| ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------- |
| [TanStack Start](./tanstack-start.md) | `@tanstack/react-start` | Vite plugin composition. `voidPlugin()` runs alongside the framework plugin. |
| [React Router v7](./react-router.md) | `@react-router/dev` | Vite plugin composition. Setup matches TanStack Start. |
| [SvelteKit](./sveltekit.md) | `@sveltejs/kit` | Vite-based, uses its own Cloudflare adapter |
| [Nuxt](./nuxt.md) | `nuxt` | Own CLI and build toolchain |
| [Analog](./analog.md) | `@analogjs/platform` | Vite-based, uses Nitro with Cloudflare preset |
| [Astro **v6+**](./astro.md) | `astro` | Own CLI and Cloudflare adapter (v6 required) |
| Framework | Package | Integration |
| ------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- |
| [TanStack Start](./tanstack-start.md) | `@tanstack/react-start` | Vite plugin composition. `voidPlugin()` runs alongside the framework plugin. |
| [React Router v7](./react-router.md) | `@react-router/dev` | Vite plugin composition. Setup matches TanStack Start. |
| [SvelteKit](./sveltekit.md) | `@sveltejs/kit` | Vite-based, uses its own Cloudflare adapter |
| [Nuxt](./nuxt.md) | `nuxt` | Own CLI and build toolchain |
| [Analog](./analog.md) | `@analogjs/platform` | Vite-based, uses Nitro with Cloudflare preset |
| [Astro **v6+**](./astro.md) | `astro` | Own CLI and Cloudflare adapter (v6 required) |
| [pracht](./pracht.md) | `@pracht/adapter-cloudflare` | Own CLI and Cloudflare adapter |

Detection is automatic. The CLI reads your `package.json` dependencies.

Expand All @@ -40,7 +41,7 @@ With `voidPlugin()` added to the framework's Vite config, frameworks get most of

For framework apps, Void revalidation caches public HTML document responses with both `void deploy` and `void deploy --platform cloudflare`. Client data requests continue to use the framework's own behavior. A response with `Set-Cookie` or `Cache-Control: private`, `no-store`, or `no-cache` is never shared.

Frameworks can also access bindings directly via the framework's own mechanisms (e.g. `env` from `cloudflare:workers` in SvelteKit 3 and Analog, `platform.env` in SvelteKit 2, `event.context.cloudflare.env` in Nuxt, `Astro.locals.runtime.env` in Astro).
Frameworks can also access bindings directly via the framework's own mechanisms (e.g. `env` from `cloudflare:workers` in SvelteKit 3 and Analog, `platform.env` in SvelteKit 2, `event.context.cloudflare.env` in Nuxt, `Astro.locals.runtime.env` in Astro, `context.env` in pracht).

## Configuration

Expand Down
109 changes: 109 additions & 0 deletions docs/integrations/frameworks/pracht.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
outline: deep
---

# pracht

Use Void with [pracht](https://pracht.resynapse.dev/) for resource detection, typed database queries, and migrations. pracht keeps its own build and Cloudflare adapter; Void plugs into its Vite configuration.

## Setup

### 1. Create a new project

```bash
npm create pracht@latest my-app -- --adapter=cf
cd my-app
```

### 2. Install dependencies

```bash
npm install -D void
```

### 3. Configure `vite.config.ts`

Add `voidPlugin()` before `pracht()`, and point the Cloudflare adapter at the Cloudflare config Void generates:

```ts
import { cloudflareAdapter } from '@pracht/adapter-cloudflare';
import { pracht } from '@pracht/vite-plugin';
import { defineConfig } from 'vite';
import { voidPlugin } from 'void';

export default defineConfig({
plugins: [
voidPlugin(),
pracht({ adapter: cloudflareAdapter({ configPath: './.void-wrangler.jsonc' }) }),
],
});
```

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

Void uses this config for the Cloudflare adapter during development and deployment. Move any bindings from the `wrangler.jsonc` that `create-pracht` wrote into it, then delete that file:

```ts
import { defineConfig } from 'void/config';

export default defineConfig({
cloudflare: {
name: 'my-app',
compatibility_date: '2026-02-24',
compatibility_flags: ['nodejs_als'],
},
});
```

`nodejs_als` is required for `void/*` runtime helpers (for example `void/db`, `void/kv`) in pracht.

### 5. Deploy

```bash
void auth login
void deploy
```

`void deploy` runs `pracht build` and deploys `dist/server/worker.js` with the assets in `dist/client`.

## Using Void Features

Most [Void platform features](../../guide/app-types.md#void-apps) work with pracht in both `pracht dev` and production.

Void-managed auth is not supported in framework mode yet. Use [`@pracht/session`](https://pracht.resynapse.dev/docs/recipes-auth) or Better Auth directly for now.

`pracht build` prerenders `ssg` and `isg` routes without Cloudflare bindings, so a route whose loader uses `void/*` helpers must use `render: "ssr"`.

### Database

Use `void/db` in loaders, API routes, and middleware:

```tsx
import { users } from '@schema';
import { db } from 'void/db';

export async function loader() {
return { users: await db.select().from(users) };
}
```

### 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/`.

### Environment Variables

Declare variables in `env.ts` and read them with `import { env } from "void/env"`. See [Environment Variables](../../guide/env-vars.md) for local values, production secrets, and public `VITE_*` values.

## Accessing Bindings Directly

pracht's Cloudflare adapter passes the Worker `env` to loaders and API routes as `context.env`:

```ts
import type { LoaderArgs } from '@pracht/core';

export async function loader({ context }: LoaderArgs) {
const { results } = await context.env.DB.prepare('SELECT * FROM users').all();
return { users: results };
}
```
4 changes: 4 additions & 0 deletions packages/deploy-cloudflare/src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type CloudflareFrameworkDeployConfig = Readonly<{
compatibilityDate: string;
compatibilityFlags?: ReadonlyArray<string>;
assetsIgnore?: ReadonlyArray<string>;
htmlHandling?: 'auto-trailing-slash' | 'force-trailing-slash' | 'drop-trailing-slash' | 'none';
crons?: ReadonlyArray<string>;
requiredSecrets?: ReadonlyArray<string>;
versionMetadataBinding?: string;
Expand Down Expand Up @@ -56,6 +57,9 @@ export function writeCloudflareFrameworkDeployConfig(
upsert(['assets', 'binding'], 'ASSETS');
upsert(['assets', 'run_worker_first'], true);
upsert(['assets', 'not_found_handling'], 'none');
if (options.htmlHandling) {
upsert(['assets', 'html_handling'], options.htmlHandling);
}
if (options.crons !== undefined) {
upsert(['triggers', 'crons'], [...options.crons]);
}
Expand Down
22 changes: 22 additions & 0 deletions packages/deploy-cloudflare/test/unit/framework.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ describe('Cloudflare framework deployment', () => {
},
);

it.each([undefined, 'drop-trailing-slash'] as const)(
'writes the framework asset trailing-slash policy (%s)',
(htmlHandling) => {
const root = mkdtempSync(join(tmpdir(), 'void-framework-html-'));
const configPath = join(root, 'wrangler.jsonc');
writeFileSync(configPath, '{}');
writeCloudflareFrameworkDeployConfig({
configPath,
deployPointerPath: join(root, '.wrangler/deploy/config.json'),
name: 'framework-app',
accountId: 'account-id',
main: 'dist/server/worker.js',
assetsDirectory: 'dist/client',
compatibilityDate: '2026-09-11',
htmlHandling,
});
expect(
(parseRootWranglerConfig(configPath).assets as { html_handling?: string }).html_handling,
).toBe(htmlHandling);
},
);

it('preserves root state and points Wrangler at the standalone Worker', () => {
const root = mkdtempSync(join(tmpdir(), 'void-framework-config-'));
const configPath = join(root, 'wrangler.jsonc');
Expand Down
1 change: 1 addition & 0 deletions packages/void/src/cli/deploy-cloudflare-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export async function prepareCloudflareFrameworkArtifact(options: {
? preset.cloudflareCompatibilityFlags
: [...(preset.cloudflareCompatibilityFlags ?? []), 'nodejs_compat'],
crons: [...new Set(crons.flatMap((job) => job.crons))],
...(preset.htmlHandling ? { htmlHandling: preset.htmlHandling } : {}),
requiredSecrets: requiredSecretNames,
versionMetadataBinding,
...(ai ? { ai: { ...ai, gateway: voidConfig.ai?.gateway } } : {}),
Expand Down
5 changes: 4 additions & 1 deletion packages/void/src/cli/deploy-managed-framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ export async function runFrameworkDeploy(
prerender,
vars: voidConfig.worker?.vars,
// Not `routing.notFound`; see `warnIgnoredNotFoundForFramework`.
assetConfig: { not_found_handling: 'none' },
assetConfig: {
not_found_handling: 'none',
...(fwPreset.htmlHandling ? { html_handling: fwPreset.htmlHandling } : {}),
},
packageOptions,
hashedAssetsPrefix: fwPreset.hashedAssetsPrefix ?? 'assets',
headerRules,
Expand Down
20 changes: 20 additions & 0 deletions packages/void/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,8 @@ function getDevCommand(root: string, framework?: DetectedFramework): string {
return 'nuxt dev';
case 'astro':
return 'astro dev';
case 'pracht':
return 'pracht dev';
default:
return formatProjectCommand(root, 'vite dev');
}
Expand Down Expand Up @@ -2428,6 +2430,24 @@ function noteManualFrameworkConfigSetup(framework: DetectedFramework): void {
return;
}

if (framework.name === 'pracht') {
note(
`Detected pracht. Void init does not patch vite.config.* automatically.\n\n` +
`Add \`voidPlugin()\` before \`pracht()\`, and point the Cloudflare adapter at the generated Cloudflare config:\n\n` +
`import { cloudflareAdapter } from "@pracht/adapter-cloudflare";\n` +
`import { pracht } from "@pracht/vite-plugin";\n` +
`import { voidPlugin } from "void";\n\n` +
`export default defineConfig({\n` +
` plugins: [\n` +
` voidPlugin(),\n` +
` pracht({ adapter: cloudflareAdapter({ configPath: "./.void-wrangler.jsonc" }) }),\n` +
` ],\n` +
`});`,
'Manual pracht config step',
);
return;
}

noteManualViteConfigSetup();
}

Expand Down
15 changes: 14 additions & 1 deletion packages/void/src/cli/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export type FrameworkPreset = {
hashedAssetsPrefix?: string;
/** Whether the worker entry needs bundling (bare imports not resolved by framework build). */
bundle?: boolean;
/** Assets `html_handling`; Cloudflare's default applies when unset. */
htmlHandling?: 'auto-trailing-slash' | 'force-trailing-slash' | 'drop-trailing-slash' | 'none';
};

/**
Expand All @@ -78,7 +80,7 @@ export type FrameworkPreset = {
* Vite-compatible build command (`vite build` or `vp build`) and have their
* output structure determined by the framework's Vite plugin.
*
* Class B/C frameworks (sveltekit, nuxt, analog, astro) use their own build CLI
* Class B/C frameworks (sveltekit, nuxt, analog, astro, pracht) use their own build CLI
* and have framework-specific output conventions.
*/
export const FRAMEWORK_PRESETS: Record<string, FrameworkPreset> = {
Expand Down Expand Up @@ -149,6 +151,17 @@ export const FRAMEWORK_PRESETS: Record<string, FrameworkPreset> = {
assetsDir: 'dist/client',
hashedAssetsPrefix: '_astro',
},
pracht: {
cloudflareArtifact: 'worker-output',
cloudflareCompatibilityFlags: ['nodejs_als'],
buildCommand: 'pracht build',
workerDir: 'dist/server',
workerMain: 'worker.js',
assetsDir: 'dist/client',
hashedAssetsPrefix: 'assets',
// pracht links to `/about`, not `/about/`.
htmlHandling: 'drop-trailing-slash',
},
};

export type StaticPreset = {
Expand Down
Loading