#
tokens: 47581/50000 9/1120 files (page 28/71)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 28 of 71. 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
│   └── stateless
│       ├── .env.example
│       ├── .gitignore
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── src
│       │   ├── app
│       │   │   ├── api
│       │   │   │   ├── auth
│       │   │   │   │   └── [...all]
│       │   │   │   │       └── route.ts
│       │   │   │   └── user
│       │   │   │       └── route.ts
│       │   │   ├── dashboard
│       │   │   │   └── page.tsx
│       │   │   ├── globals.css
│       │   │   ├── layout.tsx
│       │   │   └── page.tsx
│       │   └── lib
│       │       ├── auth-client.ts
│       │       └── auth.ts
│       ├── tailwind.config.ts
│       └── tsconfig.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
│   │       │   ├── polar.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-custom-schema.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-schema.test.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
│   │   │   │   ├── polar.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
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── expo.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.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/app/changelogs/_components/default-changelog.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import Link from "next/link";
  2 | import { useId } from "react";
  3 | import { cn } from "@/lib/utils";
  4 | import { IconLink } from "./changelog-layout";
  5 | import { BookIcon, GitHubIcon, XIcon } from "./icons";
  6 | import { DiscordLogoIcon } from "@radix-ui/react-icons";
  7 | import { StarField } from "./stat-field";
  8 | import { betterFetch } from "@better-fetch/fetch";
  9 | import Markdown from "react-markdown";
 10 | import defaultMdxComponents from "fumadocs-ui/mdx";
 11 | import rehypeHighlight from "rehype-highlight";
 12 | import "highlight.js/styles/dark.css";
 13 | 
 14 | export const dynamic = "force-static";
 15 | const ChangelogPage = async () => {
 16 | 	const { data: releases } = await betterFetch<
 17 | 		{
 18 | 			id: number;
 19 | 			tag_name: string;
 20 | 			name: string;
 21 | 			body: string;
 22 | 			html_url: string;
 23 | 			prerelease: boolean;
 24 | 			published_at: string;
 25 | 		}[]
 26 | 	>("https://api.github.com/repos/better-auth/better-auth/releases");
 27 | 
 28 | 	const messages = releases
 29 | 		?.filter((release) => !release.prerelease)
 30 | 		.map((release) => ({
 31 | 			tag: release.tag_name,
 32 | 			title: release.name,
 33 | 			content: getContent(release.body),
 34 | 			date: new Date(release.published_at).toLocaleDateString("en-US", {
 35 | 				year: "numeric",
 36 | 				month: "short",
 37 | 				day: "numeric",
 38 | 			}),
 39 | 			url: release.html_url,
 40 | 		}));
 41 | 
 42 | 	function getContent(content: string) {
 43 | 		const lines = content.split("\n");
 44 | 		const newContext = lines.map((line) => {
 45 | 			if (line.trim().startsWith("- ")) {
 46 | 				const mainContent = line.split(";")[0];
 47 | 				const context = line.split(";")[2];
 48 | 				const mentionMatches =
 49 | 					(context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? [];
 50 | 				if (mentionMatches.length === 0) {
 51 | 					return (mainContent || line).replace(/&nbsp/g, "");
 52 | 				}
 53 | 				const mentions = mentionMatches.map((match) => {
 54 | 					const username = match.slice(1);
 55 | 					const avatarUrl = `https://github.com/${username}.png`;
 56 | 					return `[![${match}](${avatarUrl})](https://github.com/${username})`;
 57 | 				});
 58 | 				// Remove &nbsp
 59 | 				return (
 60 | 					(mainContent || line).replace(/&nbsp/g, "") +
 61 | 					" – " +
 62 | 					mentions.join(" ")
 63 | 				);
 64 | 			}
 65 | 			return line;
 66 | 		});
 67 | 		return newContext.join("\n");
 68 | 	}
 69 | 
 70 | 	return (
 71 | 		<div className="grid items-start md:grid-cols-2">
 72 | 			<div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10">
 73 | 				<StarField className="top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
 74 | 				<Glow />
 75 | 
 76 | 				<div className="flex flex-col mx-auto max-w-xl h-full md:justify-center">
 77 | 					<h1 className="mt-14 font-sans text-5xl font-semibold tracking-tighter">
 78 | 						All of the changes made will be{" "}
 79 | 						<span className="">available here.</span>
 80 | 					</h1>
 81 | 					<p className="mt-4 text-sm text-gray-600 dark:text-gray-300">
 82 | 						Better Auth is comprehensive authentication library for TypeScript
 83 | 						that provides a wide range of features to make authentication easier
 84 | 						and more secure.
 85 | 					</p>
 86 | 					<hr className="mt-5 h-px bg-gray-300" />
 87 | 					<div className="flex flex-wrap gap-x-1 gap-y-3 mt-8 text-gray-600 dark:text-gray-300 sm:gap-x-2">
 88 | 						<IconLink
 89 | 							href="/docs"
 90 | 							icon={BookIcon}
 91 | 							className="flex-none text-gray-600 dark:text-gray-300"
 92 | 						>
 93 | 							Documentation
 94 | 						</IconLink>
 95 | 						<IconLink
 96 | 							href="https://github.com/better-auth/better-auth"
 97 | 							icon={GitHubIcon}
 98 | 							className="flex-none text-gray-600 dark:text-gray-300"
 99 | 						>
100 | 							GitHub
101 | 						</IconLink>
102 | 						<IconLink
103 | 							href="https://discord.gg/better-auth"
104 | 							icon={DiscordLogoIcon}
105 | 							className="flex-none text-gray-600 dark:text-gray-300"
106 | 						>
107 | 							Community
108 | 						</IconLink>
109 | 					</div>
110 | 					<p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500">
111 | 						<IconLink href="https://x.com/better_auth" icon={XIcon} compact>
112 | 							BETTER-AUTH.
113 | 						</IconLink>
114 | 					</p>
115 | 				</div>
116 | 			</div>
117 | 			<div className="relative px-4 pb-12 md:px-8 md:py-12">
118 | 				<div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div>
119 | 
120 | 				<div className="relative max-w-2xl">
121 | 					<Markdown
122 | 						rehypePlugins={[[rehypeHighlight]]}
123 | 						components={{
124 | 							pre: (props) => (
125 | 								<defaultMdxComponents.pre
126 | 									{...props}
127 | 									className={cn(props.className, " ml-10 my-2")}
128 | 								/>
129 | 							),
130 | 							h2: (props) => (
131 | 								<h2
132 | 									id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically
133 | 									className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]"
134 | 									{...props}
135 | 								>
136 | 									<div className="sticky top-0 left-[-9.9rem] hidden md:block">
137 | 										<time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal">
138 | 											{props.children?.toString().includes("date=") &&
139 | 												props.children?.toString().split("date=")[1]}
140 | 
141 | 											<div className="w-4 h-[1px] dark:bg-white/60 bg-black" />
142 | 										</time>
143 | 									</div>
144 | 									<Link
145 | 										href={
146 | 											props.children
147 | 												?.toString()
148 | 												.split("date=")[0]
149 | 												.trim()
150 | 												.endsWith(".00")
151 | 												? `/changelogs/${props.children
152 | 														?.toString()
153 | 														.split("date=")[0]
154 | 														.trim()}`
155 | 												: `#${props.children
156 | 														?.toString()
157 | 														.split("date=")[0]
158 | 														.trim()}`
159 | 										}
160 | 									>
161 | 										{props.children?.toString().split("date=")[0].trim()}
162 | 									</Link>
163 | 									<p className="hidden text-xs font-normal opacity-60">
164 | 										{props.children?.toString().includes("date=") &&
165 | 											props.children?.toString().split("date=")[1]}
166 | 									</p>
167 | 								</h2>
168 | 							),
169 | 							h3: (props) => (
170 | 								<h3 className="py-1 text-xl tracking-tighter" {...props}>
171 | 									{props.children?.toString()?.trim()}
172 | 									<hr className="h-[1px] my-1 mb-2 bg-input" />
173 | 								</h3>
174 | 							),
175 | 							p: (props) => <p className="my-0 ml-10 text-sm" {...props} />,
176 | 							ul: (props) => (
177 | 								<ul
178 | 									className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300"
179 | 									{...props}
180 | 								/>
181 | 							),
182 | 							li: (props) => <li className="my-1" {...props} />,
183 | 							a: ({ className, ...props }: any) => (
184 | 								<Link
185 | 									target="_blank"
186 | 									className={cn("font-medium underline", className)}
187 | 									{...props}
188 | 								/>
189 | 							),
190 | 							strong: (props) => (
191 | 								<strong className="font-semibold" {...props} />
192 | 							),
193 | 							img: (props) => (
194 | 								<img
195 | 									className="inline-block w-6 h-6 rounded-full border opacity-70"
196 | 									{...props}
197 | 									style={{ maxWidth: "100%" }}
198 | 								/>
199 | 							),
200 | 						}}
201 | 					>
202 | 						{messages
203 | 							?.map((message) => {
204 | 								return `
205 | ## ${message.title} date=${message.date}
206 | 
207 | ${message.content}
208 | 								`;
209 | 							})
210 | 							.join("\n")}
211 | 					</Markdown>
212 | 				</div>
213 | 			</div>
214 | 		</div>
215 | 	);
216 | };
217 | 
218 | export default ChangelogPage;
219 | 
220 | export function Glow() {
221 | 	let id = useId();
222 | 
223 | 	return (
224 | 		<div className="overflow-hidden absolute inset-0 bg-gradient-to-tr from-transparent -z-10 dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10">
225 | 			<svg
226 | 				className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]"
227 | 				aria-hidden="true"
228 | 			>
229 | 				<defs>
230 | 					<radialGradient id={`${id}-desktop`} cx="100%">
231 | 						<stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" />
232 | 						<stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" />
233 | 						<stop offset="100%" stopColor="rgba(0, 0, 0, 0)" />
234 | 					</radialGradient>
235 | 					<radialGradient id={`${id}-mobile`} cy="100%">
236 | 						<stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" />
237 | 						<stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" />
238 | 						<stop offset="100%" stopColor="rgba(0, 0, 0, 0)" />
239 | 					</radialGradient>
240 | 				</defs>
241 | 				<rect
242 | 					width="100%"
243 | 					height="100%"
244 | 					fill={`url(#${id}-desktop)`}
245 | 					className="hidden lg:block"
246 | 				/>
247 | 				<rect
248 | 					width="100%"
249 | 					height="100%"
250 | 					fill={`url(#${id}-mobile)`}
251 | 					className="lg:hidden"
252 | 				/>
253 | 			</svg>
254 | 			<div className="absolute inset-x-0 right-0 bottom-0 h-px mix-blend-overlay dark:bg-white/5 lg:left-auto lg:top-0 lg:h-auto lg:w-px" />
255 | 		</div>
256 | 	);
257 | }
258 | 
```

--------------------------------------------------------------------------------
/docs/components/ui/chart.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import * as RechartsPrimitive from "recharts";
  5 | 
  6 | import { cn } from "@/lib/utils";
  7 | 
  8 | // Format: { THEME_NAME: CSS_SELECTOR }
  9 | const THEMES = { light: "", dark: ".dark" } as const;
 10 | 
 11 | export type ChartConfig = {
 12 | 	[k in string]: {
 13 | 		label?: React.ReactNode;
 14 | 		icon?: React.ComponentType;
 15 | 	} & (
 16 | 		| { color?: string; theme?: never }
 17 | 		| { color?: never; theme: Record<keyof typeof THEMES, string> }
 18 | 	);
 19 | };
 20 | 
 21 | type ChartContextProps = {
 22 | 	config: ChartConfig;
 23 | };
 24 | 
 25 | const ChartContext = React.createContext<ChartContextProps | null>(null);
 26 | 
 27 | function useChart() {
 28 | 	const context = React.useContext(ChartContext);
 29 | 
 30 | 	if (!context) {
 31 | 		throw new Error("useChart must be used within a <ChartContainer />");
 32 | 	}
 33 | 
 34 | 	return context;
 35 | }
 36 | 
 37 | function ChartContainer({
 38 | 	id,
 39 | 	className,
 40 | 	children,
 41 | 	config,
 42 | 	...props
 43 | }: React.ComponentProps<"div"> & {
 44 | 	config: ChartConfig;
 45 | 	children: React.ComponentProps<
 46 | 		typeof RechartsPrimitive.ResponsiveContainer
 47 | 	>["children"];
 48 | }) {
 49 | 	const uniqueId = React.useId();
 50 | 	const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
 51 | 
 52 | 	return (
 53 | 		<ChartContext.Provider value={{ config }}>
 54 | 			<div
 55 | 				data-slot="chart"
 56 | 				data-chart={chartId}
 57 | 				className={cn(
 58 | 					"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
 59 | 					className,
 60 | 				)}
 61 | 				{...props}
 62 | 			>
 63 | 				<ChartStyle id={chartId} config={config} />
 64 | 				<RechartsPrimitive.ResponsiveContainer>
 65 | 					{children}
 66 | 				</RechartsPrimitive.ResponsiveContainer>
 67 | 			</div>
 68 | 		</ChartContext.Provider>
 69 | 	);
 70 | }
 71 | 
 72 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
 73 | 	const colorConfig = Object.entries(config).filter(
 74 | 		([, config]) => config.theme || config.color,
 75 | 	);
 76 | 
 77 | 	if (!colorConfig.length) {
 78 | 		return null;
 79 | 	}
 80 | 
 81 | 	return (
 82 | 		<style
 83 | 			dangerouslySetInnerHTML={{
 84 | 				__html: Object.entries(THEMES)
 85 | 					.map(
 86 | 						([theme, prefix]) => `
 87 | ${prefix} [data-chart=${id}] {
 88 | ${colorConfig
 89 | 	.map(([key, itemConfig]) => {
 90 | 		const color =
 91 | 			itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
 92 | 			itemConfig.color;
 93 | 		return color ? `  --color-${key}: ${color};` : null;
 94 | 	})
 95 | 	.join("\n")}
 96 | }
 97 | `,
 98 | 					)
 99 | 					.join("\n"),
100 | 			}}
101 | 		/>
102 | 	);
103 | };
104 | 
105 | const ChartTooltip = RechartsPrimitive.Tooltip;
106 | 
107 | function ChartTooltipContent({
108 | 	active,
109 | 	payload,
110 | 	className,
111 | 	indicator = "dot",
112 | 	hideLabel = false,
113 | 	hideIndicator = false,
114 | 	label,
115 | 	labelFormatter,
116 | 	labelClassName,
117 | 	formatter,
118 | 	color,
119 | 	nameKey,
120 | 	labelKey,
121 | }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122 | 	React.ComponentProps<"div"> & {
123 | 		hideLabel?: boolean;
124 | 		hideIndicator?: boolean;
125 | 		indicator?: "line" | "dot" | "dashed";
126 | 		nameKey?: string;
127 | 		labelKey?: string;
128 | 	}) {
129 | 	const { config } = useChart();
130 | 
131 | 	const tooltipLabel = React.useMemo(() => {
132 | 		if (hideLabel || !payload?.length) {
133 | 			return null;
134 | 		}
135 | 
136 | 		const [item] = payload;
137 | 		const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
138 | 		const itemConfig = getPayloadConfigFromPayload(config, item, key);
139 | 		const value =
140 | 			!labelKey && typeof label === "string"
141 | 				? config[label as keyof typeof config]?.label || label
142 | 				: itemConfig?.label;
143 | 
144 | 		if (labelFormatter) {
145 | 			return (
146 | 				<div className={cn("font-medium", labelClassName)}>
147 | 					{labelFormatter(value, payload)}
148 | 				</div>
149 | 			);
150 | 		}
151 | 
152 | 		if (!value) {
153 | 			return null;
154 | 		}
155 | 
156 | 		return <div className={cn("font-medium", labelClassName)}>{value}</div>;
157 | 	}, [
158 | 		label,
159 | 		labelFormatter,
160 | 		payload,
161 | 		hideLabel,
162 | 		labelClassName,
163 | 		config,
164 | 		labelKey,
165 | 	]);
166 | 
167 | 	if (!active || !payload?.length) {
168 | 		return null;
169 | 	}
170 | 
171 | 	const nestLabel = payload.length === 1 && indicator !== "dot";
172 | 
173 | 	return (
174 | 		<div
175 | 			className={cn(
176 | 				"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
177 | 				className,
178 | 			)}
179 | 		>
180 | 			{!nestLabel ? tooltipLabel : null}
181 | 			<div className="grid gap-1.5">
182 | 				{payload.map((item, index) => {
183 | 					const key = `${nameKey || item.name || item.dataKey || "value"}`;
184 | 					const itemConfig = getPayloadConfigFromPayload(config, item, key);
185 | 					const indicatorColor = color || item.payload.fill || item.color;
186 | 
187 | 					return (
188 | 						<div
189 | 							key={item.dataKey}
190 | 							className={cn(
191 | 								"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
192 | 								indicator === "dot" && "items-center",
193 | 							)}
194 | 						>
195 | 							{formatter && item?.value !== undefined && item.name ? (
196 | 								formatter(item.value, item.name, item, index, item.payload)
197 | 							) : (
198 | 								<>
199 | 									{itemConfig?.icon ? (
200 | 										<itemConfig.icon />
201 | 									) : (
202 | 										!hideIndicator && (
203 | 											<div
204 | 												className={cn(
205 | 													"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206 | 													{
207 | 														"h-2.5 w-2.5": indicator === "dot",
208 | 														"w-1": indicator === "line",
209 | 														"w-0 border-[1.5px] border-dashed bg-transparent":
210 | 															indicator === "dashed",
211 | 														"my-0.5": nestLabel && indicator === "dashed",
212 | 													},
213 | 												)}
214 | 												style={
215 | 													{
216 | 														"--color-bg": indicatorColor,
217 | 														"--color-border": indicatorColor,
218 | 													} as React.CSSProperties
219 | 												}
220 | 											/>
221 | 										)
222 | 									)}
223 | 									<div
224 | 										className={cn(
225 | 											"flex flex-1 justify-between leading-none",
226 | 											nestLabel ? "items-end" : "items-center",
227 | 										)}
228 | 									>
229 | 										<div className="grid gap-1.5">
230 | 											{nestLabel ? tooltipLabel : null}
231 | 											<span className="text-muted-foreground">
232 | 												{itemConfig?.label || item.name}
233 | 											</span>
234 | 										</div>
235 | 										{item.value && (
236 | 											<span className="text-foreground font-mono font-medium tabular-nums">
237 | 												{item.value.toLocaleString()}
238 | 											</span>
239 | 										)}
240 | 									</div>
241 | 								</>
242 | 							)}
243 | 						</div>
244 | 					);
245 | 				})}
246 | 			</div>
247 | 		</div>
248 | 	);
249 | }
250 | 
251 | const ChartLegend = RechartsPrimitive.Legend;
252 | 
253 | function ChartLegendContent({
254 | 	className,
255 | 	hideIcon = false,
256 | 	payload,
257 | 	verticalAlign = "bottom",
258 | 	nameKey,
259 | }: React.ComponentProps<"div"> &
260 | 	Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
261 | 		hideIcon?: boolean;
262 | 		nameKey?: string;
263 | 	}) {
264 | 	const { config } = useChart();
265 | 
266 | 	if (!payload?.length) {
267 | 		return null;
268 | 	}
269 | 
270 | 	return (
271 | 		<div
272 | 			className={cn(
273 | 				"flex items-center justify-center gap-4",
274 | 				verticalAlign === "top" ? "pb-3" : "pt-3",
275 | 				className,
276 | 			)}
277 | 		>
278 | 			{payload.map((item) => {
279 | 				const key = `${nameKey || item.dataKey || "value"}`;
280 | 				const itemConfig = getPayloadConfigFromPayload(config, item, key);
281 | 
282 | 				return (
283 | 					<div
284 | 						key={item.value}
285 | 						className={cn(
286 | 							"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
287 | 						)}
288 | 					>
289 | 						{itemConfig?.icon && !hideIcon ? (
290 | 							<itemConfig.icon />
291 | 						) : (
292 | 							<div
293 | 								className="h-2 w-2 shrink-0 rounded-[2px]"
294 | 								style={{
295 | 									backgroundColor: item.color,
296 | 								}}
297 | 							/>
298 | 						)}
299 | 						{itemConfig?.label}
300 | 					</div>
301 | 				);
302 | 			})}
303 | 		</div>
304 | 	);
305 | }
306 | 
307 | // Helper to extract item config from a payload.
308 | function getPayloadConfigFromPayload(
309 | 	config: ChartConfig,
310 | 	payload: unknown,
311 | 	key: string,
312 | ) {
313 | 	if (typeof payload !== "object" || payload === null) {
314 | 		return undefined;
315 | 	}
316 | 
317 | 	const payloadPayload =
318 | 		"payload" in payload &&
319 | 		typeof payload.payload === "object" &&
320 | 		payload.payload !== null
321 | 			? payload.payload
322 | 			: undefined;
323 | 
324 | 	let configLabelKey: string = key;
325 | 
326 | 	if (
327 | 		key in payload &&
328 | 		typeof payload[key as keyof typeof payload] === "string"
329 | 	) {
330 | 		configLabelKey = payload[key as keyof typeof payload] as string;
331 | 	} else if (
332 | 		payloadPayload &&
333 | 		key in payloadPayload &&
334 | 		typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335 | 	) {
336 | 		configLabelKey = payloadPayload[
337 | 			key as keyof typeof payloadPayload
338 | 		] as string;
339 | 	}
340 | 
341 | 	return configLabelKey in config
342 | 		? config[configLabelKey]
343 | 		: config[key as keyof typeof config];
344 | }
345 | 
346 | export {
347 | 	ChartContainer,
348 | 	ChartTooltip,
349 | 	ChartTooltipContent,
350 | 	ChartLegend,
351 | 	ChartLegendContent,
352 | 	ChartStyle,
353 | };
354 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/siwe/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "../../api";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import { setSessionCookie } from "../../cookies";
  4 | import * as z from "zod";
  5 | import type { InferOptionSchema } from "../../types";
  6 | import type { BetterAuthPlugin } from "@better-auth/core";
  7 | import type {
  8 | 	ENSLookupArgs,
  9 | 	ENSLookupResult,
 10 | 	SIWEVerifyMessageArgs,
 11 | 	WalletAddress,
 12 | } from "./types";
 13 | import type { User } from "../../types";
 14 | import { schema } from "./schema";
 15 | import { getOrigin } from "../../utils/url";
 16 | import { toChecksumAddress } from "../../utils/hashing";
 17 | import { mergeSchema } from "../../db/schema";
 18 | 
 19 | export interface SIWEPluginOptions {
 20 | 	domain: string;
 21 | 	emailDomainName?: string;
 22 | 	anonymous?: boolean;
 23 | 	getNonce: () => Promise<string>;
 24 | 	verifyMessage: (args: SIWEVerifyMessageArgs) => Promise<boolean>;
 25 | 	ensLookup?: (args: ENSLookupArgs) => Promise<ENSLookupResult>;
 26 | 	schema?: InferOptionSchema<typeof schema>;
 27 | }
 28 | 
 29 | export const siwe = (options: SIWEPluginOptions) =>
 30 | 	({
 31 | 		id: "siwe",
 32 | 		schema: mergeSchema(schema, options?.schema),
 33 | 		endpoints: {
 34 | 			getSiweNonce: createAuthEndpoint(
 35 | 				"/siwe/nonce",
 36 | 				{
 37 | 					method: "POST",
 38 | 					body: z.object({
 39 | 						walletAddress: z
 40 | 							.string()
 41 | 							.regex(/^0[xX][a-fA-F0-9]{40}$/i)
 42 | 							.length(42),
 43 | 						chainId: z
 44 | 							.number()
 45 | 							.int()
 46 | 							.positive()
 47 | 							.max(2147483647)
 48 | 							.optional()
 49 | 							.default(1), // Default to Ethereum mainnet
 50 | 					}),
 51 | 				},
 52 | 				async (ctx) => {
 53 | 					const { walletAddress: rawWalletAddress, chainId } = ctx.body;
 54 | 					const walletAddress = toChecksumAddress(rawWalletAddress);
 55 | 					const nonce = await options.getNonce();
 56 | 
 57 | 					// Store nonce with wallet address and chain ID context
 58 | 					await ctx.context.internalAdapter.createVerificationValue({
 59 | 						identifier: `siwe:${walletAddress}:${chainId}`,
 60 | 						value: nonce,
 61 | 						expiresAt: new Date(Date.now() + 15 * 60 * 1000),
 62 | 					});
 63 | 
 64 | 					return ctx.json({ nonce });
 65 | 				},
 66 | 			),
 67 | 			verifySiweMessage: createAuthEndpoint(
 68 | 				"/siwe/verify",
 69 | 				{
 70 | 					method: "POST",
 71 | 					body: z
 72 | 						.object({
 73 | 							message: z.string().min(1),
 74 | 							signature: z.string().min(1),
 75 | 							walletAddress: z
 76 | 								.string()
 77 | 								.regex(/^0[xX][a-fA-F0-9]{40}$/i)
 78 | 								.length(42),
 79 | 							chainId: z
 80 | 								.number()
 81 | 								.int()
 82 | 								.positive()
 83 | 								.max(2147483647)
 84 | 								.optional()
 85 | 								.default(1),
 86 | 							email: z.string().email().optional(),
 87 | 						})
 88 | 						.refine((data) => options.anonymous !== false || !!data.email, {
 89 | 							message:
 90 | 								"Email is required when the anonymous plugin option is disabled.",
 91 | 							path: ["email"],
 92 | 						}),
 93 | 					requireRequest: true,
 94 | 				},
 95 | 				async (ctx) => {
 96 | 					const {
 97 | 						message,
 98 | 						signature,
 99 | 						walletAddress: rawWalletAddress,
100 | 						chainId,
101 | 						email,
102 | 					} = ctx.body;
103 | 					const walletAddress = toChecksumAddress(rawWalletAddress);
104 | 					const isAnon = options.anonymous ?? true;
105 | 
106 | 					if (!isAnon && !email) {
107 | 						throw new APIError("BAD_REQUEST", {
108 | 							message: "Email is required when anonymous is disabled.",
109 | 							status: 400,
110 | 						});
111 | 					}
112 | 
113 | 					try {
114 | 						// Find stored nonce with wallet address and chain ID context
115 | 						const verification =
116 | 							await ctx.context.internalAdapter.findVerificationValue(
117 | 								`siwe:${walletAddress}:${chainId}`,
118 | 							);
119 | 
120 | 						// Ensure nonce is valid and not expired
121 | 						if (!verification || new Date() > verification.expiresAt) {
122 | 							throw new APIError("UNAUTHORIZED", {
123 | 								message: "Unauthorized: Invalid or expired nonce",
124 | 								status: 401,
125 | 								code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE",
126 | 							});
127 | 						}
128 | 
129 | 						// Verify SIWE message with enhanced parameters
130 | 						const { value: nonce } = verification;
131 | 						const verified = await options.verifyMessage({
132 | 							message,
133 | 							signature,
134 | 							address: walletAddress,
135 | 							chainId,
136 | 							cacao: {
137 | 								h: { t: "caip122" },
138 | 								p: {
139 | 									domain: options.domain,
140 | 									aud: options.domain,
141 | 									nonce,
142 | 									iss: options.domain,
143 | 									version: "1",
144 | 								},
145 | 								s: { t: "eip191", s: signature },
146 | 							},
147 | 						});
148 | 
149 | 						if (!verified) {
150 | 							throw new APIError("UNAUTHORIZED", {
151 | 								message: "Unauthorized: Invalid SIWE signature",
152 | 								status: 401,
153 | 							});
154 | 						}
155 | 
156 | 						// Clean up used nonce
157 | 						await ctx.context.internalAdapter.deleteVerificationValue(
158 | 							verification.id,
159 | 						);
160 | 
161 | 						// Look for existing user by their wallet addresses
162 | 						let user: User | null = null;
163 | 
164 | 						// Check if there's a wallet address record for this exact address+chainId combination
165 | 						const existingWalletAddress: WalletAddress | null =
166 | 							await ctx.context.adapter.findOne({
167 | 								model: "walletAddress",
168 | 								where: [
169 | 									{ field: "address", operator: "eq", value: walletAddress },
170 | 									{ field: "chainId", operator: "eq", value: chainId },
171 | 								],
172 | 							});
173 | 
174 | 						if (existingWalletAddress) {
175 | 							// Get the user associated with this wallet address
176 | 							user = await ctx.context.adapter.findOne({
177 | 								model: "user",
178 | 								where: [
179 | 									{
180 | 										field: "id",
181 | 										operator: "eq",
182 | 										value: existingWalletAddress.userId,
183 | 									},
184 | 								],
185 | 							});
186 | 						} else {
187 | 							// No exact match found, check if this address exists on any other chain
188 | 							const anyWalletAddress: WalletAddress | null =
189 | 								await ctx.context.adapter.findOne({
190 | 									model: "walletAddress",
191 | 									where: [
192 | 										{ field: "address", operator: "eq", value: walletAddress },
193 | 									],
194 | 								});
195 | 
196 | 							if (anyWalletAddress) {
197 | 								// Same address exists on different chain, get that user
198 | 								user = await ctx.context.adapter.findOne({
199 | 									model: "user",
200 | 									where: [
201 | 										{
202 | 											field: "id",
203 | 											operator: "eq",
204 | 											value: anyWalletAddress.userId,
205 | 										},
206 | 									],
207 | 								});
208 | 							}
209 | 						}
210 | 
211 | 						// Create new user if none exists
212 | 						if (!user) {
213 | 							const domain =
214 | 								options.emailDomainName ?? getOrigin(ctx.context.baseURL);
215 | 							// Use checksummed address for email generation
216 | 							const userEmail =
217 | 								!isAnon && email ? email : `${walletAddress}@${domain}`;
218 | 							const { name, avatar } =
219 | 								(await options.ensLookup?.({ walletAddress })) ?? {};
220 | 
221 | 							user = await ctx.context.internalAdapter.createUser({
222 | 								name: name ?? walletAddress,
223 | 								email: userEmail,
224 | 								image: avatar ?? "",
225 | 							});
226 | 
227 | 							// Create wallet address record
228 | 							await ctx.context.adapter.create({
229 | 								model: "walletAddress",
230 | 								data: {
231 | 									userId: user.id,
232 | 									address: walletAddress,
233 | 									chainId,
234 | 									isPrimary: true, // First address is primary
235 | 									createdAt: new Date(),
236 | 								},
237 | 							});
238 | 
239 | 							// Create account record for wallet authentication
240 | 							await ctx.context.internalAdapter.createAccount({
241 | 								userId: user.id,
242 | 								providerId: "siwe",
243 | 								accountId: `${walletAddress}:${chainId}`,
244 | 								createdAt: new Date(),
245 | 								updatedAt: new Date(),
246 | 							});
247 | 						} else {
248 | 							// User exists, but check if this specific address/chain combo exists
249 | 							if (!existingWalletAddress) {
250 | 								// Add this new chainId to existing user's addresses
251 | 								await ctx.context.adapter.create({
252 | 									model: "walletAddress",
253 | 									data: {
254 | 										userId: user.id,
255 | 										address: walletAddress,
256 | 										chainId,
257 | 										isPrimary: false, // Additional addresses are not primary by default
258 | 										createdAt: new Date(),
259 | 									},
260 | 								});
261 | 
262 | 								// Create account record for this new wallet+chain combination
263 | 								await ctx.context.internalAdapter.createAccount({
264 | 									userId: user.id,
265 | 									providerId: "siwe",
266 | 									accountId: `${walletAddress}:${chainId}`,
267 | 									createdAt: new Date(),
268 | 									updatedAt: new Date(),
269 | 								});
270 | 							}
271 | 						}
272 | 
273 | 						const session = await ctx.context.internalAdapter.createSession(
274 | 							user.id,
275 | 						);
276 | 
277 | 						if (!session) {
278 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
279 | 								message: "Internal Server Error",
280 | 								status: 500,
281 | 							});
282 | 						}
283 | 
284 | 						await setSessionCookie(ctx, { session, user });
285 | 
286 | 						return ctx.json({
287 | 							token: session.token,
288 | 							success: true,
289 | 							user: {
290 | 								id: user.id,
291 | 								walletAddress,
292 | 								chainId,
293 | 							},
294 | 						});
295 | 					} catch (error: unknown) {
296 | 						if (error instanceof APIError) throw error;
297 | 						throw new APIError("UNAUTHORIZED", {
298 | 							message: "Something went wrong. Please try again later.",
299 | 							error: error instanceof Error ? error.message : "Unknown error",
300 | 							status: 401,
301 | 						});
302 | 					}
303 | 				},
304 | 			),
305 | 		},
306 | 	}) satisfies BetterAuthPlugin;
307 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/generic-oauth.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Generic OAuth
  3 | description: Authenticate users with any OAuth provider
  4 | ---
  5 | 
  6 | The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth provider. It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application.
  7 | 
  8 | ## Installation
  9 | 
 10 | <Steps>
 11 |   <Step>
 12 |     ### Add the plugin to your auth config
 13 | 
 14 |     To use the Generic OAuth plugin, add it to your auth config.
 15 | 
 16 |     ```ts title="auth.ts"
 17 |     import { betterAuth } from "better-auth"
 18 |     import { genericOAuth } from "better-auth/plugins" // [!code highlight]
 19 |     
 20 |     export const auth = betterAuth({
 21 |         // ... other config options
 22 |         plugins: [ 
 23 |             genericOAuth({ // [!code highlight]
 24 |                 config: [ // [!code highlight]
 25 |                     { // [!code highlight] 
 26 |                         providerId: "provider-id", // [!code highlight]
 27 |                         clientId: "test-client-id", // [!code highlight]
 28 |                         clientSecret: "test-client-secret", // [!code highlight]
 29 |                         discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight]
 30 |                         // ... other config options // [!code highlight]
 31 |                     }, // [!code highlight]
 32 |                     // Add more providers as needed // [!code highlight]
 33 |                 ] // [!code highlight]
 34 |             }) // [!code highlight]
 35 |         ]
 36 |     })
 37 |     ```
 38 |   </Step>
 39 | 
 40 |   <Step>
 41 |     ### Add the client plugin
 42 | 
 43 |     Include the Generic OAuth client plugin in your authentication client instance.
 44 | 
 45 |     ```ts title="auth-client.ts"
 46 |     import { createAuthClient } from "better-auth/client"
 47 |     import { genericOAuthClient } from "better-auth/client/plugins"
 48 |     
 49 |     export const authClient = createAuthClient({
 50 |         plugins: [
 51 |             genericOAuthClient()
 52 |         ]
 53 |     })
 54 |     ```
 55 |   </Step>
 56 | </Steps>
 57 | 
 58 | ## Usage
 59 | 
 60 | The Generic OAuth plugin provides endpoints for initiating the OAuth flow and handling the callback. Here's how to use them:
 61 | 
 62 | ### Initiate OAuth Sign-In
 63 | 
 64 | To start the OAuth sign-in process:
 65 | 
 66 | <APIMethod path="/sign-in/oauth2" method="POST">
 67 | ```ts
 68 | type signInWithOAuth2 = {
 69 |     /**
 70 |      * The provider ID for the OAuth provider. 
 71 |      */
 72 |     providerId: string = "provider-id"
 73 |     /**
 74 |      * The URL to redirect to after sign in. 
 75 |      */
 76 |     callbackURL?: string = "/dashboard"
 77 |     /**
 78 |      * The URL to redirect to if an error occurs. 
 79 |      */
 80 |     errorCallbackURL?: string = "/error-page"
 81 |     /**
 82 |      * The URL to redirect to after login if the user is new. 
 83 |      */
 84 |     newUserCallbackURL?: string = "/welcome"
 85 |     /**
 86 |      * Disable redirect. 
 87 |      */
 88 |     disableRedirect?: boolean = false
 89 |     /**
 90 |      * Scopes to be passed to the provider authorization request. 
 91 |      */
 92 |     scopes?: string[] = ["my-scope"]
 93 |     /**
 94 |      * Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. 
 95 |      */
 96 |     requestSignUp?: boolean = false
 97 | }
 98 | ```
 99 | </APIMethod>
100 | 
101 | ### Linking OAuth Accounts
102 | 
103 | To link an OAuth account to an existing user:
104 | 
105 | <APIMethod
106 |   path="/oauth2/link"
107 |   method="POST"
108 |   requireSession
109 | >
110 | ```ts
111 | type oAuth2LinkAccount = {
112 |     /**
113 |      * The OAuth provider ID. 
114 |      */
115 |     providerId: string = "my-provider-id"
116 |     /**
117 |      * The URL to redirect to once the account linking was complete. 
118 |      */
119 |     callbackURL: string = "/successful-link"
120 | }
121 | ```
122 | </APIMethod>
123 | 
124 | ### Handle OAuth Callback
125 | 
126 | The plugin mounts a route to handle the OAuth callback `/oauth2/callback/:providerId`. This means by default `${baseURL}/api/auth/oauth2/callback/:providerId` will be used as the callback URL. Make sure your OAuth provider is configured to use this URL.
127 | 
128 | ## Configuration
129 | 
130 | When adding the plugin to your auth config, you can configure multiple OAuth providers. Each provider configuration object supports the following options:
131 | 
132 | ```ts
133 | interface GenericOAuthConfig {
134 |   providerId: string;
135 |   discoveryUrl?: string;
136 |   authorizationUrl?: string;
137 |   tokenUrl?: string;
138 |   userInfoUrl?: string;
139 |   clientId: string;
140 |   clientSecret: string;
141 |   scopes?: string[];
142 |   redirectURI?: string;
143 |   responseType?: string;
144 |   prompt?: string;
145 |   pkce?: boolean;
146 |   accessType?: string;
147 |   getUserInfo?: (tokens: OAuth2Tokens) => Promise<User | null>;
148 | }
149 | ```
150 | 
151 | ### Other Provider Configurations
152 | 
153 | **providerId**: A unique string to identify the OAuth provider configuration.
154 | 
155 | **discoveryUrl**: (Optional) URL to fetch the provider's OAuth 2.0/OIDC configuration. If provided, endpoints like `authorizationUrl`, `tokenUrl`, and `userInfoUrl` can be auto-discovered.
156 | 
157 | **authorizationUrl**: (Optional) The OAuth provider's authorization endpoint. Not required if using `discoveryUrl`.
158 | 
159 | **tokenUrl**: (Optional) The OAuth provider's token endpoint. Not required if using `discoveryUrl`.
160 | 
161 | **userInfoUrl**: (Optional) The endpoint to fetch user profile information. Not required if using `discoveryUrl`.
162 | 
163 | **clientId**: The OAuth client ID issued by your provider.
164 | 
165 | **clientSecret**: The OAuth client secret issued by your provider.
166 | 
167 | **scopes**: (Optional) An array of scopes to request from the provider (e.g., `["openid", "email", "profile"]`).
168 | 
169 | **redirectURI**: (Optional) The redirect URI to use for the OAuth flow. If not set, a default is constructed based on your app's base URL.
170 | 
171 | **responseType**: (Optional) The OAuth response type. Defaults to `"code"` for authorization code flow.
172 | 
173 | **responseMode**: (Optional) The response mode for the authorization code request, such as `"query"` or `"form_post"`.
174 | 
175 | **prompt**: (Optional) Controls the authentication experience (e.g., force login, consent, etc.).
176 | 
177 | **pkce**: (Optional) If true, enables PKCE (Proof Key for Code Exchange) for enhanced security. Defaults to `false`.
178 | 
179 | **accessType**: (Optional) The access type for the authorization request. Use `"offline"` to request a refresh token.
180 | 
181 | **getUserInfo**: (Optional) A custom function to fetch user info from the provider, given the OAuth tokens. If not provided, a default fetch is used.
182 | 
183 | **mapProfileToUser**: (Optional) A function to map the provider's user profile to your app's user object. Useful for custom field mapping or transformations.
184 | 
185 | **authorizationUrlParams**: (Optional) Additional query parameters to add to the authorization URL. These can override default parameters. You can also provide a function that returns the parameters.
186 | 
187 | **tokenUrlParams**: (Optional) Additional query parameters to add to the token URL. These can override default parameters. You can also provide a function that returns the parameters.
188 | 
189 | **disableImplicitSignUp**: (Optional) If true, disables automatic sign-up for new users. Sign-in must be explicitly requested with sign-up intent.
190 | 
191 | **disableSignUp**: (Optional) If true, disables sign-up for new users entirely. Only existing users can sign in.
192 | 
193 | **authentication**: (Optional) The authentication method for token requests. Can be `'basic'` or `'post'`. Defaults to `'post'`.
194 | 
195 | **discoveryHeaders**: (Optional) Custom headers to include in the discovery request. Useful for providers that require special headers.
196 | 
197 | **authorizationHeaders**: (Optional) Custom headers to include in the authorization request. Useful for providers that require special headers.
198 | 
199 | **overrideUserInfo**: (Optional) If true, the user's info in your database will be updated with the provider's info every time they sign in. Defaults to `false`.
200 | 
201 | ## Advanced Usage
202 | 
203 | ### Custom User Info Fetching
204 | 
205 | You can provide a custom `getUserInfo` function to handle specific provider requirements:
206 | 
207 | ```ts
208 | genericOAuth({
209 |   config: [
210 |     {
211 |       providerId: "custom-provider",
212 |       // ... other config options
213 |       getUserInfo: async (tokens) => {
214 |         // Custom logic to fetch and return user info
215 |         const userInfo = await fetchUserInfoFromCustomProvider(tokens);
216 |         return {
217 |           id: userInfo.sub,
218 |           email: userInfo.email,
219 |           name: userInfo.name,
220 |           // ... map other fields as needed
221 |         };
222 |       }
223 |     }
224 |   ]
225 | })
226 | ```
227 | 
228 | ### Map User Info Fields
229 | 
230 | If the user info returned by the provider does not match the expected format, or you need to map additional fields, you can use the `mapProfileToUser`:
231 | 
232 | ```ts
233 | genericOAuth({
234 |   config: [
235 |     {
236 |       providerId: "custom-provider",
237 |       // ... other config options
238 |       mapProfileToUser: async (profile) => {
239 |         return {
240 |           firstName: profile.given_name,
241 |           // ... map other fields as needed
242 |         };
243 |       }
244 |     }
245 |   ]
246 | })
247 | ```
248 | 
249 | ### Error Handling
250 | 
251 | The plugin includes built-in error handling for common OAuth issues. Errors are typically redirected to your application's error page with an appropriate error message in the URL parameters. If the callback URL is not provided, the user will be redirected to Better Auth's default error page.
252 | 
253 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/siwe.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Sign In With Ethereum (SIWE)
  3 | description: Sign in with Ethereum plugin for Better Auth
  4 | ---
  5 | 
  6 | The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the [ERC-4361 standard](https://eips.ethereum.org/EIPS/eip-4361). This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic.
  7 | 
  8 | ## Installation
  9 | 
 10 | <Steps>
 11 |     <Step>
 12 |         ### Add the Server Plugin
 13 | 
 14 |         Add the SIWE plugin to your auth configuration:
 15 | 
 16 |         ```ts title="auth.ts"
 17 |         import { betterAuth } from "better-auth";
 18 |         import { siwe } from "better-auth/plugins";
 19 | 
 20 |         export const auth = betterAuth({
 21 |             plugins: [
 22 |                 siwe({
 23 |                     domain: "example.com",
 24 |                     emailDomainName: "example.com", // optional
 25 |                     anonymous: false, // optional, default is true
 26 |                     getNonce: async () => {
 27 |                         // Implement your nonce generation logic here
 28 |                         return "your-secure-random-nonce";
 29 |                     },
 30 |                     verifyMessage: async (args) => {
 31 |                         // Implement your SIWE message verification logic here
 32 |                         // This should verify the signature against the message
 33 |                         return true; // return true if signature is valid
 34 |                     },
 35 |                     ensLookup: async (args) => {
 36 |                         // Optional: Implement ENS lookup for user names and avatars
 37 |                         return {
 38 |                             name: "user.eth",
 39 |                             avatar: "https://example.com/avatar.png"
 40 |                         };
 41 |                     },
 42 |                 }),
 43 |             ],
 44 |         });
 45 |         ```
 46 |     </Step>
 47 | 
 48 |     <Step>
 49 |         ### Migrate the database
 50 | 
 51 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 52 | 
 53 |         <Tabs items={["migrate", "generate"]}>
 54 |             <Tab value="migrate">
 55 |             ```bash
 56 |             npx @better-auth/cli migrate
 57 |             ```
 58 |             </Tab>
 59 |             <Tab value="generate">
 60 |             ```bash
 61 |             npx @better-auth/cli generate
 62 |             ```
 63 |             </Tab>
 64 |         </Tabs>
 65 |         See the [Schema](#schema) section to add the fields manually.
 66 |     </Step>
 67 | 
 68 |     <Step>
 69 |         ### Add the Client Plugin
 70 | 
 71 |         ```ts title="auth-client.ts"
 72 |         import { createAuthClient } from "better-auth/client";
 73 |         import { siweClient } from "better-auth/client/plugins";
 74 | 
 75 |         export const authClient = createAuthClient({
 76 |             plugins: [siweClient()],
 77 |         });
 78 |         ```
 79 |     </Step>
 80 | 
 81 | </Steps>
 82 | 
 83 | ## Usage
 84 | 
 85 | ### Generate a Nonce
 86 | 
 87 | Before signing a SIWE message, you need to generate a nonce for the wallet address:
 88 | 
 89 | ```ts title="generate-nonce.ts"
 90 | const { data, error } = await authClient.siwe.nonce({
 91 |   walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
 92 |   chainId: 1, // optional for Ethereum mainnet, required for other chains. Defaults to 1
 93 | });
 94 | 
 95 | if (data) {
 96 |   console.log("Nonce:", data.nonce);
 97 | }
 98 | ```
 99 | 
100 | ### Sign In with Ethereum
101 | 
102 | After generating a nonce and creating a SIWE message, verify the signature to authenticate:
103 | 
104 | ```ts title="sign-in-siwe.ts"
105 | const { data, error } = await authClient.siwe.verify({
106 |   message: "Your SIWE message string",
107 |   signature: "0x...", // The signature from the user's wallet
108 |   walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
109 |   chainId: 1, // optional for Ethereum mainnet, required for other chains. Must match Chain ID in SIWE message
110 |   email: "[email protected]", // optional, required if anonymous is false
111 | });
112 | 
113 | if (data) {
114 |   console.log("Authentication successful:", data.user);
115 | }
116 | ```
117 | 
118 | ### Chain-Specific Examples
119 | 
120 | Here are examples for different blockchain networks:
121 | 
122 | ```ts title="ethereum-mainnet.ts"
123 | // Ethereum Mainnet (chainId can be omitted, defaults to 1)
124 | const { data, error } = await authClient.siwe.verify({
125 |   message,
126 |   signature,
127 |   walletAddress,
128 |   // chainId: 1 (default)
129 | });
130 | ```
131 | 
132 | ```ts title="polygon.ts"
133 | // Polygon (chainId REQUIRED)
134 | const { data, error } = await authClient.siwe.verify({
135 |   message,
136 |   signature,
137 |   walletAddress,
138 |   chainId: 137, // Required for Polygon
139 | });
140 | ```
141 | 
142 | ```ts title="arbitrum.ts"
143 | // Arbitrum (chainId REQUIRED)
144 | const { data, error } = await authClient.siwe.verify({
145 |   message,
146 |   signature,
147 |   walletAddress,
148 |   chainId: 42161, // Required for Arbitrum
149 | });
150 | ```
151 | 
152 | ```ts title="base.ts"
153 | // Base (chainId REQUIRED)
154 | const { data, error } = await authClient.siwe.verify({
155 |   message,
156 |   signature,
157 |   walletAddress,
158 |   chainId: 8453, // Required for Base
159 | });
160 | ```
161 | 
162 | <Callout type="warning">
163 |   The `chainId` must match the Chain ID specified in your SIWE message. Verification will fail with a 401 error if there's a mismatch between the message's Chain ID and the `chainId` parameter.
164 | </Callout>
165 | 
166 | ## Configuration Options
167 | 
168 | ### Server Options
169 | 
170 | The SIWE plugin accepts the following configuration options:
171 | 
172 | - **domain**: The domain name of your application (required for SIWE message generation)
173 | - **emailDomainName**: The email domain name for creating user accounts when not using anonymous mode. Defaults to the domain from your base URL
174 | - **anonymous**: Whether to allow anonymous sign-ins without requiring an email. Default is `true`
175 | - **getNonce**: Function to generate a unique nonce for each sign-in attempt. You must implement this function to return a cryptographically secure random string. Must return a `Promise<string>`
176 | - **verifyMessage**: Function to verify the signed SIWE message. Receives message details and should return `Promise<boolean>`
177 | - **ensLookup**: Optional function to lookup ENS names and avatars for Ethereum addresses
178 | 
179 | ### Client Options
180 | 
181 | The SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility:
182 | 
183 | ```ts title="auth-client.ts"
184 | import { createAuthClient } from "better-auth/client";
185 | import { siweClient } from "better-auth/client/plugins";
186 | 
187 | export const authClient = createAuthClient({
188 |   plugins: [
189 |     siweClient({
190 |       // Optional client configuration can go here
191 |     }),
192 |   ],
193 | });
194 | ```
195 | 
196 | ## Schema
197 | 
198 | The SIWE plugin adds a `walletAddress` table to store user wallet associations:
199 | 
200 | | Field     | Type    | Description                               |
201 | | --------- | ------- | ----------------------------------------- |
202 | | id        | string  | Primary key                               |
203 | | userId    | string  | Reference to user.id                      |
204 | | address   | string  | Ethereum wallet address                   |
205 | | chainId   | number  | Chain ID (e.g., 1 for Ethereum mainnet)   |
206 | | isPrimary | boolean | Whether this is the user's primary wallet |
207 | | createdAt | date    | Creation timestamp                        |
208 | 
209 | ## Example Implementation
210 | 
211 | Here's a complete example showing how to implement SIWE authentication:
212 | 
213 | ```ts title="auth.ts"
214 | import { betterAuth } from "better-auth";
215 | import { siwe } from "better-auth/plugins";
216 | import { generateRandomString } from "better-auth/crypto";
217 | import { verifyMessage, createPublicClient, http } from "viem";
218 | import { mainnet } from "viem/chains";
219 | 
220 | export const auth = betterAuth({
221 |   database: {
222 |     // your database configuration
223 |   },
224 |   plugins: [
225 |     siwe({
226 |       domain: "myapp.com",
227 |       emailDomainName: "myapp.com",
228 |       anonymous: false,
229 |       getNonce: async () => {
230 |         // Generate a cryptographically secure random nonce
231 |         return generateRandomString(32);
232 |       },
233 |       verifyMessage: async ({ message, signature, address }) => {
234 |         try {
235 |           // Verify the signature using viem (recommended)
236 |           const isValid = await verifyMessage({
237 |             address: address as `0x${string}`,
238 |             message,
239 |             signature: signature as `0x${string}`,
240 |           });
241 |           return isValid;
242 |         } catch (error) {
243 |           console.error("SIWE verification failed:", error);
244 |           return false;
245 |         }
246 |       },
247 |       ensLookup: async ({ walletAddress }) => {
248 |         try {
249 |           // Optional: lookup ENS name and avatar using viem
250 |           // You can use viem's ENS utilities here
251 |           const client = createPublicClient({
252 |             chain: mainnet,
253 |             transport: http(),
254 |           });
255 | 
256 |           const ensName = await client.getEnsName({
257 |             address: walletAddress as `0x${string}`,
258 |           });
259 | 
260 |           const ensAvatar = ensName
261 |             ? await client.getEnsAvatar({
262 |                 name: ensName,
263 |               })
264 |             : null;
265 | 
266 |           return {
267 |             name: ensName || walletAddress,
268 |             avatar: ensAvatar || "",
269 |           };
270 |         } catch {
271 |           return {
272 |             name: walletAddress,
273 |             avatar: "",
274 |           };
275 |         }
276 |       },
277 |     }),
278 |   ],
279 | });
280 | ```
281 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.mssql.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Kysely, MssqlDialect } from "kysely";
  2 | import { testAdapter } from "../../test-adapter";
  3 | import { kyselyAdapter } from "../kysely-adapter";
  4 | import {
  5 | 	authFlowTestSuite,
  6 | 	normalTestSuite,
  7 | 	numberIdTestSuite,
  8 | 	performanceTestSuite,
  9 | 	transactionsTestSuite,
 10 | } from "../../tests";
 11 | import { getMigrations } from "../../../db";
 12 | import * as Tedious from "tedious";
 13 | import * as Tarn from "tarn";
 14 | import type { BetterAuthOptions } from "@better-auth/core";
 15 | 
 16 | // We are not allowed to handle the mssql connection
 17 | // we must let kysely handle it. This is because if kysely is already
 18 | // handling it, and we were to connect it ourselves, it will create bugs.
 19 | const dialect = new MssqlDialect({
 20 | 	tarn: {
 21 | 		...Tarn,
 22 | 		options: {
 23 | 			min: 0,
 24 | 			max: 10,
 25 | 		},
 26 | 	},
 27 | 	tedious: {
 28 | 		...Tedious,
 29 | 		connectionFactory: () =>
 30 | 			new Tedious.Connection({
 31 | 				authentication: {
 32 | 					options: {
 33 | 						password: "Password123!",
 34 | 						userName: "sa",
 35 | 					},
 36 | 					type: "default",
 37 | 				},
 38 | 				options: {
 39 | 					database: "master", // Start with master database, will create better_auth if needed
 40 | 					port: 1433,
 41 | 					trustServerCertificate: true,
 42 | 					encrypt: false,
 43 | 				},
 44 | 				server: "localhost",
 45 | 			}),
 46 | 		TYPES: {
 47 | 			...Tedious.TYPES,
 48 | 			DateTime: Tedious.TYPES.DateTime2,
 49 | 		},
 50 | 	},
 51 | });
 52 | 
 53 | const kyselyDB = new Kysely({
 54 | 	dialect: dialect,
 55 | });
 56 | 
 57 | // Create better_auth database if it doesn't exist
 58 | const ensureDatabaseExists = async () => {
 59 | 	try {
 60 | 		console.log("Ensuring better_auth database exists...");
 61 | 		await kyselyDB.getExecutor().executeQuery({
 62 | 			sql: `
 63 | 				IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'better_auth')
 64 | 				BEGIN
 65 | 					CREATE DATABASE better_auth;
 66 | 					PRINT 'Database better_auth created successfully';
 67 | 				END
 68 | 				ELSE
 69 | 				BEGIN
 70 | 					PRINT 'Database better_auth already exists';
 71 | 				END
 72 | 			`,
 73 | 			parameters: [],
 74 | 			query: { kind: "SelectQueryNode" },
 75 | 			queryId: { queryId: "ensure-db" },
 76 | 		});
 77 | 		console.log("Database check/creation completed");
 78 | 	} catch (error) {
 79 | 		console.error("Failed to ensure database exists:", error);
 80 | 		throw error;
 81 | 	}
 82 | };
 83 | 
 84 | // Warm up connection for CI environments
 85 | const warmupConnection = async () => {
 86 | 	const isCI =
 87 | 		process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
 88 | 	if (isCI) {
 89 | 		console.log("Warming up MSSQL connection for CI environment...");
 90 | 		console.log(
 91 | 			`Environment: CI=${process.env.CI}, GITHUB_ACTIONS=${process.env.GITHUB_ACTIONS}`,
 92 | 		);
 93 | 
 94 | 		try {
 95 | 			await ensureDatabaseExists();
 96 | 
 97 | 			// Try a simple query to establish the connection
 98 | 			await kyselyDB.getExecutor().executeQuery({
 99 | 				sql: "SELECT 1 as warmup, @@VERSION as version",
100 | 				parameters: [],
101 | 				query: { kind: "SelectQueryNode" },
102 | 				queryId: { queryId: "warmup" },
103 | 			});
104 | 			console.log("Connection warmup successful");
105 | 		} catch (error) {
106 | 			console.warn(
107 | 				"Connection warmup failed, will retry during validation:",
108 | 				error,
109 | 			);
110 | 			// Log additional debugging info for CI
111 | 			if (isCI) {
112 | 				console.log("CI Debug Info:");
113 | 				console.log("- MSSQL server may not be ready yet");
114 | 				console.log("- Network connectivity issues possible");
115 | 				console.log("- Database may not exist yet");
116 | 			}
117 | 		}
118 | 	} else {
119 | 		// For local development, also ensure database exists
120 | 		await ensureDatabaseExists();
121 | 	}
122 | };
123 | 
124 | // Add connection validation helper with CI-specific handling
125 | const validateConnection = async (retries: number = 10): Promise<boolean> => {
126 | 	const isCI =
127 | 		process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
128 | 	const maxRetries = isCI ? 15 : retries; // More retries in CI
129 | 	const baseDelay = isCI ? 2000 : 1000; // Longer delays in CI
130 | 
131 | 	console.log(
132 | 		`Validating connection (CI: ${isCI}, max retries: ${maxRetries})`,
133 | 	);
134 | 
135 | 	for (let i = 0; i < maxRetries; i++) {
136 | 		try {
137 | 			await query("SELECT 1 as test", isCI ? 10000 : 5000);
138 | 			console.log("Connection validated successfully");
139 | 			return true;
140 | 		} catch (error) {
141 | 			console.warn(
142 | 				`Connection validation attempt ${i + 1}/${maxRetries} failed:`,
143 | 				error,
144 | 			);
145 | 			if (i === maxRetries - 1) {
146 | 				console.error("All connection validation attempts failed");
147 | 				return false;
148 | 			}
149 | 			// Exponential backoff with longer delays in CI
150 | 			const delay = baseDelay * Math.pow(1.5, i);
151 | 			console.log(`Waiting ${delay}ms before retry...`);
152 | 			await new Promise((resolve) => setTimeout(resolve, delay));
153 | 		}
154 | 	}
155 | 	return false;
156 | };
157 | 
158 | const query = async (sql: string, timeoutMs: number = 30000) => {
159 | 	const isCI =
160 | 		process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
161 | 	const actualTimeout = isCI ? Math.max(timeoutMs, 60000) : timeoutMs; // Minimum 60s timeout in CI
162 | 
163 | 	try {
164 | 		console.log(
165 | 			`Executing SQL: ${sql.substring(0, 100)}... (timeout: ${actualTimeout}ms, CI: ${isCI})`,
166 | 		);
167 | 
168 | 		// Ensure we're using the better_auth database for queries
169 | 		const sqlWithContext = sql.includes("USE ")
170 | 			? sql
171 | 			: `USE better_auth; ${sql}`;
172 | 
173 | 		const result = (await Promise.race([
174 | 			kyselyDB.getExecutor().executeQuery({
175 | 				sql: sqlWithContext,
176 | 				parameters: [],
177 | 				query: { kind: "SelectQueryNode" },
178 | 				queryId: { queryId: "" },
179 | 			}),
180 | 			new Promise((_, reject) =>
181 | 				setTimeout(
182 | 					() => reject(new Error(`Query timeout after ${actualTimeout}ms`)),
183 | 					actualTimeout,
184 | 				),
185 | 			),
186 | 		])) as any;
187 | 		console.log(`Query completed successfully`);
188 | 		return { rows: result.rows, rowCount: result.rows.length };
189 | 	} catch (error) {
190 | 		console.error(`Query failed: ${error}`);
191 | 		throw error;
192 | 	}
193 | };
194 | 
195 | const showDB = async () => {
196 | 	const DB = {
197 | 		users: await query("SELECT * FROM [user]"),
198 | 		sessions: await query("SELECT * FROM [session]"),
199 | 		accounts: await query("SELECT * FROM [account]"),
200 | 		verifications: await query("SELECT * FROM [verification]"),
201 | 	};
202 | 	console.log(`DB`, DB);
203 | };
204 | 
205 | const resetDB = async (retryCount: number = 0) => {
206 | 	const isCI =
207 | 		process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
208 | 	const maxRetries = isCI ? 3 : 1; // Allow retries in CI
209 | 
210 | 	try {
211 | 		console.log(
212 | 			`Starting database reset... (attempt ${retryCount + 1}/${maxRetries + 1})`,
213 | 		);
214 | 
215 | 		// Warm up connection first (especially important for CI)
216 | 		await warmupConnection();
217 | 
218 | 		const isConnected = await validateConnection();
219 | 		if (!isConnected) {
220 | 			throw new Error("Database connection validation failed");
221 | 		}
222 | 
223 | 		// First, try to disable foreign key checks and drop constraints
224 | 		await query(
225 | 			`
226 | 			-- Disable all foreign key constraints
227 | 			EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all";
228 | 		`,
229 | 			15000,
230 | 		);
231 | 
232 | 		// Drop foreign key constraints
233 | 		await query(
234 | 			`
235 | 			DECLARE @sql NVARCHAR(MAX) = '';
236 | 			SELECT @sql = @sql + 'ALTER TABLE [' + TABLE_SCHEMA + '].[' + TABLE_NAME + '] DROP CONSTRAINT [' + CONSTRAINT_NAME + '];' + CHAR(13)
237 | 			FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
238 | 			WHERE CONSTRAINT_TYPE = 'FOREIGN KEY'
239 | 			AND TABLE_CATALOG = DB_NAME();
240 | 			IF LEN(@sql) > 0
241 | 				EXEC sp_executesql @sql;
242 | 		`,
243 | 			15000,
244 | 		);
245 | 
246 | 		// Then drop all tables
247 | 		await query(
248 | 			`
249 | 			DECLARE @sql NVARCHAR(MAX) = '';
250 | 			SELECT @sql = @sql + 'DROP TABLE [' + TABLE_NAME + '];' + CHAR(13)
251 | 			FROM INFORMATION_SCHEMA.TABLES 
252 | 			WHERE TABLE_TYPE = 'BASE TABLE' 
253 | 			AND TABLE_CATALOG = DB_NAME()
254 | 			AND TABLE_SCHEMA = 'dbo';
255 | 			IF LEN(@sql) > 0
256 | 				EXEC sp_executesql @sql;
257 | 		`,
258 | 			15000,
259 | 		);
260 | 
261 | 		console.log("Database reset completed successfully");
262 | 	} catch (error) {
263 | 		console.error("Database reset failed:", error);
264 | 
265 | 		// Retry logic for CI environments
266 | 		if (retryCount < maxRetries) {
267 | 			const delay = 5000 * (retryCount + 1); // Increasing delay
268 | 			console.log(
269 | 				`Retrying in ${delay}ms... (attempt ${retryCount + 2}/${maxRetries + 1})`,
270 | 			);
271 | 			await new Promise((resolve) => setTimeout(resolve, delay));
272 | 			return resetDB(retryCount + 1);
273 | 		}
274 | 
275 | 		// Final fallback - try to recreate the database
276 | 		try {
277 | 			console.log("Attempting database recreation...");
278 | 			// This would require a separate connection to master database
279 | 			// For now, just throw the error with better context
280 | 			throw new Error(`Database reset failed completely: ${error}`);
281 | 		} catch (finalError) {
282 | 			console.error("Final fallback also failed:", finalError);
283 | 			throw new Error(
284 | 				`Database reset failed: ${error}. All fallback attempts failed: ${finalError}`,
285 | 			);
286 | 		}
287 | 	}
288 | };
289 | 
290 | const { execute } = await testAdapter({
291 | 	adapter: () => {
292 | 		return kyselyAdapter(kyselyDB, {
293 | 			type: "mssql",
294 | 			debugLogs: { isRunningAdapterTests: true },
295 | 		});
296 | 	},
297 | 	async runMigrations(betterAuthOptions) {
298 | 		await resetDB();
299 | 		const opts = Object.assign(betterAuthOptions, {
300 | 			database: { db: kyselyDB, type: "mssql" },
301 | 		} satisfies BetterAuthOptions);
302 | 		const { runMigrations } = await getMigrations(opts);
303 | 		await runMigrations();
304 | 	},
305 | 	prefixTests: "mssql",
306 | 	tests: [
307 | 		normalTestSuite(),
308 | 		transactionsTestSuite({ disableTests: { ALL: true } }),
309 | 		authFlowTestSuite({ showDB }),
310 | 		numberIdTestSuite(),
311 | 		performanceTestSuite({ dialect: "mssql" }),
312 | 	],
313 | 	async onFinish() {
314 | 		kyselyDB.destroy();
315 | 	},
316 | });
317 | execute();
318 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/check-endpoint-conflicts.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from "vitest";
  2 | import { checkEndpointConflicts } from "./index";
  3 | import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
  4 | import { createEndpoint } from "better-call";
  5 | import type { InternalLogger, LogLevel } from "@better-auth/core/env";
  6 | 
  7 | export let mockLoggerLevel: LogLevel = "debug";
  8 | export const mockLogger = {
  9 | 	error: vi.fn(),
 10 | 	warn: vi.fn(),
 11 | 	info: vi.fn(),
 12 | 	debug: vi.fn(),
 13 | 	success: vi.fn(),
 14 | 	get level(): LogLevel {
 15 | 		return mockLoggerLevel;
 16 | 	},
 17 | } satisfies InternalLogger;
 18 | 
 19 | describe("checkEndpointConflicts", () => {
 20 | 	const endpoint = createEndpoint.create({});
 21 | 
 22 | 	beforeEach(() => {
 23 | 		mockLoggerLevel = "debug";
 24 | 		mockLogger.error.mockReset();
 25 | 		mockLogger.warn.mockReset();
 26 | 		mockLogger.info.mockReset();
 27 | 		mockLogger.debug.mockReset();
 28 | 		mockLogger.success.mockReset();
 29 | 	});
 30 | 
 31 | 	it("should not log errors when there are no endpoint conflicts", () => {
 32 | 		const plugin1: BetterAuthPlugin = {
 33 | 			id: "plugin1",
 34 | 			endpoints: {
 35 | 				endpoint1: endpoint(
 36 | 					"/api/endpoint1",
 37 | 					{
 38 | 						method: "GET",
 39 | 					},
 40 | 					vi.fn(),
 41 | 				),
 42 | 				endpoint2: endpoint(
 43 | 					"/api/endpoint2",
 44 | 					{
 45 | 						method: "POST",
 46 | 					},
 47 | 					vi.fn(),
 48 | 				),
 49 | 			},
 50 | 		};
 51 | 
 52 | 		const plugin2: BetterAuthPlugin = {
 53 | 			id: "plugin2",
 54 | 			endpoints: {
 55 | 				endpoint3: endpoint(
 56 | 					"/api/endpoint3",
 57 | 					{
 58 | 						method: "GET",
 59 | 					},
 60 | 					vi.fn(),
 61 | 				),
 62 | 				endpoint4: endpoint(
 63 | 					"/api/endpoint4",
 64 | 					{
 65 | 						method: "POST",
 66 | 					},
 67 | 					vi.fn(),
 68 | 				),
 69 | 			},
 70 | 		};
 71 | 
 72 | 		const options: BetterAuthOptions = {
 73 | 			plugins: [plugin1, plugin2],
 74 | 		};
 75 | 
 76 | 		checkEndpointConflicts(options, mockLogger);
 77 | 
 78 | 		expect(mockLogger.error).not.toHaveBeenCalled();
 79 | 	});
 80 | 
 81 | 	it("should NOT log an error when two plugins use the same endpoint path with different methods", () => {
 82 | 		const plugin1: BetterAuthPlugin = {
 83 | 			id: "plugin1",
 84 | 			endpoints: {
 85 | 				endpoint1: endpoint(
 86 | 					"/api/shared",
 87 | 					{
 88 | 						method: "GET",
 89 | 					},
 90 | 					vi.fn(),
 91 | 				),
 92 | 			},
 93 | 		};
 94 | 
 95 | 		const plugin2: BetterAuthPlugin = {
 96 | 			id: "plugin2",
 97 | 			endpoints: {
 98 | 				endpoint2: endpoint(
 99 | 					"/api/shared",
100 | 					{
101 | 						method: "POST",
102 | 					},
103 | 					vi.fn(),
104 | 				),
105 | 			},
106 | 		};
107 | 
108 | 		const options: BetterAuthOptions = {
109 | 			plugins: [plugin1, plugin2],
110 | 		};
111 | 
112 | 		checkEndpointConflicts(options, mockLogger);
113 | 
114 | 		// Should NOT report an error since methods are different
115 | 		expect(mockLogger.error).not.toHaveBeenCalled();
116 | 	});
117 | 
118 | 	it("should log an error when two plugins use the same endpoint path with the same method", () => {
119 | 		const plugin1: BetterAuthPlugin = {
120 | 			id: "plugin1",
121 | 			endpoints: {
122 | 				endpoint1: endpoint(
123 | 					"/api/shared",
124 | 					{
125 | 						method: "GET",
126 | 					},
127 | 					vi.fn(),
128 | 				),
129 | 			},
130 | 		};
131 | 
132 | 		const plugin2: BetterAuthPlugin = {
133 | 			id: "plugin2",
134 | 			endpoints: {
135 | 				endpoint2: endpoint(
136 | 					"/api/shared",
137 | 					{
138 | 						method: "GET",
139 | 					},
140 | 					vi.fn(),
141 | 				),
142 | 			},
143 | 		};
144 | 
145 | 		const options: BetterAuthOptions = {
146 | 			plugins: [plugin1, plugin2],
147 | 		};
148 | 
149 | 		checkEndpointConflicts(options, mockLogger);
150 | 
151 | 		expect(mockLogger.error).toHaveBeenCalledTimes(1);
152 | 		expect(mockLogger.error).toHaveBeenCalledWith(
153 | 			expect.stringContaining("Endpoint path conflicts detected"),
154 | 		);
155 | 		expect(mockLogger.error).toHaveBeenCalledWith(
156 | 			expect.stringContaining(
157 | 				'"/api/shared" [GET] used by plugins: plugin1, plugin2',
158 | 			),
159 | 		);
160 | 	});
161 | 
162 | 	it("should NOT detect conflicts when plugins use different methods on same paths", () => {
163 | 		const plugin1: BetterAuthPlugin = {
164 | 			id: "plugin1",
165 | 			endpoints: {
166 | 				endpoint1: endpoint(
167 | 					"/api/resource1",
168 | 					{
169 | 						method: "GET",
170 | 					},
171 | 					vi.fn(),
172 | 				),
173 | 				endpoint2: endpoint(
174 | 					"/api/resource2",
175 | 					{
176 | 						method: "POST",
177 | 					},
178 | 					vi.fn(),
179 | 				),
180 | 			},
181 | 		};
182 | 
183 | 		const plugin2: BetterAuthPlugin = {
184 | 			id: "plugin2",
185 | 			endpoints: {
186 | 				endpoint3: endpoint(
187 | 					"/api/resource1",
188 | 					{
189 | 						method: "POST",
190 | 					},
191 | 					vi.fn(),
192 | 				),
193 | 			},
194 | 		};
195 | 
196 | 		const plugin3: BetterAuthPlugin = {
197 | 			id: "plugin3",
198 | 			endpoints: {
199 | 				endpoint4: endpoint(
200 | 					"/api/resource2",
201 | 					{
202 | 						method: "GET",
203 | 					},
204 | 					vi.fn(),
205 | 				),
206 | 			},
207 | 		};
208 | 
209 | 		const options: BetterAuthOptions = {
210 | 			plugins: [plugin1, plugin2, plugin3],
211 | 		};
212 | 
213 | 		checkEndpointConflicts(options, mockLogger);
214 | 
215 | 		// Should not report errors since all methods are different
216 | 		expect(mockLogger.error).not.toHaveBeenCalled();
217 | 	});
218 | 
219 | 	it("should detect conflicts when plugins use the same method on the same path", () => {
220 | 		const plugin1: BetterAuthPlugin = {
221 | 			id: "plugin1",
222 | 			endpoints: {
223 | 				endpoint1: endpoint(
224 | 					"/api/conflict",
225 | 					{
226 | 						method: "GET",
227 | 					},
228 | 					vi.fn(),
229 | 				),
230 | 			},
231 | 		};
232 | 
233 | 		const plugin2: BetterAuthPlugin = {
234 | 			id: "plugin2",
235 | 			endpoints: {
236 | 				endpoint2: endpoint(
237 | 					"/api/conflict",
238 | 					{
239 | 						method: "GET",
240 | 					},
241 | 					vi.fn(),
242 | 				),
243 | 			},
244 | 		};
245 | 
246 | 		const options: BetterAuthOptions = {
247 | 			plugins: [plugin1, plugin2],
248 | 		};
249 | 
250 | 		checkEndpointConflicts(options, mockLogger);
251 | 
252 | 		expect(mockLogger.error).toHaveBeenCalledTimes(1);
253 | 		const errorCall = mockLogger.error.mock.calls[0]![0];
254 | 		expect(errorCall).toContain(
255 | 			'"/api/conflict" [GET] used by plugins: plugin1, plugin2',
256 | 		);
257 | 	});
258 | 
259 | 	it("should allow multiple endpoints from the same plugin using the same path with different methods", () => {
260 | 		const plugin1: BetterAuthPlugin = {
261 | 			id: "plugin1",
262 | 			endpoints: {
263 | 				endpoint1: endpoint(
264 | 					"/api/same",
265 | 					{
266 | 						method: "GET",
267 | 					},
268 | 					vi.fn(),
269 | 				),
270 | 				endpoint2: endpoint(
271 | 					"/api/same",
272 | 					{
273 | 						method: "POST",
274 | 					},
275 | 					vi.fn(),
276 | 				),
277 | 			},
278 | 		};
279 | 
280 | 		const options: BetterAuthOptions = {
281 | 			plugins: [plugin1],
282 | 		};
283 | 
284 | 		checkEndpointConflicts(options, mockLogger);
285 | 
286 | 		// Should not report error since methods are different
287 | 		expect(mockLogger.error).not.toHaveBeenCalled();
288 | 	});
289 | 
290 | 	it("should detect conflicts when same plugin has duplicate methods on same path", () => {
291 | 		const plugin1: BetterAuthPlugin = {
292 | 			id: "plugin1",
293 | 			endpoints: {
294 | 				endpoint1: endpoint(
295 | 					"/api/same",
296 | 					{
297 | 						method: "GET",
298 | 					},
299 | 					vi.fn(),
300 | 				),
301 | 				endpoint2: endpoint(
302 | 					"/api/same",
303 | 					{
304 | 						method: "GET",
305 | 					},
306 | 					vi.fn(),
307 | 				),
308 | 			},
309 | 		};
310 | 
311 | 		const options: BetterAuthOptions = {
312 | 			plugins: [plugin1],
313 | 		};
314 | 
315 | 		checkEndpointConflicts(options, mockLogger);
316 | 
317 | 		expect(mockLogger.error).toHaveBeenCalledTimes(1);
318 | 		expect(mockLogger.error).toHaveBeenCalledWith(
319 | 			expect.stringContaining('"/api/same" [GET] used by plugins: plugin1'),
320 | 		);
321 | 	});
322 | 
323 | 	it("should allow three plugins on the same path with different methods", () => {
324 | 		const plugin1: BetterAuthPlugin = {
325 | 			id: "plugin1",
326 | 			endpoints: {
327 | 				endpoint1: endpoint(
328 | 					"/api/resource",
329 | 					{
330 | 						method: "GET",
331 | 					},
332 | 					vi.fn(),
333 | 				),
334 | 			},
335 | 		};
336 | 
337 | 		const plugin2: BetterAuthPlugin = {
338 | 			id: "plugin2",
339 | 			endpoints: {
340 | 				endpoint2: endpoint(
341 | 					"/api/resource",
342 | 					{
343 | 						method: "POST",
344 | 					},
345 | 					vi.fn(),
346 | 				),
347 | 			},
348 | 		};
349 | 
350 | 		const plugin3: BetterAuthPlugin = {
351 | 			id: "plugin3",
352 | 			endpoints: {
353 | 				endpoint3: endpoint(
354 | 					"/api/resource",
355 | 					{
356 | 						method: "DELETE",
357 | 					},
358 | 					vi.fn(),
359 | 				),
360 | 			},
361 | 		};
362 | 
363 | 		const options: BetterAuthOptions = {
364 | 			plugins: [plugin1, plugin2, plugin3],
365 | 		};
366 | 
367 | 		checkEndpointConflicts(options, mockLogger);
368 | 
369 | 		// Should not report error since all methods are different
370 | 		expect(mockLogger.error).not.toHaveBeenCalled();
371 | 	});
372 | 
373 | 	it("should detect conflicts when endpoints don't specify a method (wildcard)", () => {
374 | 		const plugin1: BetterAuthPlugin = {
375 | 			id: "plugin1",
376 | 			endpoints: {
377 | 				endpoint1: endpoint(
378 | 					"/api/wildcard",
379 | 					{
380 | 						method: "*",
381 | 					},
382 | 					vi.fn(),
383 | 				),
384 | 			},
385 | 		};
386 | 
387 | 		const plugin2: BetterAuthPlugin = {
388 | 			id: "plugin2",
389 | 			endpoints: {
390 | 				endpoint2: endpoint(
391 | 					"/api/wildcard",
392 | 					{
393 | 						method: "GET",
394 | 					},
395 | 					vi.fn(),
396 | 				),
397 | 			},
398 | 		};
399 | 
400 | 		const options: BetterAuthOptions = {
401 | 			plugins: [plugin1, plugin2],
402 | 		};
403 | 
404 | 		checkEndpointConflicts(options, mockLogger);
405 | 
406 | 		expect(mockLogger.error).toHaveBeenCalledTimes(1);
407 | 		expect(mockLogger.error).toHaveBeenCalledWith(
408 | 			expect.stringContaining('"/api/wildcard"'),
409 | 		);
410 | 	});
411 | 
412 | 	it("should handle plugins with no endpoints", () => {
413 | 		const plugin1: BetterAuthPlugin = {
414 | 			id: "plugin1",
415 | 		};
416 | 
417 | 		const plugin2: BetterAuthPlugin = {
418 | 			id: "plugin2",
419 | 			endpoints: {},
420 | 		};
421 | 
422 | 		const options: BetterAuthOptions = {
423 | 			plugins: [plugin1, plugin2],
424 | 		};
425 | 
426 | 		checkEndpointConflicts(options, mockLogger);
427 | 
428 | 		expect(mockLogger.error).not.toHaveBeenCalled();
429 | 	});
430 | 
431 | 	it("should handle options with no plugins", () => {
432 | 		const options: BetterAuthOptions = {};
433 | 
434 | 		checkEndpointConflicts(options, mockLogger);
435 | 
436 | 		expect(mockLogger.error).not.toHaveBeenCalled();
437 | 	});
438 | 
439 | 	it("should handle options with empty plugins array", () => {
440 | 		const options: BetterAuthOptions = {
441 | 			plugins: [],
442 | 		};
443 | 
444 | 		checkEndpointConflicts(options, mockLogger);
445 | 
446 | 		expect(mockLogger.error).not.toHaveBeenCalled();
447 | 	});
448 | });
449 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/email-otp.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Email OTP
  3 | description: Email OTP plugin for Better Auth.
  4 | ---
  5 | 
  6 | The Email OTP plugin allows user to sign in, verify their email, or reset their password using a one-time password (OTP) sent to their email address.
  7 | 
  8 | 
  9 | ## Installation
 10 | 
 11 | <Steps>
 12 |   <Step>
 13 |     ### Add the plugin to your auth config
 14 | 
 15 |     Add the `emailOTP` plugin to your auth config and implement the `sendVerificationOTP()` method.
 16 | 
 17 |     ```ts title="auth.ts"
 18 |     import { betterAuth } from "better-auth"
 19 |     import { emailOTP } from "better-auth/plugins" // [!code highlight]
 20 |     
 21 |     export const auth = betterAuth({
 22 |         // ... other config options
 23 |         plugins: [
 24 |             emailOTP({ // [!code highlight]
 25 |                 async sendVerificationOTP({ email, otp, type }) { // [!code highlight]
 26 |                     if (type === "sign-in") { // [!code highlight]
 27 |                         // Send the OTP for sign in // [!code highlight]
 28 |                     } else if (type === "email-verification") { // [!code highlight]
 29 |                         // Send the OTP for email verification // [!code highlight]
 30 |                     } else { // [!code highlight]
 31 |                         // Send the OTP for password reset // [!code highlight]
 32 |                     } // [!code highlight]
 33 |                 }, // [!code highlight]
 34 |             }) // [!code highlight]
 35 |         ]
 36 |     })
 37 |     ```
 38 |     </Step>
 39 |     <Step>
 40 |     ### Add the client plugin
 41 | 
 42 |     ```ts title="auth-client.ts"
 43 |     import { createAuthClient } from "better-auth/client"
 44 |     import { emailOTPClient } from "better-auth/client/plugins"
 45 |     
 46 |     export const authClient = createAuthClient({
 47 |         plugins: [
 48 |             emailOTPClient()
 49 |         ]
 50 |     })
 51 |     ```
 52 |   </Step>
 53 | </Steps>
 54 | 
 55 | ## Usage
 56 | 
 57 | ### Send an OTP
 58 | 
 59 | Use the `sendVerificationOtp()` method to send an OTP to the user's email address.
 60 | 
 61 | <APIMethod path="/email-otp/send-verification-otp" method="POST">
 62 | ```ts
 63 | type sendVerificationOTP = {
 64 |     /**
 65 |      * Email address to send the OTP. 
 66 |      */
 67 |     email: string = "[email protected]"
 68 |     /**
 69 |      * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. 
 70 |      */
 71 |     type: "email-verification" | "sign-in" | "forget-password" = "sign-in"
 72 | }
 73 | ```
 74 | </APIMethod>
 75 | 
 76 | ### Check an OTP (optional)
 77 | 
 78 | Use the `checkVerificationOtp()` method to check if an OTP is valid.
 79 | 
 80 | <APIMethod path="/email-otp/check-verification-otp" method="POST">
 81 | ```ts
 82 | type checkVerificationOTP = {
 83 |     /**
 84 |      * Email address to send the OTP. 
 85 |      */
 86 |     email: string = "[email protected]"
 87 |     /**
 88 |      * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. 
 89 |      */
 90 |     type: "email-verification" | "sign-in" | "forget-password" = "sign-in"
 91 |     /**
 92 |      * OTP sent to the email. 
 93 |      */
 94 |     otp: string = "123456"
 95 | }
 96 | ```
 97 | </APIMethod>
 98 | 
 99 | ### Sign In with OTP
100 | 
101 | To sign in with OTP, use the `sendVerificationOtp()` method to send a "sign-in" OTP to the user's email address.
102 | 
103 | <APIMethod path="/email-otp/send-verification-otp" method="POST">
104 | ```ts
105 | type sendVerificationOTP = {
106 |     /**
107 |      * Email address to send the OTP. 
108 |      */
109 |     email: string = "[email protected]"
110 |     /**
111 |      * Type of the OTP.
112 |      */
113 |     type: "sign-in" = "sign-in"
114 | }
115 | ```
116 | </APIMethod>
117 | 
118 | Once the user provides the OTP, you can sign in the user using the `signIn.emailOtp()` method.
119 | 
120 | <APIMethod path="/sign-in/email-otp" method="POST">
121 | ```ts
122 | type signInEmailOTP = {
123 |     /**
124 |      * Email address to sign in. 
125 |      */
126 |     email: string = "[email protected]"
127 |     /**
128 |      * OTP sent to the email. 
129 |      */
130 |     otp: string = "123456"
131 | }
132 | ```
133 | </APIMethod>
134 | 
135 | <Callout>
136 | If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the [options](#options).
137 | </Callout>
138 | 
139 | ### Verify Email with OTP
140 | 
141 | To verify the user's email address with OTP, use the `sendVerificationOtp()` method to send an "email-verification" OTP to the user's email address.
142 | 
143 | <APIMethod path="/email-otp/send-verification-otp" method="POST">
144 | ```ts
145 | type sendVerificationOTP = {
146 |     /**
147 |      * Email address to send the OTP. 
148 |      */
149 |     email: string = "[email protected]"
150 |     /**
151 |      * Type of the OTP.
152 |      */
153 |     type: "email-verification" = "email-verification"
154 | }
155 | ```
156 | </APIMethod>
157 | 
158 | Once the user provides the OTP, use the `verifyEmail()` method to complete email verification.
159 | 
160 | <APIMethod path="/email-otp/verify-email" method="POST">
161 | ```ts
162 | type verifyEmailOTP = {
163 |     /**
164 |      * Email address to verify. 
165 |      */
166 |     email: string = "[email protected]"
167 |     /**
168 |      * OTP to verify. 
169 |      */
170 |     otp: string = "123456"
171 | }
172 | ```
173 | </APIMethod>
174 | 
175 | ### Reset Password with OTP
176 | 
177 | To reset the user's password with OTP, use the `forgetPassword.emailOTP()` method to send a "forget-password" OTP to the user's email address.
178 | 
179 | <APIMethod path="/forget-password/email-otp" method="POST">
180 | ```ts
181 | type forgetPasswordEmailOTP = {
182 |     /**
183 |      * Email address to send the OTP. 
184 |      */
185 |     email: string = "[email protected]"
186 | }
187 | ```
188 | </APIMethod>
189 | 
190 | Once the user provides the OTP, use the `checkVerificationOtp()` method to check if it's valid (optional).
191 | 
192 | <APIMethod path="/email-otp/check-verification-otp" method="POST">
193 | ```ts
194 | type checkVerificationOTP = {
195 |     /**
196 |      * Email address to send the OTP. 
197 |      */
198 |     email: string = "[email protected]"
199 |     /**
200 |      * Type of the OTP.
201 |      */
202 |     type: "forget-password" = "forget-password"
203 |     /**
204 |      * OTP sent to the email. 
205 |      */
206 |     otp: string = "123456"
207 | }
208 | ```
209 | </APIMethod>
210 | 
211 | Then, use the `resetPassword()` method to reset the user's password.
212 | 
213 | <APIMethod path="/email-otp/reset-password" method="POST">
214 | ```ts
215 | type resetPasswordEmailOTP = {
216 |     /**
217 |      * Email address to reset the password. 
218 |      */
219 |     email: string = "[email protected]"
220 |     /**
221 |      * OTP sent to the email. 
222 |      */
223 |     otp: string = "123456"
224 |     /**
225 |      * New password. 
226 |      */
227 |     password: string = "new-secure-password"
228 | }
229 | ```
230 | </APIMethod>
231 | 
232 | ### Override Default Email Verification
233 | 
234 | To override the default email verification, pass `overrideDefaultEmailVerification: true` in the options. This will make the system use an email OTP instead of the default verification link whenever email verification is triggered. In other words, the user will verify their email using an OTP rather than clicking a link.
235 | 
236 | ```ts title="auth.ts"
237 | import { betterAuth } from "better-auth";
238 | 
239 | export const auth = betterAuth({
240 |   plugins: [
241 |     emailOTP({
242 |       overrideDefaultEmailVerification: true, // [!code highlight]
243 |       async sendVerificationOTP({ email, otp, type }) {
244 |         // Implement the sendVerificationOTP method to send the OTP to the user's email address
245 |       },
246 |     }),
247 |   ],
248 | });
249 | ```
250 | 
251 | 
252 | ## Options
253 | 
254 | - `sendVerificationOTP`: A function that sends the OTP to the user's email address. The function receives an object with the following properties:
255 |   - `email`: The user's email address.
256 |   - `otp`: The OTP to send.
257 |   - `type`: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password".
258 | 
259 | - `otpLength`: The length of the OTP. Defaults to `6`.
260 | 
261 | - `expiresIn`: The expiry time of the OTP in seconds. Defaults to `300` seconds.
262 | 
263 | ```ts title="auth.ts"
264 | import { betterAuth } from "better-auth"
265 | 
266 | export const auth = betterAuth({
267 |     plugins: [
268 |         emailOTP({
269 |             otpLength: 8,
270 |             expiresIn: 600
271 |         })
272 |     ]
273 | })
274 | ```
275 | 
276 | - `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to `false`.
277 | 
278 | - `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to `false`.
279 | 
280 | - `generateOTP`: A function that generates the OTP. Defaults to a random 6-digit number.
281 | 
282 | - `allowedAttempts`: The maximum number of attempts allowed for verifying an OTP. Defaults to `3`. After exceeding this limit, the OTP becomes invalid and the user needs to request a new one.
283 | 
284 | ```ts title="auth.ts"
285 | import { betterAuth } from "better-auth"
286 | 
287 | export const auth = betterAuth({
288 |     plugins: [
289 |         emailOTP({
290 |             allowedAttempts: 5, // Allow 5 attempts before invalidating the OTP
291 |             expiresIn: 300
292 |         })
293 |     ]
294 | })
295 | ```
296 | 
297 | When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`.
298 | 
299 | - `storeOTP`: The method to store the OTP in your database, wether `encrypted`, `hashed` or `plain` text. Default is `plain` text.
300 | 
301 | <Callout>
302 | Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database.
303 | </Callout>
304 | 
305 | Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database.
306 | 
307 | **Custom encryptor**
308 | 
309 | ```ts title="auth.ts"
310 | emailOTP({
311 |     storeOTP: { 
312 |         encrypt: async (otp) => {
313 |             return myCustomEncryptor(otp);
314 |         },
315 |         decrypt: async (otp) => {
316 |             return myCustomDecryptor(otp);
317 |         },
318 |     }
319 | })
320 | ```
321 | 
322 | **Custom hasher**
323 | 
324 | ```ts title="auth.ts"
325 | emailOTP({
326 |     storeOTP: {
327 |         hash: async (otp) => {
328 |             return myCustomHasher(otp);
329 |         },
330 |     }
331 | })
332 | ```
333 | 
```

--------------------------------------------------------------------------------
/docs/components/builder/beam.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | import React from "react";
  3 | import { motion } from "framer-motion";
  4 | import { cn } from "@/lib/utils";
  5 | 
  6 | export const BackgroundBeams = React.memo(
  7 | 	({ className }: { className?: string }) => {
  8 | 		const paths = [
  9 | 			"M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875",
 10 | 			"M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867",
 11 | 			"M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859",
 12 | 			"M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851",
 13 | 			"M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843",
 14 | 			"M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835",
 15 | 			"M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827",
 16 | 			"M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819",
 17 | 			"M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811",
 18 | 			"M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803",
 19 | 			"M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795",
 20 | 			"M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787",
 21 | 			"M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779",
 22 | 			"M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771",
 23 | 			"M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763",
 24 | 			"M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755",
 25 | 			"M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747",
 26 | 			"M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739",
 27 | 			"M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731",
 28 | 			"M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723",
 29 | 			"M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715",
 30 | 			"M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707",
 31 | 			"M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699",
 32 | 			"M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691",
 33 | 			"M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683",
 34 | 			"M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675",
 35 | 			"M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667",
 36 | 			"M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659",
 37 | 			"M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651",
 38 | 			"M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643",
 39 | 			"M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635",
 40 | 			"M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627",
 41 | 			"M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619",
 42 | 			"M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611",
 43 | 			"M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603",
 44 | 			"M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595",
 45 | 			"M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587",
 46 | 			"M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579",
 47 | 			"M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571",
 48 | 			"M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563",
 49 | 			"M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555",
 50 | 			"M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547",
 51 | 			"M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539",
 52 | 			"M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531",
 53 | 			"M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523",
 54 | 			"M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515",
 55 | 			"M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507",
 56 | 			"M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499",
 57 | 			"M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491",
 58 | 			"M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483",
 59 | 		];
 60 | 		return (
 61 | 			<div
 62 | 				className={cn(
 63 | 					"absolute  h-full w-full inset-0  [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center",
 64 | 					className,
 65 | 				)}
 66 | 			>
 67 | 				<svg
 68 | 					className=" z-0 h-full w-full pointer-events-none absolute "
 69 | 					width="100%"
 70 | 					height="100%"
 71 | 					viewBox="0 0 696 316"
 72 | 					fill="none"
 73 | 					xmlns="http://www.w3.org/2000/svg"
 74 | 				>
 75 | 					<path
 76 | 						d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419"
 77 | 						stroke="url(#paint0_radial_242_278)"
 78 | 						strokeOpacity="0.05"
 79 | 						strokeWidth="0.5"
 80 | 					></path>
 81 | 
 82 | 					{paths.map((path, index) => (
 83 | 						<motion.path
 84 | 							key={`path-` + index}
 85 | 							d={path}
 86 | 							stroke={`url(#linearGradient-${index})`}
 87 | 							strokeOpacity="0.4"
 88 | 							strokeWidth="0.5"
 89 | 						></motion.path>
 90 | 					))}
 91 | 					<defs>
 92 | 						{paths.map((path, index) => (
 93 | 							<motion.linearGradient
 94 | 								id={`linearGradient-${index}`}
 95 | 								key={`gradient-${index}`}
 96 | 								initial={{
 97 | 									x1: "0%",
 98 | 									x2: "0%",
 99 | 									y1: "0%",
100 | 									y2: "0%",
101 | 								}}
102 | 								animate={{
103 | 									x1: ["0%", "100%"],
104 | 									x2: ["0%", "95%"],
105 | 									y1: ["0%", "100%"],
106 | 									y2: ["0%", `${93 + Math.random() * 8}%`],
107 | 								}}
108 | 								transition={{
109 | 									duration: Math.random() * 10 + 10,
110 | 									ease: "easeInOut",
111 | 									repeat: Infinity,
112 | 									delay: Math.random() * 10,
113 | 								}}
114 | 							>
115 | 								<stop stopColor="#18CCFC" stopOpacity="0"></stop>
116 | 								<stop stopColor="#18CCFC"></stop>
117 | 								<stop offset="32.5%" stopColor="#6344F5"></stop>
118 | 								<stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop>
119 | 							</motion.linearGradient>
120 | 						))}
121 | 
122 | 						<radialGradient
123 | 							id="paint0_radial_242_278"
124 | 							cx="0"
125 | 							cy="0"
126 | 							r="1"
127 | 							gradientUnits="userSpaceOnUse"
128 | 							gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)"
129 | 						>
130 | 							<stop offset="0.0666667" stopColor="var(--neutral-300)"></stop>
131 | 							<stop offset="0.243243" stopColor="var(--neutral-300)"></stop>
132 | 							<stop offset="0.43594" stopColor="white" stopOpacity="0"></stop>
133 | 						</radialGradient>
134 | 					</defs>
135 | 				</svg>
136 | 			</div>
137 | 		);
138 | 	},
139 | );
140 | 
141 | BackgroundBeams.displayName = "BackgroundBeams";
142 | 
```
Page 28/71FirstPrevNextLast