Skip to content
4 min read Moustakime KIFIA

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:

  1. store a derived form of the code, not the plain code
  2. limit its lifetime
  3. count failures
  4. 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.

Keep reading

A few related articles to extend the topic and keep the internal linking strong.