🎟️ Signup Invite Codes β€” Complete Wireframe & Build Plan
Every screen in the invite flow, the states that do not exist yet, and the remaining work broken into phases and individually trackable tasks.
Built on the audit of 2026-09-14 and the four decisions Leong Sen Fong made on 2026-09-15. Screens marked NEW do not exist today β€” this document is the specification for building them.
Tora32026-09-15 8 must-haves shipped21 tasks remaining

The whole flow on one page

Three ways in, one door. The door is the gate; everything else is how a person reaches it and what happens when it says no.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Opens HOOP for the first β”‚ β”‚ time β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SIGN IN / SIGN UP β”‚ β”‚ email or Apple / β”‚ β”‚ Google β†’ OTP sent β”‚ β”‚ β†’ user enters it β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ OTP correct β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Existing account? β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ yes β”‚ β”‚ no β€” this is a new person β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Is the gate on? β”‚ β”‚ β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”˜ β”‚ no β”‚ β”‚ yes β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ ASK FOR THE INVITE CODE β”‚ β”‚ β”‚ β”‚ (Leong 09-15: this step comes β”‚ β”‚ β”‚ β”‚ AFTER sign in / sign up) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ has a code β”‚ no code β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ code valid?β”‚ β”‚ WAITING LIST β”‚ β”‚ β”‚ β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”˜ β”‚ join β†’ confirm β”‚ β”‚ β”‚ yes β”‚ β”‚ no β”‚ β†’ see position β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ └─────────── the OTP is β”‚ β”‚ β”‚ retry with β”‚ NOT destroyed β”‚ β”‚ β”‚ the same OTP β”‚ (shipped 09-14) β”‚ β”‚ β”‚ β•² └────────┴────── β•² β”‚ β•² β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β•² β”‚ Daily cap β˜…NEW β”‚ β•² β”‚ has a code? β†’ always in β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ no code? β†’ cap applies β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ in β”‚ β”‚ full β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLAIM the code β”‚ β”‚ TODAY IS FULL β˜…NEW β”‚ β”‚ create account β”‚ β”‚ "your code is safe" β”‚ β”‚ mark waitlist β”‚ β”‚ remind me tomorrow β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ join the waiting list β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ IN. "Sarah invited you" β”‚ β”‚ β˜…NEW attribution β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
The one ordering rule everything else follows: a code must never be burned by a door that was closed for some other reason. That is why the capacity check happens before the code is claimed rather than after, and why a wrong invite code no longer destroys the user's sign-in code.

Where the code question sits: after sign in / sign up, decided by Leong Sen Fong on 2026-09-15. An earlier draft of this document put it before the OTP; that was written to solve a problem which has since been fixed β€” see Decisions on record.

What the three sources are, and why they stay three

SourceWho holds itExpiresCosts a quotaRecords who invited
Admin batch mintYou, to hand out personallyOptional, you chooseNoNo β€” shows as HOOP
Member "Create invite & share"A member, for a friendNeverYes, 1 of 3Yes
Waiting-list invitationSent automatically by email7 daysNoNo β€” shows as HOOP

They are one kind of code in one table with one format and one validation path. Only three columns differ at creation. They stay separate because each needs one behaviour the others must not have: attribution and a quota; a hard expiry; or no known recipient at all.

Arriving β€” the new-user journey

Order decided by Leong Sen Fong on 2026-09-15: the invite code is asked for after the person signs in or signs up. They prove the email address first, then the door asks for a code. Existing members never see this step at all β€” the gate only runs when no account exists yet, verified at all three signup paths.

Two properties make this ordering safe, and both shipped on 2026-09-14. A wrong invite code no longer burns the sign-in code β€” invite mistakes have their own counter and the OTP is kept, so the person retries with the same one. And a failed door does not consume the invite code either. The only cost left is the 10-minute sign-in window; if someone spends longer than that finding a code, they simply request a fresh sign-in code.
Typed once. Once the code is accepted it is never asked for again β€” not after a restart, not after leaving this screen, not for a second attempt. Today it survives a "resend" but not an app restart, because it is only held in the login screen's memory. Task 1.5.
And once authenticated, they go where they were headed β€” never back to the code box. Today every login lands on Home, because no intended destination is remembered; and the queue that replays a tapped link gives up after 20 seconds, which no signup finishes inside. Task 1.6.
9:41●●●
Welcome to HOOP
Enter your email and we will send you a code to sign in.
you@example.com
Continue
Continue with Apple
Continue with Google
1 Β· Welcome β€” unchanged. The gate is invisible here on purpose; we do not announce a closed door before we know whether it applies.
9:41●●●
Check your email
We sent a 6-digit code to sarah@example.com
4 8 2  _ _ _
Verify
Resend code
Standard sign-in. Nothing about invites has been mentioned yet.
2 Β· Sign in / sign up β€” unchanged, and it stays first. Someone who already has an account goes straight in from here; the screens that follow only happen for a new person.
9:41●●●
HOOP is invite-only right now
Enter the code a friend gave you. Getting it wrong costs you nothing β€” your sign-in stays valid and you can try again.
HOOP-XXXX-XXXX
Continue
I don't have a code
Codes are not case-sensitive. Spaces and dashes are ignored.
3 Β· The door EXISTS β€” appears only for a new person when the gate is on. Today's wording blames the code; task 1.1 is now about making the three refusals below say the right thing, not about moving this screen.
9:41●●●
HOOP is invite-only right now
We filled in the code from the link you tapped.
HOOP-K7M2-9QPX
βœ“ Valid β€” Sarah Tan invited you
Continue
Use a different code
4 Β· Arrived by link NEW β€” the payoff for fixing the deep link (task 1.2). The inviter's name is shown here, not just at the end.
9:41●●●
HOOP is invite-only right now
Enter the code a friend gave you.
HOOP-K7M2-9QPZ
We don't recognise that code. Check the last few characters?
Try again
I don't have a code
Your sign-in code is still valid β€” this did not use it up.
5 Β· Wrong code 1.1 β€” until 2026-09-14, five mistakes here destroyed the user's sign-in code and the error blamed the OTP. The counter is now separate and the OTP survives; what is left is making this sentence helpful.
9:41●●●
That code has already been used
Someone has already joined with it. Each code works once.
HOOP-K7M2-9QPX
Use a different code
Join the waiting list
If a friend sent you this, ask them for a fresh one β€” they get a new invite each time one is used.
6 Β· Already used NEW WORDING β€” distinct from "invalid". Also covers the race where it is claimed two seconds before you (task 1.3).
9:41●●●
πŸšͺ
HOOP is full for today
We are letting in a limited number of new people each day while we grow carefully.
Your invite code has not been used. It will still work tomorrow.
Remind me tomorrow
Join the waiting list
Doors reopen at midnight (Malaysia time).
7 Β· Capacity reached NEW β€” task 3.3. Only reachable without a code: Leong decided on 2026-09-15 that invitations are always honoured. "Remind me tomorrow" is a local notification and needs no account.
9:41●●●
πŸŽ‰
You're in
Sarah Tan invited you to HOOP.
πŸ‘‹ Say hello to Sarah
She has been on HOOP since August
Set up my profile
8 Β· Arrival NEW β€” task 1.4. The invite tree already records who invited whom; nothing has ever shown it. This is the cheapest warmth in the whole feature.
Case that is broken today and has no screen: signing up with Apple or Google using "Hide My Email". The stored contact becomes apple:<sub> with no "@" in it, so the waiting-list row is never matched and that person stays queued forever even though they are inside. Task 4.5.

The waiting list β€” from dead end to the main marketing asset

Today this is a dead end. It takes an email address and says "we'll email you an invite as soon as a spot opens up". Until 2026-09-14 no code existed anywhere that could send that email. The sending now exists; everything the person waiting sees does not.
9:41●●●
Join the waiting list
We will email you an invite as soon as a spot opens up.
you@example.com
Join the list
We will only email you about your invite. Nothing else.
1 Β· Join β€” exists today, wording unchanged.
9:41●●●
πŸ“©
Confirm your email
We sent a link to sarah@example.com. Tap it and your place is secured.
Resend the link
Until you confirm, your place is not being held β€” this stops other people putting your address on the list.
2 Β· Confirm pending NEW β€” task 2.1. Without it, anyone can squat any address, and every position number is a lie.
9:41●●●
#340
You're on the list
You are number 340. We are letting people in steadily β€” we will email you the moment it is your turn.
πŸ”“ Skip the queue
If a friend is already on HOOP, ask them for an invite β€” it gets you in straight away.
Leave the list
3 Β· Position v2.237 β€” task 2.2, shipped 2026-09-15. The number is counted over the confirmed queue only, and it is the same ordering the invitations actually go out in. It appears on the confirmation page too, because that is the moment they most want it β€” and the confirm token dies on use, so it cannot be asked for later. The app remembers a read-only ticket, never the address: an endpoint that answers "is this address on the list?" for any address is a public enumerator.
9:41●●●
🎟️
Your invite is ready
We sent it to sarah@example.com.
HOOP-K7M2-9QPX
Use it now
⏳ Expires in 6 days. After that your place goes back in the queue where it was.
4 Β· Invited NEW β€” the in-app mirror of the email. The expiry sentence is the promise that makes the 7-day clock fair.
9:41●●●
βŒ›
That invite expired
We have put you back in the queue at your original place β€” you have not lost your spot.
#340
Keep me on the list
5 Β· Expired NEW β€” the back-end for this is already built and shipped (ReclaimExpired restores the original position). Only the screen is missing.

The queue lifecycle

joined ──confirm email──▢ queued ──admin invites──▢ invited ──signs up──▢ joined βœ“ β˜…NEW β”‚ β–² β”‚ β”‚ β”‚ β”‚ 7 days pass β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ back to the SAME position, once β”‚ └── second expiry ──▢ removed, with a way back

Built today: queued β†’ invited β†’ joined, and the expiry return. Not built: the confirmation step at the front, everything the waiting person can see, and the second-expiry removal.

Inviting a friend β€” the member's side

Decision on record (2026-09-15): a member holds 3 invites at a time, not 3 for life. When a friend joins on one, the slot returns and the member may create another. Today's code is "3 for life"; changing it is task 4.1.
9:41●●●
You have 2 invites left
Each one gets a friend straight in, no waiting list.
HOOP-K7M2-9QPX Joined
Daniel joined on 12 Sep Β· slot returned to you
HOOP-3XR8-TT4N Shared
Created 14 Sep Β· not used yet
Create invite & share
You get the invite back whenever one is used.
1 Β· My invites REWORKED β€” today this lists codes with no state at all. Per-invitation status is what makes 3-at-a-time legible.
9:41●●●
Share invite
I've got an invite for you to HOOP β€” my invite code is HOOP-3XR8-TT4N. Grab the app here: hoopcomm.com/i/HOOP-3XR8-TT4N
WhatsApp
Copy
2 Β· Share sheet β€” works today. BUG the "Open HOOP" button on that landing page does nothing: no deep-link branch exists, and the one that would be added requires being logged in. Task 1.2.
9:41●●●
πŸŽ‰ Daniel joined HOOP
Your invite was used β€” here is another one to give away.
Create invite & share
Sent as a push notification, not just an in-app card.
3 Β· Invite used NEW β€” free re-engagement, and only possible under 3-at-a-time. Rewards exactly the behaviour you want more of.

Admin β€” what you operate it with

The first two screens below are live today (shipped 2026-09-14 and 2026-09-15). The capacity section is the new part.

9:41●●●
Invite-only signup
Invites per member3
44
Members
4
Codes
0
Waiting
Mint codes
Revoke
Waiting list β†’
Gate has never been switched on. Last changed 5 Aug β€” the day the row was created.
1 Β· Invites card LIVE β€” real production numbers shown. Revoke and the waiting-list button shipped this week.
9:41●●●
340
Waiting
12
Invited
96
Joined
Invite next 10
πŸ”Ž Search by email
amir@example.com Waiting
Joined 2 Sep Β· position 1
chen@example.com Invited
Sent 14 Sep Β· expires in 6 days
2 Β· Waiting list page LIVE β€” shipped v2.204. Counts double as filters. 2.4 still needs paging, it loads the whole queue today.
9:41●●●
Limit new signups daily
People per day1,000
3
Today
997
Left
0
Turned away
Resets at midnight, Malaysia time (in 7h 19m).
People with an invite code are always let in and are not counted against this limit.
3 Β· Capacity NEW β€” tasks 3.1 and 3.2. The warning box is not decoration: it is the rule Leong chose, written where the person changing the number will read it.
9:41●●●
⚠️ The gate fell open
The invite gate could not be read 3 times and signups were let through unchecked. Last error 14 Sep 09:12.
Let through unchecked
3 signups
Last known state
On
Dismiss
A database failure and a switched-off gate look identical from outside. This panel is the only thing that can tell them apart.
4 Β· Gate fell open LIVE β€” shipped 2026-09-14. 3.5 nothing alerts; someone has to look.

The emails

Email is the only channel that reaches someone who is not in the app yet, which makes it the entire waiting-list product.

HOOP <invites@hoopcomm.com>
Your HOOP invite is ready
A spot opened up 🎟️

You joined the HOOP waiting list β€” here is your invite code.

HOOP-K7M2-9QPX
Open HOOP

This invite expires in 7 days. If you do not use it, your place goes back in the queue where it was.

Invitation LIVE β€” shipped 2026-09-14, sends through Resend. BUG "Open HOOP" lands on the site but cannot hand the code to the app yet (task 1.2).
HOOP <invites@hoopcomm.com>
Confirm your place on the list
One tap and your place is held

Someone put this address on the HOOP waiting list. If it was you, confirm below.

Confirm my place

If it was not you, ignore this email and nothing happens.

Double opt-in NEW β€” task 2.1. The last line is the whole security property: silence is a safe answer.
HOOP <invites@hoopcomm.com>
Your invite expires tomorrow
⏳ One day left

Your HOOP invite expires tomorrow. After that your place goes back in the queue at the same position β€” you will not lose it.

HOOP-K7M2-9QPX
Use my invite
Expiry reminder NEW β€” task 2.3, sent at 48h and 12h. Without it the 7-day clock silently eats invitations.

Build plan β€” 4 phases, 21 tasks

Each task is one card on the workroom board, small enough to finish and verify on its own. Phases are ordered by what a real person hits first, not by what is easiest.

Already shipped and live (2026-09-14 β†’ 15): tests on the admission path Β· waiting-list invitations with email and expiry Β· the code-consumption window Β· invite mistakes no longer destroy the OTP Β· code revocation Β· gate-fell-open visibility Β· corrected architecture doc Β· rate limit on the check endpoint Β· the admin waiting-list page. That was the eight must-haves plus the operator UI.
Phase 1 Β· The arrival journey (6 tasks)
Everything an invited person touches between tapping a link and being inside. Highest value: this is the path every single invited user walks. The order of the steps is settled β€” sign in first, then the code (Leong, 2026-09-15).
TaskWhatWhy it is firstSize
1.1 v2.214Make the three refusals at the door say the right thing
(was: move the code question before the OTP β€” withdrawn on Leong's instruction, 2026-09-15)
The code step stays after sign in / sign up. What is still wrong is the wording: "invalid", "already used" and "someone just took it" are three different situations and the user is told the same unhelpful thing. Also surface that the sign-in code survives a mistake, because the person does not know that.S
1.2 v2.215hoop://invite deep link + a readiness rule that works logged-outThe share link's "Open HOOP" button has never done anything. The dispatcher has no invite branch, and it requires being logged in β€” which a person redeeming a signup code never is.M
1.3 v2.216Distinct message for "someone just used that code" A code valid at pre-check and claimed two seconds later currently says "invalid", which sends the user hunting for a typo that does not exist.S
1.4 v2.221Show who invited you, on arrival and at the gate The invite tree already records it and nothing has ever displayed it. Turns a coupon into an introduction.S
1.5 v2.223The code is typed once and never asked for again Leong's rule, 2026-09-15. Today the code lives only in the login screen's memory (_inviteCode): resending the sign-in code keeps it, but killing the app or leaving the screen loses it and the person types it again for no reason. Persist it locally until it is consumed. Pair with 1.2, which removes the typing entirely for anyone arriving by link.S
1.6 v2.224After authentication, land on the page they were going to Leong's rule, 2026-09-15. Two things block it today. There is no notion of an intended destination at all β€” every successful login does pushReplacement to Home (or the contacts priming screen on first run). And the deep-link replay queue gives up after 20 seconds (deep_links.dart: 50 ticks Γ— 400 ms), which is shorter than any signup that involves waiting for an email. So a link tapped before signing up is already gone by the time the person is authenticated.M
Phase 2 Β· The waiting list becomes real
The only place where "no" can be turned into an asset. Currently a dead end that collects addresses and says nothing back.
TaskWhatWhySize
2.1 v2.229Double opt-in β€” confirm the email before holding a place Anyone can put any address on the list today, so positions can be squatted and every position number would be a lie. Must land before 2.2.M
2.2 v2.237Show position, and the "ask a friend to skip the queue" card A number that moves is the most shareable thing in this feature, and it converts queue pressure into demand for member invites.M
2.3Expiry reminders at 48h and 12h, and the expired screen The 7-day clock already runs and already returns the place. Nobody is told, so invitations die quietly.S
2.4Admin list: paging, and funnel numbers The page loads the entire queue and filters in the app. Fine at 0 entries, wrong at 1,000.S
2.5BUG Bind a waiting-list invitation to the address it was sent to Decided 2026-09-15: tie it to the email. Today the code is not bound, so anyone holding the string can redeem it β€” and when someone else does, the person who waited is stranded forever: their row is never marked accepted, ReclaimExpired skips it (the code now has a used_contact), and the queue never draws them again. Binding fixes the stranding at its source. Read the warning below before building this β€” binding has one collision that must be handled in the same change.M
Phase 3 Β· Capacity
The daily cap. Built to Leong's rules: invitations always honoured, the limit governs strangers, the day ends at midnight Malaysia time.
TaskWhatWhySize
3.1Settings columns, daily counter, atomic slot claim The foundation. Ordering matters: capacity is checked before the code is claimed, so a full door never burns a code.L
3.2Admin capacity controls and today's numbers A limit you cannot see or change during the day is not a control.M
3.3"HOOP is full for today" screen + remind me tomorrow Needs no account β€” a local notification. The refusal has to be worth something to the person refused.M
3.4Audit trail for settings changes The switch governing every new signup records when it changed, never who or what.S
3.5Alert when the gate falls open The panel exists; it only helps someone who happens to look.S
Phase 4 Β· Attribution and hygiene
Five real defects and the quota change. None are visible in a demo; all of them corrupt the numbers you will be making decisions with.
TaskWhatWhySize
4.1Quota becomes "3 at a time" instead of "3 for life" Leong's decision, 2026-09-15. Removes the 132-person growth wall and enables the "your invite was used" moment.M
4.2Fix the minting quota race Check and insert are two statements. Two fast taps get 4 codes from a quota of 3.S
4.3Record who issued the code on all three sources, and add a source column Leong's rule, 2026-09-15. Today only a member's code names a person; batch mints and waiting-list invitations both store NULL β€” and NULL is simultaneously being used to mean "official", so the two cannot be filled in without the source column landing in the same change. The admin's id is already available at both sites and is currently thrown away. The same column fixes account deletion silently promoting a member's codes to official ones.M
4.4Fix the stats labels "Codes issued" counts all three sources as one number and two labels describe something other than what they count.S
4.5Hide My Email: match the waiting-list row Apple/Google private relay stores apple:<sub> with no "@", so that person stays queued forever despite being inside.S
⚠️ Binding collides with Apple and Google sign-in β€” handle it inside task 2.5. Someone joins the waiting list as alice@gmail.com, gets invited, then signs up by tapping "Continue with Apple". The contact we store is not that address: it is Apple's relay address, or apple:<sub> when Apple withholds the email entirely (auth/service.go). A naive binding check refuses that person their own invitation, and it is the path a privacy-minded user is most likely to take.

The fix is not to weaken the binding β€” it is to fail usefully: "this invite was sent to aΒ·Β·Β·e@gmail.com β€” sign in with that address to use it." A dead end that names the way out is a different thing from a dead end. This is the same root as task 4.5, so build them together.

Later, deliberately not scheduled

Referral credit on the waiting list Β· a public capacity indicator (only worth building once the number actually moves β€” see the note below) Β· rejection sampling in code generation Β· wider dash and lookalike normalisation Β· a founding-member marker Β· per-member quota override.

On showing "spots left" publicly: a capacity number is only a marketing asset if people can see it moving. At 3 signups a day against a limit of 1,000, a public counter would read "997 left" every day β€” which advertises that nobody is trying to get in. If it is ever built, it should appear only below a threshold, so it is never on screen unless it is genuinely true.

Decisions on record

Made by Leong Sen Fong in the workroom on 2026-09-15, after asking for the same questions to be looked at through a marketing lens rather than an engineering one. Two of the three answers changed as a result, and the changed ones are marked.

QuestionDecisionReasoning
Where does the invite code step sit in signup? After sign in / sign up Decided 2026-09-15. An earlier draft of this document proposed asking before the OTP, on the grounds that a wrong code destroyed the user's sign-in code and the 10-minute window was spent hunting for one. The destructive half of that was fixed on 2026-09-14 β€” invite mistakes now have their own counter and the OTP is preserved β€” so the reason for moving it had largely gone before the proposal was read. The residual cost is only the 10-minute window, and a fresh sign-in code can be requested.
How many times may a person type the code? Once Decided 2026-09-15. Entering it again after typing it wrongly is not a second entry β€” that one stays. What must never happen is being asked again for a code the app already had: after a restart, after leaving and returning to the screen, or after arriving through an invite link that carried the code in the first place.
Where does the person land after authenticating? The page they were going to β€” never the invite code screen Decided 2026-09-15. This does not happen today (a successful signup goes to Home), so the rule is here to stop 1.2 and 1.5 from introducing it: once a code has been accepted, no later screen may ask for it again. The positive half β€” actually landing on the intended page β€” is task 1.6, and it needs the replay queue to outlive a signup before it can work at all.
Can a waiting-list invitation be forwarded? No β€” tied to the address it was sent to Decided 2026-09-15. The queue's promise is to a person, not to whoever ends up holding the string. Untied, one forwarded code silently strands the person who waited. See the warning on the build plan: binding must not lock out someone signing in with Apple or Google.
Do we choose who to admit, the way Manus did? No β€” first come, first served, email only Decided 2026-09-15. Manus admitted people by reading what they said they wanted to use it for. We stay strictly FIFO and keep asking for nothing but an email. Worth knowing this closes the door quietly: choosing later would need information we never collected, and it cannot be gathered retrospectively from people already in the queue.
Do we record who invited each new member? Yes, on all three sources Decided 2026-09-15. Recording and displaying are separate, though: a member's code is a personal invitation and saying "Sarah invited you" is warm, whereas a batch mint or a waiting-list turn is HOOP letting you in, and naming the admin who pressed the button would be both odd and misleading. Record all three for attribution; display a name only when a real person chose to spend an invite on you. Task 4.3 records, task 1.4 displays.
Does an invite code get in when the day is full? Yes β€” always honoured CHANGED An invited friend is the best arrival there is, and turning them away embarrasses the member who invited them. Exposure stays bounded because it equals the number of live codes, every one of which exists because the admin allowed it.
When does "today" end? Midnight, Malaysia time UTC midnight is 8am local, so the counter would reset mid-morning and read as a bug. If the cap ever binds, "doors reopen at midnight" is an event worth having at midnight.
Is a member's 3 for life, or 3 at a time? 3 at a time CHANGED "For life" throttles the small group who actually evangelise and creates a hard 132-person wall. "At a time" keeps the felt scarcity β€” you still hold three and still choose who deserves them β€” and makes "your invite was used, here is another" possible.
Is the waiting list a panel or a page? A page Decided 2026-09-15 01:00. Shipped the same day as v2.204.
When does the gate open? Not yet decided The only item on this feature that nobody but the owner can answer.
Correction on record. A daily limit of 1,000/day was believed to be set. It is not, and never has been β€” the backend accepts exactly two settings, the gate switch and the per-member quota. Production read on 2026-09-15: gate false, quota 3, updated_at still 2026-08-05 08:15 (the moment the row was created), 4 codes in existence, 0 people on the waiting list. The gate has never been switched on. Opening it is not a config change β€” it is a first production run of code that has never met a real user.

Standing design rules this feature is built on

  • A code must never be burned by a door that was closed for some other reason. This single rule dictates the order of every check in the signup path.
  • Fail open, but visibly. If the gate cannot be read, signups are let through β€” a database wobble must not stop everyone joining. But a gate that falls open and a gate that was switched off look identical from outside, so the fall-open is counted and surfaced.
  • One code, three sources. Resist the pull to make "types" of code; the differences belong in columns set at creation, not in the validation path.