Skip to content

Someone who signed up without an email can't add one later #393

Open

Description

@richshaw

Needs your OK: this adds an optional Email box to the "Your spot" page for someone who signed up without one, and emails them their link once they fill it in. Swap pipeline:proposed for pipeline:ready to have it built.

Follow-up to #347.

What happens

Since #378 an organizer can make the email optional. Someone who leaves it blank gets no emails at all. Their only ways back to their spot are the link shown after signing up and the os_commit cookie in the browser they used.

They can't add an email later:

  • The page. "Your spot" (src/app/s/[slug]/c/[id]/page.tsx and edit-form.tsx) changes name, notes and spots, or cancels. Without an email it reads "You're editing this as {name}." and offers nothing more.
  • The API. PATCH /api/commitments/[id] takes name, notes, quantity and swapToSlotId (CommitmentUpdateInputSchema in src/schemas/commitments.ts), nothing else.
  • The workaround. Sign up again with an email, then cancel the old spot. If the slot has filled meanwhile, they can't.

These are the people most likely to lose their link, and the ones a reminder would help most. #657 (proposed) would email a lost link to an address already on the signup, so it can't help someone who never gave one.

#378 left this out on purpose, because it is a new way to make the app send mail. The fix below meters it exactly as signing up is metered.

Fix, in outline

  • The box. On "Your spot", only when the participant has no email: "Email (optional)", starting blank, with the line "Add your email and we'll send you this link, and a reminder before your slot." Show the reminder part only where willSendReminder says the slot gets one, as the sign-up sheet does. editLimitsForCommitment already reads the slot's time and the signup's settings. Show the sheet's "Did you mean …?" hint (suggestEmail) under the box.
  • The request. CommitmentUpdateInputSchema gains an optional email, with the same rules as signing up. Blank or missing means no change. An email sent with swapToSlotId is refused as invalid_input: the page never sends both.
  • The service. In the same-slot path of updateOwnCommitment, after the commitment lock, lock the participant (for no key update; participant last, as everywhere else). Then:
    • Already has an email: refuse with invalid_input on email. This adds an email; it never changes one.
    • Another participant on this signup has that address: refuse with conflict on email, and the message "Someone on this signup already uses that email. If it's you, use the link we sent to it."
    • Otherwise: write both columns through participantEmailColumns, and record participant.email_added with recordActivity in the same transaction, payload { participantId, commitmentId } and no address.
    • A race on the address. A sign-up or another add with the same address can still hit the participants_signup_email unique index between the check and the write. Catch that one violation and return the same conflict, not a 500.
  • The email. Have the service report that it added an email. The PATCH route then calls notifyCommitmentCreated(db, id, token, { meterRecipient: true }) from after(), as a move does. No confirmation was ever sent for this commitment, so its once-only check lets this one through. meterRecipient charges the new address against commitmentPerEmail, as signing up does, and the request has already passed commitmentTokenOpsPerIp.
  • After the save. The page refreshes and reads "You're editing this as {name} ({address})." The saved message says "Saved. We're emailing your link to {address}."
  • Reminders and moved-slot emails need no change. notifiableRules in src/jobs/notify-rules.ts skips only a participant with no email.

Watch for

  • A "time changed" email on top of the confirmation. toldDifferentTime in src/jobs/notify-rules.ts counts from the commitment's creation when nobody was ever emailed. If the slot moved after they signed up, the slot-change scan (every 2 minutes) can pick them up once the address is saved. The confirmation records commitment.confirmation_sent, which should settle it. Pin it with a DB test: a slot moved before the email was added gets the confirmation and no change email.
  • The trust docs. The privacy page (src/app/(legal)/privacy/page.tsx) says someone who leaves the email blank gets no emails. Add that they can add one later from their link, and bump "Last updated". The "four kinds of email" count doesn't change: this sends the existing confirmation.
  • Help. "Let people sign up without an email" in src/help/articles/create-and-publish-a-signup.tsx should say people can add an email later from their link. Run the help-pages skill's scripts/affected-articles.mjs.
  • The page budget. /s/[slug]/c/[id] is capped at 110 kB of first-load script. suggestEmail is small, but check pnpm budgets.
  • Nearby work. On the edit page, spaces after a name count toward the 100-character limit #723 (ready) changes name handling on the same page. Open PR Centralize participant input limits in one module #706 edits edit-form.test.tsx, participant-fields.tsx and src/schemas/commitments.ts. The "You're in." confirmation doesn't say which email address it used #715 (ready) names the address on the "You're in." sheet and says the edit page can't change an email; that stays true, since this only adds one. Whichever lands second has a small merge.
  • Telemetry. Add participant.email_added to docs/telemetry.md.
  • CHANGELOG line.

Choices this makes

  • Add only, never change. Default: the box shows only without an email, and the service refuses to replace one. Changing an address raises more questions, and nobody has asked for it.
  • Anyone with the link can add one. Default: yes. The link can already cancel the spot, so adding an address gives its holder nothing new.
  • Refuse an address already on this signup, don't merge. Default: refuse, with the message above. It tells the holder of an edit link that the address is on the signup; the sign-up sheet already says as much for a slot that address holds. Merging two participants means moving sign-ups between them, which isn't worth it here.
  • No confirming the address first. Default: the confirmation email is the check. If it doesn't arrive, they still have their link, and the "Did you mean" hint catches common typos.
  • A reminder may follow within minutes. Default: accept. If the slot is within a day, the next reminder scan (every 10 minutes) sends one soon after the confirmation, because REMINDER_SETTLE_HOURS counts from when they signed up.
  • Words. Default: the label, the line under the box and the saved message above.

Done when

  • On a signup with the email optional, someone who signed up without one opens their link, adds an address, and gets the confirmation. Reminders and moved-slot emails then reach them.
  • DB tests in src/services/commitments.db.test.ts and src/app/api/commitments/[id]/route.db.test.ts:
    • the address is stored in both columns, with the activity row in the same transaction and no address in it
    • refused when the participant already has an email, and when another participant on the signup uses it, including the unique-index race, which must not be a 500
    • the confirmation is sent once and charged to the address
    • a slot moved before the add gets the confirmation and no change email
  • edit-form.test.tsx: the box shows only without an email, and the request carries it.
  • The privacy and help pages say it, and the privacy page's date is bumped.
  • pnpm lint && pnpm typecheck && pnpm knip && pnpm test, pnpm test:db, pnpm build && pnpm budgets and tests/e2e/edit-token.spec.ts pass.

Rewritten from the original issue for the pipeline, and checked against src/app/s/[slug]/c/[id]/page.tsx, edit-form.tsx, src/app/api/commitments/[id]/route.ts, src/schemas/commitments.ts, src/services/commitments.ts, src/email/notify.ts, src/jobs/notify-rules.ts and src/app/(legal)/privacy/page.tsx on main at 2d4fdfe. https://claude.ai/code/session_016YZ9SUWWJa1udHCjU5fUma

Activity

  1. changed the title [-]Let someone who signed up without an email add one later[/-] [+]Someone who signed up without an email can't add one later[/+] on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions