mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
93e8b5dfcf
* Add Plannotator Workspaces waitlist (page + worker + CI) Marketing - New /workspaces/ page: hero+form sibling layout on desktop; mobile reorders to hero -> form -> info -> banners so the CTA is reachable on the first scroll. Uses the marketing site tokens/fonts and the existing border-x editorial column. Theme-aware via the cookie. - Adds a "head" slot to Base.astro for per-page <head> additions. - Nav: new "Workspaces" link with a "soon" pill. - Landing: small Workspaces CTA band before the final install CTA. Waitlist worker (apps/waitlist-service) - Cloudflare Worker + D1 (SQLite). Mirrors the paste-service layout. - Endpoints: POST /signup (Turnstile-protected, rate-limited, honeypot), GET /health, GET /admin/count, GET /admin/list gated by Bearer ADMIN_TOKEN. - Validation: required email + company + team_size; optional note + is_contributor; honeypot field; freemail-aware company auto-inference. - D1 schema includes a non-destructive 001 migration that adds note and is_contributor columns. 19/19 validation unit tests pass; tsc clean. CI - Adds a deploy-waitlist job to .github/workflows/deploy.yml mirroring paste-service. Uses the existing CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets, so no new repo config is required. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Force light mode on /workspaces/ The page is designed and signed off in light. Override the saved theme cookie via a head-slot inline script that adds the `.light` class before paint. Base.astro's cookie reader only adds the class (never removes), so any saved-dark preference can't undo this. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@plannotator/waitlist-service
Cloudflare Worker + D1 backend for the Plannotator Workspaces waitlist signup
form on plannotator.ai/workspaces/.
Layout
core/
cors.ts # CORS allowlist + headers
handler.ts # Routing + rate limit + signup handler
storage.ts # WaitlistStore interface + SignupRow type
validation.ts # Field normalization + freemail detection + honeypot
stores/
d1.ts # D1Database-backed WaitlistStore
targets/
cloudflare.ts # Worker entry point
schema.sql # D1 schema (waitlist + rate_limit tables)
wrangler.toml
Endpoints
| Method | Path | Notes |
|---|---|---|
| POST | /signup |
Public. Body: { name, email, company?, role?, team_size?, tools?, use_cases?, turnstile_token?, website? }. website is a honeypot. |
| GET | /health |
Public liveness probe. |
| GET | /admin/count |
Requires Authorization: Bearer $ADMIN_TOKEN. |
| GET | /admin/list?limit=500 |
Requires Authorization: Bearer $ADMIN_TOKEN. |
Defenses (all layered):
- Cloudflare Turnstile — when
TURNSTILE_SECRET_KEYis set, every signup must include a valid token verified viasiteverify. The marketing page renders the widget in invisible/interaction-only mode, so legitimate visitors see nothing. - Honeypot — a hidden
websitefield. If a bot fills it, we return a fake success without writing to D1. - Per-IP rate limit — 20 signups / UTC day, stored in the
rate_limittable.
If you skip step 1 (don't set the secret), Turnstile verification is bypassed and only steps 2 and 3 apply — useful for local development.
First-time setup
cd apps/waitlist-service
bun install # if you haven't already at the repo root
# 1. Create the D1 database.
bun run db:create
# Copy the printed `database_id` into wrangler.toml.
# 2. Apply the schema (remote = production D1 instance).
bun run db:migrate
# 3. Set the admin token used by /admin/* endpoints.
bunx wrangler secret put ADMIN_TOKEN
# 4. Create a Cloudflare Turnstile widget at
# https://dash.cloudflare.com/?to=/:account/turnstile and store the secret.
# Widget mode: "Managed". Appearance: "Interaction-only".
bunx wrangler secret put TURNSTILE_SECRET_KEY
# 5. Deploy.
bun run deploy
For local dev:
bun run db:migrate:local # apply schema to wrangler's local SQLite
bun run dev # localhost:8787
Exporting signups
bun run db:export | jq '.[0].results'
Or query directly:
bunx wrangler d1 execute plannotator-waitlist --remote \
--command="SELECT email, name, company, created_at FROM waitlist ORDER BY created_at DESC LIMIT 50"
Marketing site wiring
The Astro page reads two PUBLIC_* env vars at build time:
| Var | Default | What it does |
|---|---|---|
PUBLIC_WAITLIST_URL |
https://plannotator-waitlist.plannotator.workers.dev |
Base URL of this worker. |
PUBLIC_TURNSTILE_SITEKEY |
(unset) | Cloudflare Turnstile sitekey. When unset, the widget is not rendered and the worker (if also unconfigured) accepts signups without verification. |
Override at build time:
PUBLIC_WAITLIST_URL=https://your-worker.example.com \
PUBLIC_TURNSTILE_SITEKEY=0x4AAAAAAA… \
bun run --filter @plannotator/marketing build
For production, put these in your CI / Cloudflare Pages env. The Turnstile sitekey is safe to commit / expose; the secret key stays in the worker.