Skip to content
Gaurav TiwariDocs

Deploy to Cloudflare Workers

Version 0.2.0 Updated November 15, 2026

What a Vendwick site needs (D1, a private R2 bucket, the encryption key, an email provider and a cron trigger), the bindings, and how to run the first deploy safely. Vendwick targets EmDash on Cloudflare Workers; it also runs on EmDash’s Node adapter with a local directory for private files, which is how the demo site and the smoke tests run.

Verified locally on workerd for 0.1.0, not on a deployed Worker. A local run of 0.1.0 with real workerd, D1 and R2 exercised the R2 private-files adapter, a release upload and a signed download, the npm endpoint behind Astro’s routing with a real pnpm client, the cron tick through the scheduled handler, refunds, a recurring renewal, the page-access gate and the bridge callback outside a request. That run hasn’t been repeated for 0.2.0’s new features (cart, refunds from the admin, shipping, webhooks, PDFs, the importer), and a deployed Worker with remote R2 hasn’t been tested, so treat your first Workers deploy as a test deploy.

What the site needs

  • D1: the EmDash database. Set it up the way EmDash’s Cloudflare guide says; Vendwick keeps its data in EmDash plugin storage and adds no tables of its own.
  • A private R2 bucket bound as VENDWICK_PRIVATE. Release packages and downloads live here, and since 0.2.0 also cached invoice PDFs and uploaded store import files. Never point it at your public media bucket.
  • EMDASH_ENCRYPTION_KEY as a Worker secret. EmDash uses it to encrypt plugin secrets (gateway keys, webhook secrets, the signing secret). Generate one with npx emdash secrets generate. Losing or changing it makes the stored secrets unreadable. When that happens, every request that needs one of them fails with "Stored secrets cannot be decrypted with the current EMDASH_ENCRYPTION_KEY" (code SECRETS_UNREADABLE, logged with the setting’s name), the Vendwick admin shows a red banner naming the settings, and commerce/health lists them under secrets.unreadable. The site keeps running. Start it with the key the secrets were saved with, or save the gateway keys again on the Payments page. Vendwick never replaces an unreadable signing secret by itself, because account and download links already sent depend on it.
  • An email provider plugin that gives plugins ctx.email. Without one, receipts, license keys, magic links and reminders wait in the mail queue instead of going out. The admin Store overview shows whether an email provider and scheduled tasks are available.
  • A Cron Trigger so EmDash’s scheduled handler runs. Vendwick schedules one task, tick, every 5 minutes (*/5 * * * *), and does its hourly and daily work from it. Without cron, the outbox only drains when requests kick it, and expiry, reminders and report repair never run.

Bindings

The R2 part of wrangler.jsonc:

{
  "r2_buckets": [
    { "binding": "VENDWICK_PRIVATE", "bucket_name": "your-private-bucket" }
  ],
  "triggers": { "crons": ["*/5 * * * *"] }
}

To use a different binding name, pass it to both sides:

vendwick({ privateFiles: { kind: "r2", binding: "MY_BUCKET" } })
vendwickSite({ privateFilesBinding: "MY_BUCKET" })

If the binding is missing, the store still sells, but release uploads, downloads and store imports fail, invoice PDFs are rendered on every request instead of cached, and the Store settings, General page shows no private file store.

On Node, use a directory instead, on both sides:

vendwick({ privateFiles: { kind: "node", directory: "./data/private" } })
vendwickSite({ privateFilesDirectory: "./data/private" })

On Node, rate limits key on the adapter’s clientAddress. Behind a reverse proxy that’s the proxy’s address unless the adapter is set up to report the visitor’s, and without one the x-forwarded-for-style headers Vendwick falls back to can be forged. See Rate limits.

Site URL and paths

Set siteUrl on vendwick() to your public origin. Cron jobs and emails have no request URL to work from, and provider return URLs must be absolute. If you change checkoutPath, portalBase or productBase, pass the same values to both vendwick() and vendwickSite(), or emails will link to the wrong pages.

The first deploy: migrate before the first request

A fresh D1 database needs every EmDash core migration before the site can serve anything. On a new database that can take minutes, and it must not run inside a live request: a Worker request that tries to migrate a fresh database can be cut off partway, and a cut-off run may leave the migration lock held.

Do this instead:

  1. Create the D1 database and the R2 bucket, and set EMDASH_ENCRYPTION_KEY.
  2. Run the migrations before any traffic reaches the site, in one of two ways:
    • Run npx emdash migrate against the remote D1 with a Cloudflare API token that has D1 edit access. npx emdash migrate --check and --status report what’s pending and whether a lock is held.
    • Or run the site locally as a Worker bound to the remote D1 and let it migrate from there.
  3. Deploy, and only then send the first request.

While a migration is running, don’t run wrangler secret put and don’t redeploy. Both restart the Worker and can cut the migration off mid-way. If a run was interrupted, npx emdash migrate --status shows the lock, and --release-lock or --take-over-lock clears it.

Run npx emdash migrate --check again after every EmDash upgrade.

After the first deploy

Work through the first-run checklist in the README: gateway keys and webhooks (see Payment Gateways: Stripe, PayPal, Razorpay and Polar), a test purchase in test mode, your first product and plan, and a release upload. /_vendwick/health answers when the companion is wired up, the admin Store overview shows whether scheduled tasks and email are available, and Store settings, General shows the private file store and the configured paths. The setup wizard (Setup guide on the Store overview) walks through the rest.

Share

Was this helpful?

Still stuck? Open a support ticket