#Deploying OpenHive
OpenHive is a single Cloudflare Worker with a D1 database, an R2 bucket, two Durable Objects, the Email Sending binding and a minute cron. This guide takes you from zero to a live instance.
#1. Prerequisites
- A Cloudflare account and a domain on Cloudflare DNS (for sending email and hosting the app).
- Node.js 20 or newer.
- The OpenHive source code (
npm installinside the folder).
#2. Log in
npx wrangler login
On a server or CI, use an API token instead: create one at My Profile → API Tokens with the "Edit Cloudflare Workers" template plus D1 and R2 edit permissions, then export CLOUDFLARE_API_TOKEN=... and export CLOUDFLARE_ACCOUNT_ID=....
#3. Create resources
npx wrangler d1 create openhive
npx wrangler r2 bucket create openhive-media
Copy the printed database_id into wrangler.jsonc under d1_databases.
#4. Configure
Edit the vars block in wrangler.jsonc:
| Variable | Meaning |
|---|---|
APP_URL | Public URL of your instance, e.g. https://news.example.com (no trailing slash) |
MAIL_FROM | Default sender, e.g. newsletter@example.com (domain must be enabled for Email Sending) |
APP_NAME | Shown in transactional emails |
PLATFORM_FEE_PERCENT | Fee you take on sales when hosting other creators (default 0) |
PURCHASE_URL, PRICE_LABEL | Used by the sales landing page at / |
#5. Email sending
npx wrangler email sending enable example.com
npx wrangler email sending list
Cloudflare adds the SPF and DKIM records for you. Add a DMARC record (_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com") and read DELIVERABILITY.md before your first big send.
Each publication can add extra sender addresses (Settings → Email & senders) and its own sending domain (Settings → Domains), which shows the exact DNS records to add and checks them live.
#6. Database
npx wrangler d1 migrations apply openhive --remote
Run this again after every update.
#7. Deploy
npx wrangler deploy
Open APP_URL, sign up — the first account becomes the owner of its workspace. To mark yourself as the instance operator (ad-network review, platform settings), add your email to the SUPERADMIN_EMAILS variable.
#8. Domains
- App domain: Workers → openhive → Settings → Domains & Routes → Add Custom Domain (e.g.
news.example.com). - Publication domains: in the dashboard, Settings → Domains → add
blog.creator.com. Then add that hostname as another Custom Domain on the worker (or a routeblog.creator.com/*). OpenHive serves the right publication based on the hostname.
#9. Payments (optional)
Without Stripe keys, checkout runs in test mode (a simulated card page) so you can try every flow.
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
In the Stripe dashboard add a webhook endpoint https://YOUR_APP/hooks/stripe for checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed and invoice.paid. Creators connect their own Stripe account under Settings → Payments (Stripe Connect) so payouts go straight to them.
#Hosted service plans (Lnkbase Newsletter)
The same key also bills workspaces for their platform plan (Free / Starter / Pro / Scale — Settings → Plan & usage) as Stripe subscriptions on your own account (no Connect). Products and prices are created automatically on the first checkout (or paste existing price ids in Platform admin → Plans). Enable the customer portal in Stripe (Settings → Billing → Customer portal) so "Manage billing" works. Without a key, plan upgrades use a simulated checkout.
- Plans and limits:
DEFAULT_PLANS/LIMITSinsrc/lib/plans.ts, seeded intoplatform_plans(migration 0022) and editable in Platform admin (/app/admin,SUPERADMIN_EMAILSonly). SHARED_FROM_DOMAIN(var, e.g.news.example.com): publications without a verified domain send from<slug>@SHARED_FROM_DOMAIN. Enable it for Email Sending (npx wrangler email sending enable news.example.com), add DMARC, and route*@replies/bounces as in DELIVERABILITY.md. Defaults to theMAIL_FROMdomain.- The pricing page reads the catalog with
listPlans(env).
#10. AI features (optional)
Uncomment in wrangler.jsonc:
"ai": { "binding": "AI" },
and deploy. This enables the AI writer, AI images, the website builder's AI mode, audio versions of posts and podcast transcripts.
Podcast transcripts are automatic: every uploaded or imported episode is queued for transcription (Whisper @cf/openai/whisper-large-v3-turbo) as a background job. Long MP3s are split into ~2 MB frame-aligned byte ranges of the stored file and stitched back together (text + time-shifted WebVTT, published as podcast:transcript in the feed). Non-MP3 files over 20 MB are skipped with a note (upload an MP3 or a transcript file). Without the AI binding the job is marked skipped with an explanation on the episode page; nothing fails. "Regenerate transcript" on an episode re-runs it.
#11. Bot protection (optional)
Forms already use honeypots, timing checks, rate limits and email validation. To add Cloudflare Turnstile, create a widget and set TURNSTILE_SITE_KEY (var) and TURNSTILE_SECRET (secret).
#11b. Large live events (optional)
Webinars stream peer-to-peer from the host's browser by default, which suits small and medium rooms. For audiences in the thousands, create a Cloudflare Realtime SFU app (Cloudflare dashboard → Realtime → SFU) and either:
- per publication: Settings → Integrations → Cloudflare Realtime, paste the App ID and App Secret, or
- instance-wide:
npx wrangler secret put REALTIME_APP_IDandnpx wrangler secret put REALTIME_APP_TOKEN.
The host then publishes once to Cloudflare's edge and every attendee pulls from the nearest location. The App Secret never reaches browsers — signaling goes through /live/:eventId/sfu.
#11c. Webinar recordings
Webinars record automatically when the host goes live (event setting Record automatically, on by default). The studio's browser uploads a chunk every few seconds to R2 (<pub>/private/recordings/<id>/NNNNNN.part); when the host ends the event — or 5 minutes after the browser disappears — a background job stitches the chunks into one WebM with an R2 multipart upload and sets it as the replay. Recording runs in the host's browser: Cloudflare Realtime's SFU API has no server-side recording (that exists only in RealtimeKit, which OpenHive doesn't use), so a host who never opens the studio produces no recording.
#11d. Discord verified linking (optional)
Members link Discord for tier-based roles. With an OAuth2 app configured they click Connect with Discord (scope identify) and their account is verified; otherwise they type a user ID, which is shown as unverified.
- In the Discord developer portal → your application → OAuth2, add the redirect URL
https://<APP_URL host>/oauth/discord/callback(one URL for every publication). - Either per publication — Community → Settings → Discord → OAuth2 client ID / secret — or instance-wide:
npx wrangler secret put DISCORD_CLIENT_ID
npx wrangler secret put DISCORD_CLIENT_SECRET
Role sync itself still needs the bot token and server ID in Community → Settings. DISCORD_API_BASE exists only to point the token exchange at a mock server in tests.
#11e. Stored credentials: APP_SECRET (required for social posting and SSO)
Social account credentials (Bluesky app passwords, Mastodon access tokens) and Enterprise SSO client secrets are encrypted at rest with AES-256-GCM, using a key derived (HKDF-SHA-256) from APP_SECRET. Set it once to any long random string and never change it (changing it makes stored credentials unreadable; reconnect the accounts if you must rotate):
openssl rand -base64 48 | npx wrangler secret put APP_SECRET
Without it the dashboard refuses to store credentials (only on localhost a development key is used).
#11f. Marketing channels
- Social posts (Marketing → Social posts): Bluesky connects with a handle + app password (AT Protocol
createSession/uploadBlob/createRecord, link cards with image thumbs); Mastodon with an instance URL + access token (scopesread:accounts write:statuses write:media). LinkedIn, X, Threads and Facebook are share-intent links only — their posting APIs need app review or paid access. Scheduled posts run on the job queue; auto-share posts each newsletter issue when it is published. - Calendar and Campaigns need no setup. Campaign attribution uses
utm_campaign = <campaign slug>, added to links in campaign posts (draft/scheduled) and social links. - Ad audiences: hashed (SHA-256) customer lists for Google Ads Customer Match and Meta Custom Audiences, downloadable or pulled from a secret URL
https://<APP_URL host>/audiences/aud_….csv.
#11g. Single sign-on (optional)
Sign in with Google appears on the login and signup pages once both secrets are set. In Google Cloud Console → APIs & Services → Credentials, create an OAuth client ID (Web application) with the authorized redirect URI https://<APP_URL host>/sso/callback, then:
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
Accounts link by verified email. (GOOGLE_ISSUER exists only to point at a mock provider in tests.)
Enterprise SSO is per workspace (Settings → Workspace → Enterprise SSO): any OpenID Connect provider (Okta, Entra ID, Google Workspace, Auth0…), redirect URI https://<APP_URL host>/sso/callback. The workspace proves it owns the email domain with a DNS TXT record (_lnkbase-sso.<domain> = lnkbase-sso=<token>); public mail domains can't be claimed. After a successful Test sign-in, "Require SSO" refuses password, magic-link and Google login for that domain (platform superadmins excepted). New people from the domain are added to the workspace with the configured role on first sign-in.
#12. Cron and background work
triggers.crons runs every minute: scheduled posts, send batches, automation steps, deferred-mail retries, webhook retries, segment refreshes, warmup and blocklist checks. Nothing else to set up.
#Updating
Replace the source with the new version (keep your wrangler.jsonc), then:
npm install
npx wrangler d1 migrations apply openhive --remote
npx wrangler deploy
#Troubleshooting
- Emails not arriving — check Settings → Email → Outbox for errors.
E_SENDER_NOT_VERIFIEDmeans the sender domain isn't enabled for Email Sending. - Custom domain shows "Publication not found" — the hostname must be saved on the publication and attached to the worker.
- Logs —
npx wrangler tail, or Workers → openhive → Logs (observability is enabled).
#Instant demo (no Cloudflare login)
npx wrangler deploy --temporary -c wrangler.demo.jsonc --var EMAIL_MODE:simulate --var APP_URL:<the workers.dev URL it prints> deploys to a temporary Cloudflare preview account (claim it within 60 minutes from the URL Wrangler prints to keep it). Preview accounts have no R2 and no cron triggers: OpenHive then stores uploads in D1 (src/lib/mediashim.ts) and runs background jobs from incoming traffic, at most once a minute.
#Optional extras added in October 2026
- Image resizing: the
imagesbinding inwrangler.jsonc(Cloudflare Images) resizes imported and pasted images to 1,600px WebP. Remove the binding to keep originals. - Large webinars: create a Realtime SFU app (Cloudflare dashboard → Realtime, or
POST /accounts/:id/calls/apps), thenwrangler secret put REALTIME_APP_IDandwrangler secret put REALTIME_APP_TOKEN. - "Send me a sample" page (
/try): demo only. SetTRY_IT=1(thedeploy:demoscript does) to let visitors get one real issue of the publication named byTRY_PUB_SLUG(defaultdemo). Capped byTRY_DAILY_CAP(default 100 a day), one per address a day, three per visitor an hour.