Lnkbase Newsletter Docs

#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

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

VariableMeaning
APP_URLPublic URL of your instance, e.g. https://news.example.com (no trailing slash)
MAIL_FROMDefault sender, e.g. newsletter@example.com (domain must be enabled for Email Sending)
APP_NAMEShown in transactional emails
PLATFORM_FEE_PERCENTFee you take on sales when hosting other creators (default 0)
PURCHASE_URL, PRICE_LABELUsed 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

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

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

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.

  1. In the Discord developer portal → your application → OAuth2, add the redirect URL https://<APP_URL host>/oauth/discord/callback (one URL for every publication).
  2. 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

#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

#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