Method notes
The behaviour of each method that isn't obvious from its name.
Solana
Better Auth's siwe plugin only accepts 0x… addresses, so convex/lib/solana.ts
implements Sign In With Solana directly. The server composes the message and
stores it; verification consumes it, so a signature can't be replayed.
Passkeys
Bound to SITE_URL's hostname as the relying party id, and usable from every
origin in TRUSTED_ORIGINS via WebAuthn Related Origin Requests.
A passkey is normally locked to the one domain that created it, which for a shared
auth server would make the most portable-feeling method the least portable.
convex/http.ts publishes the allowed origins at /.well-known/webauthn, proxied
through the site's own domain because the browser fetches it from the relying
party's domain.
The spec matches on eTLD+1 and allows at most five distinct labels, so this is a handful of apps rather than an open list. Anything past the fifth is dropped knowingly, with a warning in the logs, rather than silently ignored by the browser.
Passkey names
Nobody types one. The authenticator identifies itself in the AAGUID it returns at
registration and the server turns that into a label ("iCloud Keychain",
"1Password"); when it doesn't say — Apple zeroes the AAGUID under
attestation: "none" — the User-Agent stands in.
Account numbers
Sign-up returns a 16-digit number once and stores it as the user's username.
There is no email, password or recovery path. Mullvad's model: the number is the
entire credential.
SMS and email OTP
The code fields use autocomplete="one-time-code", which is what makes iOS
Passwords and Android offer the code above the keyboard.
Magic links
Sent by Resend when RESEND_API_KEY is set, and written to the Convex logs when
it isn't and SITE_URL is a localhost one. Off a real domain the key is
required — see quickstart. Good for one sign-in.
The demo account
Everyone who clicks "Try the demo" lands on the same user, so the session it hands out is deliberately read-only.
Setting a password, linking a social account, registering a passkey, minting an
API key and revoking sessions are all refused for it. Otherwise the first visitor
to set a password would own demo@aussieauth.com permanently and every later
visitor would be sharing an account with them.
The deny-list is LOCKED in convex/lib/demo.ts — adding a method means adding
it there too.
Anonymous
A throwaway session that can be upgraded later by linking a real credential onto
it. The address is …@anonymous.invalid, which is unroutable on purpose.
Agent auth
API keys, not sessions. Mint one on the account page; the agent sends it as an
x-api-key header.
Keys are numbered, never named. The next key is one past the highest number in use, so revoking key 2 doesn't hand the number out twice.
Two-factor (TOTP)
A second factor rather than a sixteenth method: authenticator-app codes offered after a password sign-in when the account has switched them on, from the account page. Enabling and disabling both require the password — that's Better Auth's rule, so an account without a password can't enrol.
It also covers the sign-in methods AussieAuth wrote itself. Better Auth's plugin
only gates the three paths it ships (/sign-in/email, /sign-in/username,
/sign-in/phone-number), which left a linked wallet or an account number as a
way in that never asked for the code. convex/lib/twoFactorGate.ts widens that
gate to /sign-in/solana and /sign-in/account-number — so once TOTP is on, it
is on for every door into the account, not just the one it was switched on
from.
Enrolment hands out backup codes once. Each works once, from the "use a backup code" link on the challenge screen; a fresh set can be minted from the account page (which invalidates the old one). The challenge offers "don't ask again on this device for 30 days", which is Better Auth's trusted-device cookie.
The demo account can't enrol: /two-factor/ is on the deny-list, for the same
reason it can't set a password.
Sessions
The account page lists every live session with a coarse device label from the User-Agent, and can end any session except the current one. "Sign out everywhere" on the account header is the bulk version.
Your data
The account page's footer card exports everything the server holds about you as JSON — assembled client-side from the same endpoints the page already uses — and can delete the account outright, password-confirmed. The demo account is refused both ways.
Apple
https://appleid.apple.com is a permanent trusted origin, because Apple returns
its callback as a form POST and the browser sends Apple's origin rather than ours.
Email verification
Sent on sign-up, but never required to sign in.
It's there because linking checks it: attaching a social account to an existing user also requires that existing user's address to be verified, so without the sign-up mail a credential user could never later add Google — they'd get "account not linked" and no way forward.
The one-line alternative is accountLinking.requireLocalEmailVerified: false,
which opens the pre-registration attack: sign up under someone else's address,
wait for their Google sign-in to merge into your account.
Rate limiting
On, and backed by the rateLimit table rather than memory. Both defaults were
wrong here:
- Better Auth enables rate limiting only when
NODE_ENV === "production", which a Convex deployment isn't — so the stock three-sign-ins-per-ten-seconds rule was off rather than merely loose. - Its default memory store is a Map inside one HTTP-action isolate: not shared between concurrent requests, and gone when the isolate is recycled.
Matters most for account numbers, where the number is the entire credential and there's no second factor to fall back on.
Account linking
Signing in with Google and later with GitHub on the same address lands on one
account, not a duplicate-email error. trustedProviders is what makes that merge
happen at sign-in rather than only through an explicit link from the account page.
Only real provider ids belong there — it's matched against the incoming account's providerId, so the credential provider is never a candidate.
Questions
Why can't the demo account set a password? Because it's shared. The first visitor to set one would own it, and everyone after them would be locked into an account someone else controls.
How many apps can share one passkey? Five distinct eTLD+1 labels, which is a WebAuthn limit rather than an AussieAuth one. Several origins on one site cost one between them.
Is there a recovery path for account numbers? No. That's the trade — no email, no password, nothing to phish, and nothing to recover if you lose the number.
Why does an anonymous user have an email address?
The schema needs one. It's set to an unroutable …@anonymous.invalid address and
replaced if the user later links a real credential.