npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@saastemly/voidcommerce

v0.24.0

Published

Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.

Readme

voidcommerce

Void, with a shop in it.

vc init          # void init, then a form for the shop
vc --help        # void's help, with vc's commands merged in
vc dev           # everything else is void's, verbatim

vc extends void; it replaces nothing

Every command vc does not name goes to void untouched — same arguments, same terminal, same exit code. The named ones run void's version and add vc's part after it:

| | void's part | then vc's | |---|---|---| | vc init | void init, when there is no Void app here yet | the shop form | | vc --help | void --help | a shop group merged into the Commands box | | vc init --help | void init --help | a second box, vc init | | vc --version | void's version | vc's | | vc dev, vc build, vc preview | — void has no such commands | the app's own script, where the app is |

vc init --github and void's other partial modes are void's alone. vc help db execute is void's. When void is not installed, vc says so and stops — except for help, which it prints alone and says why. dev, build and preview are the scripts void init writes into the app's package.json; vc only decides where they run — api/ from a monorepo root, .vc/app at a strict root.

The first question: one app, or api + frontend

| | where it runs | what vc init does | |---|---|---| | One app | one Void app on Cloudflare Workers at <domain> and www — the API and the generated storefront and panel together | void init here, then the form | | Monorepo | api/ on Workers at api.<domain> with the panel; frontend/ a static Void site on GitHub Pages at <domain> | void init in api/ (the D1 starter), void init in frontend/ (Static Pages), then the form | | Strict (experimental) | api.<domain> on Workers; the storefront on <domain> | no void init at all: the repository root IS the storefront, and the worker is generated under .vc/app |

The layout decides what the domain means, so it is asked before anything else, and it cannot change after init — moving files is not a regeneration. vc init --layout app|monorepo|strict skips the question for scripts.

The domain need not be a registrable apex. shop.example.com is a shop inside the zone example.com, and the two are different questions: the domain is where the shop answers, the zone is where records go and which zone Cloudflare needs for the worker's custom domain. The wizard asks for the zone only when the domain is not one. A subdomain has no www, so it does not get a route nobody would type.

Every layout puts the worker on a Cloudflare custom domain, which Cloudflare creates in its own zone — so the zone must be on Cloudflare, and DNS at Simply is refused with that reason rather than generating a shop with no hostname. Where Cloudflare also does the mail, there are no records left for the DNS plugin to write and the shop needs no DNS token at all.

In a monorepo the Pages host (<owner>.github.io) is required, the frontend gets the custom storefront kit, and a workflow publishes it: every push builds frontend/, prerenders it, and force-pushes the tree to a static branch with the shop's domain in CNAME. Pages serves that branch from /.

Strict: a monorepo turned inside out

A monorepo has api/ and frontend/ side by side. Strict has the same two halves and the same hosting, but only one of them is a directory you can see:

voidcommerce.json   the answers — the worker is generated from this
package.json        ONE package.json, shared by the storefront and the worker
pages/  lib/        the STOREFRONT. Yours, and what the repository looks like
vite.config.ts      its Vite config; void.json sets output: "static"
data/               the catalogue, pushed by vc import
content/            faqs.json and posts.json — launch content, snapshotted in
branding/           static assets, copied to public/branding/
migrations/         what production has already applied — see below
patches/            a void fix that has not shipped upstream
.env                local values, shared by both halves
.vc/app/            the WORKER. Generated, gitignored, nobody's to edit

So the repository is, in technicality, a frontend app that builds a backend app — and in practice a monorepo without the ceremony of being one. What you edit is the shop front; the API, the panel, the cron, the queue and the live stream are generated underneath it from the manifest and rewritten by vc generate. A hand edit to a generated app drifts from the thing it exists to describe, and strict is the mode that refuses to let it. If you need a file the manifest cannot produce, that is the signal to use the one-app layout.

Two branches come out of one push:

| branch | what is on it | who serves it | |---|---|---| | void-dist | the worker, as a self-contained Void app | Cloudflare Workers Builds | | frontend-static | the prerendered storefront | GitHub Pages, or Cloudflare Pages |

Locally, bun run dev is the shop — the worker with its panel — and bun run dev:storefront is the shop front, which calls it at http://localhost:5173. Start the worker first; the storefront takes the next free port.

At a strict root every Void command runs in the artifact: vc dev is void dev in .vc/app, generated first if it is not there. vc generate also runs void prepare and void db generate, so the app typechecks and carries its migrations. Migrations are the one artifact that must be committed: regenerated on a fresh clone they would get new names, and a deploy would try to apply them twice — so they live in migrations/ at the root and the app reaches them through a symlink.

Strict pins void to the version its vendored patch applies to. A strict project has been generated, typechecked and production-built against real packages; it has not yet been served or deployed end to end.

Push, and it is live

With everything on Cloudflare, a shop can reach production without a single credential in CI:

main ──push──▶ GitHub Actions: vc dist ──▶ void-dist branch ──▶ Cloudflare Workers Builds ──▶ live

main is the manifest and the data. The workflow regenerates the app and force-pushes a self-contained Void app to void-dist; Cloudflare builds that branch and deploys it, so it never needs to know voidcommerce exists. The generated DEPLOY.md lists the one-time setup, including the one genuinely non-obvious part: the API token Cloudflare generates for a build has no D1 permission, so a build that applies migrations needs a token you make.

Email is the same story. Cloudflare Email Service is reached through a Worker binding rather than an API key, and onboarding the sending domain writes its own SPF, DKIM and DMARC records — so mail costs no secret and no DNS work. Sending to real customers needs a Workers Paid plan.

Deploying

vc preflight                          # is the shop ready to advertise on?
vc deploy                             # preflight, then void deploy — the Void platform
vc deploy --cloudflare --provision    # first time: your own Cloudflare account, with wrangler
vc deploy --cloudflare                # every time after

vc deploy extends void deploy: vc's preflight runs first, then void's deploy exactly as you typed it, with void's login. The Void platform is in private beta; the pass-through is there for when it is not.

--cloudflare is the other road, and needs no Void account at all. It is Void's documented manual path, automated with wrangler:

  1. wrangler is installed and logged in (wrangler login, or CLOUDFLARE_API_TOKEN). When wrangler sees exactly one account, vc pins it in wrangler.jsonc for you; with several, it asks you to choose.
  2. Preflight. Every required key is either a worker secret (wrangler secret put) or a committed plaintext value; the worker's hostnames are the shop's. Not ready means no deploy, unless --force. On a first deploy the worker does not exist yet, so its secrets cannot be listed: preflight prints the wrangler secret put lines, and running them creates the draft worker.
  3. --provision creates the D1 database and the queue, and records them in wrangler.jsonc and voidcommerce.json — so a regenerated strict artifact carries the real ids.
  4. Build, then scrub. Void's build bakes every .env* value into the worker's vars as plaintext — a dev cron secret, unset for each credential — and a var shadows the secret of the same name. Every secret-class key and every unset is removed from the emitted config before it is uploaded; the worker reads those from its secrets.
  5. Migrate the remote D1 from the committed migrations, then wrangler deploy exactly the config that was scrubbed.

At a monorepo root vc deploy deploys api/; the frontend deploys itself from GitHub Actions on push. At a strict root it deploys the artifact.

The Cloudflare road has been exercised against a fake wrangler that answers like the real one, and its wrangler deploy invocation dry-run against a real build; it has not yet put a shop on a real account.

What vc init asks

void init asks three questions, which is right for Void — a Void app can be anything. A shop cannot be anything: it has a payment rail or it does not trade, it has email or nobody can sign in, it has a tax rate or it sells unlawfully. So vc init asks about all of it, in the order a person thinks about it:

| | you choose | |---|---| | Who signs in | magic link, email code, passkeys, Google/Apple/GitHub, password, username, phone, guest | | Account features | every official Better Auth plugin — 2FA, organizations, API keys, JWT, multi-session, … | | Catalogue | custom fields, personalization, inventory, bundles, price lists, translations, search | | Selling | channels, discounts, gift cards, loyalty, subscriptions, draft orders, returns, wishlists, reviews, customer groups, agentic commerce | | Operations | webhooks, workflows, files, marketplace | | Payment rail | Adyen or Stripe — one, never both | | Tax | declared rates, TaxJar, Avalara | | Carriers | Shippo, EasyPost, UPS, FedEx, DHL, USPS, Royal Mail, Aramex, DoorDash, Stuart | | ERP | Business Central | | Email | Cloudflare Email Service (a Worker binding, no API key) or Resend | | DNS, content | Cloudflare or Simply; blog and FAQ | | Interfaces | the generated storefront and panel (better-admin-ui, standard theming, one line to turn off), or the custom storefront kit (better-auth-ui + better-commerce-ui, hosted in your own app) |

Every choice carries a one-line hint written for the person choosing — "teams that share a shop account — a club ordering trophies as one buyer" — not a plugin name. Required choices are shown so you know what you are getting; recommended ones are ticked, because a shop that starts with reviews off has to remember to turn them on and never does.

The manifest

Answers are written to voidcommerce.json, and everything else is generated from it. Run vc init again to change your mind: the answers are pre-filled, regenerated files are rewritten, and files you own are kept.

That is the difference between a scaffolder and a generator. A wizard that writes auth.ts once and forgets what it asked is useless on day thirty when you want subscriptions and cannot remember which of forty checkboxes you ticked.

What gets generated

voidcommerce.json          the answers
auth.ts                    the plugin ring — every choice, with the hint as a comment
env.ts                     every key REQUIRED, none optional, none defaulted
.env.example               with `unset` for each secret
.env.production            plaintext values only
lib/deploy/requirements.ts what breaks without each key — read by `vc preflight`
wrangler.jsonc             the worker's custom domain, api.<domain>

And, written once then yours: .env, lib/domain.ts, lib/payment.ts, lib/notifications.ts, lib/erp.ts. They are what a shop edits when it grows past the wizard, so they are never overwritten.

A monorepo puts all of that under api/, and adds:

package.json                          workspaces: api, frontend; dev:api, dev:frontend, deploy …
.github/workflows/frontend-pages.yml  build frontend/, publish to the static branch, CNAME
frontend/lib/api.ts                   the one client: better-auth/react + commerceClient  (yours)
frontend/.env                         VITE_API_ORIGIN=http://localhost:5173                (yours)
frontend/.env.production              VITE_API_ORIGIN=https://api.<domain>
frontend/void.json                    target: node, output: static — merged into void's

Why the files are plain

Void's gen has no extensibility and its directory set is closed — it will not scan a plugins/ folder. So vc writes ordinary Void files a reader can see, the way rails generate writes into files you own, rather than hiding registrations in a virtual module. A vc project is a valid Void project with nothing magic in it, which is what would let Void absorb these generators one day as a copy rather than a negotiation.

The rules the wizard enforces

  • Nothing is optional. If a shop supports an integration, the deployment sets it up. env.ts has no .optional() and no defaults — a default is compiled into the worker's vars and shadows the real secret.
  • One integration per job. Adyen or Stripe, Cloudflare or Simply. Carrying both means every reader works out which half is live.
  • DNS at Simply is refused when the API is on api.<domain>. A Worker custom domain needs the zone on Cloudflare; the wizard says so rather than generating a configuration that resolves and then errors for every visitor.
  • The country's VAT is declared so a fresh deploy is never tax-free. For a country the wizard does not know, it writes TODO VERIFY rather than a guess.
  • The worker carries no catalogue. catalogImport is registered with no source; vc import pushes from data/catalog.json.
  • Passwords are explicitly off unless chosen, because Better Auth's default is off and a version bump could flip it.

The generated shop typechecks clean against the real packages.