# The complete guide to Void: full-stack Vite apps with Hono and Cloudflare

> Build and deploy full-stack Vite apps with Void, Hono, React, and Cloudflare D1. Learn typed routes, forms, migrations, authentication, and team hosting.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-15 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/void/

**Void** lets you build a frontend and a backend in one Vite project, then deploy them together to Cloudflare Workers. You write a server route, import a database client, and let Void create the Cloudflare resources the app needs.

Cloudflare [open-sourced Void on October 15, 2026](https://blog.cloudflare.com/deploy-with-void/). It includes an SDK, a deployment CLI, and a platform you can host for your team. The project lives at [voidzero-dev/void](https://github.com/voidzero-dev/void) under the MIT license.

The Hono integration is a good reason to look at it. Void uses Hono for server routing and middleware. If you already understand Hono's request context, you can use that knowledge while building a complete app with a UI and a database.

We'll build a reading list. You can save an article, see saved articles on a server-rendered page, and retrieve them through a typed API. Then we'll cover deployment, protecting the app, and setting up a shared Void platform.

## What Void adds to Vite

[Vite](https://flaviocopes.com/vite-tutorial/) handles the development server and production builds. A full-stack application also needs somewhere to handle requests, store data, and run server code.

Void supplies those pieces through a Vite plugin and application conventions. You can put API endpoints in `routes/`, UI components in `pages/`, and database tables in `db/schema.ts`.

Its different parts have different jobs:

| Part | What it does |
| --- | --- |
| Void SDK | Routing, server-rendered pages, typed resource APIs, and framework adapters |
| Void CLI | Project setup, builds, provisioning, migrations, deployment, and operations |
| Void Platform | A shared Cloudflare deployment environment that administrators run for other users |

You can deploy straight to your own Cloudflare account. Installing a team platform is optional.

For a native Void app, the request flow looks like this:

```text
Browser
  → Cloudflare Worker
  → Hono middleware and a Void route or page loader
  → Database query
  → JSON response or rendered page
```

The browser never connects directly to D1. Your server handlers decide what it can read and change.

Void also integrates with existing Vite-based frameworks. In that arrangement, your framework keeps its routing and rendering. You can add Void's resource APIs and deployment workflow around it. The [supported app types guide](https://void.cloud/guide/app-types) explains which features apply to each setup.

## Where Hono fits

[Hono](https://flaviocopes.com/hono/) is a JavaScript web framework built around the standard `Request` and `Response` APIs. It supplies the request context that Void passes to handlers.

Here is a complete Void API endpoint. Save it as `routes/api/health.ts`:

```ts
import { defineHandler } from 'void'

export const GET = defineHandler((c) => {
  c.header('Cache-Control', 'no-store')
  return c.json({ status: 'ok' })
})
```

The file creates `GET /api/health`. `c` is Hono's context. `c.req` gives you the request, and `c.json()` creates a JSON response.

You can return a plain object from a Void handler too. Void converts it to JSON. Use Hono's response helpers when you need to set a status or construct a particular response.

Hono middleware works here as well. Install `hono` as a direct dependency before importing its modules, even though Void uses it internally:

```bash
npm install hono
```

For example, this complete `middleware/01.logger.ts` logs requests:

```ts
import { logger } from 'hono/logger'
import { defineMiddleware } from 'void'

export default defineMiddleware(logger())
```

Global middleware files run in order, so numeric prefixes help keep the order explicit. You can also pass middleware to individual handlers.

The [Hono integration guide](https://void.cloud/integrations/hono) covers helpers and middleware. If Hono itself is new to you, my free [Hono course](https://flaviocopes.com/courses/hono/) starts with routing and request handling.

## Create a project

Install a current Node.js release that meets [Void's Node requirement](https://github.com/voidzero-dev/void#readme). Use a recent LTS release with its latest patches, or a newer supported release. An old Node installation can fail before the application starts.

On macOS, you can install Node with Homebrew:

```bash
brew install node
```

On Omarchy, use its package command:

```bash
omarchy pkg add nodejs npm
```

Check what your terminal is using:

```bash
node --version
npm --version
```

Create an empty project and install Void locally:

```bash
mkdir reading-list
cd reading-list
npm install -D void
npx void init
```

The setup wizard offers a toolchain, a UI framework, a starter, and a deployment target.

For this tutorial, choose:

- **Vite** as the toolchain, so the generated scripts use Vite directly.
- **React** as the Pages framework.
- **Static Pages** as the starter. We'll add our own database schema below.
- **Cloudflare** to deploy to your account, or **Skip deployment setup** to work locally first.

Let setup install dependencies. If it offers Git initialization, accept it for a new project.

Vite+ is the wizard's default toolchain. You can keep that option if you prefer; the `npm run dev` and `npm run build` commands below still work through the generated scripts. You don't need a global Vite+ installation.

The [quickstart](https://void.cloud/guide/quickstart) also covers pnpm, Bun, and adding Void to an existing app.

## Understand the project files

The React starter creates a page and the Vite configuration. We'll add the database and server files until the relevant part of the project looks like this:

```text
reading-list/
  db/
    schema.ts
    migrations/
  middleware/
    01.logger.ts
  pages/
    index.server.ts
    index.tsx
  routes/
    api/
      health.ts
      articles/
        index.ts
        [id].ts
  vite.config.ts
  void.config.ts
  tsconfig.json
  package.json
```

The examples use Void's default project-root directories. If you configure `sourceDir: 'src'`, those directories move under `src/`. Pick one arrangement and keep it consistent.

The starter already wires up the plugins. For reference, this is a complete minimal `vite.config.ts` for our React app:

```ts
import { defineConfig } from 'vite'
import { voidPlugin } from 'void'
import { voidReact } from '@void/react/plugin'

export default defineConfig({
  plugins: [voidPlugin(), voidReact()],
})
```

`voidPlugin()` handles Void's server integration. `voidReact()` supplies its React adapter and the React build integration. You don't need to add a second React Vite plugin.

Keep the generated `void.config.ts`. It holds application and Cloudflare settings; `vite.config.ts` holds build-plugin configuration. Void's [project structure reference](https://void.cloud/reference/structure) explains the other directories as you need them.

## Define a D1 database

Our reading list needs an article title and a URL. We'll use **D1**, Cloudflare's managed SQLite database, because it also runs locally without installing a separate database server.

Create `db/schema.ts`:

```ts
import { integer, sqliteTable, text } from 'void/schema-d1'
import { createInsertSchema } from 'void/drizzle-zod'

export const articles = sqliteTable('articles', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  url: text('url').notNull(),
})

export const insertArticleSchema = createInsertSchema(articles, {
  title: (schema) => schema.trim().min(1).max(200),
  url: (schema) => schema.url().max(2000).regex(/^https?:\/\//),
}).pick({ title: true, url: true })
```

Void includes Drizzle's schema helpers and query APIs. You don't need a separate Drizzle installation for this example.

The table defines the stored data. `insertArticleSchema` defines the input we accept from a browser. The ID is generated by the database, so the validator accepts only `title` and `url`.

A title containing only spaces fails validation. URLs must be valid HTTP or HTTPS URLs. That last restriction also prevents someone from storing a `javascript:` URL that we later render as a link.

TypeScript helps us while writing code, but requests can come from any client. The server still needs to validate them.

My [Drizzle introduction](https://flaviocopes.com/drizzle/) explains the table syntax. Void's [database guide](https://void.cloud/guide/database) covers its schema aliases and query integration.

## Generate and apply migrations

A schema file describes the database we want. A **migration** contains the SQL changes needed to create or update it.

Generate the first migration, then apply it locally:

```bash
npx void db generate
npx void db migrate
```

Void writes SQL and migration metadata under `db/migrations/`. Commit the whole generated migration history along with your schema.

For this first migration, read the SQL and confirm that it creates the `articles` table with the expected columns. Later migrations deserve more care, particularly if they rename or remove columns containing real data.

You can inspect local rows with:

```bash
npx void db execute 'SELECT * FROM articles'
```

Void also offers `db push` for quickly applying a schema locally without generating migrations. That is useful while experimenting, but deployment needs checked-in migrations. We'll use `generate` and `migrate` throughout this tutorial.

Be careful with `db seed`: it resets the development database before loading seed data. It is not a command for appending a few example rows to an existing database. The [database CLI reference](https://void.cloud/reference/cli/database) describes which commands reset data and which preserve it.

## Load articles on the server

A Void page can have a companion `.server.ts` file. Its **loader** runs on the server and returns data for the UI.

Create `pages/index.server.ts`:

```ts
import { articles, insertArticleSchema } from '@schema'
import { defineHandler, type InferProps } from 'void'
import { db, desc } from 'void/db'

export const loader = defineHandler(async () => {
  const rows = await db
    .select()
    .from(articles)
    .orderBy(desc(articles.id))
    .limit(100)

  return { articles: rows }
})

export const action = defineHandler.withValidator({
  body: insertArticleSchema,
})(async (_c, { body }) => {
  await db.insert(articles).values(body)
})

export type Props = InferProps<typeof loader>
```

`@schema` is an alias Void creates for `db/schema.ts`. `db` is the configured database client.

The loader returns the latest 100 articles. The `action` handles form submissions and inserts a validated article. After a successful action that returns nothing, Void runs the loader again and refreshes the page data.

`InferProps` derives the page's props from the loader. We don't maintain a separate interface that can drift away from the database query. The browser imports that type with `import type`, which doesn't pull the database code into its JavaScript bundle.

The [loader documentation](https://void.cloud/guide/pages-routing/loaders) explains the same pattern for Vue, Svelte, and Solid, including loading slow data separately.

## Build the page and form

Now we need a page that displays those articles and submits new ones. Replace `pages/index.tsx` with this complete file:

```tsx
import { Form, useForm } from '@void/react'
import type { Props } from './index.server'

export default function ReadingList({ articles }: Props) {
  const form = useForm('/', { title: '', url: '' })

  return (
    <main>
      <h1>Reading list</h1>
      <p>Save articles you want to read later.</p>

      <Form form={form}>
        <p>
          <label>
            Title
            <input
              name="title"
              value={form.data.title}
              onChange={(event) => form.setData('title', event.target.value)}
              required
              maxLength={200}
            />
          </label>
          {form.errors.title && <span role="alert">{form.errors.title}</span>}
        </p>

        <p>
          <label>
            URL
            <input
              name="url"
              type="url"
              value={form.data.url}
              onChange={(event) => form.setData('url', event.target.value)}
              required
              maxLength={2000}
            />
          </label>
          {form.errors.url && <span role="alert">{form.errors.url}</span>}
        </p>

        <button type="submit" disabled={form.pending}>
          {form.pending ? 'Saving...' : 'Save article'}
        </button>
        {form.recentlySuccessful && <p role="status">Article saved.</p>}
      </Form>

      {articles.length === 0 ? (
        <p>No articles saved yet.</p>
      ) : (
        <ul>
          {articles.map((article) => (
            <li key={article.id}>
              <a href={article.url}>{article.title}</a>
            </li>
          ))}
        </ul>
      )}
    </main>
  )
}
```

`useForm()` knows the input shape from the page action. `Form` submits it and displays action errors. We show field errors beside the relevant inputs and disable the button during submission.

The browser's `required` and `type="url"` checks help the person filling in the form. The server validator remains responsible for deciding what gets stored.

Void renders the page on the server initially. Its browser integration handles interaction and updates after actions. We didn't write a `useEffect()` to fetch the initial list or a separate state container to hold a copy of it.

The form keeps its values after submission. That lets you correct a failed request without retyping everything. You can add a reset after a successful save later if that fits your interface.

Read [Actions & Forms](https://void.cloud/guide/pages-routing/actions-and-forms) for named actions, redirects, and the form API.

## Run the application

Start development:

```bash
npm run dev
```

Open `http://localhost:5173`. You should see the empty reading list.

Save an article using these values:

```text
Title: Hono: a modern web framework for JavaScript
URL: https://flaviocopes.com/hono/
```

The new link should appear without a full page reload. Reload the page yourself and confirm that it is still there. That checks that the data reached the database.

Try a title containing only spaces. The server should reject it and show a field error. Then correct the title and submit again.

The list deliberately belongs to the whole app. We haven't added user accounts or private lists. Before deploying a personal copy, we'll put a password gate in front of it.

## Add a JSON API

Page loaders are convenient for the UI, but you may also want an API for another client.

Create `routes/api/articles/index.ts`:

```ts
import { articles, insertArticleSchema } from '@schema'
import { defineHandler } from 'void'
import { db, desc } from 'void/db'

export const GET = defineHandler(async () => {
  return db
    .select()
    .from(articles)
    .orderBy(desc(articles.id))
    .limit(100)
})

export const POST = defineHandler.withValidator({
  body: insertArticleSchema,
})(async (c, { body }) => {
  const [article] = await db.insert(articles).values(body).returning()
  return c.json(article, 201)
})
```

This exposes `GET /api/articles` and `POST /api/articles`. The POST route uses the same validator as our page action and returns the created row with status `201`.

While the app is still running locally without the password gate, test it from another terminal:

```bash
curl http://localhost:5173/api/articles
```

Create another article:

```bash
curl -i http://localhost:5173/api/articles \
  -H 'Content-Type: application/json' \
  -d '{"title":"Drizzle ORM: an introduction","url":"https://flaviocopes.com/drizzle/"}'
```

Now send invalid input:

```bash
curl -i http://localhost:5173/api/articles \
  -H 'Content-Type: application/json' \
  -d '{"title":"","url":"invalid"}'
```

The invalid request should receive `400`. Reload the page and confirm that the valid API-created article appears there too.

The page and the API use the same table. Changes reach the page on its next loader run; this example doesn't push live updates into other open tabs.

Void's [server routing guide](https://void.cloud/guide/server-routing) covers method exports, parameter validation, and per-route middleware.

## Add a route parameter and a 404

A file named `[id].ts` creates a dynamic route. We'll use it to retrieve one article.

Create `routes/api/articles/[id].ts`:

```ts
import { articles } from '@schema'
import { HTTPException } from 'hono/http-exception'
import { defineHandler } from 'void'
import { db, eq } from 'void/db'

export const GET = defineHandler(async (c) => {
  const id = Number(c.req.param('id'))

  if (!Number.isSafeInteger(id) || id < 1) {
    throw new HTTPException(400, { message: 'Invalid article ID' })
  }

  const [article] = await db
    .select()
    .from(articles)
    .where(eq(articles.id, id))
    .limit(1)

  if (!article) {
    throw new HTTPException(404, { message: 'Article not found' })
  }

  return article
})
```

The brackets are a file convention. Inside the handler, `c.req.param('id')` is Hono's parameter API.

An invalid ID receives `400`, and a valid ID with no matching row receives `404`. We return the article only when it exists, so the successful response has a useful inferred type.

Test all three paths:

```bash
curl -i http://localhost:5173/api/articles/1
curl -i http://localhost:5173/api/articles/999999
curl -i http://localhost:5173/api/articles/banana
```

## Call the API with typed fetch

Void's browser client connects the route definitions to their callers.

For example, code inside a browser event handler can retrieve our articles like this:

```ts
import { fetch } from 'void/client'

const articles = await fetch('/api/articles')
const article = await fetch('/api/articles/:id', {
  params: { id: '1' },
})
```

Use the route pattern `:id` with `params` in the typed client. The actual HTTP request still goes to `/api/articles/1`.

This `fetch` returns parsed data. It doesn't return the native browser `Response`, so you don't call `.json()` on its result.

A POST call takes an object body and serializes it for you:

```ts
import { fetch } from 'void/client'

const article = await fetch('/api/articles', {
  method: 'POST',
  body: {
    title: 'Hono on Cloudflare Workers',
    url: 'https://flaviocopes.com/hono-cloudflare-workers/',
  },
})
```

These snippets illustrate an alternative client for the API; our page already uses a loader and action. Don't add a second submission path to the form.

Type checking catches misspelled routes, unsupported methods, and bodies that don't match the handler's validator. It cannot prove that a string contains a valid URL; the runtime validator checks that.

Non-success responses throw `FetchError`. Catch it to show a useful message:

```ts
import { fetch, FetchError } from 'void/client'

try {
  await fetch('/api/articles/:id', { params: { id: '999999' } })
} catch (error) {
  if (error instanceof FetchError && error.status === 404) {
    console.log('That article no longer exists')
  } else {
    throw error
  }
}
```

The [typed fetch reference](https://void.cloud/guide/typed-fetch) describes request options and error details.

## How Void discovers resources

We imported `db` from `void/db`, but never copied a D1 database ID into our route code.

Void scans supported source files for resource usage. It uses that information to supply local bindings and provision resources when deploying. Configuration lets you connect existing resources or override inference.

The common imports are:

| Import | Resource |
| --- | --- |
| `void/db` | D1 by default, or the configured SQL database |
| `void/kv` | Workers KV |
| `void/storage` | R2 object storage |
| `void/ai` | AI access |
| `void/queues` | Queue producers, alongside consumers in `queues/` |

Keep resource access in server code. A browser component needs an API or loader to reach it.

Inference also depends on where code lives and which file types Void scans. If you organize server code outside the usual directories, check [resource inference](https://void.cloud/reference/resource-inference) before assuming Void found everything.

The generated `.void-wrangler.jsonc` supports Cloudflare tooling. Keep it ignored. Commit `void.config.ts` and `void.lock.json` when generated; the lock records resource identities and migration history.

## Protect a personal deployment

A reading list with an unrestricted write API will accept submissions from anyone who can reach it. For a personal copy, a shared password gate is a useful first step.

Void exports a Basic Auth wrapper that keeps its reserved tooling endpoints available. Create `env.ts`:

```ts
import { defineEnv, string } from 'void/env'

export default defineEnv({
  BASIC_AUTH_USERNAME: string(),
  BASIC_AUTH_PASSWORD: string(),
})
```

Then create `middleware/02.basic-auth.ts`:

```ts
import { basicAuth } from 'void'
import { env } from 'void/env'

export default basicAuth({
  username: () => env.BASIC_AUTH_USERNAME,
  password: () => env.BASIC_AUTH_PASSWORD,
  realm: 'Reading list',
})
```

For local development, add a root `.env` file containing `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD`. Use a password manager to generate and store the password, and keep `.env` out of Git.

Restart development after adding these files, then open the app again. Your browser should ask for credentials. The gate also protects the article API. For authenticated curl tests, add `-u flavio` if you chose `flavio` as the username; curl will prompt for the password.

Because browsers remember Basic Auth credentials, also protect browser form submissions against requests from another site. Add `middleware/03.csrf.ts`:

```ts
import { csrf } from 'hono/csrf'
import { defineMiddleware } from 'void'

export default defineMiddleware(csrf())
```

Our form and API use the same origin, so this fits the app. If you later add cross-origin clients, review the origin policy deliberately.

A shared password doesn't provide individual accounts or per-user permissions. For a small personal tool it gives us a usable gate before putting the app online. The [global middleware documentation](https://void.cloud/guide/server-routing#global-middleware) covers this wrapper.

## Set production secrets

Void reads local development values from the root `.env`. It doesn't upload that file during deployment.

If you skipped deployment setup, connect your account now:

```bash
npx void connect --platform cloudflare
```

Set the gate credentials on the deployment target:

```bash
npx void secret put BASIC_AUTH_USERNAME
npx void secret put BASIC_AUTH_PASSWORD
```

Paste each value when prompted. Check local configuration with:

```bash
npx void env check
```

Values whose names start with `VITE_` are public build values. Browser JavaScript can contain them, so never put an API key or password there. Supply public production values through the build environment; Void ignores `.env` client values during builds.

The [environment guide](https://void.cloud/guide/env-vars) explains schema validation and how server secrets differ from public build settings.

## Build and deploy to Cloudflare

Before deploying, prepare Void's generated types and check the application:

```bash
npx void prepare
npx tsc --noEmit
npm run build
```

The generated `.void/` files supply route and page types. Run `prepare` after a fresh clone or before type checking in CI.

Review and commit your source files and generated migrations. Keep `.env`, `node_modules/`, `.void/`, and `dist/` ignored.

Deploy to your own account:

```bash
npx void deploy --platform cloudflare
```

Void handles the build, resource provisioning, pending database migrations, and publishing the app. It prompts you to sign in or choose an account when needed, then prints the deployed URL.

Open that URL, sign in through the password gate, and save an article. Reload the page and test the API as well.

Your local database rows stay local. Provisioning the production database and applying its migrations doesn't copy your development reading list into it.

The same application APIs work when deploying directly to Cloudflare or through a Void platform. Direct native apps can run within Workers Free quotas, but enabled products have their own availability and billing. A team platform has additional requirements. Check the [deployment guide](https://void.cloud/guide/deployment) before choosing a target.

## Logs, updates, and rollback

After the first deploy, the saved destination lets you deploy again with:

```bash
npx void deploy
```

For later schema changes, generate a migration, review it, and apply it locally before deploying. Void stops a deployment when it detects schema changes without migrations.

Use project commands to inspect the app:

```bash
npx void project status
npx void project logs
```

On a direct Cloudflare target, logs open a live tail. A Void platform can retain log history. Don't assume that a historical range filter works the same way on both targets.

If an update breaks the app, select an earlier deployment:

```bash
npx void project rollback
```

A rollback restores application code. It does **not** undo database migrations. If an update removed a column, older code that still queries it can fail after rollback. Plan schema changes so the current and previous application versions can coexist when possible.

The [project CLI reference](https://void.cloud/reference/cli/project) explains status, target-specific logs, and rollback behavior.

## Deploy from GitHub

Once your deployment target is saved, generate a workflow:

```bash
npx void init --github
```

Review the generated workflow before committing it. For direct Cloudflare deployment, add `CLOUDFLARE_API_TOKEN` as a GitHub Actions repository secret, scoped to the account and resources the app uses.

A fresh CI environment also needs generated types before running TypeScript checks. Put `void prepare` after dependency installation if your workflow checks types before deployment.

Team-platform CI has a different credential flow. A standard self-hosted platform can use a scoped project deploy token in your own workflow. Optional GitHub integrations can support other workflows, including OIDC. Follow the [deployment instructions for your target](https://void.cloud/guide/deployment#github) rather than copying a Cloudflare-token workflow into a team platform unchanged.

## Add real user accounts

If several people need private reading lists, replace the shared gate with individual authentication and ownership rules.

Void-managed authentication uses [Better Auth](https://www.better-auth.com/). Native Void apps on Cloudflare can enable it through auth imports or configuration.

For example, this complete `routes/api/profile.ts` requires a signed-in user:

```ts
import { defineHandler } from 'void'
import { requireAuth } from 'void/auth'

export const GET = defineHandler((c) => {
  const user = requireAuth(c)
  return { id: user.id, email: user.email }
})
```

The browser client exported as `auth` from `void/client` provides signup, sign-in, and sign-out methods. Importing that client can enable email/password authentication.

Adding `requireAuth()` alone doesn't make our existing list private per person. You would also add an owner ID to each article, filter reads by the authenticated user, and set ownership on the server during writes. Apply those rules to page loaders, actions, and API endpoints.

Generate migrations again after enabling auth. Direct Cloudflare deployments need Void's managed auth tables in the checked-in migrations.

This is a separate upgrade to the reading-list example. Follow the [authentication guide](https://void.cloud/guide/auth) for the complete setup. Void-managed auth currently applies to native Void apps on Cloudflare; framework integrations and other runtimes use Better Auth directly.

## Add storage and background work

The reading list only needs D1, but a larger app may need other resources.

For a cache or settings store, Void exposes [Workers KV](https://void.cloud/guide/kv). A server helper could store the preferred page size:

```ts
import { kv } from 'void/kv'

await kv.put('settings:page-size', 20)
const pageSize = await kv.get<number>('settings:page-size')
```

KV has eventual consistency. Keep records that need database queries and ownership checks in your database.

For uploaded files, use [R2 storage](https://void.cloud/guide/storage). This server-side operation writes an exported reading list:

```ts
import { storage } from 'void/storage'

await storage.put('exports/reading-list.json', JSON.stringify(articles), {
  httpMetadata: { contentType: 'application/json' },
})
```

Here `articles` would be the rows your handler queried. For per-user exports, choose keys and enforce access on the server.

[Queues](https://void.cloud/guide/queues) let you process work asynchronously. Consumers live in `queues/`, and `void/queues` gives producers a typed interface derived from those consumers. Design consumers to tolerate retries so a repeated delivery doesn't repeat a payment or send the same message twice.

[Cron jobs](https://void.cloud/guide/jobs) live in `crons/`. Each file exports a schedule and a handler. Local development doesn't fire schedules automatically; use the authenticated development trigger Void prints when the server starts.

Void also provides [AI APIs](https://void.cloud/guide/ai). Workers AI calls during development use a remote binding, so they require Cloudflare access and consume account allowance. Adding an AI import doesn't give you an offline model.

These are optional additions. Get the database workflow working before adding resources you don't need yet.

## Keep an existing framework

You can adopt Void without replacing an established app's rendering model.

Its [framework integrations](https://void.cloud/integrations/frameworks/overview) include React Router, TanStack Start, SvelteKit, Nuxt, and Astro. Setup details vary because each framework controls its own server build.

For Astro, Void sits in the Vite configuration alongside Astro's Cloudflare adapter. Runtime pages must opt out of prerendering, and the integration requires the documented AsyncLocalStorage compatibility flag. Follow the [Astro integration](https://void.cloud/integrations/frameworks/astro) for the adapter's generated-config path and runtime requirements.

Don't copy the native `pages/index.server.ts` pattern into an Astro application and expect Astro to discover it. Keep Astro pages and endpoints, then use the supported Void APIs inside their server code.

## Host a Void platform for a team

A team platform is useful when you want people to deploy apps without giving everyone Cloudflare account credentials.

An administrator installs the platform in the organization's Cloudflare account. Developers sign in through the platform and deploy to projects they can access. The administrator controls access and resource limits.

You need Workers for Platforms, R2, Analytics Engine, a dedicated hosted PostgreSQL database, and a supported login provider. A domain is the normal production setup; `workers.dev` is available for testing. The platform database stores platform records such as users and deployments. Individual applications can still use D1.

This is paid infrastructure. Workers for Platforms requires a paid plan, and external PostgreSQL and Cloudflare usage are separate costs. The SDK's open-source license doesn't pay for hosting.

Start with a plan that previews Cloudflare changes:

```bash
npx void platform install --plan
```

Review the resources and credential requirements. When ready, run the installer:

```bash
npx void platform install
```

The wizard guides you through account selection, domains, PostgreSQL, and login configuration. It needs credentials for managing the installation and separate runtime credentials for provisioning apps. Use the [prerequisites](https://void.cloud/guide/platform/installation/prerequisites) and [installation guide](https://void.cloud/guide/platform/installation/setup) for that setup.

After installation, developers connect using the platform API URL their administrator provides:

```bash
npx void connect https://api.your-team-domain.com
npx void deploy --platform void --project reading-list
```

Replace the URL with your actual platform URL. Sign in and create or link the project when prompted.

Be careful about the difference between the full source repository and a standard installation. The core platform supports CLI deployments, shared access, resources, and retained logs. The user dashboard and managed GitHub builds are optional services outside the standard core installation. Custom per-project domains are also unavailable in that core installation.

The [platform feature comparison](https://void.cloud/guide/self-hosted-platform) lists what each deployment target includes. Read it before promising a particular workflow to your team.

## Using Void with a coding agent

Void ships its Markdown documentation inside the installed package at `node_modules/void/docs/`. That gives an agent documentation matching the package in your app.

`void init` creates a small `AGENTS.md` for a new project and links skills for supported agents it detects. In an existing project it asks before changing the instructions and preserves text outside its own section.

I would give an agent a bounded task that names the files and the checks:

```text
Read node_modules/void/docs/guide/quickstart.md and the database guide.

Add an archive flag to our reading list. Keep the existing Hono middleware.
Validate the input on the server and generate a database migration.
Update the page loader and API so archived articles are hidden by default.

Run void prepare, TypeScript checks, and the production build.
Test archiving through the browser and check the API afterward.
Stop before deploying.
```

The generated route types help catch mistakes across files. The agent still needs to test runtime behavior, especially authorization and migrations.

Void's [coding-agent guide](https://void.cloud/integrations/agents) explains where the bundled skills and instructions go.

## What I would use Void for

I would try Void for a new internal application that needs a UI, a few server endpoints, and a database on Cloudflare. A reading list, a small inventory tool, or a review dashboard gives it a useful amount of work without requiring a big architecture.

Hono is a reason I would consider it. Request handlers and middleware stay familiar, while Void takes care of wiring them into the build and deployment.

I would start by deploying directly to my own Cloudflare account. Running a team platform makes sense once several people need a shared deployment environment and someone is responsible for maintaining it.

For this blog, which already builds with Astro and runs on Cloudflare Pages, I would need a specific reason to change the deployment setup. A new SDK is easier to evaluate in a new application.

## Limits to understand before choosing it

Void reduces configuration work, but your app still depends on the resources it uses. D1, KV, and R2 remain Cloudflare services with their own limits and behavior.

Void can build standalone servers for Node.js, Bun, or Deno. Those targets don't provide Cloudflare bindings such as D1, KV, R2, and Workers AI. Moving our D1 reading list to a VPS would require a database change. Hono's portability doesn't make every resource portable. See the [other-runtime guide](https://void.cloud/integrations/nodejs-bun-deno).

Keep the CLI, installed SDK, and adapters compatible when upgrading. Commit your dependency lockfile and read the migration guidance before updating a production app.

For a small API that only needs Hono and one binding, a direct [Hono Worker](https://flaviocopes.com/hono-cloudflare-workers/) may already be enough. Void earns its place when its page integration, typed APIs, and resource workflow save you work across the application.

Build the reading list locally first. Test an empty list, a valid save, validation failures, and a missing article. Then put the gate in place and repeat those checks at the deployed URL. If you want to learn the Cloudflare pieces underneath, the free [Cloudflare Workers course](https://flaviocopes.com/courses/cloudflare-workers/) builds an application from the platform APIs upward.
