Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6c09f17
feat(oauth-provider): pairwise subject identifiers (OIDC Core §8) (#8…
gustavovalverde Mar 3, 2026
eb848c4
fix(adapters): restore deprecated createAdapter and type exports for …
himself65 Mar 6, 2026
3ecd22d
fix(telemetry): use conditional exports to replace dynamic import hac…
himself65 Mar 6, 2026
2bd994b
fix: preserve custom session fields on focus refresh (#8354)
jslno Mar 9, 2026
b6222b2
chore(client): re-export necessary types (#8497)
jslno Mar 9, 2026
5003d94
chore: replace deprecated build configs (#8498)
jslno Mar 9, 2026
e3e6664
fix: throw on duplicate email when `autoSignIn: false` without `requi…
himself65 Mar 9, 2026
11ef01a
fix(cli): resolve path aliases from extended tsconfig files (#8520)
himself65 Mar 9, 2026
8e089ed
chore: exclude worktrees from biome lint scope
himself65 Mar 9, 2026
857fbb3
docs: add warning about localhost in trusted origins (#8527)
himself65 Mar 9, 2026
9272202
docs: fix enterprise contact form url (#8531)
jslno Mar 10, 2026
d803657
docs: sidebar navigation accordion text tracking (#8533)
Rudraksh88 Mar 10, 2026
3f16e9f
fix(prisma-adapter): fall back to updateMany for non-unique updates (…
himself65 Mar 9, 2026
c03666a
fix(oauth-provider): avoid fetch redirect CORS after login (#8519)
GautamBytes Mar 9, 2026
b9e54c9
fix(db): use `CREATE INDEX` for postgres migration (#8538)
himself65 Mar 10, 2026
024e7a9
docs: add better-auth-usos plugin (#8493)
qamarq Mar 9, 2026
67c6dc2
fix(blog): fix RSS feed link path, image path and blog date (#8483)
0-Sandy Mar 9, 2026
362df86
docs(dodopayments): use checkoutSession in docs (#8501)
Mnigos Mar 9, 2026
12f16c3
docs(metadata): centralize SEO metadata with createMetadata helper (#…
0-Sandy Mar 9, 2026
ff352c6
fix(oidc-provider): validate redirect_uri for prompt=none (#8398)
jslno Mar 11, 2026
497b1db
fix: add origin check middleware to password reset request (#8392)
jslno Mar 11, 2026
90597e0
docs: update SolidStart auth handler file path (#8470)
zeljkovranjes Mar 11, 2026
db5a444
chore: release v1.5.5
himself65 Mar 11, 2026
1c6f5d3
chore: merge upstream better-auth v1.5.5
cursoragent Mar 13, 2026
30b5159
chore: sync upstream v1.5.5 + bump @btst to v2.1.1
cursoragent Mar 13, 2026
7dbf692
Update biome.json
olliethedev Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cspell/names.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ guilhermejansen
iamjasonkendrick
ejirocodes
0-Sandy
olliethedev
qamarq
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"!**/.output",
"!**/.tmp",
"!**/tmp-docs-fetch",
"!**/.claude/worktrees/**",
"!**/btst/**",
"!scripts/**"
]
Expand Down
1 change: 0 additions & 1 deletion docs/content/blogs/1-5.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,6 @@ All previously deprecated APIs have been removed. This includes deprecated adapt

| Removed | Replacement |
| --- | --- |
| `createAdapter` | `createAdapterFactory` |
| `Adapter` | `DBAdapter` |
| `TransactionAdapter` | `DBTransactionAdapter` |
| `Store` (client) | `ClientStore` |
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/integrations/solid-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Before you start, make sure you have a Better Auth instance configured. If you h

### Mount the handler

We need to mount the handler to SolidStart server. Put the following code in your `*auth.ts` file inside `/routes/api/auth` folder.
We need to mount the handler to SolidStart server. Put the following code in your `[...auth].ts` file inside `/routes/api/auth` folder.

```ts title="*auth.ts"
```ts title="[...auth].ts"
import { auth } from "~/lib/auth";
import { toSolidStartHandler } from "better-auth/solid-start";

Expand Down
25 changes: 10 additions & 15 deletions docs/content/docs/plugins/dodopayments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const auth = betterAuth({
<Step title="Set up client-side integration">
Create or update `src/lib/auth-client.ts`:
```typescript
import { createAuthClient } from "better-auth/react";
import { dodopaymentsClient } from "@dodopayments/better-auth";

export const authClient = createAuthClient({
Expand All @@ -114,27 +115,21 @@ export const authClient = createAuthClient({
### Creating a Checkout Session

```typescript
const { data: checkout, error } = await authClient.dodopayments.checkout({
const { data: checkoutSession, error } =
await authClient.dodopayments.checkoutSession({
slug: "premium-plan",
customer: {
email: "customer@example.com",
name: "John Doe",
},
billing: {
city: "San Francisco",
country: "US",
state: "CA",
street: "123 Market St",
zipcode: "94103",
},
referenceId: "order_123",
});

if (checkout) {
window.location.href = checkout.url;
if (checkoutSession) {
window.location.href = checkoutSession.url;
}
```

<Callout type="warn">
`authClient.dodopayments.checkout()` is deprecated. Use
`authClient.dodopayments.checkoutSession()` for new integrations.
</Callout>

### Accessing the Customer Portal

```typescript
Expand Down
56 changes: 56 additions & 0 deletions docs/content/docs/plugins/oauth-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,56 @@ oauthProvider({
```


### Pairwise Subject Identifiers

By default, the `sub` (subject) claim in tokens uses the user's internal ID, which is the same across all clients. This is the **public** subject type per [OIDC Core Section 8](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes).

You can enable **pairwise** subject identifiers so each client receives a unique, unlinkable `sub` for the same user. This prevents relying parties from correlating users across services.

```ts title="auth.ts"
oauthProvider({
pairwiseSecret: "your-256-bit-secret", // [!code highlight]
})
```

When `pairwiseSecret` is configured, the server advertises both `"public"` and `"pairwise"` in the discovery endpoint's `subject_types_supported`. Clients opt in by setting `subject_type: "pairwise"` at registration.

#### Per-Client Configuration

```ts title="register-client.ts"
const response = await auth.api.createOAuthClient({
headers,
body: {
client_name: 'Privacy-Sensitive App',
redirect_uris: ['https://app.example.com/callback'],
token_endpoint_auth_method: 'client_secret_post',
subject_type: 'pairwise', // Enable pairwise sub for this client
}
});
```

#### How It Works

Pairwise identifiers are computed using HMAC-SHA256 over the **sector identifier** (the host of the client's first redirect URI) and the user ID, keyed with `pairwiseSecret`. This means:

- Two clients with different redirect URI hosts always receive different `sub` values for the same user
- Two clients sharing the same redirect URI host receive the **same** pairwise `sub` (per OIDC Core Section 8.1)
- The same client always receives the same `sub` for the same user (deterministic)

Pairwise `sub` appears in:
- `id_token`
- `/oauth2/userinfo` response
- Token introspection (`/oauth2/introspect`)

JWT access tokens always use the real user ID as `sub`, since resource servers may need to look up users directly.

<Callout type="warn">
**Limitations:**
- `sector_identifier_uri` is not yet supported. All `redirect_uris` for a pairwise client must share the same host. Clients with redirect URIs on different hosts will be rejected at registration.
- `pairwiseSecret` must be at least 32 characters long.
- Rotating `pairwiseSecret` will change all pairwise `sub` values, breaking existing RP sessions. Treat this secret as permanent once set.
</Callout>

### MCP

You can easily make your APIs [MCP-compatible](https://modelcontextprotocol.io/specification/draft/basic/authorization) simply by adding a resource server which directs users to this OAuth 2.1 authorization server.
Expand Down Expand Up @@ -1577,6 +1627,12 @@ Table Name: `oauthClient`
description: "Field that indicates if the application can logout via an id_token. You may choose to enable this for trusted applications.",
isOptional: true,
},
{
name: "subjectType",
type: "string",
description: "Subject identifier type for this client. Set to \"pairwise\" to receive unique, unlinkable sub claims per user. Requires pairwiseSecret to be configured on the server.",
isOptional: true,
},
{
name: "scopes",
type: "string[]",
Expand Down
5 changes: 4 additions & 1 deletion docs/content/docs/reference/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Trusted origins prevent CSRF attacks and block open redirects. You can set a lis

### Basic Usage

The most basic usage is to specify exact origins:
The most basic usage is to specify exact origins, below is an example of a trusted origins configuration:

```typescript
{
Expand All @@ -211,6 +211,9 @@ The most basic usage is to specify exact origins:
]
}
```
<Callout type="warning">
Do not leave the localhost origin in a trusted origins list of a production auth instance.
</Callout>

### Wildcard Origins

Expand Down
1 change: 1 addition & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"better-auth",
"c12",
"chalk",
"get-tsconfig",
"open",
"prettier",
"prompts",
Expand Down
9 changes: 5 additions & 4 deletions landing/app/blog/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BlogLeftPanel } from "@/components/blog/blog-left-panel";
import { Callout } from "@/components/ui/callout";
import { createMetadata } from "@/lib/metadata";
import { blogs } from "@/lib/source";
import { cn } from "@/lib/utils";

Expand Down Expand Up @@ -270,10 +271,10 @@ export async function generateMetadata({
}) {
const { slug } = await params;
if (!slug) {
return {
return createMetadata({
title: "Blog - Better Auth",
description: "Latest updates, articles, and insights about Better Auth",
};
});
}
const page = blogs.getPage(slug);
if (!page || page.data.draft) return notFound();
Expand All @@ -296,7 +297,7 @@ export async function generateMetadata({

const ogImage = image || ogUrl;

return {
return createMetadata({
title,
description,
openGraph: {
Expand All @@ -311,7 +312,7 @@ export async function generateMetadata({
description,
images: [ogImage],
},
};
});
}

export function generateStaticParams() {
Expand Down
29 changes: 21 additions & 8 deletions landing/app/blog/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import { RootProvider } from "fumadocs-ui/provider/next";
import type { Metadata } from "next";
import { createMetadata } from "@/lib/metadata";

export const metadata: Metadata = {
title: "Blog - Better Auth",
description: "Latest updates, articles, and insights about Better Auth",
const description = "Latest updates, articles, and insights about Better Auth";

export const metadata: Metadata = createMetadata({
title: "Blog",
description,
openGraph: {
url: "/blog",
title: "Blog - Better Auth",
description: "Latest updates, articles, and insights about Better Auth",
description,
images: ["/api/og-release?heading=Better%20Auth%20Blog"],
},
twitter: {
card: "summary_large_image",
title: "Blog - Better Auth",
description: "Latest updates, articles, and insights about Better Auth",
images: ["/api/og-release?heading=Better%20Auth%20Blog"],
title: "Blog - Better Auth",
description,
},
alternates: {
types: {
"application/rss+xml": [
{
title: "Better Auth Blog",
url: "https://better-auth.com/blog/rss.xml",
},
],
},
},
};
});

export default function BlogLayout({
children,
Expand Down
5 changes: 3 additions & 2 deletions landing/app/careers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Metadata } from "next";
import { createMetadata } from "@/lib/metadata";
import { CareersPageClient } from "./careers-client";

export const metadata: Metadata = {
export const metadata: Metadata = createMetadata({
title: "Careers",
description: "Join the Better Auth team — open positions and how to apply.",
};
});

export default function CareersPage() {
return <CareersPageClient />;
Expand Down
7 changes: 4 additions & 3 deletions landing/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { HalftoneBackground } from "@/components/landing/halftone-bg";
import { createMetadata } from "@/lib/metadata";
import { ChangelogContent } from "./changelog-content";

export const dynamic = "force-static";
Expand Down Expand Up @@ -148,7 +149,7 @@ export default async function ChangelogPage() {
);
}

export const metadata = {
title: "Changelog - Better Auth",
export const metadata = createMetadata({
title: "Changelog",
description: "Latest changes, fixes, and updates to Better Auth",
};
});
5 changes: 3 additions & 2 deletions landing/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Metadata } from "next";
import { getCommunityStats } from "@/lib/community-stats";
import { createMetadata } from "@/lib/metadata";
import { CommunityPageClient } from "./community-client";

export const metadata: Metadata = {
export const metadata: Metadata = createMetadata({
title: "Community",
description:
"Join the Better Auth community — contributors, Discord members, and ecosystem stats.",
};
});

export const revalidate = 21600; // Revalidate every 6 hours

Expand Down
5 changes: 3 additions & 2 deletions landing/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
GenerateSecret,
} from "@/components/docs/mdx-components";
import { Callout } from "@/components/ui/callout";
import { createMetadata } from "@/lib/metadata";
import { getSource } from "@/lib/source";
import { cn } from "@/lib/utils";
import { LLMCopyButton, ViewOptions } from "./page.client";
Expand Down Expand Up @@ -164,7 +165,7 @@ export async function generateMetadata({

const ogUrl = `/api/og?${ogSearchParams.toString()}`;

return {
return createMetadata({
title: page.data.title,
description: page.data.description,
openGraph: {
Expand All @@ -186,5 +187,5 @@ export async function generateMetadata({
description: page.data.description,
images: [ogUrl],
},
};
});
}
2 changes: 1 addition & 1 deletion landing/app/enterprise/enterprise-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function EnterprisePageClient() {
const url =
process.env.NODE_ENV === "development"
? "http://localhost:3001/api/enterprise/contact"
: "/api/enterprise/contact";
: "https://dash.better-auth.com/api/enterprise/contact";
const response = await fetch(url, {
method: "POST",
body: JSON.stringify({
Expand Down
5 changes: 3 additions & 2 deletions landing/app/enterprise/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Metadata } from "next";
import { createMetadata } from "@/lib/metadata";
import { EnterprisePageClient } from "./enterprise-client";

export const metadata: Metadata = {
export const metadata: Metadata = createMetadata({
title: "Enterprise",
description:
"Better Auth for enterprise — SSO, SAML, audit logs, and dedicated support.",
};
});

export default function EnterprisePage() {
return <EnterprisePageClient />;
Expand Down
Loading
Loading