#
tokens: 48086/50000 12/1100 files (page 21/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 21 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── proxy.ts
│       ├── public
│       │   ├── __og.png
│       │   ├── _og.png
│       │   ├── favicon
│       │   │   ├── android-chrome-192x192.png
│       │   │   ├── android-chrome-512x512.png
│       │   │   ├── apple-touch-icon.png
│       │   │   ├── favicon-16x16.png
│       │   │   ├── favicon-32x32.png
│       │   │   ├── favicon.ico
│       │   │   ├── light
│       │   │   │   ├── android-chrome-192x192.png
│       │   │   │   ├── android-chrome-512x512.png
│       │   │   │   ├── apple-touch-icon.png
│       │   │   │   ├── favicon-16x16.png
│       │   │   │   ├── favicon-32x32.png
│       │   │   │   ├── favicon.ico
│       │   │   │   └── site.webmanifest
│       │   │   └── site.webmanifest
│       │   ├── logo.svg
│       │   └── og.png
│       ├── README.md
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── turbo.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-declaration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── organization.ts
│       │   │   │   │   ├── user-additional-fields.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   ├── vitest.config.ts
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.base.json
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/docs/components/logo-context-menu.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import type React from "react";
  4 | import { useState, useRef, useEffect } from "react";
  5 | import { Code, Image, Type } from "lucide-react";
  6 | import { toast } from "sonner";
  7 | import { useTheme } from "next-themes";
  8 | import type { StaticImageData } from "next/image";
  9 | 
 10 | interface LogoAssets {
 11 | 	darkSvg: string;
 12 | 	whiteSvg: string;
 13 | 	darkWordmark: string;
 14 | 	whiteWordmark: string;
 15 | 	darkPng: StaticImageData;
 16 | 	whitePng: StaticImageData;
 17 | }
 18 | 
 19 | interface ContextMenuProps {
 20 | 	logo: React.ReactNode;
 21 | 	logoAssets: LogoAssets;
 22 | }
 23 | 
 24 | export default function LogoContextMenu({
 25 | 	logo,
 26 | 	logoAssets,
 27 | }: ContextMenuProps) {
 28 | 	const [showMenu, setShowMenu] = useState<boolean>(false);
 29 | 	const menuRef = useRef<HTMLDivElement>(null);
 30 | 	const logoRef = useRef<HTMLDivElement>(null);
 31 | 	const { theme } = useTheme();
 32 | 
 33 | 	const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
 34 | 		e.preventDefault();
 35 | 		e.stopPropagation();
 36 | 		const rect = logoRef.current?.getBoundingClientRect();
 37 | 		if (rect) {
 38 | 			setShowMenu(true);
 39 | 		}
 40 | 	};
 41 | 
 42 | 	const copySvgToClipboard = (
 43 | 		e: React.MouseEvent,
 44 | 		svgContent: string,
 45 | 		type: string,
 46 | 	) => {
 47 | 		e.preventDefault();
 48 | 		e.stopPropagation();
 49 | 		navigator.clipboard
 50 | 			.writeText(svgContent)
 51 | 			.then(() => {
 52 | 				toast.success("", {
 53 | 					description: `${type} copied to clipboard`,
 54 | 				});
 55 | 			})
 56 | 			.catch((err) => {
 57 | 				toast.error("", {
 58 | 					description: `Failed to copy ${type} to clipboard`,
 59 | 				});
 60 | 			});
 61 | 		setShowMenu(false);
 62 | 	};
 63 | 
 64 | 	const downloadPng = (
 65 | 		e: React.MouseEvent,
 66 | 		pngData: StaticImageData,
 67 | 		fileName: string,
 68 | 	) => {
 69 | 		e.preventDefault();
 70 | 		e.stopPropagation();
 71 | 		const link = document.createElement("a");
 72 | 		link.href = pngData.src;
 73 | 		link.download = fileName;
 74 | 
 75 | 		document.body.appendChild(link);
 76 | 		link.click();
 77 | 		document.body.removeChild(link);
 78 | 
 79 | 		toast.success(`Downloading the asset...`);
 80 | 
 81 | 		setShowMenu(false);
 82 | 	};
 83 | 
 84 | 	const downloadAllAssets = (e: React.MouseEvent) => {
 85 | 		e.preventDefault();
 86 | 		e.stopPropagation();
 87 | 		const link = document.createElement("a");
 88 | 		link.href = "/branding/better-auth-brand-assets.zip";
 89 | 		link.download = "better-auth-branding-assets.zip";
 90 | 
 91 | 		document.body.appendChild(link);
 92 | 		link.click();
 93 | 		document.body.removeChild(link);
 94 | 
 95 | 		toast.success("Downloading all assets...");
 96 | 		setShowMenu(false);
 97 | 	};
 98 | 
 99 | 	useEffect(() => {
100 | 		const handleClickOutside = (event: MouseEvent) => {
101 | 			if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
102 | 				setShowMenu(false);
103 | 			}
104 | 		};
105 | 
106 | 		document.addEventListener("mousedown", handleClickOutside);
107 | 		return () => {
108 | 			document.removeEventListener("mousedown", handleClickOutside);
109 | 		};
110 | 	}, []);
111 | 
112 | 	const getAsset = <T,>(darkAsset: T, lightAsset: T): T => {
113 | 		return theme === "dark" ? darkAsset : lightAsset;
114 | 	};
115 | 
116 | 	return (
117 | 		<div className="relative">
118 | 			<div
119 | 				ref={logoRef}
120 | 				onContextMenu={handleContextMenu}
121 | 				className="cursor-pointer"
122 | 			>
123 | 				{logo}
124 | 			</div>
125 | 
126 | 			{showMenu && (
127 | 				<div
128 | 					ref={menuRef}
129 | 					className="fixed mx-10 z-50 bg-white dark:bg-black border border-gray-200 dark:border-border p-1 rounded-sm shadow-xl w-56 overflow-hidden animate-fd-dialog-in duration-500"
130 | 				>
131 | 					<div className="">
132 | 						<div className="flex p-0 gap-1 flex-col text-xs">
133 | 							<button
134 | 								onClick={(e) =>
135 | 									copySvgToClipboard(
136 | 										e,
137 | 										getAsset(logoAssets.darkSvg, logoAssets.whiteSvg),
138 | 										"Logo SVG",
139 | 									)
140 | 								}
141 | 								className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer"
142 | 							>
143 | 								<div className="flex items-center">
144 | 									<span className="text-gray-400 dark:text-zinc-400/30">[</span>
145 | 
146 | 									<Code className="h-[13.8px] w-[13.8px] mx-[3px]" />
147 | 									<span className="text-gray-400 dark:text-zinc-400/30">]</span>
148 | 								</div>
149 | 								<span>Copy Logo as SVG </span>
150 | 							</button>
151 | 							<hr className="border-border/[60%]" />
152 | 							<button
153 | 								onClick={(e) =>
154 | 									copySvgToClipboard(
155 | 										e,
156 | 										getAsset(logoAssets.darkWordmark, logoAssets.whiteWordmark),
157 | 										"Logo Wordmark",
158 | 									)
159 | 								}
160 | 								className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer"
161 | 							>
162 | 								<div className="flex items-center">
163 | 									<span className="text-gray-400 dark:text-zinc-400/30">[</span>
164 | 
165 | 									<Type className="h-[13.8px] w-[13.8px] mx-[3px]" />
166 | 									<span className="text-gray-400 dark:text-zinc-400/30">]</span>
167 | 								</div>
168 | 								<span>Copy Logo as Wordmark </span>
169 | 							</button>
170 | 
171 | 							<hr className="border-border/[60%]" />
172 | 							<button
173 | 								onClick={(e) =>
174 | 									downloadPng(
175 | 										e,
176 | 										getAsset(logoAssets.darkPng, logoAssets.whitePng),
177 | 										`better-auth-logo-${theme}.png`,
178 | 									)
179 | 								}
180 | 								className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer"
181 | 							>
182 | 								<div className="flex items-center">
183 | 									<span className="text-gray-400 dark:text-zinc-400/30">[</span>
184 | 
185 | 									<Image className="h-[13.8px] w-[13.8px] mx-[3px]" />
186 | 									<span className="text-gray-400 dark:text-zinc-400/30">]</span>
187 | 								</div>
188 | 								<span>Download Logo PNG</span>
189 | 							</button>
190 | 							<hr className="borde-border" />
191 | 							<button
192 | 								onClick={(e) => downloadAllAssets(e)}
193 | 								className="flex items-center gap-3 w-full p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-zinc-900 rounded-md transition-colors cursor-pointer"
194 | 							>
195 | 								<div className="flex items-center">
196 | 									<span className="text-gray-400 dark:text-zinc-400/30">[</span>
197 | 
198 | 									<svg
199 | 										xmlns="http://www.w3.org/2000/svg"
200 | 										width="1em"
201 | 										height="1em"
202 | 										viewBox="0 0 24 24"
203 | 										className="h-[13.8px] w-[13.8px] mx-[3px]"
204 | 									>
205 | 										<path
206 | 											fill="none"
207 | 											stroke="currentColor"
208 | 											strokeLinecap="round"
209 | 											strokeLinejoin="round"
210 | 											strokeWidth="2"
211 | 											d="M4 8v8.8c0 1.12 0 1.68.218 2.108a2 2 0 0 0 .874.874c.427.218.987.218 2.105.218h9.606c1.118 0 1.677 0 2.104-.218c.377-.192.683-.498.875-.874c.218-.428.218-.987.218-2.105V8M4 8h16M4 8l1.365-2.39c.335-.585.503-.878.738-1.092c.209-.189.456-.332.723-.42C7.13 4 7.466 4 8.143 4h7.714c.676 0 1.015 0 1.318.099c.267.087.513.23.721.42c.236.213.404.506.74 1.093L20 8m-8 3v6m0 0l3-2m-3 2l-3-2"
212 | 										></path>
213 | 									</svg>
214 | 									<span className="text-gray-400 dark:text-zinc-400/30">]</span>
215 | 								</div>
216 | 								<span>Brand Assets</span>
217 | 							</button>
218 | 						</div>
219 | 					</div>
220 | 				</div>
221 | 			)}
222 | 		</div>
223 | 	);
224 | }
225 | 
```

--------------------------------------------------------------------------------
/docs/app/docs/[[...slug]]/page.client.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | import { useState, useTransition } from "react";
  3 | import {
  4 | 	Check,
  5 | 	Copy,
  6 | 	ChevronDown,
  7 | 	ExternalLink,
  8 | 	MessageCircle,
  9 | } from "lucide-react";
 10 | import { cn } from "@/lib/utils";
 11 | import { buttonVariants } from "@/components/ui/button";
 12 | import {
 13 | 	Popover,
 14 | 	PopoverContent,
 15 | 	PopoverTrigger,
 16 | } from "fumadocs-ui/components/ui/popover";
 17 | import { cva } from "class-variance-authority";
 18 | 
 19 | import { type MouseEventHandler, useEffect, useRef } from "react";
 20 | import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
 21 | 
 22 | export function useCopyButton(
 23 | 	onCopy: () => void | Promise<void>,
 24 | ): [checked: boolean, onClick: MouseEventHandler] {
 25 | 	const [checked, setChecked] = useState(false);
 26 | 	const timeoutRef = useRef<number | null>(null);
 27 | 
 28 | 	const onClick: MouseEventHandler = useEffectEvent(() => {
 29 | 		if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
 30 | 		const res = Promise.resolve(onCopy());
 31 | 
 32 | 		void res.then(() => {
 33 | 			setChecked(true);
 34 | 			timeoutRef.current = window.setTimeout(() => {
 35 | 				setChecked(false);
 36 | 			}, 1500);
 37 | 		});
 38 | 	});
 39 | 
 40 | 	// Avoid updates after being unmounted
 41 | 	useEffect(() => {
 42 | 		return () => {
 43 | 			if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
 44 | 		};
 45 | 	}, []);
 46 | 
 47 | 	return [checked, onClick];
 48 | }
 49 | 
 50 | const cache = new Map<string, string>();
 51 | 
 52 | export function LLMCopyButton() {
 53 | 	const [isLoading, startTransition] = useTransition();
 54 | 	const [checked, onClick] = useCopyButton(async () => {
 55 | 		startTransition(async () => {
 56 | 			const url = window.location.pathname + ".mdx";
 57 | 			const cached = cache.get(url);
 58 | 
 59 | 			if (cached) {
 60 | 				await navigator.clipboard.writeText(cached);
 61 | 			} else {
 62 | 				await navigator.clipboard.write([
 63 | 					new ClipboardItem({
 64 | 						"text/plain": fetch(url).then(async (res) => {
 65 | 							const content = await res.text();
 66 | 							cache.set(url, content);
 67 | 
 68 | 							return content;
 69 | 						}),
 70 | 					}),
 71 | 				]);
 72 | 			}
 73 | 		});
 74 | 	});
 75 | 
 76 | 	return (
 77 | 		<button
 78 | 			disabled={isLoading}
 79 | 			className={cn(
 80 | 				buttonVariants({
 81 | 					variant: "secondary",
 82 | 					size: "sm",
 83 | 					className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground",
 84 | 				}),
 85 | 			)}
 86 | 			onClick={onClick}
 87 | 		>
 88 | 			{checked ? <Check /> : <Copy />}
 89 | 			Copy Markdown
 90 | 		</button>
 91 | 	);
 92 | }
 93 | 
 94 | const optionVariants = cva(
 95 | 	"text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4",
 96 | );
 97 | 
 98 | export function ViewOptions(props: { markdownUrl: string; githubUrl: string }) {
 99 | 	const markdownUrl = new URL(props.markdownUrl, "https://better-auth.com");
100 | 	const q = `Read ${markdownUrl}, I want to ask questions about it.`;
101 | 
102 | 	const claude = `https://claude.ai/new?${new URLSearchParams({
103 | 		q,
104 | 	})}`;
105 | 	const gpt = `https://chatgpt.com/?${new URLSearchParams({
106 | 		hints: "search",
107 | 		q,
108 | 	})}`;
109 | 	const t3 = `https://t3.chat/new?${new URLSearchParams({
110 | 		q,
111 | 	})}`;
112 | 
113 | 	return (
114 | 		<Popover>
115 | 			<PopoverTrigger
116 | 				className={cn(
117 | 					buttonVariants({
118 | 						variant: "secondary",
119 | 						size: "sm",
120 | 						className: "gap-2",
121 | 					}),
122 | 				)}
123 | 			>
124 | 				Open in
125 | 				<ChevronDown className="size-3.5 text-fd-muted-foreground" />
126 | 			</PopoverTrigger>
127 | 			<PopoverContent className="flex flex-col overflow-auto">
128 | 				{[
129 | 					{
130 | 						title: "Open in GitHub",
131 | 						href: props.githubUrl,
132 | 						icon: (
133 | 							<svg fill="currentColor" role="img" viewBox="0 0 24 24">
134 | 								<title>GitHub</title>
135 | 								<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
136 | 							</svg>
137 | 						),
138 | 					},
139 | 					{
140 | 						title: "Open in ChatGPT",
141 | 						href: gpt,
142 | 						icon: (
143 | 							<svg
144 | 								role="img"
145 | 								viewBox="0 0 24 24"
146 | 								fill="currentColor"
147 | 								xmlns="http://www.w3.org/2000/svg"
148 | 							>
149 | 								<title>OpenAI</title>
150 | 								<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
151 | 							</svg>
152 | 						),
153 | 					},
154 | 					{
155 | 						title: "Open in Claude",
156 | 						href: claude,
157 | 						icon: (
158 | 							<svg
159 | 								fill="currentColor"
160 | 								role="img"
161 | 								viewBox="0 0 24 24"
162 | 								xmlns="http://www.w3.org/2000/svg"
163 | 							>
164 | 								<title>Anthropic</title>
165 | 								<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
166 | 							</svg>
167 | 						),
168 | 					},
169 | 					{
170 | 						title: "Open in T3 Chat",
171 | 						href: t3,
172 | 						icon: <MessageCircle />,
173 | 					},
174 | 				].map((item) => (
175 | 					<a
176 | 						key={item.href}
177 | 						href={item.href}
178 | 						rel="noreferrer noopener"
179 | 						target="_blank"
180 | 						className={cn(optionVariants())}
181 | 					>
182 | 						{item.icon}
183 | 						{item.title}
184 | 						<ExternalLink className="text-fd-muted-foreground size-3.5 ms-auto" />
185 | 					</a>
186 | 				))}
187 | 			</PopoverContent>
188 | 		</Popover>
189 | 	);
190 | }
191 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/commands/mcp.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Command } from "commander";
  2 | import { execSync } from "child_process";
  3 | import * as os from "os";
  4 | import * as fs from "fs";
  5 | import * as path from "path";
  6 | import chalk from "chalk";
  7 | import { base64 } from "@better-auth/utils/base64";
  8 | 
  9 | interface MCPOptions {
 10 | 	cursor?: boolean;
 11 | 	claudeCode?: boolean;
 12 | 	openCode?: boolean;
 13 | 	manual?: boolean;
 14 | }
 15 | 
 16 | export async function mcpAction(options: MCPOptions) {
 17 | 	const mcpUrl = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
 18 | 	const mcpName = "Better Auth";
 19 | 
 20 | 	if (options.cursor) {
 21 | 		await handleCursorAction(mcpUrl, mcpName);
 22 | 	} else if (options.claudeCode) {
 23 | 		handleClaudeCodeAction(mcpUrl);
 24 | 	} else if (options.openCode) {
 25 | 		handleOpenCodeAction(mcpUrl);
 26 | 	} else if (options.manual) {
 27 | 		handleManualAction(mcpUrl, mcpName);
 28 | 	} else {
 29 | 		showAllOptions(mcpUrl, mcpName);
 30 | 	}
 31 | }
 32 | 
 33 | async function handleCursorAction(mcpUrl: string, mcpName: string) {
 34 | 	const mcpConfig = {
 35 | 		url: mcpUrl,
 36 | 	};
 37 | 
 38 | 	const encodedConfig = base64.encode(
 39 | 		new TextEncoder().encode(JSON.stringify(mcpConfig)),
 40 | 	);
 41 | 	const deeplinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(mcpName)}&config=${encodedConfig}`;
 42 | 
 43 | 	console.log(chalk.bold.blue("🚀 Adding Better Auth MCP to Cursor..."));
 44 | 
 45 | 	try {
 46 | 		const platform = os.platform();
 47 | 		let command: string;
 48 | 
 49 | 		switch (platform) {
 50 | 			case "darwin":
 51 | 				command = `open "${deeplinkUrl}"`;
 52 | 				break;
 53 | 			case "win32":
 54 | 				command = `start "" "${deeplinkUrl}"`;
 55 | 				break;
 56 | 			case "linux":
 57 | 				command = `xdg-open "${deeplinkUrl}"`;
 58 | 				break;
 59 | 			default:
 60 | 				throw new Error(`Unsupported platform: ${platform}`);
 61 | 		}
 62 | 
 63 | 		execSync(command, { stdio: "inherit" });
 64 | 		console.log(chalk.green("\n✓ Cursor MCP installed successfully!"));
 65 | 	} catch (error) {
 66 | 		console.log(
 67 | 			chalk.yellow(
 68 | 				"\n⚠ Could not automatically open Cursor. Please copy the deeplink URL above and open it manually.",
 69 | 			),
 70 | 		);
 71 | 		console.log(
 72 | 			chalk.gray(
 73 | 				"\nYou can also manually add this configuration to your Cursor MCP settings:",
 74 | 			),
 75 | 		);
 76 | 		console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)));
 77 | 	}
 78 | 
 79 | 	console.log(chalk.bold.white("\n✨ Next Steps:"));
 80 | 	console.log(
 81 | 		chalk.gray("• The MCP server will be added to your Cursor configuration"),
 82 | 	);
 83 | 	console.log(
 84 | 		chalk.gray("• You can now use Better Auth features directly in Cursor"),
 85 | 	);
 86 | }
 87 | 
 88 | function handleClaudeCodeAction(mcpUrl: string) {
 89 | 	console.log(chalk.bold.blue("🤖 Adding Better Auth MCP to Claude Code..."));
 90 | 
 91 | 	const command = `claude mcp add --transport http better-auth ${mcpUrl}`;
 92 | 
 93 | 	try {
 94 | 		execSync(command, { stdio: "inherit" });
 95 | 		console.log(chalk.green("\n✓ Claude Code MCP installed successfully!"));
 96 | 	} catch (error) {
 97 | 		console.log(
 98 | 			chalk.yellow(
 99 | 				"\n⚠ Could not automatically add to Claude Code. Please run this command manually:",
100 | 			),
101 | 		);
102 | 		console.log(chalk.cyan(command));
103 | 	}
104 | 
105 | 	console.log(chalk.bold.white("\n✨ Next Steps:"));
106 | 	console.log(
107 | 		chalk.gray(
108 | 			"• The MCP server will be added to your Claude Code configuration",
109 | 		),
110 | 	);
111 | 	console.log(
112 | 		chalk.gray(
113 | 			"• You can now use Better Auth features directly in Claude Code",
114 | 		),
115 | 	);
116 | }
117 | 
118 | function handleOpenCodeAction(mcpUrl: string) {
119 | 	console.log(chalk.bold.blue("🔧 Adding Better Auth MCP to Open Code..."));
120 | 
121 | 	const openCodeConfig = {
122 | 		$schema: "https://opencode.ai/config.json",
123 | 		mcp: {
124 | 			"Better Auth": {
125 | 				type: "remote",
126 | 				url: mcpUrl,
127 | 				enabled: true,
128 | 			},
129 | 		},
130 | 	};
131 | 
132 | 	const configPath = path.join(process.cwd(), "opencode.json");
133 | 
134 | 	try {
135 | 		let existingConfig: {
136 | 			mcp?: Record<string, unknown>;
137 | 			[key: string]: unknown;
138 | 		} = {};
139 | 		if (fs.existsSync(configPath)) {
140 | 			const existingContent = fs.readFileSync(configPath, "utf8");
141 | 			existingConfig = JSON.parse(existingContent);
142 | 		}
143 | 
144 | 		const mergedConfig = {
145 | 			...existingConfig,
146 | 			...openCodeConfig,
147 | 			mcp: {
148 | 				...existingConfig.mcp,
149 | 				...openCodeConfig.mcp,
150 | 			},
151 | 		};
152 | 
153 | 		fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
154 | 		console.log(
155 | 			chalk.green(`\n✓ Open Code configuration written to ${configPath}`),
156 | 		);
157 | 		console.log(chalk.green("✓ Better Auth MCP added successfully!"));
158 | 	} catch (error) {
159 | 		console.log(
160 | 			chalk.yellow(
161 | 				"\n⚠ Could not automatically write opencode.json. Please add this configuration manually:",
162 | 			),
163 | 		);
164 | 		console.log(chalk.cyan(JSON.stringify(openCodeConfig, null, 2)));
165 | 	}
166 | 
167 | 	console.log(chalk.bold.white("\n✨ Next Steps:"));
168 | 	console.log(chalk.gray("• Restart Open Code to load the new MCP server"));
169 | 	console.log(
170 | 		chalk.gray("• You can now use Better Auth features directly in Open Code"),
171 | 	);
172 | }
173 | 
174 | function handleManualAction(mcpUrl: string, mcpName: string) {
175 | 	console.log(chalk.bold.blue("📝 Adding Better Auth MCP Configuration..."));
176 | 
177 | 	const manualConfig = {
178 | 		[mcpName]: {
179 | 			url: mcpUrl,
180 | 		},
181 | 	};
182 | 
183 | 	const configPath = path.join(process.cwd(), "mcp.json");
184 | 
185 | 	try {
186 | 		let existingConfig = {};
187 | 		if (fs.existsSync(configPath)) {
188 | 			const existingContent = fs.readFileSync(configPath, "utf8");
189 | 			existingConfig = JSON.parse(existingContent);
190 | 		}
191 | 
192 | 		const mergedConfig = {
193 | 			...existingConfig,
194 | 			...manualConfig,
195 | 		};
196 | 
197 | 		fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
198 | 		console.log(chalk.green(`\n✓ MCP configuration written to ${configPath}`));
199 | 		console.log(chalk.green("✓ Better Auth MCP added successfully!"));
200 | 	} catch (error) {
201 | 		console.log(
202 | 			chalk.yellow(
203 | 				"\n⚠ Could not automatically write mcp.json. Please add this configuration manually:",
204 | 			),
205 | 		);
206 | 		console.log(chalk.cyan(JSON.stringify(manualConfig, null, 2)));
207 | 	}
208 | 
209 | 	console.log(chalk.bold.white("\n✨ Next Steps:"));
210 | 	console.log(chalk.gray("• Restart your MCP client to load the new server"));
211 | 	console.log(
212 | 		chalk.gray(
213 | 			"• You can now use Better Auth features directly in your MCP client",
214 | 		),
215 | 	);
216 | }
217 | 
218 | function showAllOptions(mcpUrl: string, mcpName: string) {
219 | 	console.log(chalk.bold.blue("🔌 Better Auth MCP Server"));
220 | 	console.log(chalk.gray("Choose your MCP client to get started:"));
221 | 	console.log();
222 | 
223 | 	console.log(chalk.bold.white("Available Commands:"));
224 | 	console.log(chalk.cyan("  --cursor      ") + chalk.gray("Add to Cursor"));
225 | 	console.log(
226 | 		chalk.cyan("  --claude-code ") + chalk.gray("Add to Claude Code"),
227 | 	);
228 | 	console.log(chalk.cyan("  --open-code   ") + chalk.gray("Add to Open Code"));
229 | 	console.log(
230 | 		chalk.cyan("  --manual      ") + chalk.gray("Manual configuration"),
231 | 	);
232 | 	console.log();
233 | }
234 | 
235 | export const mcp = new Command("mcp")
236 | 	.description("Add Better Auth MCP server to MCP Clients")
237 | 	.option("--cursor", "Automatically open Cursor with the MCP configuration")
238 | 	.option("--claude-code", "Show Claude Code MCP configuration command")
239 | 	.option("--open-code", "Show Open Code MCP configuration")
240 | 	.option("--manual", "Show manual MCP configuration for mcp.json")
241 | 	.action(mcpAction);
242 | 
```

--------------------------------------------------------------------------------
/packages/core/src/types/context.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type {
  2 | 	Account,
  3 | 	BetterAuthDBSchema,
  4 | 	SecondaryStorage,
  5 | 	Session,
  6 | 	User,
  7 | 	Verification,
  8 | } from "../db";
  9 | import type { OAuthProvider } from "../oauth2";
 10 | import { createLogger } from "../env";
 11 | import type { DBAdapter, Where } from "../db/adapter";
 12 | import type { BetterAuthCookies } from "./cookie";
 13 | import type { DBPreservedModels } from "../db";
 14 | import type { LiteralUnion } from "./helper";
 15 | import type { CookieOptions, EndpointContext } from "better-call";
 16 | import type {
 17 | 	BetterAuthOptions,
 18 | 	BetterAuthRateLimitOptions,
 19 | } from "./init-options";
 20 | 
 21 | export type GenericEndpointContext<
 22 | 	Options extends BetterAuthOptions = BetterAuthOptions,
 23 | > = EndpointContext<string, any> & {
 24 | 	context: AuthContext<Options>;
 25 | };
 26 | 
 27 | export interface InternalAdapter<
 28 | 	Options extends BetterAuthOptions = BetterAuthOptions,
 29 | > {
 30 | 	createOAuthUser(
 31 | 		user: Omit<User, "id" | "createdAt" | "updatedAt">,
 32 | 		account: Omit<Account, "userId" | "id" | "createdAt" | "updatedAt"> &
 33 | 			Partial<Account>,
 34 | 	): Promise<{ user: User; account: Account }>;
 35 | 
 36 | 	createUser<T extends Record<string, any>>(
 37 | 		user: Omit<User, "id" | "createdAt" | "updatedAt" | "emailVerified"> &
 38 | 			Partial<User> &
 39 | 			Record<string, any>,
 40 | 	): Promise<T & User>;
 41 | 
 42 | 	createAccount<T extends Record<string, any>>(
 43 | 		account: Omit<Account, "id" | "createdAt" | "updatedAt"> &
 44 | 			Partial<Account> &
 45 | 			T,
 46 | 	): Promise<T & Account>;
 47 | 
 48 | 	listSessions(userId: string): Promise<Session[]>;
 49 | 
 50 | 	listUsers(
 51 | 		limit?: number,
 52 | 		offset?: number,
 53 | 		sortBy?: { field: string; direction: "asc" | "desc" },
 54 | 		where?: Where[],
 55 | 	): Promise<User[]>;
 56 | 
 57 | 	countTotalUsers(where?: Where[]): Promise<number>;
 58 | 
 59 | 	deleteUser(userId: string): Promise<void>;
 60 | 
 61 | 	createSession(
 62 | 		userId: string,
 63 | 		dontRememberMe?: boolean,
 64 | 		override?: Partial<Session> & Record<string, any>,
 65 | 		overrideAll?: boolean,
 66 | 	): Promise<Session>;
 67 | 
 68 | 	findSession(token: string): Promise<{
 69 | 		session: Session & Record<string, any>;
 70 | 		user: User & Record<string, any>;
 71 | 	} | null>;
 72 | 
 73 | 	findSessions(
 74 | 		sessionTokens: string[],
 75 | 	): Promise<{ session: Session; user: User }[]>;
 76 | 
 77 | 	updateSession(
 78 | 		sessionToken: string,
 79 | 		session: Partial<Session> & Record<string, any>,
 80 | 	): Promise<Session | null>;
 81 | 
 82 | 	deleteSession(token: string): Promise<void>;
 83 | 
 84 | 	deleteAccounts(userId: string): Promise<void>;
 85 | 
 86 | 	deleteAccount(accountId: string): Promise<void>;
 87 | 
 88 | 	deleteSessions(userIdOrSessionTokens: string | string[]): Promise<void>;
 89 | 
 90 | 	findOAuthUser(
 91 | 		email: string,
 92 | 		accountId: string,
 93 | 		providerId: string,
 94 | 	): Promise<{ user: User; accounts: Account[] } | null>;
 95 | 
 96 | 	findUserByEmail(
 97 | 		email: string,
 98 | 		options?: { includeAccounts: boolean },
 99 | 	): Promise<{ user: User; accounts: Account[] } | null>;
100 | 
101 | 	findUserById(userId: string): Promise<User | null>;
102 | 
103 | 	linkAccount(
104 | 		account: Omit<Account, "id" | "createdAt" | "updatedAt"> & Partial<Account>,
105 | 	): Promise<Account>;
106 | 
107 | 	// fixme: any type
108 | 	updateUser(
109 | 		userId: string,
110 | 		data: Partial<User> & Record<string, any>,
111 | 	): Promise<any>;
112 | 
113 | 	updateUserByEmail(
114 | 		email: string,
115 | 		data: Partial<User & Record<string, any>>,
116 | 	): Promise<User>;
117 | 
118 | 	updatePassword(userId: string, password: string): Promise<void>;
119 | 
120 | 	findAccounts(userId: string): Promise<Account[]>;
121 | 
122 | 	findAccount(accountId: string): Promise<Account | null>;
123 | 
124 | 	findAccountByProviderId(
125 | 		accountId: string,
126 | 		providerId: string,
127 | 	): Promise<Account | null>;
128 | 
129 | 	findAccountByUserId(userId: string): Promise<Account[]>;
130 | 
131 | 	updateAccount(id: string, data: Partial<Account>): Promise<Account>;
132 | 
133 | 	createVerificationValue(
134 | 		data: Omit<Verification, "createdAt" | "id" | "updatedAt"> &
135 | 			Partial<Verification>,
136 | 	): Promise<Verification>;
137 | 
138 | 	findVerificationValue(identifier: string): Promise<Verification | null>;
139 | 
140 | 	deleteVerificationValue(id: string): Promise<void>;
141 | 
142 | 	deleteVerificationByIdentifier(identifier: string): Promise<void>;
143 | 
144 | 	updateVerificationValue(
145 | 		id: string,
146 | 		data: Partial<Verification>,
147 | 	): Promise<Verification>;
148 | }
149 | 
150 | type CreateCookieGetterFn = (
151 | 	cookieName: string,
152 | 	overrideAttributes?: Partial<CookieOptions>,
153 | ) => {
154 | 	name: string;
155 | 	attributes: CookieOptions;
156 | };
157 | 
158 | type CheckPasswordFn<Options extends BetterAuthOptions = BetterAuthOptions> = (
159 | 	userId: string,
160 | 	ctx: GenericEndpointContext<Options>,
161 | ) => Promise<boolean>;
162 | 
163 | export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
164 | 	{
165 | 		options: Options;
166 | 		appName: string;
167 | 		baseURL: string;
168 | 		trustedOrigins: string[];
169 | 		oauthConfig?: {
170 | 			/**
171 | 			 * This is dangerous and should only be used in dev or staging environments.
172 | 			 */
173 | 			skipStateCookieCheck?: boolean;
174 | 		};
175 | 		/**
176 | 		 * New session that will be set after the request
177 | 		 * meaning: there is a `set-cookie` header that will set
178 | 		 * the session cookie. This is the fetched session. And it's set
179 | 		 * by `setNewSession` method.
180 | 		 */
181 | 		newSession: {
182 | 			session: Session & Record<string, any>;
183 | 			user: User & Record<string, any>;
184 | 		} | null;
185 | 		session: {
186 | 			session: Session & Record<string, any>;
187 | 			user: User & Record<string, any>;
188 | 		} | null;
189 | 		setNewSession: (
190 | 			session: {
191 | 				session: Session & Record<string, any>;
192 | 				user: User & Record<string, any>;
193 | 			} | null,
194 | 		) => void;
195 | 		socialProviders: OAuthProvider[];
196 | 		authCookies: BetterAuthCookies;
197 | 		logger: ReturnType<typeof createLogger>;
198 | 		rateLimit: {
199 | 			enabled: boolean;
200 | 			window: number;
201 | 			max: number;
202 | 			storage: "memory" | "database" | "secondary-storage";
203 | 		} & BetterAuthRateLimitOptions;
204 | 		adapter: DBAdapter<Options>;
205 | 		internalAdapter: InternalAdapter<Options>;
206 | 		createAuthCookie: CreateCookieGetterFn;
207 | 		secret: string;
208 | 		sessionConfig: {
209 | 			updateAge: number;
210 | 			expiresIn: number;
211 | 			freshAge: number;
212 | 		};
213 | 		generateId: (options: {
214 | 			model: LiteralUnion<DBPreservedModels, string>;
215 | 			size?: number;
216 | 		}) => string | false;
217 | 		secondaryStorage: SecondaryStorage | undefined;
218 | 		password: {
219 | 			hash: (password: string) => Promise<string>;
220 | 			verify: (data: { password: string; hash: string }) => Promise<boolean>;
221 | 			config: {
222 | 				minPasswordLength: number;
223 | 				maxPasswordLength: number;
224 | 			};
225 | 			checkPassword: CheckPasswordFn<Options>;
226 | 		};
227 | 		tables: BetterAuthDBSchema;
228 | 		runMigrations: () => Promise<void>;
229 | 		publishTelemetry: (event: {
230 | 			type: string;
231 | 			anonymousId?: string;
232 | 			payload: Record<string, any>;
233 | 		}) => Promise<void>;
234 | 		/**
235 | 		 * This skips the origin check for all requests.
236 | 		 *
237 | 		 * set to true by default for `test` environments and `false`
238 | 		 * for other environments.
239 | 		 *
240 | 		 * It's inferred from the `options.advanced?.disableCSRFCheck`
241 | 		 * option or `options.advanced?.disableOriginCheck` option.
242 | 		 *
243 | 		 * @default false
244 | 		 */
245 | 		skipOriginCheck: boolean;
246 | 		/**
247 | 		 * This skips the CSRF check for all requests.
248 | 		 *
249 | 		 * This is inferred from the `options.advanced?.
250 | 		 * disableCSRFCheck` option.
251 | 		 *
252 | 		 * @default false
253 | 		 */
254 | 		skipCSRFCheck: boolean;
255 | 	};
256 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/routes/callback.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as z from "zod";
  2 | import { setSessionCookie } from "../../cookies";
  3 | import { setTokenUtil } from "../../oauth2/utils";
  4 | import { handleOAuthUserInfo } from "../../oauth2/link-account";
  5 | import { parseState } from "../../oauth2/state";
  6 | import { HIDE_METADATA } from "../../utils/hide-metadata";
  7 | import { createAuthEndpoint } from "@better-auth/core/api";
  8 | import { safeJSONParse } from "../../utils/json";
  9 | import type { OAuth2Tokens } from "@better-auth/core/oauth2";
 10 | 
 11 | const schema = z.object({
 12 | 	code: z.string().optional(),
 13 | 	error: z.string().optional(),
 14 | 	device_id: z.string().optional(),
 15 | 	error_description: z.string().optional(),
 16 | 	state: z.string().optional(),
 17 | 	user: z.string().optional(),
 18 | });
 19 | 
 20 | export const callbackOAuth = createAuthEndpoint(
 21 | 	"/callback/:id",
 22 | 	{
 23 | 		method: ["GET", "POST"],
 24 | 		body: schema.optional(),
 25 | 		query: schema.optional(),
 26 | 		metadata: HIDE_METADATA,
 27 | 	},
 28 | 	async (c) => {
 29 | 		let queryOrBody: z.infer<typeof schema>;
 30 | 		const defaultErrorURL =
 31 | 			c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
 32 | 		try {
 33 | 			if (c.method === "GET") {
 34 | 				queryOrBody = schema.parse(c.query);
 35 | 			} else if (c.method === "POST") {
 36 | 				queryOrBody = schema.parse(c.body);
 37 | 			} else {
 38 | 				throw new Error("Unsupported method");
 39 | 			}
 40 | 		} catch (e) {
 41 | 			c.context.logger.error("INVALID_CALLBACK_REQUEST", e);
 42 | 			throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
 43 | 		}
 44 | 
 45 | 		const { code, error, state, error_description, device_id } = queryOrBody;
 46 | 
 47 | 		if (!state) {
 48 | 			c.context.logger.error("State not found", error);
 49 | 			const sep = defaultErrorURL.includes("?") ? "&" : "?";
 50 | 			const url = `${defaultErrorURL}${sep}state=state_not_found`;
 51 | 			throw c.redirect(url);
 52 | 		}
 53 | 
 54 | 		const {
 55 | 			codeVerifier,
 56 | 			callbackURL,
 57 | 			link,
 58 | 			errorURL,
 59 | 			newUserURL,
 60 | 			requestSignUp,
 61 | 		} = await parseState(c);
 62 | 
 63 | 		function redirectOnError(error: string, description?: string) {
 64 | 			const baseURL = errorURL ?? defaultErrorURL;
 65 | 
 66 | 			const params = new URLSearchParams({ error });
 67 | 			if (description) params.set("error_description", description);
 68 | 
 69 | 			const sep = baseURL.includes("?") ? "&" : "?";
 70 | 			const url = `${baseURL}${sep}${params.toString()}`;
 71 | 
 72 | 			throw c.redirect(url);
 73 | 		}
 74 | 
 75 | 		if (error) {
 76 | 			redirectOnError(error, error_description);
 77 | 		}
 78 | 
 79 | 		if (!code) {
 80 | 			c.context.logger.error("Code not found");
 81 | 			throw redirectOnError("no_code");
 82 | 		}
 83 | 		const provider = c.context.socialProviders.find(
 84 | 			(p) => p.id === c.params.id,
 85 | 		);
 86 | 
 87 | 		if (!provider) {
 88 | 			c.context.logger.error(
 89 | 				"Oauth provider with id",
 90 | 				c.params.id,
 91 | 				"not found",
 92 | 			);
 93 | 			throw redirectOnError("oauth_provider_not_found");
 94 | 		}
 95 | 
 96 | 		let tokens: OAuth2Tokens;
 97 | 		try {
 98 | 			tokens = await provider.validateAuthorizationCode({
 99 | 				code: code,
100 | 				codeVerifier,
101 | 				deviceId: device_id,
102 | 				redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
103 | 			});
104 | 		} catch (e) {
105 | 			c.context.logger.error("", e);
106 | 			throw redirectOnError("invalid_code");
107 | 		}
108 | 		const userInfo = await provider
109 | 			.getUserInfo({
110 | 				...tokens,
111 | 				user: c.body?.user ? safeJSONParse<any>(c.body.user) : undefined,
112 | 			})
113 | 			.then((res) => res?.user);
114 | 
115 | 		if (!userInfo) {
116 | 			c.context.logger.error("Unable to get user info");
117 | 			return redirectOnError("unable_to_get_user_info");
118 | 		}
119 | 
120 | 		if (!callbackURL) {
121 | 			c.context.logger.error("No callback URL found");
122 | 			throw redirectOnError("no_callback_url");
123 | 		}
124 | 
125 | 		if (link) {
126 | 			const trustedProviders =
127 | 				c.context.options.account?.accountLinking?.trustedProviders;
128 | 			const isTrustedProvider = trustedProviders?.includes(
129 | 				provider.id as "apple",
130 | 			);
131 | 			if (
132 | 				(!isTrustedProvider && !userInfo.emailVerified) ||
133 | 				c.context.options.account?.accountLinking?.enabled === false
134 | 			) {
135 | 				c.context.logger.error("Unable to link account - untrusted provider");
136 | 				return redirectOnError("unable_to_link_account");
137 | 			}
138 | 
139 | 			if (
140 | 				userInfo.email !== link.email &&
141 | 				c.context.options.account?.accountLinking?.allowDifferentEmails !== true
142 | 			) {
143 | 				return redirectOnError("email_doesn't_match");
144 | 			}
145 | 
146 | 			const existingAccount = await c.context.internalAdapter.findAccount(
147 | 				String(userInfo.id),
148 | 			);
149 | 
150 | 			if (existingAccount) {
151 | 				if (existingAccount.userId.toString() !== link.userId.toString()) {
152 | 					return redirectOnError("account_already_linked_to_different_user");
153 | 				}
154 | 				const updateData = Object.fromEntries(
155 | 					Object.entries({
156 | 						accessToken: await setTokenUtil(tokens.accessToken, c.context),
157 | 						refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
158 | 						idToken: tokens.idToken,
159 | 						accessTokenExpiresAt: tokens.accessTokenExpiresAt,
160 | 						refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
161 | 						scope: tokens.scopes?.join(","),
162 | 					}).filter(([_, value]) => value !== undefined),
163 | 				);
164 | 				await c.context.internalAdapter.updateAccount(
165 | 					existingAccount.id,
166 | 					updateData,
167 | 				);
168 | 			} else {
169 | 				const newAccount = await c.context.internalAdapter.createAccount({
170 | 					userId: link.userId,
171 | 					providerId: provider.id,
172 | 					accountId: String(userInfo.id),
173 | 					...tokens,
174 | 					accessToken: await setTokenUtil(tokens.accessToken, c.context),
175 | 					refreshToken: await setTokenUtil(tokens.refreshToken, c.context),
176 | 					scope: tokens.scopes?.join(","),
177 | 				});
178 | 				if (!newAccount) {
179 | 					return redirectOnError("unable_to_link_account");
180 | 				}
181 | 			}
182 | 			let toRedirectTo: string;
183 | 			try {
184 | 				const url = callbackURL;
185 | 				toRedirectTo = url.toString();
186 | 			} catch {
187 | 				toRedirectTo = callbackURL;
188 | 			}
189 | 			throw c.redirect(toRedirectTo);
190 | 		}
191 | 
192 | 		if (!userInfo.email) {
193 | 			c.context.logger.error(
194 | 				"Provider did not return email. This could be due to misconfiguration in the provider settings.",
195 | 			);
196 | 			return redirectOnError("email_not_found");
197 | 		}
198 | 
199 | 		const result = await handleOAuthUserInfo(c, {
200 | 			userInfo: {
201 | 				...userInfo,
202 | 				id: String(userInfo.id),
203 | 				email: userInfo.email,
204 | 				name: userInfo.name || userInfo.email,
205 | 			},
206 | 			account: {
207 | 				providerId: provider.id,
208 | 				accountId: String(userInfo.id),
209 | 				...tokens,
210 | 				scope: tokens.scopes?.join(","),
211 | 			},
212 | 			callbackURL,
213 | 			disableSignUp:
214 | 				(provider.disableImplicitSignUp && !requestSignUp) ||
215 | 				provider.options?.disableSignUp,
216 | 			overrideUserInfo: provider.options?.overrideUserInfoOnSignIn,
217 | 		});
218 | 		if (result.error) {
219 | 			c.context.logger.error(result.error.split(" ").join("_"));
220 | 			return redirectOnError(result.error.split(" ").join("_"));
221 | 		}
222 | 		const { session, user } = result.data!;
223 | 		await setSessionCookie(c, {
224 | 			session,
225 | 			user,
226 | 		});
227 | 		let toRedirectTo: string;
228 | 		try {
229 | 			const url = result.isRegister ? newUserURL || callbackURL : callbackURL;
230 | 			toRedirectTo = url.toString();
231 | 		} catch {
232 | 			toRedirectTo = result.isRegister
233 | 				? newUserURL || callbackURL
234 | 				: callbackURL;
235 | 		}
236 | 		throw c.redirect(toRedirectTo);
237 | 	},
238 | );
239 | 
```

--------------------------------------------------------------------------------
/packages/cli/src/commands/login.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Command } from "commander";
  2 | import { logger } from "better-auth";
  3 | import { createAuthClient } from "better-auth/client";
  4 | import { deviceAuthorizationClient } from "better-auth/client/plugins";
  5 | import chalk from "chalk";
  6 | import open from "open";
  7 | import yoctoSpinner from "yocto-spinner";
  8 | import * as z from "zod/v4";
  9 | import { intro, outro, confirm, isCancel, cancel } from "@clack/prompts";
 10 | import fs from "fs/promises";
 11 | import path from "path";
 12 | import os from "os";
 13 | 
 14 | const DEMO_URL = "https://demo.better-auth.com";
 15 | const CLIENT_ID = "better-auth-cli";
 16 | const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
 17 | const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
 18 | 
 19 | export async function loginAction(opts: any) {
 20 | 	const options = z
 21 | 		.object({
 22 | 			serverUrl: z.string().optional(),
 23 | 			clientId: z.string().optional(),
 24 | 		})
 25 | 		.parse(opts);
 26 | 
 27 | 	const serverUrl = options.serverUrl || DEMO_URL;
 28 | 	const clientId = options.clientId || CLIENT_ID;
 29 | 
 30 | 	intro(chalk.bold("🔐 Better Auth CLI Login (Demo)"));
 31 | 
 32 | 	console.log(
 33 | 		chalk.yellow(
 34 | 			"⚠️  This is a demo feature for testing device authorization flow.",
 35 | 		),
 36 | 	);
 37 | 	console.log(
 38 | 		chalk.gray(
 39 | 			"   It connects to the Better Auth demo server for testing purposes.\n",
 40 | 		),
 41 | 	);
 42 | 
 43 | 	// Check if already logged in
 44 | 	const existingToken = await getStoredToken();
 45 | 	if (existingToken) {
 46 | 		const shouldReauth = await confirm({
 47 | 			message: "You're already logged in. Do you want to log in again?",
 48 | 			initialValue: false,
 49 | 		});
 50 | 
 51 | 		if (isCancel(shouldReauth) || !shouldReauth) {
 52 | 			cancel("Login cancelled");
 53 | 			process.exit(0);
 54 | 		}
 55 | 	}
 56 | 
 57 | 	// Create the auth client
 58 | 	const authClient = createAuthClient({
 59 | 		baseURL: serverUrl,
 60 | 		plugins: [deviceAuthorizationClient()],
 61 | 	});
 62 | 
 63 | 	const spinner = yoctoSpinner({ text: "Requesting device authorization..." });
 64 | 	spinner.start();
 65 | 
 66 | 	try {
 67 | 		// Request device code
 68 | 		const { data, error } = await authClient.device.code({
 69 | 			client_id: clientId,
 70 | 			scope: "openid profile email",
 71 | 		});
 72 | 
 73 | 		spinner.stop();
 74 | 
 75 | 		if (error || !data) {
 76 | 			logger.error(
 77 | 				`Failed to request device authorization: ${error?.error_description || "Unknown error"}`,
 78 | 			);
 79 | 			process.exit(1);
 80 | 		}
 81 | 
 82 | 		const {
 83 | 			device_code,
 84 | 			user_code,
 85 | 			verification_uri,
 86 | 			verification_uri_complete,
 87 | 			interval = 5,
 88 | 			expires_in,
 89 | 		} = data;
 90 | 
 91 | 		// Display authorization instructions
 92 | 		console.log("");
 93 | 		console.log(chalk.cyan("📱 Device Authorization Required"));
 94 | 		console.log("");
 95 | 		console.log(`Please visit: ${chalk.underline.blue(verification_uri)}`);
 96 | 		console.log(`Enter code: ${chalk.bold.green(user_code)}`);
 97 | 		console.log("");
 98 | 
 99 | 		// Ask if user wants to open browser
100 | 		const shouldOpen = await confirm({
101 | 			message: "Open browser automatically?",
102 | 			initialValue: true,
103 | 		});
104 | 
105 | 		if (!isCancel(shouldOpen) && shouldOpen) {
106 | 			const urlToOpen = verification_uri_complete || verification_uri;
107 | 			await open(urlToOpen);
108 | 		}
109 | 
110 | 		// Start polling
111 | 		console.log(
112 | 			chalk.gray(
113 | 				`Waiting for authorization (expires in ${Math.floor(expires_in / 60)} minutes)...`,
114 | 			),
115 | 		);
116 | 
117 | 		const token = await pollForToken(
118 | 			authClient,
119 | 			device_code,
120 | 			clientId,
121 | 			interval,
122 | 		);
123 | 
124 | 		if (token) {
125 | 			// Store the token
126 | 			await storeToken(token);
127 | 
128 | 			// Get user info
129 | 			const { data: session } = await authClient.getSession({
130 | 				fetchOptions: {
131 | 					headers: {
132 | 						Authorization: `Bearer ${token.access_token}`,
133 | 					},
134 | 				},
135 | 			});
136 | 
137 | 			outro(
138 | 				chalk.green(
139 | 					`✅ Demo login successful! Logged in as ${session?.user?.name || session?.user?.email || "User"}`,
140 | 				),
141 | 			);
142 | 
143 | 			console.log(
144 | 				chalk.gray(
145 | 					"\n📝 Note: This was a demo authentication for testing purposes.",
146 | 				),
147 | 			);
148 | 
149 | 			console.log(
150 | 				chalk.blue(
151 | 					"\nFor more information, visit: https://better-auth.com/docs/plugins/device-authorization",
152 | 				),
153 | 			);
154 | 		}
155 | 	} catch (err) {
156 | 		spinner.stop();
157 | 		logger.error(
158 | 			`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`,
159 | 		);
160 | 		process.exit(1);
161 | 	}
162 | }
163 | 
164 | async function pollForToken(
165 | 	authClient: any,
166 | 	deviceCode: string,
167 | 	clientId: string,
168 | 	initialInterval: number,
169 | ): Promise<any> {
170 | 	let pollingInterval = initialInterval;
171 | 	const spinner = yoctoSpinner({ text: "", color: "cyan" });
172 | 	let dots = 0;
173 | 
174 | 	return new Promise((resolve, reject) => {
175 | 		const poll = async () => {
176 | 			// Update spinner text with animated dots
177 | 			dots = (dots + 1) % 4;
178 | 			spinner.text = chalk.gray(
179 | 				`Polling for authorization${".".repeat(dots)}${" ".repeat(3 - dots)}`,
180 | 			);
181 | 			if (!spinner.isSpinning) spinner.start();
182 | 
183 | 			try {
184 | 				const { data, error } = await authClient.device.token({
185 | 					grant_type: "urn:ietf:params:oauth:grant-type:device_code",
186 | 					device_code: deviceCode,
187 | 					client_id: clientId,
188 | 					fetchOptions: {
189 | 						headers: {
190 | 							"user-agent": `Better Auth CLI`,
191 | 						},
192 | 					},
193 | 				});
194 | 
195 | 				if (data?.access_token) {
196 | 					spinner.stop();
197 | 					resolve(data);
198 | 					return;
199 | 				} else if (error) {
200 | 					switch (error.error) {
201 | 						case "authorization_pending":
202 | 							// Continue polling
203 | 							break;
204 | 						case "slow_down":
205 | 							pollingInterval += 5;
206 | 							spinner.text = chalk.yellow(
207 | 								`Slowing down polling to ${pollingInterval}s`,
208 | 							);
209 | 							break;
210 | 						case "access_denied":
211 | 							spinner.stop();
212 | 							logger.error("Access was denied by the user");
213 | 							process.exit(1);
214 | 							break;
215 | 						case "expired_token":
216 | 							spinner.stop();
217 | 							logger.error("The device code has expired. Please try again.");
218 | 							process.exit(1);
219 | 							break;
220 | 						default:
221 | 							spinner.stop();
222 | 							logger.error(`Error: ${error.error_description}`);
223 | 							process.exit(1);
224 | 					}
225 | 				}
226 | 			} catch (err) {
227 | 				spinner.stop();
228 | 				logger.error(
229 | 					`Network error: ${err instanceof Error ? err.message : "Unknown error"}`,
230 | 				);
231 | 				process.exit(1);
232 | 			}
233 | 
234 | 			setTimeout(poll, pollingInterval * 1000);
235 | 		};
236 | 
237 | 		// Start polling after initial interval
238 | 		setTimeout(poll, pollingInterval * 1000);
239 | 	});
240 | }
241 | 
242 | async function storeToken(token: any): Promise<void> {
243 | 	try {
244 | 		// Ensure config directory exists
245 | 		await fs.mkdir(CONFIG_DIR, { recursive: true });
246 | 
247 | 		// Store token with metadata
248 | 		const tokenData = {
249 | 			access_token: token.access_token,
250 | 			token_type: token.token_type || "Bearer",
251 | 			scope: token.scope,
252 | 			created_at: new Date().toISOString(),
253 | 		};
254 | 
255 | 		await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2), "utf-8");
256 | 	} catch (error) {
257 | 		logger.warn("Failed to store authentication token locally");
258 | 	}
259 | }
260 | 
261 | async function getStoredToken(): Promise<any> {
262 | 	try {
263 | 		const data = await fs.readFile(TOKEN_FILE, "utf-8");
264 | 		return JSON.parse(data);
265 | 	} catch {
266 | 		return null;
267 | 	}
268 | }
269 | 
270 | export const login = new Command("login")
271 | 	.description(
272 | 		"Demo: Test device authorization flow with Better Auth demo server",
273 | 	)
274 | 	.option("--server-url <url>", "The Better Auth server URL", DEMO_URL)
275 | 	.option("--client-id <id>", "The OAuth client ID", CLIENT_ID)
276 | 	.action(loginAction);
277 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/oauth-proxy/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as z from "zod";
  2 | import { originCheck } from "../../api";
  3 | import {
  4 | 	createAuthEndpoint,
  5 | 	createAuthMiddleware,
  6 | } from "@better-auth/core/api";
  7 | import { symmetricDecrypt, symmetricEncrypt } from "../../crypto";
  8 | import type { BetterAuthPlugin } from "@better-auth/core";
  9 | import { env } from "@better-auth/core/env";
 10 | import { getOrigin } from "../../utils/url";
 11 | import type { EndpointContext } from "better-call";
 12 | 
 13 | function getVenderBaseURL() {
 14 | 	const vercel = env.VERCEL_URL ? `https://${env.VERCEL_URL}` : undefined;
 15 | 	const netlify = env.NETLIFY_URL;
 16 | 	const render = env.RENDER_URL;
 17 | 	const aws = env.AWS_LAMBDA_FUNCTION_NAME;
 18 | 	const google = env.GOOGLE_CLOUD_FUNCTION_NAME;
 19 | 	const azure = env.AZURE_FUNCTION_NAME;
 20 | 
 21 | 	return vercel || netlify || render || aws || google || azure;
 22 | }
 23 | 
 24 | export interface OAuthProxyOptions {
 25 | 	/**
 26 | 	 * The current URL of the application.
 27 | 	 * The plugin will attempt to infer the current URL from your environment
 28 | 	 * by checking the base URL from popular hosting providers,
 29 | 	 * from the request URL if invoked by a client,
 30 | 	 * or as a fallback, from the `baseURL` in your auth config.
 31 | 	 * If the URL is not inferred correctly, you can provide a value here."
 32 | 	 */
 33 | 	currentURL?: string;
 34 | 	/**
 35 | 	 * If a request in a production url it won't be proxied.
 36 | 	 *
 37 | 	 * default to `BETTER_AUTH_URL`
 38 | 	 */
 39 | 	productionURL?: string;
 40 | }
 41 | 
 42 | /**
 43 |  * A proxy plugin, that allows you to proxy OAuth requests.
 44 |  * Useful for development and preview deployments where
 45 |  * the redirect URL can't be known in advance to add to the OAuth provider.
 46 |  */
 47 | export const oAuthProxy = (opts?: OAuthProxyOptions) => {
 48 | 	const resolveCurrentURL = (ctx: EndpointContext<string, any>) => {
 49 | 		return new URL(
 50 | 			opts?.currentURL ||
 51 | 				ctx.request?.url ||
 52 | 				getVenderBaseURL() ||
 53 | 				ctx.context.baseURL,
 54 | 		);
 55 | 	};
 56 | 
 57 | 	const checkSkipProxy = (ctx: EndpointContext<string, any>) => {
 58 | 		// if skip proxy header is set, we don't need to proxy
 59 | 		const skipProxy = ctx.request?.headers.get("x-skip-oauth-proxy");
 60 | 		if (skipProxy) {
 61 | 			return true;
 62 | 		}
 63 | 		const productionURL = opts?.productionURL || env.BETTER_AUTH_URL;
 64 | 		if (productionURL === ctx.context.options.baseURL) {
 65 | 			return true;
 66 | 		}
 67 | 		return false;
 68 | 	};
 69 | 
 70 | 	return {
 71 | 		id: "oauth-proxy",
 72 | 		options: opts,
 73 | 		endpoints: {
 74 | 			oAuthProxy: createAuthEndpoint(
 75 | 				"/oauth-proxy-callback",
 76 | 				{
 77 | 					method: "GET",
 78 | 					query: z.object({
 79 | 						callbackURL: z.string().meta({
 80 | 							description: "The URL to redirect to after the proxy",
 81 | 						}),
 82 | 						cookies: z.string().meta({
 83 | 							description: "The cookies to set after the proxy",
 84 | 						}),
 85 | 					}),
 86 | 					use: [originCheck((ctx) => ctx.query.callbackURL)],
 87 | 					metadata: {
 88 | 						openapi: {
 89 | 							description: "OAuth Proxy Callback",
 90 | 							parameters: [
 91 | 								{
 92 | 									in: "query",
 93 | 									name: "callbackURL",
 94 | 									required: true,
 95 | 									description: "The URL to redirect to after the proxy",
 96 | 								},
 97 | 								{
 98 | 									in: "query",
 99 | 									name: "cookies",
100 | 									required: true,
101 | 									description: "The cookies to set after the proxy",
102 | 								},
103 | 							],
104 | 							responses: {
105 | 								302: {
106 | 									description: "Redirect",
107 | 									headers: {
108 | 										Location: {
109 | 											description: "The URL to redirect to",
110 | 											schema: {
111 | 												type: "string",
112 | 											},
113 | 										},
114 | 									},
115 | 								},
116 | 							},
117 | 						},
118 | 					},
119 | 				},
120 | 				async (ctx) => {
121 | 					const cookies = ctx.query.cookies;
122 | 
123 | 					const decryptedCookies = await symmetricDecrypt({
124 | 						key: ctx.context.secret,
125 | 						data: cookies,
126 | 					}).catch((e) => {
127 | 						ctx.context.logger.error(e);
128 | 						return null;
129 | 					});
130 | 					const error =
131 | 						ctx.context.options.onAPIError?.errorURL ||
132 | 						`${ctx.context.options.baseURL}/api/auth/error`;
133 | 					if (!decryptedCookies) {
134 | 						throw ctx.redirect(
135 | 							`${error}?error=OAuthProxy - Invalid cookies or secret`,
136 | 						);
137 | 					}
138 | 
139 | 					const isSecureContext = resolveCurrentURL(ctx).protocol === "https:";
140 | 					const prefix =
141 | 						ctx.context.options.advanced?.cookiePrefix || "better-auth";
142 | 					const cookieToSet = isSecureContext
143 | 						? decryptedCookies
144 | 						: decryptedCookies
145 | 								.replace("Secure;", "")
146 | 								.replace(`__Secure-${prefix}`, prefix);
147 | 					ctx.setHeader("set-cookie", cookieToSet);
148 | 					throw ctx.redirect(ctx.query.callbackURL);
149 | 				},
150 | 			),
151 | 		},
152 | 		hooks: {
153 | 			after: [
154 | 				{
155 | 					matcher(context) {
156 | 						return !!(
157 | 							context.path?.startsWith("/callback") ||
158 | 							context.path?.startsWith("/oauth2/callback")
159 | 						);
160 | 					},
161 | 					handler: createAuthMiddleware(async (ctx) => {
162 | 						const headers = ctx.context.responseHeaders;
163 | 						const location = headers?.get("location");
164 | 						if (location?.includes("/oauth-proxy-callback?callbackURL")) {
165 | 							if (!location.startsWith("http")) {
166 | 								return;
167 | 							}
168 | 							const locationURL = new URL(location);
169 | 							const origin = locationURL.origin;
170 | 							/**
171 | 							 * We don't want to redirect to the proxy URL if the origin is the same
172 | 							 * as the current URL
173 | 							 */
174 | 							const productionURL =
175 | 								opts?.productionURL ||
176 | 								ctx.context.options.baseURL ||
177 | 								ctx.context.baseURL;
178 | 							if (origin === getOrigin(productionURL)) {
179 | 								const newLocation = locationURL.searchParams.get("callbackURL");
180 | 								if (!newLocation) {
181 | 									return;
182 | 								}
183 | 								ctx.setHeader("location", newLocation);
184 | 								return;
185 | 							}
186 | 
187 | 							const setCookies = headers?.get("set-cookie");
188 | 
189 | 							if (!setCookies) {
190 | 								return;
191 | 							}
192 | 							const encryptedCookies = await symmetricEncrypt({
193 | 								key: ctx.context.secret,
194 | 								data: setCookies,
195 | 							});
196 | 							const locationWithCookies = `${location}&cookies=${encodeURIComponent(
197 | 								encryptedCookies,
198 | 							)}`;
199 | 							ctx.setHeader("location", locationWithCookies);
200 | 						}
201 | 					}),
202 | 				},
203 | 			],
204 | 			before: [
205 | 				{
206 | 					matcher() {
207 | 						return true;
208 | 					},
209 | 					handler: createAuthMiddleware(async (ctx) => {
210 | 						const skipProxy = checkSkipProxy(ctx);
211 | 						if (skipProxy || ctx.path !== "/callback/:id") {
212 | 							return;
213 | 						}
214 | 						return {
215 | 							context: {
216 | 								context: {
217 | 									oauthConfig: {
218 | 										skipStateCookieCheck: true,
219 | 									},
220 | 								},
221 | 							},
222 | 						};
223 | 					}),
224 | 				},
225 | 				{
226 | 					matcher(context) {
227 | 						return !!(
228 | 							context.path?.startsWith("/sign-in/social") ||
229 | 							context.path?.startsWith("/sign-in/oauth2")
230 | 						);
231 | 					},
232 | 					handler: createAuthMiddleware(async (ctx) => {
233 | 						const skipProxy = checkSkipProxy(ctx);
234 | 						if (skipProxy) {
235 | 							return;
236 | 						}
237 | 						const url = resolveCurrentURL(ctx);
238 | 						if (!ctx.body) {
239 | 							return;
240 | 						}
241 | 						ctx.body.callbackURL = `${url.origin}${
242 | 							ctx.context.options.basePath || "/api/auth"
243 | 						}/oauth-proxy-callback?callbackURL=${encodeURIComponent(
244 | 							ctx.body.callbackURL || ctx.context.baseURL,
245 | 						)}`;
246 | 						return {
247 | 							context: ctx,
248 | 						};
249 | 					}),
250 | 				},
251 | 			],
252 | 		},
253 | 	} satisfies BetterAuthPlugin;
254 | };
255 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/db/get-tables.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { BetterAuthOptions } from "@better-auth/core";
  2 | import type {
  3 | 	BetterAuthDBSchema,
  4 | 	DBFieldAttribute,
  5 | } from "@better-auth/core/db";
  6 | 
  7 | export const getAuthTables = (
  8 | 	options: BetterAuthOptions,
  9 | ): BetterAuthDBSchema => {
 10 | 	const pluginSchema = (options.plugins ?? []).reduce(
 11 | 		(acc, plugin) => {
 12 | 			const schema = plugin.schema;
 13 | 			if (!schema) return acc;
 14 | 			for (const [key, value] of Object.entries(schema)) {
 15 | 				acc[key] = {
 16 | 					fields: {
 17 | 						...acc[key]?.fields,
 18 | 						...value.fields,
 19 | 					},
 20 | 					modelName: value.modelName || key,
 21 | 				};
 22 | 			}
 23 | 			return acc;
 24 | 		},
 25 | 		{} as Record<
 26 | 			string,
 27 | 			{ fields: Record<string, DBFieldAttribute>; modelName: string }
 28 | 		>,
 29 | 	);
 30 | 
 31 | 	const shouldAddRateLimitTable = options.rateLimit?.storage === "database";
 32 | 	const rateLimitTable = {
 33 | 		rateLimit: {
 34 | 			modelName: options.rateLimit?.modelName || "rateLimit",
 35 | 			fields: {
 36 | 				key: {
 37 | 					type: "string",
 38 | 					fieldName: options.rateLimit?.fields?.key || "key",
 39 | 				},
 40 | 				count: {
 41 | 					type: "number",
 42 | 					fieldName: options.rateLimit?.fields?.count || "count",
 43 | 				},
 44 | 				lastRequest: {
 45 | 					type: "number",
 46 | 					bigint: true,
 47 | 					fieldName: options.rateLimit?.fields?.lastRequest || "lastRequest",
 48 | 				},
 49 | 			},
 50 | 		},
 51 | 	} satisfies BetterAuthDBSchema;
 52 | 
 53 | 	const { user, session, account, ...pluginTables } = pluginSchema;
 54 | 
 55 | 	const sessionTable = {
 56 | 		session: {
 57 | 			modelName: options.session?.modelName || "session",
 58 | 			fields: {
 59 | 				expiresAt: {
 60 | 					type: "date",
 61 | 					required: true,
 62 | 					fieldName: options.session?.fields?.expiresAt || "expiresAt",
 63 | 				},
 64 | 				token: {
 65 | 					type: "string",
 66 | 					required: true,
 67 | 					fieldName: options.session?.fields?.token || "token",
 68 | 					unique: true,
 69 | 				},
 70 | 				createdAt: {
 71 | 					type: "date",
 72 | 					required: true,
 73 | 					fieldName: options.session?.fields?.createdAt || "createdAt",
 74 | 					defaultValue: () => new Date(),
 75 | 				},
 76 | 				updatedAt: {
 77 | 					type: "date",
 78 | 					required: true,
 79 | 					fieldName: options.session?.fields?.updatedAt || "updatedAt",
 80 | 					onUpdate: () => new Date(),
 81 | 				},
 82 | 				ipAddress: {
 83 | 					type: "string",
 84 | 					required: false,
 85 | 					fieldName: options.session?.fields?.ipAddress || "ipAddress",
 86 | 				},
 87 | 				userAgent: {
 88 | 					type: "string",
 89 | 					required: false,
 90 | 					fieldName: options.session?.fields?.userAgent || "userAgent",
 91 | 				},
 92 | 				userId: {
 93 | 					type: "string",
 94 | 					fieldName: options.session?.fields?.userId || "userId",
 95 | 					references: {
 96 | 						model: options.user?.modelName || "user",
 97 | 						field: "id",
 98 | 						onDelete: "cascade",
 99 | 					},
100 | 					required: true,
101 | 				},
102 | 				...session?.fields,
103 | 				...options.session?.additionalFields,
104 | 			},
105 | 			order: 2,
106 | 		},
107 | 	} satisfies BetterAuthDBSchema;
108 | 
109 | 	return {
110 | 		user: {
111 | 			modelName: options.user?.modelName || "user",
112 | 			fields: {
113 | 				name: {
114 | 					type: "string",
115 | 					required: true,
116 | 					fieldName: options.user?.fields?.name || "name",
117 | 					sortable: true,
118 | 				},
119 | 				email: {
120 | 					type: "string",
121 | 					unique: true,
122 | 					required: true,
123 | 					fieldName: options.user?.fields?.email || "email",
124 | 					sortable: true,
125 | 				},
126 | 				emailVerified: {
127 | 					type: "boolean",
128 | 					defaultValue: false,
129 | 					required: true,
130 | 					fieldName: options.user?.fields?.emailVerified || "emailVerified",
131 | 				},
132 | 				image: {
133 | 					type: "string",
134 | 					required: false,
135 | 					fieldName: options.user?.fields?.image || "image",
136 | 				},
137 | 				createdAt: {
138 | 					type: "date",
139 | 					defaultValue: () => new Date(),
140 | 					required: true,
141 | 					fieldName: options.user?.fields?.createdAt || "createdAt",
142 | 				},
143 | 				updatedAt: {
144 | 					type: "date",
145 | 					defaultValue: () => new Date(),
146 | 					onUpdate: () => new Date(),
147 | 					required: true,
148 | 					fieldName: options.user?.fields?.updatedAt || "updatedAt",
149 | 				},
150 | 				...user?.fields,
151 | 				...options.user?.additionalFields,
152 | 			},
153 | 			order: 1,
154 | 		},
155 | 		//only add session table if it's not stored in secondary storage
156 | 		...(!options.secondaryStorage || options.session?.storeSessionInDatabase
157 | 			? sessionTable
158 | 			: {}),
159 | 		account: {
160 | 			modelName: options.account?.modelName || "account",
161 | 			fields: {
162 | 				accountId: {
163 | 					type: "string",
164 | 					required: true,
165 | 					fieldName: options.account?.fields?.accountId || "accountId",
166 | 				},
167 | 				providerId: {
168 | 					type: "string",
169 | 					required: true,
170 | 					fieldName: options.account?.fields?.providerId || "providerId",
171 | 				},
172 | 				userId: {
173 | 					type: "string",
174 | 					references: {
175 | 						model: options.user?.modelName || "user",
176 | 						field: "id",
177 | 						onDelete: "cascade",
178 | 					},
179 | 					required: true,
180 | 					fieldName: options.account?.fields?.userId || "userId",
181 | 				},
182 | 				accessToken: {
183 | 					type: "string",
184 | 					required: false,
185 | 					fieldName: options.account?.fields?.accessToken || "accessToken",
186 | 				},
187 | 				refreshToken: {
188 | 					type: "string",
189 | 					required: false,
190 | 					fieldName: options.account?.fields?.refreshToken || "refreshToken",
191 | 				},
192 | 				idToken: {
193 | 					type: "string",
194 | 					required: false,
195 | 					fieldName: options.account?.fields?.idToken || "idToken",
196 | 				},
197 | 				accessTokenExpiresAt: {
198 | 					type: "date",
199 | 					required: false,
200 | 					fieldName:
201 | 						options.account?.fields?.accessTokenExpiresAt ||
202 | 						"accessTokenExpiresAt",
203 | 				},
204 | 				refreshTokenExpiresAt: {
205 | 					type: "date",
206 | 					required: false,
207 | 					fieldName:
208 | 						options.account?.fields?.refreshTokenExpiresAt ||
209 | 						"refreshTokenExpiresAt",
210 | 				},
211 | 				scope: {
212 | 					type: "string",
213 | 					required: false,
214 | 					fieldName: options.account?.fields?.scope || "scope",
215 | 				},
216 | 				password: {
217 | 					type: "string",
218 | 					required: false,
219 | 					fieldName: options.account?.fields?.password || "password",
220 | 				},
221 | 				createdAt: {
222 | 					type: "date",
223 | 					required: true,
224 | 					fieldName: options.account?.fields?.createdAt || "createdAt",
225 | 					defaultValue: () => new Date(),
226 | 				},
227 | 				updatedAt: {
228 | 					type: "date",
229 | 					required: true,
230 | 					fieldName: options.account?.fields?.updatedAt || "updatedAt",
231 | 					onUpdate: () => new Date(),
232 | 				},
233 | 				...account?.fields,
234 | 				...options.account?.additionalFields,
235 | 			},
236 | 			order: 3,
237 | 		},
238 | 		verification: {
239 | 			modelName: options.verification?.modelName || "verification",
240 | 			fields: {
241 | 				identifier: {
242 | 					type: "string",
243 | 					required: true,
244 | 					fieldName: options.verification?.fields?.identifier || "identifier",
245 | 				},
246 | 				value: {
247 | 					type: "string",
248 | 					required: true,
249 | 					fieldName: options.verification?.fields?.value || "value",
250 | 				},
251 | 				expiresAt: {
252 | 					type: "date",
253 | 					required: true,
254 | 					fieldName: options.verification?.fields?.expiresAt || "expiresAt",
255 | 				},
256 | 				createdAt: {
257 | 					type: "date",
258 | 					required: true,
259 | 					defaultValue: () => new Date(),
260 | 					fieldName: options.verification?.fields?.createdAt || "createdAt",
261 | 				},
262 | 				updatedAt: {
263 | 					type: "date",
264 | 					required: true,
265 | 					defaultValue: () => new Date(),
266 | 					onUpdate: () => new Date(),
267 | 					fieldName: options.verification?.fields?.updatedAt || "updatedAt",
268 | 				},
269 | 			},
270 | 			order: 4,
271 | 		},
272 | 		...pluginTables,
273 | 		...(shouldAddRateLimitTable ? rateLimitTable : {}),
274 | 	} satisfies BetterAuthDBSchema;
275 | };
276 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/last-login-method/last-login-method.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, beforeAll, afterAll, afterEach } from "vitest";
  2 | import { setupServer } from "msw/node";
  3 | import { http, HttpResponse } from "msw";
  4 | import { getTestInstance } from "../../test-utils/test-instance";
  5 | import { lastLoginMethod } from ".";
  6 | import { lastLoginMethodClient } from "./client";
  7 | import { parseCookies, parseSetCookieHeader } from "../../cookies";
  8 | import { DEFAULT_SECRET } from "../../utils/constants";
  9 | import type { GoogleProfile } from "@better-auth/core/social-providers";
 10 | import { signJWT } from "../../crypto";
 11 | 
 12 | let testIdToken: string;
 13 | let handlers: ReturnType<typeof http.post>[];
 14 | 
 15 | const server = setupServer();
 16 | 
 17 | beforeAll(async () => {
 18 | 	const data: GoogleProfile = {
 19 | 		email: "[email protected]",
 20 | 		email_verified: true,
 21 | 		name: "OAuth Test User",
 22 | 		picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw",
 23 | 		exp: 1234567890,
 24 | 		sub: "1234567890",
 25 | 		iat: 1234567890,
 26 | 		aud: "test",
 27 | 		azp: "test",
 28 | 		nbf: 1234567890,
 29 | 		iss: "test",
 30 | 		locale: "en",
 31 | 		jti: "test",
 32 | 		given_name: "OAuth",
 33 | 		family_name: "Test",
 34 | 	};
 35 | 	testIdToken = await signJWT(data, DEFAULT_SECRET);
 36 | 
 37 | 	handlers = [
 38 | 		http.post("https://oauth2.googleapis.com/token", () => {
 39 | 			return HttpResponse.json({
 40 | 				access_token: "test-access-token",
 41 | 				refresh_token: "test-refresh-token",
 42 | 				id_token: testIdToken,
 43 | 			});
 44 | 		}),
 45 | 	];
 46 | 
 47 | 	server.listen({ onUnhandledRequest: "bypass" });
 48 | 	server.use(...handlers);
 49 | });
 50 | 
 51 | afterEach(() => {
 52 | 	server.resetHandlers();
 53 | 	server.use(...handlers);
 54 | });
 55 | 
 56 | afterAll(() => server.close());
 57 | 
 58 | describe("lastLoginMethod", async () => {
 59 | 	const { client, cookieSetter, testUser } = await getTestInstance(
 60 | 		{
 61 | 			plugins: [lastLoginMethod()],
 62 | 		},
 63 | 		{
 64 | 			clientOptions: {
 65 | 				plugins: [lastLoginMethodClient()],
 66 | 			},
 67 | 		},
 68 | 	);
 69 | 
 70 | 	it("should set the last login method cookie", async () => {
 71 | 		const headers = new Headers();
 72 | 		await client.signIn.email(
 73 | 			{
 74 | 				email: testUser.email,
 75 | 				password: testUser.password,
 76 | 			},
 77 | 			{
 78 | 				onSuccess(context) {
 79 | 					cookieSetter(headers)(context);
 80 | 				},
 81 | 			},
 82 | 		);
 83 | 		const cookies = parseCookies(headers.get("cookie") || "");
 84 | 		expect(cookies.get("better-auth.last_used_login_method")).toBe("email");
 85 | 	});
 86 | 
 87 | 	it("should set the last login method in the database", async () => {
 88 | 		const { client, auth } = await getTestInstance({
 89 | 			plugins: [lastLoginMethod({ storeInDatabase: true })],
 90 | 		});
 91 | 		const data = await client.signIn.email(
 92 | 			{
 93 | 				email: testUser.email,
 94 | 				password: testUser.password,
 95 | 			},
 96 | 			{ throw: true },
 97 | 		);
 98 | 		const session = await auth.api.getSession({
 99 | 			headers: new Headers({
100 | 				authorization: `Bearer ${data.token}`,
101 | 			}),
102 | 		});
103 | 		expect(session?.user.lastLoginMethod).toBe("email");
104 | 	});
105 | 
106 | 	it("should NOT set the last login method cookie on failed authentication", async () => {
107 | 		const headers = new Headers();
108 | 		const response = await client.signIn.email(
109 | 			{
110 | 				email: testUser.email,
111 | 				password: "wrong-password",
112 | 			},
113 | 			{
114 | 				onError(context) {
115 | 					cookieSetter(headers)(context);
116 | 				},
117 | 			},
118 | 		);
119 | 
120 | 		expect(response.error).toBeDefined();
121 | 
122 | 		const cookies = parseCookies(headers.get("cookie") || "");
123 | 		expect(cookies.get("better-auth.last_used_login_method")).toBeUndefined();
124 | 	});
125 | 
126 | 	it("should NOT set the last login method cookie on failed OAuth callback", async () => {
127 | 		const headers = new Headers();
128 | 		const response = await client.$fetch("/callback/google", {
129 | 			method: "GET",
130 | 			query: {
131 | 				code: "invalid-code",
132 | 				state: "invalid-state",
133 | 			},
134 | 			onError(context) {
135 | 				cookieSetter(headers)(context);
136 | 			},
137 | 		});
138 | 
139 | 		expect(response.error).toBeDefined();
140 | 
141 | 		const cookies = parseCookies(headers.get("cookie") || "");
142 | 		expect(cookies.get("better-auth.last_used_login_method")).toBeUndefined();
143 | 	});
144 | 	it("should update the last login method in the database on subsequent logins", async () => {
145 | 		const { client, auth } = await getTestInstance({
146 | 			plugins: [lastLoginMethod({ storeInDatabase: true })],
147 | 		});
148 | 
149 | 		await client.signUp.email(
150 | 			{
151 | 				email: "[email protected]",
152 | 				password: "password123",
153 | 				name: "Test User",
154 | 			},
155 | 			{ throw: true },
156 | 		);
157 | 
158 | 		const emailSignInData = await client.signIn.email(
159 | 			{
160 | 				email: "[email protected]",
161 | 				password: "password123",
162 | 			},
163 | 			{ throw: true },
164 | 		);
165 | 
166 | 		let session = await auth.api.getSession({
167 | 			headers: new Headers({
168 | 				authorization: `Bearer ${emailSignInData.token}`,
169 | 			}),
170 | 		});
171 | 		expect((session?.user as any).lastLoginMethod).toBe("email");
172 | 
173 | 		await client.signOut();
174 | 
175 | 		const emailSignInData2 = await client.signIn.email(
176 | 			{
177 | 				email: "[email protected]",
178 | 				password: "password123",
179 | 			},
180 | 			{ throw: true },
181 | 		);
182 | 
183 | 		session = await auth.api.getSession({
184 | 			headers: new Headers({
185 | 				authorization: `Bearer ${emailSignInData2.token}`,
186 | 			}),
187 | 		});
188 | 
189 | 		expect((session?.user as any).lastLoginMethod).toBe("email");
190 | 	});
191 | 
192 | 	it("should update the last login method in the database on subsequent logins with email and OAuth", async () => {
193 | 		const { client, auth, cookieSetter } = await getTestInstance({
194 | 			plugins: [lastLoginMethod({ storeInDatabase: true })],
195 | 			account: {
196 | 				accountLinking: {
197 | 					enabled: true,
198 | 					trustedProviders: ["google"],
199 | 				},
200 | 			},
201 | 		});
202 | 
203 | 		await client.signUp.email(
204 | 			{
205 | 				email: "[email protected]",
206 | 				password: "password123",
207 | 				name: "GitHub Issue Demo User",
208 | 			},
209 | 			{ throw: true },
210 | 		);
211 | 
212 | 		const emailSignInData = await client.signIn.email(
213 | 			{
214 | 				email: "[email protected]",
215 | 				password: "password123",
216 | 			},
217 | 			{ throw: true },
218 | 		);
219 | 
220 | 		let session = await auth.api.getSession({
221 | 			headers: new Headers({
222 | 				authorization: `Bearer ${emailSignInData.token}`,
223 | 			}),
224 | 		});
225 | 
226 | 		expect((session?.user as any).lastLoginMethod).toBe("email");
227 | 
228 | 		await client.signOut();
229 | 
230 | 		const oAuthHeaders = new Headers();
231 | 		const signInRes = await client.signIn.social({
232 | 			provider: "google",
233 | 			callbackURL: "/callback",
234 | 			fetchOptions: {
235 | 				onSuccess: cookieSetter(oAuthHeaders),
236 | 			},
237 | 		});
238 | 		expect(signInRes.data).toMatchObject({
239 | 			url: expect.stringContaining("google.com"),
240 | 			redirect: true,
241 | 		});
242 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
243 | 
244 | 		const headers = new Headers();
245 | 		await client.$fetch("/callback/google", {
246 | 			query: {
247 | 				state,
248 | 				code: "test",
249 | 			},
250 | 			headers: oAuthHeaders,
251 | 			method: "GET",
252 | 			onError(context) {
253 | 				expect(context.response.status).toBe(302);
254 | 				const location = context.response.headers.get("location");
255 | 				expect(location).toBeDefined();
256 | 
257 | 				cookieSetter(headers)(context as any);
258 | 
259 | 				const cookies = parseSetCookieHeader(
260 | 					context.response.headers.get("set-cookie") || "",
261 | 				);
262 | 				const lastLoginMethod = cookies.get(
263 | 					"better-auth.last_used_login_method",
264 | 				)?.value;
265 | 				if (lastLoginMethod) {
266 | 					expect(lastLoginMethod).toBe("google");
267 | 				}
268 | 			},
269 | 		});
270 | 
271 | 		const oauthSession = await client.getSession({
272 | 			fetchOptions: {
273 | 				headers: headers,
274 | 			},
275 | 		});
276 | 		expect((oauthSession?.data?.user as any).lastLoginMethod).toBe("google");
277 | 	});
278 | });
279 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/guides/browser-extension-guide.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Browser Extension Guide
  3 | description: A step-by-step guide to creating a browser extension with Better Auth.
  4 | ---
  5 | 
  6 | In this guide, we'll walk you through the steps of creating a browser extension using <Link href="https://docs.plasmo.com/">Plasmo</Link> with Better Auth for authentication.
  7 | 
  8 | If you would like to view a completed example, you can check out the <Link href="https://github.com/better-auth/examples/tree/main/browser-extension-example">browser extension example</Link>.
  9 | 
 10 | <Callout type="warn">
 11 |   The Plasmo framework does not provide a backend for the browser extension.
 12 |   This guide assumes you have{" "}
 13 |   <Link href="/docs/integrations/hono">a backend setup</Link> of Better Auth and
 14 |   are ready to create a browser extension to connect to it.
 15 | </Callout>
 16 | 
 17 | <Steps>
 18 | 
 19 |     <Step>
 20 |         ## Setup & Installations
 21 | 
 22 |         Initialize a new Plasmo project with TailwindCSS and a src directory.
 23 | 
 24 |         ```bash
 25 |         pnpm create plasmo --with-tailwindcss --with-src
 26 |         ```
 27 | 
 28 |         Then, install the Better Auth package.
 29 | 
 30 |         ```bash
 31 |         pnpm add better-auth
 32 |         ```
 33 | 
 34 |         To start the Plasmo development server, run the following command.
 35 | 
 36 |         ```bash
 37 |         pnpm dev
 38 |         ```
 39 |     </Step>
 40 | 
 41 | 
 42 |     <Step>
 43 |         ## Configure tsconfig
 44 | 
 45 |         Configure the `tsconfig.json` file to include `strict` mode.
 46 | 
 47 |         For this demo, we have also changed the import alias from `~` to `@` and set it to the `src` directory.
 48 | 
 49 |         ```json title="tsconfig.json"
 50 |         {
 51 |             "compilerOptions": {
 52 |                 "paths": {
 53 |                     "@/_": [
 54 |                         "./src/_"
 55 |                     ]
 56 |                 },
 57 |                 "strict": true,
 58 |                 "baseUrl": "."
 59 |             }
 60 |         }
 61 |         ```
 62 |     </Step>
 63 | 
 64 | 
 65 |     <Step>
 66 |         ## Create the client auth instance
 67 | 
 68 |         Create a new file at `src/auth/auth-client.ts` and add the following code.
 69 | 
 70 |        <Files>
 71 |             <Folder name="src" defaultOpen>
 72 |                 <Folder name="auth" defaultOpen>
 73 |                     <File name="auth-client.ts" />
 74 |                 </Folder>
 75 |             </Folder>
 76 |        </Files>
 77 | 
 78 |         ```ts title="auth-client.ts"
 79 |         import { createAuthClient } from "better-auth/react"
 80 | 
 81 |         export const authClient = createAuthClient({
 82 |             baseURL: "http://localhost:3000" /* Base URL of your Better Auth backend. */,
 83 |             plugins: [],
 84 |         });
 85 |         ```
 86 |     </Step>
 87 | 
 88 |     <Step>
 89 |         ## Configure the manifest
 90 | 
 91 |         We must ensure the extension knows the URL to the Better Auth backend.
 92 | 
 93 |         Head to your package.json file, and add the following code.
 94 | 
 95 |         ```json title="package.json"
 96 |         {
 97 |             //...
 98 |             "manifest": {
 99 |                 "host_permissions": [
100 |                     "https://URL_TO_YOUR_BACKEND" // localhost works too (e.g. http://localhost:3000)
101 |                 ]
102 |             }
103 |         }
104 |         ```
105 |     </Step>
106 | 
107 | 
108 |     <Step>
109 |         ## You're now ready!
110 | 
111 |         You have now set up Better Auth for your browser extension.
112 | 
113 |         Add your desired UI and create your dream extension!
114 | 
115 |         To learn more about the client Better Auth API, check out the <Link href="/docs/concepts/client">client documentation</Link>.
116 | 
117 | 
118 |         Here's a quick example 😎
119 | 
120 |         ```tsx title="src/popup.tsx"
121 |         import { authClient } from "./auth/auth-client"
122 | 
123 | 
124 |         function IndexPopup() {
125 |             const {data, isPending, error} = authClient.useSession();
126 |             if(isPending){
127 |                 return <>Loading...</>
128 |             }
129 |             if(error){
130 |                 return <>Error: {error.message}</>
131 |             }
132 |             if(data){
133 |                 return <>Signed in as {data.user.name}</>
134 |             }
135 |         }
136 | 
137 |         export default IndexPopup;
138 |         ```
139 | 
140 |     </Step>
141 | 
142 | 
143 |     <Step>
144 |         ## Bundle your extension
145 | 
146 |         To get a production build, run the following command.
147 | 
148 |         ```bash
149 |         pnpm build
150 |         ```
151 | 
152 |         Head over to <Link href="chrome://extensions" target="_blank">chrome://extensions</Link> and enable developer mode.
153 | 
154 |         <img src="https://docs.plasmo.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdeveloper_mode.76f090f7.png&w=1920&q=75" />
155 | 
156 |         Click on "Load Unpacked" and navigate to your extension's `build/chrome-mv3-dev` (or `build/chrome-mv3-prod`) directory.
157 | 
158 |         To see your popup, click on the puzzle piece icon on the Chrome toolbar, and click on your extension.
159 | 
160 |         Learn more about <Link href="https://docs.plasmo.com/framework#loading-the-extension-in-chrome">bundling your extension here.</Link>
161 |     </Step>
162 | 
163 |     <Step>
164 |         ## Configure the server auth instance
165 | 
166 |         First, we will need your extension URL.
167 |         
168 |         An extension URL formed like this: `chrome-extension://YOUR_EXTENSION_ID`.
169 | 
170 |         You can find your extension ID at <Link href="chrome://extensions" target="_blank">chrome://extensions</Link>.
171 | 
172 |         <img src="/extension-id.png" width={500} />
173 | 
174 |         Head to your server's auth file, and make sure that your extension's URL is added to the `trustedOrigins` list.
175 | 
176 | 
177 |         ```ts title="server.ts"
178 |         import { betterAuth } from "better-auth"
179 |         import { auth } from "@/auth/auth"
180 | 
181 |         export const auth = betterAuth({
182 |             trustedOrigins: ["chrome-extension://YOUR_EXTENSION_ID"],
183 |         })
184 |         ```
185 | 
186 |         If you're developing multiple extensions or need to support different browser extensions with different IDs, you can use wildcard patterns:
187 | 
188 |         ```ts title="server.ts"
189 |         export const auth = betterAuth({
190 |             trustedOrigins: [
191 |                 // Support a specific extension ID
192 |                 "chrome-extension://YOUR_EXTENSION_ID",
193 |                 
194 |                 // Or support multiple extensions with wildcard (less secure)
195 |                 "chrome-extension://*"
196 |             ],
197 |         })
198 |         ```
199 | 
200 |         <Callout type="warn">
201 |           Using wildcards for extension origins (`chrome-extension://*`) reduces security by trusting all extensions. 
202 |           It's safer to explicitly list each extension ID you trust. Only use wildcards for development and testing.
203 |         </Callout>
204 |     </Step>
205 | 
206 |     <Step>
207 |         ## That's it!
208 | 
209 |         Everything is set up! You can now start developing your extension. 🎉
210 |     </Step>
211 | 
212 | </Steps>
213 | 
214 | 
215 | ## Wrapping Up
216 | 
217 | Congratulations! You've successfully created a browser extension using Better Auth and Plasmo.
218 | We highly recommend you visit the <Link href="https://docs.plasmo.com/">Plasmo documentation</Link> to learn more about the framework.
219 | 
220 | If you would like to view a completed example, you can check out the <Link href="https://github.com/better-auth/examples/tree/main/browser-extension-example">browser extension example</Link>.
221 | 
222 | If you have any questions, feel free to open an issue on our <Link href="https://github.com/better-auth/better-auth/issues">GitHub repo</Link>, or join our <Link href="https://discord.gg/better-auth">Discord server</Link> for support.
223 | 
```

--------------------------------------------------------------------------------
/packages/stripe/src/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type {
  2 | 	GenericEndpointContext,
  3 | 	InferOptionSchema,
  4 | 	Session,
  5 | 	User,
  6 | } from "better-auth";
  7 | import type Stripe from "stripe";
  8 | import type { subscriptions, user } from "./schema";
  9 | 
 10 | export type StripePlan = {
 11 | 	/**
 12 | 	 * Monthly price id
 13 | 	 */
 14 | 	priceId?: string;
 15 | 	/**
 16 | 	 * To use lookup key instead of price id
 17 | 	 *
 18 | 	 * https://docs.stripe.com/products-prices/
 19 | 	 * manage-prices#lookup-keys
 20 | 	 */
 21 | 	lookupKey?: string;
 22 | 	/**
 23 | 	 * A yearly discount price id
 24 | 	 *
 25 | 	 * useful when you want to offer a discount for
 26 | 	 * yearly subscription
 27 | 	 */
 28 | 	annualDiscountPriceId?: string;
 29 | 	/**
 30 | 	 * To use lookup key instead of price id
 31 | 	 *
 32 | 	 * https://docs.stripe.com/products-prices/
 33 | 	 * manage-prices#lookup-keys
 34 | 	 */
 35 | 	annualDiscountLookupKey?: string;
 36 | 	/**
 37 | 	 * Plan name
 38 | 	 */
 39 | 	name: string;
 40 | 	/**
 41 | 	 * Limits for the plan
 42 | 	 */
 43 | 	limits?: Record<string, number>;
 44 | 	/**
 45 | 	 * Plan group name
 46 | 	 *
 47 | 	 * useful when you want to group plans or
 48 | 	 * when a user can subscribe to multiple plans.
 49 | 	 */
 50 | 	group?: string;
 51 | 	/**
 52 | 	 * Free trial days
 53 | 	 */
 54 | 	freeTrial?: {
 55 | 		/**
 56 | 		 * Number of days
 57 | 		 */
 58 | 		days: number;
 59 | 		/**
 60 | 		 * A function that will be called when the trial
 61 | 		 * starts.
 62 | 		 *
 63 | 		 * @param subscription
 64 | 		 * @returns
 65 | 		 */
 66 | 		onTrialStart?: (subscription: Subscription) => Promise<void>;
 67 | 		/**
 68 | 		 * A function that will be called when the trial
 69 | 		 * ends
 70 | 		 *
 71 | 		 * @param subscription - Subscription
 72 | 		 * @returns
 73 | 		 */
 74 | 		onTrialEnd?: (
 75 | 			data: {
 76 | 				subscription: Subscription;
 77 | 			},
 78 | 			ctx: GenericEndpointContext,
 79 | 		) => Promise<void>;
 80 | 		/**
 81 | 		 * A function that will be called when the trial
 82 | 		 * expired.
 83 | 		 * @param subscription - Subscription
 84 | 		 * @returns
 85 | 		 */
 86 | 		onTrialExpired?: (
 87 | 			subscription: Subscription,
 88 | 			ctx: GenericEndpointContext,
 89 | 		) => Promise<void>;
 90 | 	};
 91 | };
 92 | 
 93 | export interface Subscription {
 94 | 	/**
 95 | 	 * Database identifier
 96 | 	 */
 97 | 	id: string;
 98 | 	/**
 99 | 	 * The plan name
100 | 	 */
101 | 	plan: string;
102 | 	/**
103 | 	 * Stripe customer id
104 | 	 */
105 | 	stripeCustomerId?: string;
106 | 	/**
107 | 	 * Stripe subscription id
108 | 	 */
109 | 	stripeSubscriptionId?: string;
110 | 	/**
111 | 	 * Trial start date
112 | 	 */
113 | 	trialStart?: Date;
114 | 	/**
115 | 	 * Trial end date
116 | 	 */
117 | 	trialEnd?: Date;
118 | 	/**
119 | 	 * Price Id for the subscription
120 | 	 */
121 | 	priceId?: string;
122 | 	/**
123 | 	 * To what reference id the subscription belongs to
124 | 	 * @example
125 | 	 * - userId for a user
126 | 	 * - workspace id for a saas platform
127 | 	 * - website id for a hosting platform
128 | 	 *
129 | 	 * @default - userId
130 | 	 */
131 | 	referenceId: string;
132 | 	/**
133 | 	 * Subscription status
134 | 	 */
135 | 	status:
136 | 		| "active"
137 | 		| "canceled"
138 | 		| "incomplete"
139 | 		| "incomplete_expired"
140 | 		| "past_due"
141 | 		| "paused"
142 | 		| "trialing"
143 | 		| "unpaid";
144 | 	/**
145 | 	 * The billing cycle start date
146 | 	 */
147 | 	periodStart?: Date;
148 | 	/**
149 | 	 * The billing cycle end date
150 | 	 */
151 | 	periodEnd?: Date;
152 | 	/**
153 | 	 * Cancel at period end
154 | 	 */
155 | 	cancelAtPeriodEnd?: boolean;
156 | 	/**
157 | 	 * A field to group subscriptions so you can have multiple subscriptions
158 | 	 * for one reference id
159 | 	 */
160 | 	groupId?: string;
161 | 	/**
162 | 	 * Number of seats for the subscription (useful for team plans)
163 | 	 */
164 | 	seats?: number;
165 | }
166 | 
167 | export interface StripeOptions {
168 | 	/**
169 | 	 * Stripe Client
170 | 	 */
171 | 	stripeClient: Stripe;
172 | 	/**
173 | 	 * Stripe Webhook Secret
174 | 	 *
175 | 	 * @description Stripe webhook secret key
176 | 	 */
177 | 	stripeWebhookSecret: string;
178 | 	/**
179 | 	 * Enable customer creation when a user signs up
180 | 	 */
181 | 	createCustomerOnSignUp?: boolean;
182 | 	/**
183 | 	 * A callback to run after a customer has been created
184 | 	 * @param customer - Customer Data
185 | 	 * @param stripeCustomer - Stripe Customer Data
186 | 	 * @returns
187 | 	 */
188 | 	onCustomerCreate?: (
189 | 		data: {
190 | 			stripeCustomer: Stripe.Customer;
191 | 			user: User & { stripeCustomerId: string };
192 | 		},
193 | 		ctx: GenericEndpointContext,
194 | 	) => Promise<void>;
195 | 	/**
196 | 	 * A custom function to get the customer create
197 | 	 * params
198 | 	 * @param data - data containing user and session
199 | 	 * @returns
200 | 	 */
201 | 	getCustomerCreateParams?: (
202 | 		user: User,
203 | 		ctx: GenericEndpointContext,
204 | 	) => Promise<Partial<Stripe.CustomerCreateParams>>;
205 | 	/**
206 | 	 * Subscriptions
207 | 	 */
208 | 	subscription?: {
209 | 		enabled: boolean;
210 | 		/**
211 | 		 * Subscription Configuration
212 | 		 */
213 | 		/**
214 | 		 * List of plan
215 | 		 */
216 | 		plans: StripePlan[] | (() => StripePlan[] | Promise<StripePlan[]>);
217 | 		/**
218 | 		 * Require email verification before a user is allowed to upgrade
219 | 		 * their subscriptions
220 | 		 *
221 | 		 * @default false
222 | 		 */
223 | 		requireEmailVerification?: boolean;
224 | 		/**
225 | 		 * A callback to run after a user has subscribed to a package
226 | 		 * @param event - Stripe Event
227 | 		 * @param subscription - Subscription Data
228 | 		 * @returns
229 | 		 */
230 | 		onSubscriptionComplete?: (
231 | 			data: {
232 | 				event: Stripe.Event;
233 | 				stripeSubscription: Stripe.Subscription;
234 | 				subscription: Subscription;
235 | 				plan: StripePlan;
236 | 			},
237 | 			ctx: GenericEndpointContext,
238 | 		) => Promise<void>;
239 | 		/**
240 | 		 * A callback to run after a user is about to cancel their subscription
241 | 		 * @returns
242 | 		 */
243 | 		onSubscriptionUpdate?: (data: {
244 | 			event: Stripe.Event;
245 | 			subscription: Subscription;
246 | 		}) => Promise<void>;
247 | 		/**
248 | 		 * A callback to run after a user is about to cancel their subscription
249 | 		 * @returns
250 | 		 */
251 | 		onSubscriptionCancel?: (data: {
252 | 			event?: Stripe.Event;
253 | 			subscription: Subscription;
254 | 			stripeSubscription: Stripe.Subscription;
255 | 			cancellationDetails?: Stripe.Subscription.CancellationDetails | null;
256 | 		}) => Promise<void>;
257 | 		/**
258 | 		 * A function to check if the reference id is valid
259 | 		 * and belongs to the user
260 | 		 *
261 | 		 * @param data - data containing user, session and referenceId
262 | 		 * @param ctx - the context object
263 | 		 * @returns
264 | 		 */
265 | 		authorizeReference?: (
266 | 			data: {
267 | 				user: User & Record<string, any>;
268 | 				session: Session & Record<string, any>;
269 | 				referenceId: string;
270 | 				action:
271 | 					| "upgrade-subscription"
272 | 					| "list-subscription"
273 | 					| "cancel-subscription"
274 | 					| "restore-subscription"
275 | 					| "billing-portal";
276 | 			},
277 | 			ctx: GenericEndpointContext,
278 | 		) => Promise<boolean>;
279 | 		/**
280 | 		 * A callback to run after a user has deleted their subscription
281 | 		 * @returns
282 | 		 */
283 | 		onSubscriptionDeleted?: (data: {
284 | 			event: Stripe.Event;
285 | 			stripeSubscription: Stripe.Subscription;
286 | 			subscription: Subscription;
287 | 		}) => Promise<void>;
288 | 		/**
289 | 		 * parameters for session create params
290 | 		 *
291 | 		 * @param data - data containing user, session and plan
292 | 		 * @param ctx - the context object
293 | 		 */
294 | 		getCheckoutSessionParams?: (
295 | 			data: {
296 | 				user: User & Record<string, any>;
297 | 				session: Session & Record<string, any>;
298 | 				plan: StripePlan;
299 | 				subscription: Subscription;
300 | 			},
301 | 			ctx: GenericEndpointContext,
302 | 		) =>
303 | 			| Promise<{
304 | 					params?: Stripe.Checkout.SessionCreateParams;
305 | 					options?: Stripe.RequestOptions;
306 | 			  }>
307 | 			| {
308 | 					params?: Stripe.Checkout.SessionCreateParams;
309 | 					options?: Stripe.RequestOptions;
310 | 			  };
311 | 		/**
312 | 		 * Enable organization subscription
313 | 		 */
314 | 		organization?: {
315 | 			enabled: boolean;
316 | 		};
317 | 	};
318 | 	/**
319 | 	 * A callback to run after a stripe event is received
320 | 	 * @param event - Stripe Event
321 | 	 * @returns
322 | 	 */
323 | 	onEvent?: (event: Stripe.Event) => Promise<void>;
324 | 	/**
325 | 	 * Schema for the stripe plugin
326 | 	 */
327 | 	schema?: InferOptionSchema<typeof subscriptions & typeof user>;
328 | }
329 | 
330 | export interface InputSubscription extends Omit<Subscription, "id"> {}
331 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/concepts/rate-limit.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Rate Limit
  3 | description: How to limit the number of requests a user can make to the server in a given time period.
  4 | ---
  5 | 
  6 | Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to:
  7 | 
  8 | - Window: 60 seconds
  9 | - Max Requests: 100 requests
 10 | 
 11 | <Callout type="warning">
 12 | Server-side requests made using `auth.api` aren't affected by rate limiting. Rate limits only apply to client-initiated requests.
 13 | </Callout>
 14 | 
 15 | You can easily customize these settings by passing the rateLimit object to the betterAuth function.
 16 | 
 17 | ```ts title="auth.ts"
 18 | import { betterAuth } from "better-auth";
 19 | 
 20 | export const auth = betterAuth({
 21 |     rateLimit: {
 22 |         window: 10, // time window in seconds
 23 |         max: 100, // max requests in the window
 24 |     },
 25 | })
 26 | ```
 27 | 
 28 | Rate limiting is disabled in development mode by default. In order to enable it, set `enabled` to `true`:
 29 | 
 30 | ```ts title="auth.ts"
 31 | export const auth = betterAuth({
 32 |     rateLimit: {
 33 |         enabled: true,
 34 |         //...other options
 35 |     },
 36 | })
 37 | ```
 38 | 
 39 | In addition to the default settings, Better Auth provides custom rules for specific paths. For example:
 40 | - `/sign-in/email`: Is limited to 3 requests within 10 seconds.
 41 | 
 42 | In addition, plugins also define custom rules for specific paths. For example, `twoFactor` plugin has custom rules:
 43 | - `/two-factor/verify`: Is limited to 3 requests within 10 seconds.
 44 | 
 45 | These custom rules ensure that sensitive operations are protected with stricter limits.
 46 | 
 47 | ## Configuring Rate Limit
 48 | 
 49 | ### Connecting IP Address
 50 | 
 51 | Rate limiting uses the connecting IP address to track the number of requests made by a user. The
 52 | default header checked is `x-forwarded-for`, which is commonly used in production environments. If
 53 | you are using a different header to track the user's IP address, you'll need to specify it.
 54 | 
 55 | ```ts title="auth.ts" 
 56 | export const auth = betterAuth({
 57 |     //...other options
 58 |     advanced: {
 59 |         ipAddress: {
 60 |           ipAddressHeaders: ["cf-connecting-ip"], // Cloudflare specific header example
 61 |       },
 62 |     },
 63 |     rateLimit: {
 64 |         enabled: true,
 65 |         window: 60, // time window in seconds
 66 |         max: 100, // max requests in the window
 67 |     },
 68 | })
 69 | ```
 70 | 
 71 | ### Rate Limit Window
 72 | 
 73 | ```ts title="auth.ts"
 74 | import { betterAuth } from "better-auth";
 75 | 
 76 | export const auth = betterAuth({
 77 |     //...other options
 78 |     rateLimit: {
 79 |         window: 60, // time window in seconds
 80 |         max: 100, // max requests in the window
 81 |     },
 82 | })
 83 | ```
 84 | 
 85 | You can also pass custom rules for specific paths.
 86 | 
 87 | ```ts title="auth.ts"
 88 | import { betterAuth } from "better-auth";
 89 | 
 90 | export const auth = betterAuth({
 91 |     //...other options
 92 |     rateLimit: {
 93 |         window: 60, // time window in seconds
 94 |         max: 100, // max requests in the window
 95 |         customRules: {
 96 |             "/sign-in/email": {
 97 |                 window: 10,
 98 |                 max: 3,
 99 |             },
100 |             "/two-factor/*": async (request)=> {
101 |                 // custom function to return rate limit window and max
102 |                 return {
103 |                     window: 10,
104 |                     max: 3,
105 |                 }
106 |             }
107 |         },
108 |     },
109 | })
110 | ```
111 | 
112 | If you like to disable rate limiting for a specific path, you can set it to `false` or return `false` from the custom rule function.
113 | 
114 | ```ts title="auth.ts"
115 | import { betterAuth } from "better-auth";
116 | 
117 | export const auth = betterAuth({
118 |     //...other options
119 |     rateLimit: {
120 |         customRules: {
121 |             "/get-session": false,
122 |         },
123 |     },
124 | })
125 | ```
126 | 
127 | ### Storage
128 | 
129 | By default, rate limit data is stored in memory, which may not be suitable for many use cases, particularly in serverless environments. To address this, you can use a database, secondary storage, or custom storage for storing rate limit data.
130 | 
131 | **Using Database**
132 | 
133 | ```ts title="auth.ts"
134 | import { betterAuth } from "better-auth";
135 | 
136 | export const auth = betterAuth({
137 |     //...other options
138 |     rateLimit: {
139 |         storage: "database",
140 |         modelName: "rateLimit", //optional by default "rateLimit" is used
141 |     },
142 | })
143 | ```
144 | 
145 | Make sure to run `migrate` to create the rate limit table in your database.
146 | 
147 | ```bash
148 | npx @better-auth/cli migrate
149 | ```
150 | 
151 | **Using Secondary Storage**
152 | 
153 | If a [Secondary Storage](/docs/concepts/database#secondary-storage) has been configured you can use that to store rate limit data.
154 | 
155 | ```ts title="auth.ts"
156 | import { betterAuth } from "better-auth";
157 | 
158 | export const auth = betterAuth({
159 |     //...other options
160 |     rateLimit: {
161 | 		storage: "secondary-storage"
162 |     },
163 | })
164 | ```
165 | 
166 | **Custom Storage**
167 | 
168 | If none of the above solutions suits your use case you can implement a `customStorage`.
169 | 
170 | ```ts title="auth.ts"
171 | import { betterAuth } from "better-auth";
172 | 
173 | export const auth = betterAuth({
174 |     //...other options
175 |     rateLimit: {
176 |         customStorage: {
177 |             get: async (key) => {
178 |                 // get rate limit data
179 |             },
180 |             set: async (key, value) => {
181 |                 // set rate limit data
182 |             },
183 |         },
184 |     },
185 | })
186 | ```
187 | 
188 | ## Handling Rate Limit Errors
189 | 
190 | When a request exceeds the rate limit, Better Auth returns the following header:
191 | 
192 | - `X-Retry-After`: The number of seconds until the user can make another request.
193 | 
194 | To handle rate limit errors on the client side, you can manage them either globally or on a per-request basis. Since Better Auth clients wrap over Better Fetch, you can pass `fetchOptions` to handle rate limit errors
195 | 
196 | **Global Handling**
197 | 
198 | ```ts title="auth-client.ts"
199 | import { createAuthClient } from "better-auth/client";
200 | 
201 | export const authClient = createAuthClient({
202 |     fetchOptions: {
203 |         onError: async (context) => {
204 |             const { response } = context;
205 |             if (response.status === 429) {
206 |                 const retryAfter = response.headers.get("X-Retry-After");
207 |                 console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
208 |             }
209 |         },
210 |     }
211 | })
212 | ```
213 | 
214 | 
215 | **Per Request Handling**
216 | 
217 | ```ts title="auth-client.ts"
218 | import { authClient } from "./auth-client";
219 | 
220 | await authClient.signIn.email({
221 |     fetchOptions: {
222 |         onError: async (context) => {
223 |             const { response } = context;
224 |             if (response.status === 429) {
225 |                 const retryAfter = response.headers.get("X-Retry-After");
226 |                 console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
227 |             }
228 |         },
229 |     }
230 | })
231 | ```
232 | 
233 | ### Schema
234 | 
235 | If you are using a database to store rate limit data you need this schema:
236 | 
237 | Table Name: `rateLimit`
238 | 
239 | <DatabaseTable
240 |     fields={[
241 |         { 
242 |         name: "id", 
243 |         type: "string", 
244 |         description: "Database ID",
245 |         isPrimaryKey: true
246 |         },
247 |         { 
248 |         name: "key", 
249 |         type: "string", 
250 |         description: "Unique identifier for each rate limit key",
251 |         },
252 |         { 
253 |         name: "count", 
254 |         type: "integer", 
255 |         description: "Time window in seconds" 
256 |         },
257 |         { 
258 |         name: "lastRequest", 
259 |         type: "bigint", 
260 |         description: "Max requests in the window" 
261 |         }]}
262 |     />
263 | 
```
Page 21/69FirstPrevNextLast