mirror of
https://github.com/hanzo-apps/waitlist.git
synced 2026-10-30 14:48:32 +00:00
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
2.6 KiB
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
-
Storage — Two Base collections owned by the plugin.
waitlists—{ id, slug, name, createdAt, updatedAt }. One row per project.slugis 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
-
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.
-
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.