Merge remote-tracking branch 'origin/main' into v1.3

This commit is contained in:
Bereket Engida
2025-07-10 10:12:11 -07:00
14 changed files with 574 additions and 32 deletions

View File

@@ -1,30 +1,219 @@
# Contributing to Better Auth
Thanks for taking the time to improve Better Auth! This is a small document to get you started.
Thank you for your interest in contributing to Better Auth. This guide will help you get started with the contribution process.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Security Issues](#security-issues)
- [Project Structure](#project-structure)
- [Development Guidelines](#development-guidelines)
- [Getting Started](#getting-started)
- [Code Formatting with BiomeJS](#code-formatting-with-biomejs)
- [Development Workflow](#development-workflow)
- [Testing](#testing)
- [Pull Request Process](#pull-request-process)
- [Code Style](#code-style)
- [Component-Specific Guidelines](#component-specific-guidelines)
- [Core Library](#core-library)
- [Documentation](#documentation)
- [Plugins](#plugins)
- [Examples](#examples)
## Code of Conduct
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code.
## Security Issues
If you see any security issue we prefer you to disclose it via an email (security@better-auth.com). All reports will be promptly addressed, and you'll be credited accordingly.
For security-related issues, please email security@better-auth.com. Include a detailed description of the vulnerability and steps to reproduce it. All reports will be reviewed and addressed promptly. For more information, see our [security documentation](/docs/reference/security).
Learn more in our [security documentation](/docs/reference/security).
## Project Structure
## A Few Guidelines to keep in mind
The Better Auth monorepo is organized as follows:
- Rather than extensive configurations, focus instead on providing opinionated, best-practice defaults.
- Try to make a consistent and predictable API across all supported frameworks
- Everything should be type-safe and embrace typescript magic when necessary.
- `/packages/better-auth` - Core authentication library
- `/packages/cli` - Command-line interface tools
- `/packages/expo` - Expo integration
- `/packages/stripe` - Stripe payment integration
- `/docs` - Documentation website
- `/examples` - Example applications
- `/demo` - Demo applications
## Development
## Development Guidelines
1. Fork the repo
2. clone your fork.
3. install node.js (preferable latest LTS).
4. run `cp -n ./docs/.env.example ./docs/.env` to create a `.env` file (if it doesn't exist)
5. run `pnpm i` in your terminal to install dependencies.
6. create a branch.
7. build the project using `pnpm build`
8. run `pnpm -F docs dev` (to run the docs section)
9. create a draft pull request. link the relevant issue by referring to it in the PR's description. Eg.closes #123 will link the PR to issue/pull request #123.
10. implement your changes.
When contributing to Better Auth, please keep these principles in mind:
- Provide opinionated, best-practice defaults rather than extensive configurations
- Maintain a consistent and predictable API across all supported frameworks
- Ensure all code is type-safe and leverages TypeScript features effectively
- Write clear, self-documenting code with appropriate comments
- Follow existing code style and patterns
- Keep changes focused and well-documented
## Prerequisites
Before you start development, ensure you have the following:
- Node.js LTS (latest version recommended)
- pnpm package manager
- Git
- (Optional) Any authentication provider accounts you plan to work with (Google, GitHub, etc.)
- (Optional) Database server if working with database-related features
## Getting Started
1. Fork the repository to your GitHub account
2. Clone your fork locally:
```bash
git clone https://github.com/your-username/better-auth.git
cd better-auth
```
3. Install Node.js (LTS version recommended)
4. Install pnpm if you haven't already:
```bash
npm install -g pnpm
```
5. Install project dependencies:
```bash
pnpm install
```
6. Create a `.env` file from the example:
- On Unix-based systems:
```bash
cp -n ./docs/.env.example ./docs/.env
```
- On Windows:
```cmd
copy /Y .\docs\.env.example .\docs\.env
```
7. Build the project:
```bash
pnpm build
```
8. Run the documentation locally:
```bash
pnpm -F docs dev
```
## Code Formatting with BiomeJS
We use [BiomeJS](https://biomejs.dev/) for code formatting and linting. Before committing, please ensure your code is properly formatted:
```bash
# Format all code
pnpm format
# Check for linting issues
pnpm lint
# Fix auto-fixable issues
pnpm lint:fix
```
## Development Workflow
1. Create a new branch for your changes:
```bash
git checkout -b type/description
# Example: git checkout -b feat/oauth-provider
```
Branch type prefixes:
- `feat/` - New features
- `fix/` - Bug fixes
- `docs/` - Documentation changes
- `refactor/` - Code refactoring
- `test/` - Test-related changes
- `chore/` - Build process or tooling changes
2. Make your changes following the code style guidelines
3. Add tests for your changes
4. Run the test suite:
```bash
# Run all tests
pnpm test
# Run tests for a specific package
pnpm --filter "{packagename}" test
```
5. Ensure all tests pass and the code is properly formatted
6. Commit your changes with a descriptive message following the [Conventional Commits](https://www.conventionalcommits.org/) format:
```
type(scope): description
[optional body]
[optional footer(s)]
```
7. Push your branch to your fork
8. Open a pull request against the main branch
## Testing
All contributions must include appropriate tests. Follow these guidelines:
- Write unit tests for new features
- Ensure all tests pass before submitting a pull request
- Update existing tests if your changes affect their behavior
- Follow the existing test patterns and structure
- Test across different environments when applicable
## Pull Request Process
1. Create a draft pull request early to facilitate discussion
2. Reference any related issues in your PR description (e.g., 'Closes #123')
3. Ensure all tests pass and the build is successful
4. Update documentation as needed
5. Keep your PR focused on a single feature or bug fix
6. Be responsive to code review feedback
7. Update the CHANGELOG.md if your changes are user-facing
## Code Style
- Follow the existing code style
- Use TypeScript types and interfaces effectively
- Keep functions small and focused
- Use meaningful variable and function names
- Add comments for complex logic
- Update relevant documentation when making API changes
- Follow the BiomeJS formatting rules
## Component-Specific Guidelines
### Core Library (`/packages/better-auth`)
- Keep the core library focused on essential authentication functionality
- Add new authentication methods as plugins when possible
- Ensure all public APIs are well-documented with JSDoc comments
- Maintain backward compatibility or provide a clear migration path
- Follow the existing patterns for error handling and logging
### Documentation (`/docs`)
- Keep documentation up-to-date with code changes
- Use clear, concise language
- Include code examples for common use cases
- Document any breaking changes in the migration guide
- Follow the existing documentation style and structure
### Plugins
- Keep plugins focused on a single responsibility
- Follow the naming convention `@better-auth/plugin-name`
- Document all configuration options and requirements
- Include TypeScript type definitions
- Add tests for all plugin functionality
- Document any required setup or dependencies
### Examples (`/examples` and `/demo`)
- Keep examples simple and focused
- Include a README with setup instructions
- Document any prerequisites or setup steps
- Keep dependencies up to date
- Ensure examples follow security best practices

View File

@@ -103,7 +103,7 @@ export default async function Page({
GitHub
</IconLink>
<IconLink
href="https://discord.com/better-auth"
href="https://discord.gg/better-auth"
icon={DiscordLogoIcon}
className="flex-none text-gray-600 dark:text-gray-300"
>

View File

@@ -40,7 +40,7 @@ export async function BlogPage() {
GitHub
</IconLink>
<IconLink
href="https://discord.com/better-auth"
href="https://discord.gg/better-auth"
icon={DiscordLogoIcon}
className="flex-none text-gray-600 dark:text-gray-300"
>

View File

@@ -69,7 +69,7 @@ export function Intro() {
GitHub
</IconLink>
<IconLink
href="https://discord.com/better-auth"
href="https://discord.gg/better-auth"
icon={DiscordLogoIcon}
className="flex-none text-gray-600 dark:text-gray-300"
>

View File

@@ -97,7 +97,7 @@ const ChangelogPage = async () => {
GitHub
</IconLink>
<IconLink
href="https://discord.com/better-auth"
href="https://discord.gg/better-auth"
icon={DiscordLogoIcon}
className="flex-none text-gray-600 dark:text-gray-300"
>

View File

@@ -1611,6 +1611,24 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
</svg>
),
},
{
title: "Autumn Billing",
href: "/docs/plugins/autumn",
isNew: true,
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
>
<path
d="M3.292,20.708a1,1,0,0,1,0-1.411L6.12,16.469A8.041,8.041,0,0,1,8.03,7.041C13.072,2,20.9,3.1,20.9,3.1S22,10.928,16.959,15.97a8.041,8.041,0,0,1-9.428,1.91L4.7,20.708A1,1,0,0,1,3.292,20.708Z"
fill="currentColor"
></path>
</svg>
),
},
{
title: "Dub",
href: "/docs/plugins/dub",

View File

@@ -65,3 +65,10 @@ That's all! Now you can copy the Client ID and Client Secret of your app!
<Callout>
If you get "email_not_found" error, it's because you selected a Github app & did not configure this part!
</Callout>
### Why don't I have a refresh token?
Github doesn't issue refresh tokens for OAuth apps. For regular OAuth apps,
GitHub issues access tokens that remain valid indefinitely unless the user revokes them,
the app revokes them, or they go unused for a year.
There's no need for a refresh token because the access token doesn't expire on a short interval like Google or Discord.

View File

@@ -107,3 +107,27 @@ This will trigger a new OAuth flow that requests the additional scopes. After co
<Callout>
Ensure you're using Better Auth version 1.2.7 or later to avoid "Social account already linked" errors when requesting additional scopes from the same provider.
</Callout>
### Always get refresh token
Google only issues a refresh token the first time a user consents to your app.
If the user has already authorized your app, subsequent OAuth flows will only return an access token, not a refresh token.
To always get a refresh token, you can set the `accessType` to `offline`, and `prompt` to `select_account+consent` in the provider options.
```ts
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
accessType: "offline", // [!code highlight]
prompt: "select_account+consent", // [!code highlight]
},
}
```
<Callout>
**Revoking Access:** If you want to get a new refresh token for a user who has already authorized your app,
you must have them revoke your app's access in their Google account settings, then re-authorize.
</Callout>

View File

@@ -0,0 +1,271 @@
---
title: Autumn Billing
description: Better Auth Plugin for Autumn Billing
---
import { HomeIcon } from "lucide-react";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
[Autumn](https://useautumn.com) is open source infrastructure to run SaaS pricing plans. It sits between your app and Stripe, and acts as the database for your customers' subscription status, usage metering and feature permissions.
<Card href="https://discord.gg/STqxY92zuS" title="Get help on Autumn's Discord">
We're online to help you with any questions you have.
</Card>
## Features
- One function for all checkout, subscription and payment flows
- No webhooks required: query Autumn for the data you need
- Manages your application's free and paid plans
- Usage tracking for usage billing and periodic limits
- Custom plans and pricing changes through Autumn's dashboard
<Steps>
<Step>
### Setup Autumn Account
First, create your pricing plans in Autumn's [dashboard](https://app.useautumn.com), where you define what each plan and product gets access to and how it should be billed. In this example, we're handling the free and pro plans for an AI chatbot, which comes with a number of `messages` per month.
</Step>
<Step>
### Install Autumn SDK
```package-install
autumn-js
```
<Callout>
If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
</Callout>
</Step>
<Step>
### Add `AUTUMN_SECRET_KEY` to your environment variables
You can find it in Autumn's dashboard under "[Developer](https://app.useautumn.com/sandbox/onboarding)".
```bash title=".env"
AUTUMN_SECRET_KEY=am_sk_xxxxxxxxxx
```
</Step>
<Step>
### Add the Autumn plugin to your `auth` config
```ts title="auth.ts"
import { autumn } from "autumn-js/better-auth";
export const auth = betterAuth({
// ...
plugins: [autumn()],
});
```
<Callout>
Autumn will auto-create your customers when they sign up, and assign them any
default plans you created (eg your Free plan)
</Callout>
</Step>
<Step>
### Add `<AutumnProvider />`
Client side, wrap your application with the AutumnProvider component, and pass in the `baseUrl` that you define within better-auth's `authClient`.
```tsx title="app/layout.tsx"
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
{/* or meta.env.BETTER_AUTH_URL for vite */}
<AutumnProvider betterAuthUrl={process.env.NEXT_PUBLIC_BETTER_AUTH_URL}>
{children}
</AutumnProvider>
</body>
</html>
);
}
```
</Step>
</Steps>
## Usage
### Handle payments
Call `attach` to redirect the customer to a Stripe checkout page when they want to purchase the Pro plan.
If their payment method is already on file, `AttachDialog` will open instead to let the customer confirm their new subscription or purchase, and handle the payment.
<Callout type="warn">
{" "}
Make sure you've pasted in your [Stripe test secret
key](https://dashboard.stripe.com/test/apikeys) in the [Autumn
dashboard](https://app.useautumn.com/integrations/stripe).
</Callout>
```tsx
import { useCustomer, AttachDialog } from "autumn-js/react";
export default function PurchaseButton() {
const { attach } = useCustomer();
return (
<button
onClick={async () => {
await attach({
productId: "pro",
dialog: AttachDialog,
});
}}
>
Upgrade to Pro
</button>
);
}
```
The AttachDialog component can be used directly from the `autumn-js/react`
library (as shown in the example above), or downloaded as a [shadcn/ui component](https://docs.useautumn.com/quickstart/shadcn) to customize.
### Integrate Pricing Logic
Integrate your client and server pricing tiers logic with the following functions:
- `check` to see if the customer is `allowed` to send a message.
- `track` a usage event in Autumn (typically done server-side)
- `customer` to display any relevant billing data in your UI (subscriptions, feature balances)
Server-side, you can access Autumn's functions through the `auth` object.
<Tabs items={["Client", "Server"]}>
<Tab value="Client">
```jsx
import { useCustomer } from "autumn-js/react";
export default function SendChatMessage() {
const { customer, allowed, refetch } = useCustomer();
return (
<>
<button
onClick={async () => {
if (allowed({ featureId: "messages" })) {
//... send chatbot message server-side, then
await refetch(); // refetch customer usage data
alert(
"Remaining messages: " + customer?.features.messages?.balance
);
} else {
alert("You're out of messages");
}
}}
>
Send Message
</button>
</>
);
}
```
</Tab>
<Tab value="Server">
```typescript Server
import { auth } from "@/lib/auth";
// check on the backend if the customer can send a message
const { allowed } = await auth.api.check({
headers: await headers(), // pass the request headers
body: {
feature_id: "messages",
},
});
// server-side function to send the message
// then track the usage
await auth.api.track({
headers: await headers(),
body: {
feature_id: "messages",
value: 2,
},
});
```
</Tab>
</Tabs>
### Additional Functions
#### openBillingPortal()
Opens a billing portal where the customer can update their payment method or cancel their plan.
```tsx
import { useCustomer } from "autumn-js/react";
export default function BillingSettings() {
const { openBillingPortal } = useCustomer();
return (
<button
onClick={async () => {
await openBillingPortal({
returnUrl: "/settings/billing",
});
}}
>
Manage Billing
</button>
);
}
```
#### cancel()
Cancel a product or subscription.
```tsx
import { useCustomer } from "autumn-js/react";
export default function CancelSubscription() {
const { cancel } = useCustomer();
return (
<button
onClick={async () => {
await cancel({ productId: "pro" });
}}
>
Cancel Subscription
</button>
);
}
```
#### Get invoice history
Pass in an `expand` param into `useCustomer` to get additional information. You can expand `invoices`, `trials_used`, `payment_method`, or `rewards`.
```tsx
import { useCustomer } from "autumn-js/react";
export default function CustomerProfile() {
const { customer } = useCustomer({ expand: ["invoices"] });
return (
<div>
<h2>Customer Profile</h2>
<p>Name: {customer?.name}</p>
<p>Email: {customer?.email}</p>
<p>Balance: {customer?.features.chat_messages?.balance}</p>
</div>
);
}
```

View File

@@ -25,6 +25,7 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
email: string;
password: string;
callbackURL?: string;
rememberMe?: boolean;
} & AdditionalUserFieldsInput<O>,
},
openapi: {
@@ -52,6 +53,11 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
description:
"The URL to use for email verification callback",
},
rememberMe: {
type: "boolean",
description:
"If this is false, the session will not be remembered. Default is `true`.",
},
},
required: ["name", "email", "password"],
},
@@ -139,11 +145,19 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
const body = ctx.body as any as User & {
password: string;
callbackURL?: string;
rememberMe?: boolean;
} & {
[key: string]: any;
};
const { name, email, password, image, callbackURL, ...additionalFields } =
body;
const {
name,
email,
password,
image,
callbackURL,
rememberMe,
...additionalFields
} = body;
const isValidEmail = z.string().email().safeParse(email);
if (!isValidEmail.success) {
@@ -275,16 +289,21 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
const session = await ctx.context.internalAdapter.createSession(
createdUser.id,
ctx,
rememberMe === false,
);
if (!session) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
});
}
await setSessionCookie(ctx, {
await setSessionCookie(
ctx,
{
session,
user: createdUser,
});
},
rememberMe === false,
);
return ctx.json({
token: session.token,
user: {

View File

@@ -18,6 +18,9 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
accessTokenExpiresAt: data.expires_in
? getDate(data.expires_in, "sec")
: undefined,
refreshTokenExpiresAt: data.refresh_token_expires_in
? getDate(data.refresh_token_expires_in, "sec")
: undefined,
scopes: data?.scope
? typeof data.scope === "string"
? data.scope.split(" ")

View File

@@ -56,6 +56,11 @@ export const username = (options?: UsernameOptions) => {
description: "Remember the user session",
})
.optional(),
callbackURL: z
.string({
description: "The URL to redirect to after the user signs in",
})
.optional(),
}),
metadata: {
openapi: {

View File

@@ -76,12 +76,15 @@ export const facebook = (options: FacebookOptions) => {
/* limited login */
// check is limited token
if (token.split(".").length) {
if (token.split(".").length === 3) {
try {
const { payload: jwtClaims } = await jwtVerify(
token,
createRemoteJWKSet(
new URL("https://www.facebook.com/.well-known/oauth/openid/jwks"),
// https://developers.facebook.com/docs/facebook-login/limited-login/token/#jwks
new URL(
"https://limited.facebook.com/.well-known/oauth/openid/jwks/",
),
),
{
algorithms: ["RS256"],
@@ -122,7 +125,7 @@ export const facebook = (options: FacebookOptions) => {
return options.getUserInfo(token);
}
if (token.idToken) {
if (token.idToken && token.idToken.split(".").length === 3) {
const profile = decodeJwt(token.idToken) as {
sub: string;
email: string;

View File

@@ -74,6 +74,9 @@ export const google = (options: GoogleOptions) => {
: ["email", "profile", "openid"];
options.scope && _scopes.push(...options.scope);
scopes && _scopes.push(...scopes);
if (options.prompt === "select_account+consent")
//@ts-expect-error - Google expects there to be a space not `+` in the prompt
options.prompt = "select_account consent";
const url = await createAuthorizationURL({
id: "google",
options,