Skip to content

feat: turn waitlist members into invited signups#252

Merged
cameronapak merged 2 commits into
mainfrom
claude/wayfinder-151-251-fc2fe8
Jul 15, 2026
Merged

feat: turn waitlist members into invited signups#252
cameronapak merged 2 commits into
mainfrom
claude/wayfinder-151-251-fc2fe8

Conversation

@cameronapak

Copy link
Copy Markdown
Owner

Summary

Turns the alpha waitlist into invited signups: an admin mints per-email, single-use, email-bound invite codes from the waitlist, and each is redeemed once at signup by the address it was sent to. Keeps the existing INVITE_CODES shared secret as an admin/testing backdoor.

Fixes #251

Changes

  1. Signup now accepts a per-email invite code bound to the exact email being registered — or the shared INVITE_CODES value — and stays fail-closed otherwise.
  2. New admin-only POST /api/admin/invite (session + ADMIN_EMAILS, 404 to everyone else) mints codes and emails them through the one worker/email.ts seam; it lives on the Worker because the send_email binding only exists inside fetch.
  3. bun run invite drives that endpoint from the CLI — explicit addresses, --limit N, or --all pending waitlist rows.
  4. A redeemed code is single-use: signup validates the code without consuming it, then stamps redeemedAt only after the account is actually created, via an atomic conditional update.
  5. Migration 0007 adds the invites table (email-keyed, unique code).

Start here: change 4 — validation is in the Better Auth before hook, the stamp is in after (gated on success, so a code isn't burned when signup later fails).

Flow

graph LR
  Admin[Admin: bun run invite] --> EP[POST /api/admin/invite]
  EP --> DB[(invites)]
  EP --> Mail[Invite email]
  Mail --> Invitee
  Invitee --> Signup[/sign-up/email/]
  Signup --> Before[before: validate code]
  Before --> After[after: stamp redeemed]
  After --> DB
Loading

Breaking / Migration

Test plan

  • Gates green: typecheck (app + worker + test), bun test (639), lint, fmt:check, and migration 0007 applies cleanly to local D1.
  • Unit tests for code minting (12-char unambiguous alphabet, no collisions), email normalization, and invite-email contents.
  • Drove the full flow against a live local Worker + D1 (10/10): happy-path redeem, wrong-code and email-bound rejections, single-use (redeemedAt stamped), idempotent re-mint, admin route returns 404 for unauthenticated GET and POST; invite email fired through the real send_email binding.
  • /security-review (auth/signup-gate diff): no HIGH/MEDIUM findings.
  • Needs manual check: a real end-to-end invite email in prod (bun run invite with DOTFLOWY_ADMIN_EMAIL/_PASSWORD set) after deploy — the send path is not e2e-reachable.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@cameronapak, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 130585f3-3594-4b59-ad3b-e15ef3374ce3

📥 Commits

Reviewing files that changed from the base of the PR and between 2e2da77 and 229fb7e.

📒 Files selected for processing (9)
  • .changeset/witty-invites-convert.md
  • migrations/0007_create_invites.sql
  • package.json
  • scripts/invite.ts
  • worker/auth.ts
  • worker/index.ts
  • worker/invites.test.ts
  • worker/invites.ts
  • worker/wire.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wayfinder-151-251-fc2fe8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cameronapak and others added 2 commits July 13, 2026 09:07
Convert the waitlist into invited signups (ticket #251). A per-email,
email-bound, single-use invite code is minted + emailed from the waitlist by
an admin, and redeemed at /sign-up/email.

- migration 0007: `invites` table (email PK, unique code, sentAt, redeemedAt)
- worker/invites.ts: code minting, invite email, pending-waitlist selection,
  and the redeem validate/stamp split
- worker/auth.ts: /sign-up/email now accepts EITHER a per-email invite bound to
  the exact email OR the shared INVITE_CODES backdoor. Validate in `before`
  (no burn on later failure); stamp redeemedAt in `after` on success only, via a
  conditional UPDATE that can't double-burn
- POST /api/admin/invite (session + ADMIN_EMAILS, 404 for non-admins): mints +
  emails codes. Lives on the Worker because the send needs the send_email
  binding, which only exists in fetch
- scripts/invite.ts (`bun run invite`): thin admin-authenticated CLI driving
  the endpoint (explicit emails, --limit N, or --all)

Verified against a live local Worker + D1: happy-path redeem, wrong-code and
email-bound rejections, single-use (redeemedAt stamped), idempotent re-mint,
admin 404 for non-admins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cameronapak cameronapak force-pushed the claude/wayfinder-151-251-fc2fe8 branch from ae60d7d to 229fb7e Compare July 15, 2026 12:14
@cameronapak cameronapak merged commit 9d6c1aa into main Jul 15, 2026
2 checks passed
@cameronapak cameronapak deleted the claude/wayfinder-151-251-fc2fe8 branch July 15, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire waitlist→invite: per-email single-use codes + invite script

1 participant