Lnkbase Newsletter Docs

#OpenHive architecture

OpenHive is an open-source newsletter platform that runs entirely on Cloudflare: Workers (Hono + server-rendered JSX), D1 (SQLite), R2 (media), Durable Objects (real-time editing and live events), Email Sending (the send_email binding), Cron Triggers (jobs), and optional Workers AI. Payments use Stripe directly; without a Stripe key a built-in simulated checkout lets every flow run locally.

#Layout

src/index.tsx          entry: mounts modules, resolves publication sites, cron, email() (Email Routing) and queue() (Email Sending events)
src/types.ts           Env, Vars, Module contract
src/lib/*              shared core (see below). Do not change signatures; add new files instead.
src/ui/components.tsx  dashboard UI kit (Card, Table, Field, Btn, Modal, charts, flash/back helpers)
src/ui/layout.tsx      DashLayout (sidebar, ⌘K palette, ⌘J AI drawer), PlainLayout, NAV
src/ui/site.tsx        SiteLayout for publication websites (theme tokens, navbar, footer, modals, pop-ups), SubscribeForm
src/routes/<module>.tsx  one file per module (plus optional src/routes/<module>/*.tsx helpers)
public/static/*        app.css, app.js (window.OH), rules.js (rule builder), site.css, site.js, editor.js/.css …
migrations/*.sql       D1 schema (0001 = core). Modules add numbered migrations.

#Module contract

Each src/routes/<module>.tsx default-exports { dash?, site?, root? } (Hono apps typed Hono<AppEnv>):

appmounted atcontext available
dash/app/<module> (dashboard at /app, auth at /app/onboarding)c.get('user'), c.get('pub'), c.get('role'), c.get('pubs')
site/s/:slug and on custom domainsc.get('pub'), c.get('reader') ({email, subscriber, premium} or null)
root/none (public)

Rules:

#Roles

owner > admin > member > contributor (workspace-level rows have pub_id = '', publication-level rows override).

#Route ownership

Public site (/s/:slug/...):

Root:

Dashboard shared endpoints:

#Core library (src/lib)

filepurpose
db.tsone/all/run/val/insert/update/batch, cursor helpers
util.tsids, hashing, esc, formatting, CSV, form(c.req) body parser
auth.tspasswords, sessions, requireUser, requirePerm, can, reader sessions, single-use tokens (issueToken/consumeToken), audit
guard.tsemail validation (syntax, disposable, typo, MX), botScore, rateLimit, Turnstile, crawler classification, bot-click filter
email.tssendMail (binding or simulated outbox), suppression (global/pub/transactional, temporary/permanent), bounce classification, X-OpenHive-Feedback-ID, one-click unsubscribe headers, sendTransactional
inbound.tsDSN / ARF / mailto-unsubscribe parsing and the shared bounce + complaint application used by the Worker email() handler (Email Routing), queue() handler (Email Sending events) and /hooks/bounce, /hooks/fbl
render.tseditor doc → web HTML / table-based email HTML; merge tags; dynamic content; paywall; TOC; footnotes; emailLayout
sanitize.tsallowlist sanitizer that balances tags
sender.tsstartSend, batches, A/B + multivariate tests (subject, preview text, sender name, posts.alt_content body; normalizeAb), timed sends, cohort tests, warmup caps, deferred retries (26h), publishPost, sendTest, buildEmail
subscribers.tssubscribe (validation, double opt-in, referrals, lists, tags), unsubscribe, tags, fields, lists, referral rewards, engagement, signed unsubscribe tokens
segments.tsrule engine → SQL (compile, countMatching, listMatching, subscriberMatches, recalcSegment), FIELDS
catalog.tsruleCatalog(env, pubId) for the rule builder
automations.tsengine: triggers (several entry points per automation, OR'ed: triggerList/packTriggers), enroll, processRun, steps, polling triggers, publishDraft
automation-templates.ts45-template automation library (categories, real email copy, flow outline); templates whose trigger type isn't registered (e.g. cart_abandoned) are hidden
automation-webhooks.tsincoming-webhook entry point: POST /hooks/automations/<secret> (JSON or form; email/name/field paths) → subscribe + enroll
sendtime.tssend-time optimization (each subscriber's usual open hour) and delivery by time zone (subscribers.timezone from request.cf.timezone); sender.ts parks recipients as scheduled deliveries
events.tsemit() bus (activity log + listeners), WEBHOOK_EVENTS
webhooks.tssigned webhook delivery with retries; payload versions (WEBHOOK_VERSIONS, per-version upgrade transforms, buildPayload); every attempt stored in webhook_delivery_attempts; attemptDelivery for manual retry
jobs.tsenqueue, registerJob, registerPeriodic, runJobs, kick, logError
payments.tscreateCheckout, markPaid, onOrderPaid(kind, fn), applyOffer, Stripe helpers
media.tsstoreFile, importFromUrl, serveObject (range requests)
tracking.tsrecordOpen, recordClick, trackView, channelOf, logBot
ai.tsWorkers AI text / JSON / image / speech / transcription (graceful when not configured)
pub.tssettings(pub), siteUrl, appUrl, senderFor (applies the shared sender)
plans.tshosted-service plans: listPlans(env), DEFAULT_PLANS, LIMITS, workspace usage, planStatus, checkSend/assertCanSend (SendBlockedError), branding, 80% usage warning
platformbilling.tsplatform plan billing: changePlan (upgrade now / downgrade at period end), Stripe Checkout subscriptions or simulated checkout, handlePlatformStripeEvent, failed-payment grace, lifecycle job
safety.tsmulti-tenant sending safety: campaign auto-pause (bounces/complaints), account suspension, first-large-campaign approval, new-account ramp
commerce.tsstore sync: Shopify / WooCommerce / Stripe webhook verification + parsing, customers (consent-respecting), external orders, buyer tags, external_purchase + cart_abandoned enrollment, cart abandonment tick, {{cart.*}} merge tags (docs/COMMERCE.md)
commerce-rules.tssegment fields over store purchases (ext_*, cart_abandoned), used by segments.ts
transactional.tstransactional email API: sendTx (templates, merge vars, idempotency, tracking, caps), message log, templates
inbox.tsreply Inbox: per-delivery Reply-To (reply+<slug>.<delivery>@SHARED_FROM_DOMAIN), inbound reply storage, threaded replies
sharedsender.ts<slug>@SHARED_FROM_DOMAIN From address for publications without a verified domain; Reply-To fallback to the owner

#Editor document

Posts, templates, automation emails and website page sections share one JSON format: { "blocks": Block[] }, Block = { id, type, data }.

typedata
paragraph{html, align?} inline HTML (b, i, u, s, a, code, mark, sub, sup, <span data-footnote="…">)
heading{level: 1-4, html, align?}
list{ordered, items: html[]} · checklist {items: [{html, checked}]}
quote`{html, cite?, style: {variant: lineboxpull, border_color, border_width, bg, color, italic}}`
image{src, alt, title, caption(html), width(%), align, link, radius} · gallery `{images: [{src, alt, caption}], columns, layout: gridcarousel}`
button{label, url, align, bg, color} · divider `{style: linedotsstarsspace, color} · spacer {height}`
code{lang, code} · html {html} (sanitized; media allowed on web only)
embed{url, caption} — YouTube, X, TikTok, Instagram, Bluesky, Stocktwits, Spotify, Vimeo, Apple Podcasts, Loom
video / audio / attachment{src, poster?, caption?} / {src, title} / {url, name, size}
table{rows: html[][], header}
columns`{columns: [{blocks, width?}], valign: topmiddlebottom}`
section{blocks, style: {bg, bg_image, color, padding, radius, border_width, border_style, border_color, margin}}
dynamic`{mode: showhide, rule: {tiers?, segment_ids?, tags?, cohorts?, match: anyall}, blocks}`
paywall{} — everything after is premium
toc{} · subscribe {label} (web only)
poll / referral / recommendations / ad / podcast / product / event{poll_id} / {title} / {title, limit} / {opportunity_id or campaign_id or headline/body/url/image/cta} / {episode_id} / {product_id} / {event_id}

Merge tags: {{subscriber.first_name|there}}, {{subscriber.email}}, {{custom.<key>}}, {{referral_url}}, {{referral_count}}, {{publication.name}}, {{date}}.

#Editor client API (public/static/editor.js, owned by posts)

const ed = OHEditor.mount(containerEl, {
  doc,                    // initial {blocks}
  input: hiddenInputEl,   // optional: kept in sync with JSON
  onChange(doc) {},       // optional
  compact: false,         // true = smaller toolbar (automation emails, page sections)
  features: { paywall: true, dynamic: true, polls: true, ai: true, collab: false },
  catalog: {...},         // ruleCatalog for dynamic-content rules (optional)
});
ed.getDoc(); ed.setDoc(doc); ed.plainText(); ed.insertText(str); ed.destroy();

Include with <link rel="stylesheet" href="/static/editor.css"> and <script src="/static/editor.js" defer>.

#Events (webhooks + automations)

emit(env, {pubId, type, subscriberId?, refId?, data?}). Public webhook events are listed in WEBHOOK_EVENTS. Internal events used by automations: subscriber.welcome, tag.added, tag.removed, field.updated, email.opened, email.clicked, automation.enrolled, post.published.

#Background work

enqueue(env, kind, payload, runAt) + registerJob(kind, handler); periodic tasks via registerPeriodic(name, everyMs, fn). The minute cron runs periodic tasks then drains jobs. After enqueueing from a request call kick(c.env, c.executionCtx). Module job handlers must be registered at module import time (top level of the route file or a lib file it imports).

#Local development

npm install
npx wrangler d1 migrations apply openhive --local
npx wrangler dev            # http://localhost:8787

Emails are simulated locally (stored in the outbox table, visible in Settings → Email → Outbox). Cron: curl "http://localhost:8787/__scheduled" with wrangler dev --test-scheduled.