Running Void without the Cloudflare lock-in

I wanted to build with Void without being stuck on one host. So I built OpenVoid, which runs Void apps on Docker, AWS or a plain server.

Read by an AI voice (ElevenLabs). Download MP3

I wanted to build apps with Void, and I didn’t want one company’s servers to be the only place I could run them.

Void is a framework from VoidZero, the company behind Vite.[1][2] Its README sums it up as “Build and deploy full-stack apps with Vite, running on Cloudflare Workers.”[3] You write routes and pages, and you import void/db, void/kv and void/storage for a database, a key-value store and file storage.[4] Void can already build for Node.js, Bun or Deno instead of Cloudflare.[5] On that Node target, though, the database, key-value and storage APIs are switched off, and those were the parts I wanted.[4]

I also don’t like building one-offs, so if I was going to write the glue to run one Void app on my own server, I wanted any Void app to be able to use it. So I built OpenVoid, which runs Void apps on your own infrastructure without forking or patching Void.[4]

Where the lock-in lives

Void doesn’t hide its Cloudflare parts, and its Node target keeps a list of seven imports it calls “Imports that require CF bindings and are not available in Node.js mode”: void/db, void/kv, void/auth, void/storage, void/ai, void/sandbox and void/env.[6] Import one of them on a Node target and the build stops with an error that ends with CF bindings require target: "cloudflare".[6]

Deploys work the same way, since Void’s deploy code knows exactly two targets, 'cloudflare' and 'void'.[7] Sign-in is blocked too: on a Node target, Void throws “Void-managed Better Auth is only supported in Cloudflare Void apps.”[8]

Then there’s Void’s hosted platform, whose API package has 495 TypeScript files, and 94 of them mention Durable Objects, KV, R2 or Hyperdrive.[9] A Durable Object is a Cloudflare Worker that “uniquely combines compute with storage.”[10] That part is a product built on Cloudflare, and I left it alone.

How OpenVoid plugs in

Void looks up its bindings in one place, a store on globalThis under Symbol.for('void.envContext.v1'), with a second store for each request under Symbol.for('void.runtimeRequestScope.v1').[11] OpenVoid fills those same two stores with its own bindings.[12]

It ships as a Vite plugin, and the one rule is that it goes before Void’s plugin:[13][14]

import { defineConfig } from 'vite';
import { voidPlugin } from 'void';
import { openvoid } from '@openvoid/node';

export default defineConfig({
  // openvoid() must come first: it resolves
  // void/db, void/kv and void/storage before
  // Void's Node target would reject them.
  plugins: [openvoid(), voidPlugin()],
});

Void rejects those imports in a check that runs early, so OpenVoid has to answer them before Void sees them.[13][6] For void/db, OpenVoid generates the database module itself at build time, using Drizzle over SQLite or PostgreSQL.[15][4]

The risky part is depending on names inside Void, and if a Void release renames those two symbols, one test fails first.[12][16] It starts with a negative control: outside OpenVoid, the real void package must refuse to read the key-value store.[16] Only then does it check that Void reads OpenVoid’s bindings.[16]

Pick your backends

On Cloudflare, these APIs sit on D1, Workers KV and R2.[5] With OpenVoid you choose what sits behind each one, using environment variables, so the same build runs anywhere:[4]

  • void/db: SQLite or PostgreSQL.[4]
  • void/kv: SQLite, Redis or DynamoDB.[4]
  • void/storage: files on disk or an S3-compatible service.[4]

D1, KV and R2

D1 is Cloudflare’s serverless database with SQLite’s SQL semantics, Workers KV stores data globally, and R2 stores unstructured data without egress fees.[17][18][19]

Your routes don’t change, and you keep writing void/db, void/kv and void/storage the way the Void docs show.[4]

Where it runs

Each place OpenVoid runs has a recipe, which is a tested way to run an app on one platform.[20] It lists the backend for each API, the files to copy and the commands to run.[20]

  • Docker: one container with SQLite and a volume, or Compose with PostgreSQL, Redis and SeaweedFS for storage.[21] Its test writes a note, replaces the container, and checks that the note is still there.[21]
  • AWS Lambda Web Functions: the database goes on PostgreSQL, the key-value store in DynamoDB and storage in S3.[22]
  • Plain Node.js: a normal process on a VM or your own server, kept alive by systemd.[23]

I asked for that AWS recipe as “AWS Web Workers,” but AWS calls them Lambda Web Functions, which launched on October 12 as a way for your code to handle HTTP requests directly.[24] In the same launch, AWS renamed every existing Lambda function to a Lambda Event Function.[24] So my name for it was wrong, but that week, so was everyone’s name for Lambda.

Web Functions run Node.js 24 on Arm64, and each environment handles up to 128 requests at once.[25] Your server listens on the address Lambda puts in AWS_LAMBDA_HTTP_ENDPOINT.[25] Since Web Functions only run on arm64, OpenVoid’s CI runs the packaged app on arm64 too.[26]

Sign-in and permissions

Sign-in was the hard part, because Void stops the build when a Node app imports its auth.[27][8] OpenVoid has its own sign-in instead, with the same function names, requireAuth, getUser and getSession.[27] Moving an app over means changing one import.[27]

I wanted my family’s own sign-in and Google and GitHub at the same time, so there are three modes, and all of them produce the same user.[27]

  • Proxy: a login proxy such as oauth2-proxy signs people in and sends who they are in headers.[28][29] The app only believes those headers when they carry a shared secret, since anyone who could reach the app directly could otherwise claim to be anyone.[28]
  • OIDC: OpenID Connect for any provider that speaks it, like Authelia, Keycloak or Entra, plus GitHub, with no Cloudflare account and no third-party auth service.[30]
  • Better Auth: email and password, plus Google, GitHub or any OIDC provider, with accounts in the app’s own database.[31][32]

On top of any of them, OpenFGA can answer questions like who may edit which document.[27][33] It’s a Cloud Native Computing Foundation project built on ideas from Google’s Zanzibar paper.[34]

How I know it works

The latest CI run on main passed all 97 unit and conformance tests.[35] It also passed every end-to-end scenario, from Docker Compose to a real sign-in through oauth2-proxy and the Lambda Web Functions check on arm64.[35] The sign-in tests use real Dex, oauth2-proxy and OpenFGA containers and sign in over HTTP.[26] Dex is an OpenID Connect provider that “Sits in front of LDAP, SAML, GitHub, Google and more.”[36]

What’s missing

  • void/ai, void/sandbox, queues, cron triggers, Durable Objects, void/live and WebSocket routes still only work on Cloudflare.[4]
  • The AWS recipe has been tested against the Web Functions contract but hasn’t been deployed to a real AWS account yet.[22][20]
  • Sign-in was tested end to end against Dex, and GitHub against a stand-in for its API.[20] Authelia, Keycloak, Zitadel, Google, Entra and Cognito follow each provider’s documentation but haven’t been run.[20]
  • The examples only use API routes, and pages and static site generation haven’t been tested with OpenVoid yet.[4]
  • @openvoid/node isn’t on npm yet, so you install it from the repo.[4][37] The plain openvoid name on npm is already taken by a one-file, 260-byte package.[38]

OpenVoid is on GitHub as blbaker/openvoid, under the MIT license.[39] If you like Void but want to choose where it runs, start with the recipe for wherever you already host things.

Sources

  1. VoidZero is joining Cloudflare (Cloudflare blog)
  2. VoidZero is Joining Cloudflare (VoidZero blog, by Evan You)
  3. voidzero-dev/void repository README
  4. OpenVoid: README.md (origin/main)
  5. Void docs: Node.js, Bun, and Deno
  6. Void: plugin-node-target.ts, CF_ONLY_IMPORTS and the node-target guard
  7. Void: deploy-core/src/target.ts, DeployTarget and DeployTargetAdapter
  8. Void: index.ts, the Void-managed Better Auth node-target block (line ~792)
  9. Void: measured size of platform/packages/api
  10. Cloudflare Durable Objects docs overview
  11. Void: runtime/env-raw.ts and runtime/env.ts, the globalThis env symbols
  12. OpenVoid: packages/node/src/scope.ts, the seam with Void's runtime
  13. OpenVoid: packages/node/src/plugin.ts, Vite plugin ordering before voidPlugin
  14. OpenVoid: examples/notes/vite.config.ts usage snippet
  15. OpenVoid: packages/node/src/codegen.ts, generating void/db at build time
  16. OpenVoid: packages/node/test/void-contract.test.ts
  17. Cloudflare D1 docs overview
  18. Cloudflare Workers KV docs overview
  19. Cloudflare R2 docs overview
  20. OpenVoid: recipes/README.md index and honesty notes
  21. OpenVoid: recipes/docker/README.md
  22. OpenVoid: recipes/aws-lambda-web-functions/README.md
  23. OpenVoid: recipes/node/README.md
  24. AWS Lambda announces Lambda Web Functions (AWS What's New)
  25. Announcing AWS Lambda Web Functions (AWS News Blog)
  26. OpenVoid: .github/workflows/ci.yml
  27. OpenVoid: docs/identity.md, why OpenVoid has its own auth
  28. OpenVoid: recipes/auth-proxy/README.md
  29. OAuth2 Proxy docs homepage
  30. OpenVoid: recipes/auth-oidc/README.md
  31. OpenVoid: recipes/auth-better-auth/README.md
  32. Better Auth homepage
  33. OpenVoid: recipes/authz-openfga/README.md
  34. OpenFGA homepage
  35. OpenVoid: latest successful CI run on main (39149603463)
  36. Dex homepage
  37. OpenVoid: npm registry check for @openvoid/node
  38. npm registry entry for the openvoid package
  39. OpenVoid: GitHub repo API (blbaker/openvoid)

About the author

Brandon Baker is a software engineer in Ames, Iowa. He builds OpenVoid and react-native-impeller.

hello@bbaker.dev

All writing