Why
During holiday breaks WXYC suspends the regular schedule and fills shows by signup. An inactive DJ who walks into the control room with no account cannot log their show at all — account creation is admin-only end to end, and no manager is on site.
This epic adds a passcode-gated self-signup path: a short code on a sticky note in the control room, which a DJ enters to create their own account at dj role. Every account made this way is flagged for manager review.
The governing constraint: availability, not secrecy
This feature runs during weeks when nobody is watching. Every control must fail toward "wait a few minutes," never "the control room is locked out until someone drives in."
That constraint is load-bearing and easy to violate. Three separate mechanisms had to be redesigned during planning because each converted a legitimate user's mistake into an outage:
- a per-IP rate limiter — every DJ shares one IP, the control-room computer, so three fumbled codes would lock the room out for 15 minutes
- an auto-revoking failure counter — a failed attempt matches no passcode row, so the only implementable version revoked every active code on a few wrong guesses from anywhere on the internet
- a use cap that counted typos — validation ran after the use was claimed, so 25 fumbled usernames would burn the code
Check any new counter or limit against this before adding one.
Design decisions worth knowing before you start
- The passcode is encrypted, not hashed (AES-256-GCM), because it is meant to be read back: a manager reveals the current code and phones it to a stranded DJ rather than rotating, since rotating under the two-active-row cap can invalidate the note the rest of the room is using. The key sits beside
DB_PASSWORD in the EC2 .env, so this protects dumps and snapshots, not host compromise.
disableSignUp: true stays on for both emailAndPassword and emailOTP. This is a separate endpoint that happens to create users, not better-auth's signup.
- Self-signed DJs get
dj immediately. member has flowsheet: ['read'] only, so a lower role could not do the thing they walked in to do. The review flag, not a reduced role, is what bounds the risk — and between signup and review, the passcode is sufficient to obtain flowsheet write access. That is the deliberate trade.
- Review state is three
auth_user columns with no derived boolean, and the predicate is exported once and shared between the roster filter and the badge, so the two cannot drift.
Sub-issues
Ship in dependency order. The first two define the schema and the module surface everything else consumes, so they land sequentially before anything fans out.
dj-site side
The frontend half is tracked at WXYC/dj-site#1356. Cross-repo dependencies are wired natively: dj-site's signup form is blocked by the endpoint issue here, its roster queue by the schema issue, its manager panel by the admin API issue.
Plan
Full plan: ~/Downloads/wxyc-station-passcode-signup-plan.md — to move into docs/plans/ once the first PR lands. It carries the reasoning behind each decision, the rejected alternatives, and the rollout stance.
Why
During holiday breaks WXYC suspends the regular schedule and fills shows by signup. An inactive DJ who walks into the control room with no account cannot log their show at all — account creation is admin-only end to end, and no manager is on site.
This epic adds a passcode-gated self-signup path: a short code on a sticky note in the control room, which a DJ enters to create their own account at
djrole. Every account made this way is flagged for manager review.The governing constraint: availability, not secrecy
This feature runs during weeks when nobody is watching. Every control must fail toward "wait a few minutes," never "the control room is locked out until someone drives in."
That constraint is load-bearing and easy to violate. Three separate mechanisms had to be redesigned during planning because each converted a legitimate user's mistake into an outage:
Check any new counter or limit against this before adding one.
Design decisions worth knowing before you start
DB_PASSWORDin the EC2.env, so this protects dumps and snapshots, not host compromise.disableSignUp: truestays on for bothemailAndPasswordandemailOTP. This is a separate endpoint that happens to create users, not better-auth's signup.djimmediately.memberhasflowsheet: ['read']only, so a lower role could not do the thing they walked in to do. The review flag, not a reduced role, is what bounds the risk — and between signup and review, the passcode is sufficient to obtain flowsheet write access. That is the deliberate trade.auth_usercolumns with no derived boolean, and the predicate is exported once and shared between the roster filter and the badge, so the two cannot drift.Sub-issues
Ship in dependency order. The first two define the schema and the module surface everything else consumes, so they land sequentially before anything fans out.
dj-site side
The frontend half is tracked at WXYC/dj-site#1356. Cross-repo dependencies are wired natively: dj-site's signup form is blocked by the endpoint issue here, its roster queue by the schema issue, its manager panel by the admin API issue.
Plan
Full plan:
~/Downloads/wxyc-station-passcode-signup-plan.md— to move intodocs/plans/once the first PR lands. It carries the reasoning behind each decision, the rejected alternatives, and the rollout stance.