Inviting someone to an organisation is one of the first features every business product builds, and it is usually built as a row with an address, a role, a token and an accepted flag. That model handles the case where a person receives an email and clicks the link within the hour.
Everything else — the forward, the second invitation, the person who already has an account, the administrator who leaves before the invitation is accepted, the address that bounces — falls outside it. Those are not edge cases in a product with organisations. They are most of the traffic once you have more than a handful of customers.
The states you already have, whether or not you named them
Write down the transitions and the shape of the feature changes immediately. An invitation is created, and from there it can be accepted, revoked, expired, superseded by a newer invitation to the same address, or blocked because the address bounced. Each of those has a different meaning to an administrator looking at a list of pending members, and a single boolean collapses all of them into not accepted yet.
The states also decide who can see what. An accepted invitation should disappear from the pending list and appear in the members list. A revoked one should remain visible for a while, because the question who invited this person and who cancelled it is an audit question. An expired one wants a resend button, not a mystery.
- pending — issued, delivered, not yet acted upon
- accepted — consumed exactly once, with the resulting membership recorded
- revoked — cancelled by an administrator, with actor and time retained
- expired — passed its validity window and no longer redeemable
- superseded — replaced by a later invitation to the same address in the same organisation
- undeliverable — the address bounced, which is a state and not a silent failure
The token is a key, not the invitation
Conflating the two causes most of the security problems here. The invitation is a record you own; the token is a bearer credential that travels through email and may be forwarded, logged by a mail gateway, or opened by a link scanner before the human ever sees it. Store a hash of it, give it a defined lifetime, and let it be consumed exactly once with the consumption enforced by the database rather than by a check in your handler.
It follows that the token proves possession of a message, not identity. If the invitation was addressed to one person and accepted by someone signed in as another, that is a decision your code must make on purpose: bind acceptance to the invited address, or allow any authenticated recipient to redeem it and record who actually did. Both are defensible. Not choosing means whichever behaviour your code accidentally has is the one you will explain to a customer later.
The token proves somebody received the email. It does not prove they are the person you meant to invite.
Collisions with everything else that creates members
Invitations rarely operate alone. There is usually also a domain rule that lets anyone with a matching address join, and a directory that provisions people directly, and just-in-time creation on first sign-on. All three can produce a membership for the same person, and they can run concurrently.
Decide the precedence and enforce it in one place. If a person is provisioned by the directory while an invitation is pending, the invitation should resolve as superseded rather than lingering to grant a second membership with a different role later. The role is the part that bites: an invitation carrying administrator, accepted after the directory has already created the person as a member, is a privilege change that nobody reviewed.
- One place decides how a membership comes into existence, whatever triggered it
- Resolve pending invitations when the same person arrives by another route
- Re-evaluate the role at acceptance rather than trusting what was recorded at issue time
- Cap the number of outstanding invitations per organisation to bound the abuse surface
The questions an administrator will ask
Every invitation feature eventually receives the same four questions from customers, and a state machine answers all of them cheaply. Who invited this person. Was the invitation ever delivered. Why does this person have administrator access. Can I stop the one I sent by mistake, right now, before they click it.
That last one is worth designing for explicitly, because revocation has to beat a token that is already in somebody's inbox. Check state at redemption rather than only at issue, so a revoked invitation fails even though the link is intact. It is one query in the acceptance path, and it is the difference between an administrator who can correct a mistake and one who has to ask you to fix it.
Everything here, already built
Sign-in, enterprise SSO, directory provisioning, roles and an audit trail behind one API. Start with the quickstart and have a working sign-in this afternoon.