Inviting a member by code: hash, TTL, rate-limit
How we built an invitation and login-code flow with hashing, expiration, lockout and IP-level guardrails.
- Auth
- Engineering
Sending a code sounds harmless. That is exactly why this kind of flow often gets treated too quickly, as if the whole subject stopped at “generate six characters and compare them later”.
The real subject is elsewhere: an invitation code is already an access secret. Short and temporary, but still a secret.
The problem to solve
In a product where you want to invite new members and reconnect existing ones, a code-based flow has to hold several constraints together:
- stay simple for the user
- work across several delivery channels
- expire cleanly
- limit abuse
- still look defensible when you revisit the system six months later
The classic trap is to treat the code as a small product detail. You send a code, store it somewhere, compare it, and move on.
The problem is that as soon as that code can unlock access, it has to be reasoned about like a temporary password.
The solution we kept
The solution we kept is intentionally simple:
- store a derived form of the code, not the plain code
- limit its lifetime
- count failures
- add an IP-level guardrail
In other words, we are not looking for a clever “passwordless” trick. We are looking for a flow that stays readable, bounded and predictable.
The path itself stays clear:
- issue a short code
- deliver it to the member
- verify it
- refuse if it has expired, been tried too many times or is used in a suspicious window
The important point is not the complexity of the flow. It is not forgetting half of the constraints around it.
Why this holds up well
This kind of solution holds up when the security rules stay visible.
The important guardrails are known in advance:
- code lifetime
- maximum number of attempts
- IP-level rate limiting window
- clear separation between issuing, verifying and refusing
When those rules are explicit, they become:
- easier to review
- easier to adjust
- consistent across channels
- simpler to explain to the team
Code-based login flows age badly when their thresholds are hidden in several places. They age better when the policy can be read in a few named rules.
How we implement it
The interesting implementation detail is not “how to compare a string”. The interesting part is the order of checks.
You first recover the right context, then verify:
- that the code is still valid
- that it has not exceeded its allowed attempts
- that the current attempt still fits inside the allowed window
- and only then that the submitted secret matches what is expected
What matters here is less the exact code and more the system’s responsibility:
- never make the secret readable again
- make expiration explicit
- make failures traceable
- refuse cleanly once the threshold is crossed
The real pivot
The important moment in this kind of topic is not adding SMS or email support. The important moment is when you stop treating the code as “temporary data, so not a big deal”.
Many flows start with a very ordinary shortcut: keeping too much information because it is convenient in the early days.
Then the flow grows:
- more use cases
- more members
- more support scenarios
- more value carried by that small code
And what started as a convenience becomes a real security concern.
The right pivot is therefore to do early what teams often postpone:
- hash the code
- bound its lifetime
- count failures
- slow down abuse
The guardrail people forget most often
Hashing alone is not enough.
If you stop at “we no longer store the code in plain text”, you only protect one part of the problem, not the whole flow.
You still need to think about:
- expiration
- lockout after repeated failures
- IP-level limiting
- explicit refusal, not implicit failure
Robustness does not come from a single best practice. It comes from the way those practices fit together.
What the solution removes from the system
The gain is both security-related and structural.
This pattern removes:
- direct secret comparisons
- informal exceptions kept around “just for debug”
- thresholds hidden across several services
- behavior that changes depending on the entry channel
It also gives the whole team a better mental model: an invitation code is not a small UX detail. It is a short-lived access key.
What I want other developers to take away
I do not want to show a “modern” code-based flow. I want to show a quality bar.
A solid invitation-code flow is one where:
- the secret is not readable
- expiration is explicit
- failures are counted
- abuse is bounded
- the system refuses clearly at the right time
An invitation code is still a short secret. Treating it like a temporary password is often the right analogy.