Skip to main content

Multi-use links

A magic link is single-use by default. To hand out a link that may be redeemed a bounded number of times — a shared invite, a multi-device sign-in — pass maxUses:

$link = $issuer->issueLink($user, maxUses: 3); // redeemable three times

Why concurrent redemptions cannot exceed the limit

Each redemption decrements a remaining-uses counter in the same conditional UPDATE that consumes the token, so concurrent redemptions can never exceed the limit — the count is checked and decremented atomically, never read-then-written.

The behavior is proven against real PostgreSQL and MySQL row locking, not only SQLite: consumed_at is set before the decrement in the SET list so MySQL agrees with PostgreSQL and SQLite about what the row looked like when the condition was evaluated.

When the uses run out

Once exhausted the link behaves exactly like a spent or expired one — the same generic, enumeration-resistant failure, and the same invalid-link response you configured. A reader can never tell an exhausted link from an unknown one.

Set the default for every link with the max_uses config key:

'max_uses' => 1,

One-time codes are always single-use regardless of this value.