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.

The sign-in mail says how often a multi-use link works ("can be used 3 times"), so its reader does not treat the mail as spent after the first sign-in. If you publish the translations, the line is mail_link_expiry_reusable, with :minutes and :uses.