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

By

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

~~~

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. It includes an SDK, a deployment CLI, and a platform you can host for your team. The project lives at 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 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:

PartWhat it does
Void SDKRouting, server-rendered pages, typed resource APIs, and framework adapters
Void CLIProject setup, builds, provisioning, migrations, deployment, and operations
Void PlatformA 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:

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 explains which features apply to each setup.

Where Hono fits

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:

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:

npm install hono

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

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 covers helpers and middleware. If Hono itself is new to you, my free Hono course starts with routing and request handling.

Create a project

Install a current Node.js release that meets Void’s Node requirement. 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:

brew install node

On Omarchy, use its package command:

omarchy pkg add nodejs npm

Check what your terminal is using:

node --version
npm --version

Create an empty project and install Void locally:

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 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:

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:

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 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:

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 explains the table syntax. Void’s database guide 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:

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:

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 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:

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 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:

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 for named actions, redirects, and the form API.

Run the application

Start development:

npm run dev

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

Save an article using these values:

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:

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:

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

Create another article:

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:

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 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:

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:

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:

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:

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:

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 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:

ImportResource
void/dbD1 by default, or the configured SQL database
void/kvWorkers KV
void/storageR2 object storage
void/aiAI access
void/queuesQueue 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 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:

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

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

Then create middleware/02.basic-auth.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:

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 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:

npx void connect --platform cloudflare

Set the gate credentials on the deployment target:

npx void secret put BASIC_AUTH_USERNAME
npx void secret put BASIC_AUTH_PASSWORD

Paste each value when prompted. Check local configuration with:

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 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:

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:

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 before choosing a target.

Logs, updates, and rollback

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

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:

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:

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 explains status, target-specific logs, and rollback behavior.

Deploy from GitHub

Once your deployment target is saved, generate a workflow:

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 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. 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:

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 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. A server helper could store the preferred page size:

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. This server-side operation writes an exported reading list:

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 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 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. 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 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 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:

npx void platform install --plan

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

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 and installation guide for that setup.

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

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 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:

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 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.

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 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 builds an application from the platform APIs upward.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cloudflare: