Files
waitlist/docs/architecture.md
zeekay 44935451c0 rebrand: @hanzo/waitlist (monochromatic, brand-neutral, Base-backed)
Drops the Next.js + Upstash Redis stack. The viral waitlist now ships as
a brand-neutral React widget against a Hanzo Base Go plugin.

What changed
- packages/widget/: new @hanzo/waitlist — React component, framework-free
  fetch client, drop-in <hanzo-waitlist> custom element. Zero brand
  colors, zero logos, zero brand names. Consumer brings logo via `logo`
  prop or `<slot name="logo">`. Theming via --hw-* CSS variables with
  fallback chain at usage so ancestor selectors win cleanly.
- apps/web/: Next.js demo rebuilt monochrome. Five generic presets
  (Neutral, Preset A/B/C/D) prove brand swap works without per-brand
  builds. Demo's placeholder logo lives in the demo, not the package.
- apps/api/: removed. Backend logic now lives in
  ~/work/hanzo/base/plugins/waitlist (committed separately).
- packages/sdk/: removed (Preact + Vite). Replaced by packages/widget.
- docs/: collapsed to architecture / api / widget. Old planning docs
  moved to docs/archive/.
- Latest deps: Next 16, React 19.2, Vite 8, TS 5.9, vite-plugin-dts 5,
  @vitejs/plugin-react 6, eslint-config-next 16.

Build output
- ESM 6.76kB, CJS 5.22kB, IIFE 196.99kB / 62.65kB gzip
- Widget styles: 3.89kB / 1.18kB gzip
- Demo first-load: 106kB
2026-06-09 22:57:41 -07:00

2.6 KiB

Architecture

Three pieces. No more.

+------------------------------+        +-----------------------------+
|  @hanzo/waitlist (browser)   |  --->  |  Hanzo Base (Go server)     |
|  React widget / web element  |   /v1  |  plugin: plugins/waitlist   |
|  client.ts (fetch only)      |        |  SQLite (local-first)       |
+------------------------------+        +-----------------------------+
                                                       |
                                                       v
                                              encrypted CRDT sync
                                              (Base's normal behavior)

Layers

  1. Storage — Two Base collections owned by the plugin.

    • waitlists — { id, slug, name, createdAt, updatedAt }. One row per project. slug is unique and is what the widget posts.
    • waitlist_entries — { id, waitlist, email, refCode, referredBy, referralCount, createdAt }. Indexes:
      • UNIQUE (waitlist, email)
      • UNIQUE (waitlist, refCode)
      • (waitlist, referralCount DESC, createdAt ASC) — rank query
  2. Server logic — ~/work/hanzo/base/plugins/waitlist/.

    • Auto-creates the collections on bootstrap.
    • Mounts three endpoints under /v1/waitlist.
    • All mutating logic runs inside app.RunInTransaction. Referral counts increment atomically; duplicate registrations are idempotent.
    • Anti-abuse: Cloudflare Turnstile verify + in-memory sliding-window rate-limit (default 5/IP/hour) + disposable-domain blocklist.
  3. Client — packages/widget/ published as @hanzo/waitlist.

    • WaitlistClient (plain TS fetch — no React).
    • <Waitlist> (React component).
    • <hanzo-waitlist> (custom element bundle for zero-bundler use).
    • Monochromatic CSS variables only. No brand colors in the package.

Why these choices

  • Why Base instead of Redis: Base already gives us atomic SQL transactions, an auth/admin surface, a dashboard, IAM integration, CRDT replication, and chain-anchored audit. Re-adding Redis would be a parallel-truth source for one feature.
  • Why a Go plugin instead of plain Collection rules: rate-limit and Turnstile verification need code paths that don't fit cleanly in Base's record rules. Putting the logic in one Go file is simpler than splitting it across rules + hooks + a sidecar.
  • Why monochrome: the widget is consumed by many brands. Hardcoded brand colors would either look wrong on most of them, or force a per-brand build. CSS variables let each consumer override a handful of knobs.

Wire format

See api.md.