Four changes, each with its own acceptance spec.
Passkeys
@better-auth/passkey is registered on the server and in the browser client.
Enrollment lives on the account page beside two-factor; /sign-in leads with a
passkey button that asks for no email address, because the assertion already
carries the credential id.
Proving it was the interesting part. WebAuthn lives behind
navigator.credentials, so a headless browser has nothing to answer the
ceremony. Rather than stub it and get a green test that proves nothing, the
acceptance spec drives the real endpoints with a software authenticator that
generates real ES256 keys and signs real assertions.
You need to do nothing. Passkeys need no keys and no account.
Social sign-in, generalised
socialProviders used to be a single ternary for Google. It is now a catalogue:
one entry per provider in lib/social-providers.ts, one line each in
socialProvidersFrom, and nothing anywhere else names a provider.
GitHub ships as the second one. Registering the OAuth app is your step — see
guide/keys/github.md. Until you do, the button is absent rather than broken.
A content source behind the blog
The blog was hard-wired to content/blog/*.mdx. It now reads through a
two-method interface, so moving to a hosted CMS is an adapter rather than a
rewrite of every route, the sitemap and the SEO layer:
export interface ContentSource<T> {
list(): Promise<T[]>;
get(slug: string): Promise<T | undefined>;
}
You need to do nothing unless you want a CMS, in which case
exampleSources in lib/content.ts is the template.
This changelog
A second collection behind that same interface, fed into the same page registry — so it has metadata, a canonical, an OG image, a sitemap entry and an RSS feed without a line of changelog-specific SEO code.