Files
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

1.7 KiB

API

Three endpoints, mounted by plugins/waitlist at /v1/waitlist/*.

All paths are relative to BASE_API_PREFIX (default /v1). The three endpoints below assume the default.

POST /v1/waitlist/join

Register a new entry, or return the existing one if (waitlist, email) already exists. Idempotent.

Request

{
  "waitlist": "my-product",
  "email": "[email protected]",
  "referrerCode": "Bcd6789F",
  "turnstileToken": "0.AAAA..."
}

waitlist is the slug or the record id. referrerCode and turnstileToken are optional.

Response

{
  "ok": true,
  "waitlist": "my-product",
  "email": "[email protected]",
  "refCode": "Jk9mNpQr",
  "rank": 42,
  "total": 1234,
  "referralCount": 0,
  "shareUrl": "?ref=Jk9mNpQr",
  "alreadyJoined": false
}

alreadyJoined: true means the email already existed; the response returns the existing entry unchanged.

Errors

  • 400 invalid email / disposable domain / missing fields / captcha failed
  • 404 waitlist slug not found
  • 429 per-IP rate limit exceeded
  • 500 storage error

GET /v1/waitlist/status?waitlist=&email=

Look up an entry's current rank.

Response

{
  "ok": true,
  "waitlist": "my-product",
  "email": "[email protected]",
  "refCode": "Jk9mNpQr",
  "rank": 42,
  "total": 1234,
  "aheadOf": 1192,
  "referralCount": 3,
  "shareUrl": "?ref=Jk9mNpQr"
}

GET /v1/waitlist/export?waitlist=

Admin CSV dump. Either:

  • Authorization: Bearer <WAITLIST_ADMIN_SECRET> header, or
  • a Base superuser session.

Response is text/csv with columns:

rank,email,refCode,referredBy,referralCount,createdAt

Rank semantics

Rank = 1 + count(entries with (referralCount > me) OR (referralCount = me AND createdAt < me)). Ties go to the earlier joiner.