#
tokens: 47561/50000 8/1119 files (page 31/70)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 31 of 70. 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
│   ├── 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/components/landing/testimonials.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { cn } from "@/lib/utils";
  2 | import { Icons } from "../icons";
  3 | import Link from "next/link";
  4 | import { noSSR } from "foxact/no-ssr";
  5 | import { Suspense } from "react";
  6 | 
  7 | const testimonials = [
  8 | 	{
  9 | 		name: "Dev Ed",
 10 | 		profession: "Content Creator",
 11 | 		link: "https://x.com/edgarasben/status/1856336936505590160",
 12 | 		description:
 13 | 			"This has been the best auth experience by a mileee, auto generated my drizzle schemas for users, sessions etc, full type safe and dead simple api, well done @better_auth 👏👏",
 14 | 		avatar: "/people-say/dev-ed.png",
 15 | 		image: "",
 16 | 		social: <Icons.x />,
 17 | 	},
 18 | 	{
 19 | 		name: "Lazar Nikolov",
 20 | 		profession: "Software Engineer & Educator",
 21 | 		link: "https://x.com/NikolovLazar/status/1888992999872331985",
 22 | 		description:
 23 | 			"I didn't know  @better_auth was THAT good. I'm implementing it in TanStack Start and I can't believe how good the DX is. This is my favorite stack now (along with  @DrizzleORM and @shadcn ui).",
 24 | 		avatar: "/people-say/lazar-nikolov.png",
 25 | 		image: "",
 26 | 		social: <Icons.x />,
 27 | 	},
 28 | 	{
 29 | 		name: "Theo - t3.gg",
 30 | 		profession: "CEO of t3.chat",
 31 | 		link: "https://x.com/theo/status/1879769267866120341",
 32 | 		description: "Very exciting project and a super easy rec",
 33 | 		avatar: "/people-say/theo.png",
 34 | 		image: "",
 35 | 		social: <Icons.x />,
 36 | 	},
 37 | 	{
 38 | 		name: "kitze",
 39 | 		profession: "http://sizzy.co",
 40 | 		link: "https://x.com/thekitze/status/1911524156115476831",
 41 | 		description:
 42 | 			"I rarely stumble upon a framework/library that makes me rethink things. @better_auth is a rare exception. it literally delayed @zerotoshipped for a week...",
 43 | 		avatar: "/people-say/kitze.jpg",
 44 | 		image: "",
 45 | 		social: <Icons.x />,
 46 | 	},
 47 | 	{
 48 | 		name: "Sébastien Chopin",
 49 | 		profession: "Creator of Nuxt & NuxtLabs",
 50 | 		link: "https://x.com/Atinux/status/1853751424561336322",
 51 | 		description:
 52 | 			"When @better_auth meets @nuxt_hub to build full-stack Nuxt apps on Cloudflare (using D1 & KV).",
 53 | 		avatar: "/people-say/sebastien-chopin.png",
 54 | 		image: "",
 55 | 		social: <Icons.x />,
 56 | 	},
 57 | 
 58 | 	{
 59 | 		name: "Dax",
 60 | 		profession: "Creator of SST",
 61 | 		link: "https://x.com/thdxr/status/1866222656468705426",
 62 | 		description:
 63 | 			"between better-auth and openauth one of those options should cover how you want to do things for 95% of cases. the problem of defaulting to SaaS for auth in js is finally fixed...",
 64 | 		avatar: "/people-say/dax.png",
 65 | 		image: "",
 66 | 		social: <Icons.x />,
 67 | 	},
 68 | 	{
 69 | 		name: "SaltyAtom",
 70 | 		profession: "Creator of ElysiaJS",
 71 | 		link: "https://x.com/saltyAom/status/1916919136565051491",
 72 | 		description: `Strategies to win at Auth:
 73 | 1. Copy Better Auth - 
 74 | 2. Go back to 1`,
 75 | 		avatar: "/people-say/saltyatom.jpg",
 76 | 		image: "",
 77 | 		social: <Icons.x />,
 78 | 	},
 79 | 	{
 80 | 		name: "Josh Tried Coding",
 81 | 		profession: "devrel @upstash",
 82 | 		description: `using better-auth for the first time
 83 | 
 84 | holy sh** is it good, works so nice with typescript + drizzle`,
 85 | 		avatar: "/people-say/josh-tried-coding.jpg",
 86 | 		image: "",
 87 | 		link: "https://x.com/joshtriedcoding/status/1916108678672900301",
 88 | 		social: <Icons.x />,
 89 | 	},
 90 | 	{
 91 | 		name: "Xavier Pladevall",
 92 | 		profession: "Founder of IndexBI",
 93 | 		description: `We've been using @better_auth in prod @IndexBI and absolutely love it. Super comprehensive from day one.👏`,
 94 | 		avatar: "/people-say/xavier-pladevall.jpg",
 95 | 		image: "",
 96 | 		link: "https://x.com/xavierpladevall/status/1915490484891341211",
 97 | 		social: <Icons.x />,
 98 | 	},
 99 | 	{
100 | 		name: "Code with Antonio",
101 | 		profession: "Content Creator",
102 | 		description: `i swear @polar_sh and @better_auth developer experience should be mandatory teaching for all CS students`,
103 | 		link: "https://x.com/YTCodeAntonio/status/1920214390680236396",
104 | 		avatar: "/people-say/code-with-antonio.jpg",
105 | 		image: "",
106 | 		social: <Icons.x />,
107 | 	},
108 | 	{
109 | 		name: "Ryan Vogel",
110 | 		profession: "Founder of exon",
111 | 		description:
112 | 			"i have been using better-auth for exon todo and it is like so fast, I set it up once and it just works",
113 | 		link: "https://x.com/ryandavogel/status/1914789770451964150",
114 | 		avatar: "/people-say/ryan-vogel.jpg",
115 | 		image: "",
116 | 		social: <Icons.x />,
117 | 	},
118 | 	{
119 | 		name: "Dagmawi Babi",
120 | 		profession: "Developer",
121 | 		link: "https://x.com/DagmawiBabi/status/1845966382703280458",
122 | 		description:
123 | 			"@better_auth exceeded all expectations, and it's just getting started",
124 | 		avatar: "/people-say/dagmawi-babi.png",
125 | 		image: "",
126 | 		social: <Icons.x />,
127 | 	},
128 | 	{
129 | 		name: "Tech Nerd",
130 | 		profession: "Developer",
131 | 		link: "https://x.com/TechNerd556/status/1863523931614822784",
132 | 		description:
133 | 			"Using @better_auth with custom components feels like having someone hand you the remote while you're comfortably on the sofa. The ease I'm feeling rn is insane Auth done in under 5 minutes 🤌⚡️.",
134 | 		avatar: "/people-say/tech-nerd.png",
135 | 		image: "",
136 | 		social: <Icons.x />,
137 | 	},
138 | 	{
139 | 		name: "Omar McAdam",
140 | 		profession: "Creator of AugmentedHQ",
141 | 		link: "https://x.com/McPizza0/status/1879526862046839249",
142 | 		description:
143 | 			"if you're building a code project in 2025 use @better_auth. It has everything you need now and everything you'll need at scale. dont take this suggestion lightly..",
144 | 		avatar: "/people-say/omar-mcadam.png",
145 | 		image: "",
146 | 		social: <Icons.x />,
147 | 	},
148 | 	{
149 | 		name: "Guillermo Rauch",
150 | 		profession: "CEO of Vercel",
151 | 		link: "https://x.com/rauchg/status/1871628287962906846",
152 | 		description: "Great project & maintainer",
153 | 		avatar: "/people-say/guillermo-rauch.png",
154 | 		image: "",
155 | 		social: <Icons.x />,
156 | 	},
157 | 	{
158 | 		name: "Nizzy",
159 | 		profession: "Co-founder of Zero",
160 | 		link: "https://x.com/NizzyABI/status/1889178812459422162",
161 | 		description:
162 | 			"i cant believe how easy @better_auth is compared to @authjs all i had to do was connect it to my drizzle schema and create a sign up page w the auth :)))",
163 | 		avatar: "/people-say/nizzy.png",
164 | 		image: "",
165 | 		social: <Icons.x />,
166 | 	},
167 | 
168 | 	{
169 | 		name: "Vybhav Bhargav",
170 | 		profession: "Founding engineer @glyfspace",
171 | 		link: "https://x.com/vybhavab/status/1891589126513684669",
172 | 		description: "better-auth is a work of art.",
173 | 		avatar: "/people-say/vybhav-bhargav.png",
174 | 		social: <Icons.x />,
175 | 	},
176 | 	{
177 | 		name: "EGOIST",
178 | 		profession: "Creator of tsup, ChatWise",
179 | 		link: "https://x.com/localhost_5173/status/1951152679461278068",
180 | 		description: "better-auth is great, I use it everywhere",
181 | 		avatar: "/people-say/egoist.png",
182 | 		social: <Icons.x />,
183 | 	},
184 | ];
185 | 
186 | type TestimonialProps = (typeof testimonials)[number];
187 | 
188 | const TestimonialItem = ({
189 | 	reverse = false,
190 | 	testimonials,
191 | 	noSsr,
192 | }: {
193 | 	reverse?: boolean;
194 | 	testimonials: TestimonialProps[];
195 | 	noSsr?: boolean;
196 | }) => {
197 | 	noSsr && noSSR();
198 | 	const animeSeconds = testimonials.length * 10;
199 | 	return (
200 | 		<div className="max-w-full mx-auto">
201 | 			<div
202 | 				className={`[--anime-duration:${animeSeconds}s] px-10 mx-auto w-full`}
203 | 			>
204 | 				<div
205 | 					style={{
206 | 						animationDuration: `${animeSeconds}s`,
207 | 					}}
208 | 					className={cn(
209 | 						"scroller flex flex-nowrap w-max min-w-full duration-[1000s] hover:[animation-play-state:paused] overflow-hidden relative gap-5 justify-around shrink-0",
210 | 						reverse ? "animate-hrtl-scroll-reverse " : "animate-hrtl-scroll",
211 | 					)}
212 | 				>
213 | 					{testimonials.map((testimonial, indx) => {
214 | 						return (
215 | 							<div
216 | 								key={indx}
217 | 								className={cn(
218 | 									"flex flex-col justify-between h-[220px] rounded-none border-[1.2px] border-black/20 shrink-0 grow-0 w-[450px] dark:border-white/10",
219 | 								)}
220 | 							>
221 | 								<p className="px-5 py-5 tracking-tight text-md font-extralight sm:text-xl md:text-lg text-pretty text-text-primary dark:text-dark-text-primary">
222 | 									&quot;{testimonial.description}.&quot;
223 | 								</p>
224 | 								<div className="flex overflow-hidden h-[28%] gap-1 w-full border-t-[1.2px]">
225 | 									<div className="flex items-center w-3/4 gap-3 px-4 py-3">
226 | 										<img
227 | 											src={testimonial.avatar}
228 | 											className="w-10 h-10 rounded-full"
229 | 											alt="avatar"
230 | 										/>
231 | 										<div className="flex flex-col items-start justify-start flex-1 gap-0">
232 | 											<h5 className="text-base font-medium md:text-md">
233 | 												{testimonial.name}
234 | 											</h5>
235 | 											<p className="text-sm md:text-base text-black/30 mt-[-4px] text-text-tertiary dark:text-white/50 dark:text-dark-text-tertiary">
236 | 												{testimonial.profession}
237 | 											</p>
238 | 										</div>
239 | 									</div>
240 | 									<div className="w-[1px] bg-black/20 dark:bg-white/20" />
241 | 									<div className="flex items-center justify-center max-w-full mx-auto">
242 | 										<Link href={testimonial.link} target="_blank">
243 | 											{testimonial.social}
244 | 										</Link>
245 | 									</div>
246 | 								</div>
247 | 							</div>
248 | 						);
249 | 					})}
250 | 				</div>
251 | 			</div>
252 | 		</div>
253 | 	);
254 | };
255 | 
256 | export const Testimonial = () => {
257 | 	return (
258 | 		<div className="max-w-full py-5 mx-auto overflow-hidden">
259 | 			<div className="flex flex-col gap-3">
260 | 				<div
261 | 					style={{
262 | 						maskImage:
263 | 							"linear-gradient(to left, transparent 0%, black 20%, black 80%, transparent 95%)",
264 | 					}}
265 | 					className="relative flex justify-around gap-5 overflow-hidden shrink-0"
266 | 				>
267 | 					<Suspense
268 | 						fallback={
269 | 							<TestimonialItem
270 | 								testimonials={Array(15)
271 | 									.fill(
272 | 										testimonials.slice(
273 | 											Math.floor(testimonials.length / 2) + 1,
274 | 											testimonials.length - 1,
275 | 										),
276 | 									)
277 | 									.flat()}
278 | 							/>
279 | 						}
280 | 					>
281 | 						<TestimonialItem
282 | 							noSsr
283 | 							reverse
284 | 							testimonials={Array(15)
285 | 								.sort(() => Math.random() - 0.5)
286 | 								.fill(
287 | 									testimonials.slice(0, Math.floor(testimonials.length / 2)),
288 | 								)
289 | 								.flat()}
290 | 						/>
291 | 					</Suspense>
292 | 				</div>
293 | 				<div
294 | 					style={{
295 | 						maskImage:
296 | 							"linear-gradient(to left, transparent 0%, black 20%, black 80%, transparent 95%)",
297 | 					}}
298 | 					className="relative flex justify-around gap-5 overflow-hidden shrink-0"
299 | 				>
300 | 					<Suspense
301 | 						fallback={
302 | 							<TestimonialItem
303 | 								testimonials={Array(15)
304 | 									.fill(
305 | 										testimonials.slice(
306 | 											Math.floor(testimonials.length / 2) + 1,
307 | 											testimonials.length - 1,
308 | 										),
309 | 									)
310 | 									.flat()}
311 | 							/>
312 | 						}
313 | 					>
314 | 						<TestimonialItem
315 | 							noSsr
316 | 							testimonials={Array(15)
317 | 								.sort(() => Math.random() - 0.5)
318 | 								.fill(
319 | 									testimonials.slice(
320 | 										Math.floor(testimonials.length / 2) + 1,
321 | 										testimonials.length - 1,
322 | 									),
323 | 								)
324 | 								.flat()}
325 | 						/>
326 | 					</Suspense>
327 | 				</div>
328 | 			</div>
329 | 		</div>
330 | 	);
331 | };
332 | 
```

--------------------------------------------------------------------------------
/docs/components/builder/sign-up.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { Button } from "@/components/ui/button";
  4 | import {
  5 | 	Card,
  6 | 	CardContent,
  7 | 	CardDescription,
  8 | 	CardFooter,
  9 | 	CardHeader,
 10 | 	CardTitle,
 11 | } from "@/components/ui/card";
 12 | import { Input } from "@/components/ui/input";
 13 | import { Label } from "@/components/ui/label";
 14 | import { useState } from "react";
 15 | import Image from "next/image";
 16 | import { Loader2, X } from "lucide-react";
 17 | import { useRouter } from "next/navigation";
 18 | 
 19 | export function SignUp() {
 20 | 	const [firstName, setFirstName] = useState("");
 21 | 	const [lastName, setLastName] = useState("");
 22 | 	const [email, setEmail] = useState("");
 23 | 	const [password, setPassword] = useState("");
 24 | 	const [passwordConfirmation, setPasswordConfirmation] = useState("");
 25 | 	const [image, setImage] = useState<File | null>(null);
 26 | 	const [imagePreview, setImagePreview] = useState<string | null>(null);
 27 | 	const router = useRouter();
 28 | 
 29 | 	const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
 30 | 		const file = e.target.files?.[0];
 31 | 		if (file) {
 32 | 			setImage(file);
 33 | 			const reader = new FileReader();
 34 | 			reader.onloadend = () => {
 35 | 				setImagePreview(reader.result as string);
 36 | 			};
 37 | 			reader.readAsDataURL(file);
 38 | 		}
 39 | 	};
 40 | 	const [loading, setLoading] = useState(false);
 41 | 
 42 | 	return (
 43 | 		<Card className="z-50 rounded-md rounded-t-none max-w-md">
 44 | 			<CardHeader>
 45 | 				<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
 46 | 				<CardDescription className="text-xs md:text-sm">
 47 | 					Enter your information to create an account
 48 | 				</CardDescription>
 49 | 			</CardHeader>
 50 | 			<CardContent>
 51 | 				<div className="grid gap-4">
 52 | 					<div className="grid grid-cols-2 gap-4">
 53 | 						<div className="grid gap-2">
 54 | 							<Label htmlFor="first-name">First name</Label>
 55 | 							<Input
 56 | 								id="first-name"
 57 | 								placeholder="Max"
 58 | 								required
 59 | 								onChange={(e) => {
 60 | 									setFirstName(e.target.value);
 61 | 								}}
 62 | 								value={firstName}
 63 | 							/>
 64 | 						</div>
 65 | 						<div className="grid gap-2">
 66 | 							<Label htmlFor="last-name">Last name</Label>
 67 | 							<Input
 68 | 								id="last-name"
 69 | 								placeholder="Robinson"
 70 | 								required
 71 | 								onChange={(e) => {
 72 | 									setLastName(e.target.value);
 73 | 								}}
 74 | 								value={lastName}
 75 | 							/>
 76 | 						</div>
 77 | 					</div>
 78 | 					<div className="grid gap-2">
 79 | 						<Label htmlFor="email">Email</Label>
 80 | 						<Input
 81 | 							id="email"
 82 | 							type="email"
 83 | 							placeholder="[email protected]"
 84 | 							required
 85 | 							onChange={(e) => {
 86 | 								setEmail(e.target.value);
 87 | 							}}
 88 | 							value={email}
 89 | 						/>
 90 | 					</div>
 91 | 					<div className="grid gap-2">
 92 | 						<Label htmlFor="password">Password</Label>
 93 | 						<Input
 94 | 							id="password"
 95 | 							type="password"
 96 | 							value={password}
 97 | 							onChange={(e) => setPassword(e.target.value)}
 98 | 							autoComplete="new-password"
 99 | 							placeholder="Password"
100 | 						/>
101 | 					</div>
102 | 					<div className="grid gap-2">
103 | 						<Label htmlFor="password">Confirm Password</Label>
104 | 						<Input
105 | 							id="password_confirmation"
106 | 							type="password"
107 | 							value={passwordConfirmation}
108 | 							onChange={(e) => setPasswordConfirmation(e.target.value)}
109 | 							autoComplete="new-password"
110 | 							placeholder="Confirm Password"
111 | 						/>
112 | 					</div>
113 | 					<div className="grid gap-2">
114 | 						<Label htmlFor="image">Profile Image (optional)</Label>
115 | 						<div className="flex items-end gap-4">
116 | 							{imagePreview && (
117 | 								<div className="relative w-16 h-16 rounded-sm overflow-hidden">
118 | 									<Image
119 | 										src={imagePreview}
120 | 										alt="Profile preview"
121 | 										layout="fill"
122 | 										objectFit="cover"
123 | 									/>
124 | 								</div>
125 | 							)}
126 | 							<div className="flex items-center gap-2 w-full">
127 | 								<Input
128 | 									id="image"
129 | 									type="file"
130 | 									accept="image/*"
131 | 									onChange={handleImageChange}
132 | 									className="w-full"
133 | 								/>
134 | 								{imagePreview && (
135 | 									<X
136 | 										className="cursor-pointer"
137 | 										onClick={() => {
138 | 											setImage(null);
139 | 											setImagePreview(null);
140 | 										}}
141 | 									/>
142 | 								)}
143 | 							</div>
144 | 						</div>
145 | 					</div>
146 | 					<Button
147 | 						type="submit"
148 | 						className="w-full"
149 | 						disabled={loading}
150 | 						onClick={async () => {}}
151 | 					>
152 | 						{loading ? (
153 | 							<Loader2 size={16} className="animate-spin" />
154 | 						) : (
155 | 							"Create an account"
156 | 						)}
157 | 					</Button>
158 | 				</div>
159 | 			</CardContent>
160 | 			<CardFooter>
161 | 				<div className="flex justify-center w-full border-t py-4">
162 | 					<p className="text-center text-xs text-neutral-500">
163 | 						Secured by <span className="text-orange-400">better-auth.</span>
164 | 					</p>
165 | 				</div>
166 | 			</CardFooter>
167 | 		</Card>
168 | 	);
169 | }
170 | 
171 | async function convertImageToBase64(file: File): Promise<string> {
172 | 	return new Promise((resolve, reject) => {
173 | 		const reader = new FileReader();
174 | 		reader.onloadend = () => resolve(reader.result as string);
175 | 		reader.onerror = reject;
176 | 		reader.readAsDataURL(file);
177 | 	});
178 | }
179 | 
180 | export const signUpString = `"use client";
181 | 
182 | import { Button } from "@/components/ui/button";
183 | import {
184 | 	Card,
185 | 	CardContent,
186 | 	CardDescription,
187 | 	CardFooter,
188 | 	CardHeader,
189 | 	CardTitle,
190 | } from "@/components/ui/card";
191 | import { Input } from "@/components/ui/input";
192 | import { Label } from "@/components/ui/label";
193 | import { useState } from "react";
194 | import Image from "next/image";
195 | import { Loader2, X } from "lucide-react";
196 | import { signUp } from "@/lib/auth-client";
197 | import { toast } from "sonner";
198 | import { useRouter } from "next/navigation";
199 | 
200 | export default function SignUp() {
201 | 	const [firstName, setFirstName] = useState("");
202 | 	const [lastName, setLastName] = useState("");
203 | 	const [email, setEmail] = useState("");
204 | 	const [password, setPassword] = useState("");
205 | 	const [passwordConfirmation, setPasswordConfirmation] = useState("");
206 | 	const [image, setImage] = useState<File | null>(null);
207 | 	const [imagePreview, setImagePreview] = useState<string | null>(null);
208 | 	const router = useRouter();
209 | 	const [loading, setLoading] = useState(false);
210 | 
211 | 	const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
212 | 		const file = e.target.files?.[0];
213 | 		if (file) {
214 | 			setImage(file);
215 | 			const reader = new FileReader();
216 | 			reader.onloadend = () => {
217 | 				setImagePreview(reader.result as string);
218 | 			};
219 | 			reader.readAsDataURL(file);
220 | 		}
221 | 	};
222 | 
223 | 	return (
224 | 		<Card className="z-50 rounded-md rounded-t-none max-w-md">
225 | 			<CardHeader>
226 | 				<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
227 | 				<CardDescription className="text-xs md:text-sm">
228 | 					Enter your information to create an account
229 | 				</CardDescription>
230 | 			</CardHeader>
231 | 			<CardContent>
232 | 				<div className="grid gap-4">
233 | 					<div className="grid grid-cols-2 gap-4">
234 | 						<div className="grid gap-2">
235 | 							<Label htmlFor="first-name">First name</Label>
236 | 							<Input
237 | 								id="first-name"
238 | 								placeholder="Max"
239 | 								required
240 | 								onChange={(e) => {
241 | 									setFirstName(e.target.value);
242 | 								}}
243 | 								value={firstName}
244 | 							/>
245 | 						</div>
246 | 						<div className="grid gap-2">
247 | 							<Label htmlFor="last-name">Last name</Label>
248 | 							<Input
249 | 								id="last-name"
250 | 								placeholder="Robinson"
251 | 								required
252 | 								onChange={(e) => {
253 | 									setLastName(e.target.value);
254 | 								}}
255 | 								value={lastName}
256 | 							/>
257 | 						</div>
258 | 					</div>
259 | 					<div className="grid gap-2">
260 | 						<Label htmlFor="email">Email</Label>
261 | 						<Input
262 | 							id="email"
263 | 							type="email"
264 | 							placeholder="[email protected]"
265 | 							required
266 | 							onChange={(e) => {
267 | 								setEmail(e.target.value);
268 | 							}}
269 | 							value={email}
270 | 						/>
271 | 					</div>
272 | 					<div className="grid gap-2">
273 | 						<Label htmlFor="password">Password</Label>
274 | 						<Input
275 | 							id="password"
276 | 							type="password"
277 | 							value={password}
278 | 							onChange={(e) => setPassword(e.target.value)}
279 | 							autoComplete="new-password"
280 | 							placeholder="Password"
281 | 						/>
282 | 					</div>
283 | 					<div className="grid gap-2">
284 | 						<Label htmlFor="password">Confirm Password</Label>
285 | 						<Input
286 | 							id="password_confirmation"
287 | 							type="password"
288 | 							value={passwordConfirmation}
289 | 							onChange={(e) => setPasswordConfirmation(e.target.value)}
290 | 							autoComplete="new-password"
291 | 							placeholder="Confirm Password"
292 | 						/>
293 | 					</div>
294 | 					<div className="grid gap-2">
295 | 						<Label htmlFor="image">Profile Image (optional)</Label>
296 | 						<div className="flex items-end gap-4">
297 | 							{imagePreview && (
298 | 								<div className="relative w-16 h-16 rounded-sm overflow-hidden">
299 | 									<Image
300 | 										src={imagePreview}
301 | 										alt="Profile preview"
302 | 										layout="fill"
303 | 										objectFit="cover"
304 | 									/>
305 | 								</div>
306 | 							)}
307 | 							<div className="flex items-center gap-2 w-full">
308 | 								<Input
309 | 									id="image"
310 | 									type="file"
311 | 									accept="image/*"
312 | 									onChange={handleImageChange}
313 | 									className="w-full"
314 | 								/>
315 | 								{imagePreview && (
316 | 									<X
317 | 										className="cursor-pointer"
318 | 										onClick={() => {
319 | 											setImage(null);
320 | 											setImagePreview(null);
321 | 										}}
322 | 									/>
323 | 								)}
324 | 							</div>
325 | 						</div>
326 | 					</div>
327 | 					<Button
328 | 						type="submit"
329 | 						className="w-full"
330 | 						disabled={loading}
331 | 						onClick={async () => {
332 | 							await signUp.email({
333 | 								email,
334 | 								password,
335 | 								name: \`\${firstName} \${lastName}\`,
336 | 								image: image ? await convertImageToBase64(image) : "",
337 | 								callbackURL: "/dashboard",
338 | 								fetchOptions: {
339 | 									onResponse: () => {
340 | 										setLoading(false);
341 | 									},
342 | 									onRequest: () => {
343 | 										setLoading(true);
344 | 									},
345 | 									onError: (ctx) => {
346 | 										toast.error(ctx.error.message);
347 | 									},
348 | 									onSuccess: async () => {
349 | 										router.push("/dashboard");
350 | 									},
351 | 								},
352 | 							});
353 | 						}}
354 | 					>
355 | 						{loading ? (
356 | 							<Loader2 size={16} className="animate-spin" />
357 | 						) : (
358 | 							"Create an account"
359 | 						)}
360 | 					</Button>
361 | 				</div>
362 | 			</CardContent>
363 | 			<CardFooter>
364 | 				<div className="flex justify-center w-full border-t py-4">
365 | 					<p className="text-center text-xs text-neutral-500">
366 | 						Secured by <span className="text-orange-400">better-auth.</span>
367 | 					</p>
368 | 				</div>
369 | 			</CardFooter>
370 | 		</Card>
371 | 	);
372 | }
373 | 
374 | async function convertImageToBase64(file: File): Promise<string> {
375 | 	return new Promise((resolve, reject) => {
376 | 		const reader = new FileReader();
377 | 		reader.onloadend = () => resolve(reader.result as string);
378 | 		reader.onerror = reject;
379 | 		reader.readAsDataURL(file);
380 | 	});
381 | }`;
382 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/two-factor/otp/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "better-call";
  2 | import * as z from "zod";
  3 | import { createAuthEndpoint } from "@better-auth/core/api";
  4 | import { verifyTwoFactor } from "../verify-two-factor";
  5 | import type { TwoFactorProvider, UserWithTwoFactor } from "../types";
  6 | import { TWO_FACTOR_ERROR_CODES } from "../error-code";
  7 | import {
  8 | 	generateRandomString,
  9 | 	symmetricDecrypt,
 10 | 	symmetricEncrypt,
 11 | } from "../../../crypto";
 12 | import { setSessionCookie } from "../../../cookies";
 13 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
 14 | import { defaultKeyHasher } from "../utils";
 15 | import type { GenericEndpointContext } from "@better-auth/core";
 16 | 
 17 | export interface OTPOptions {
 18 | 	/**
 19 | 	 * How long the opt will be valid for in
 20 | 	 * minutes
 21 | 	 *
 22 | 	 * @default "3 mins"
 23 | 	 */
 24 | 	period?: number;
 25 | 	/**
 26 | 	 * Number of digits for the OTP code
 27 | 	 *
 28 | 	 * @default 6
 29 | 	 */
 30 | 	digits?: number;
 31 | 	/**
 32 | 	 * Send the otp to the user
 33 | 	 *
 34 | 	 * @param user - The user to send the otp to
 35 | 	 * @param otp - The otp to send
 36 | 	 * @param request - The request object
 37 | 	 * @returns void | Promise<void>
 38 | 	 */
 39 | 	sendOTP?: (
 40 | 		/**
 41 | 		 * The user to send the otp to
 42 | 		 * @type UserWithTwoFactor
 43 | 		 * @default UserWithTwoFactors
 44 | 		 */
 45 | 		data: {
 46 | 			user: UserWithTwoFactor;
 47 | 			otp: string;
 48 | 		},
 49 | 		/**
 50 | 		 * The request object
 51 | 		 */
 52 | 		request?: Request,
 53 | 	) => Promise<void> | void;
 54 | 	/**
 55 | 	 * The number of allowed attempts for the OTP
 56 | 	 *
 57 | 	 * @default 5
 58 | 	 */
 59 | 	allowedAttempts?: number;
 60 | 	storeOTP?:
 61 | 		| "plain"
 62 | 		| "encrypted"
 63 | 		| "hashed"
 64 | 		| { hash: (token: string) => Promise<string> }
 65 | 		| {
 66 | 				encrypt: (token: string) => Promise<string>;
 67 | 				decrypt: (token: string) => Promise<string>;
 68 | 		  };
 69 | }
 70 | 
 71 | /**
 72 |  * The otp adapter is created from the totp adapter.
 73 |  */
 74 | export const otp2fa = (options?: OTPOptions) => {
 75 | 	const opts = {
 76 | 		storeOTP: "plain",
 77 | 		digits: 6,
 78 | 		...options,
 79 | 		period: (options?.period || 3) * 60 * 1000,
 80 | 	};
 81 | 
 82 | 	async function storeOTP(ctx: GenericEndpointContext, otp: string) {
 83 | 		if (opts.storeOTP === "hashed") {
 84 | 			return await defaultKeyHasher(otp);
 85 | 		}
 86 | 		if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) {
 87 | 			return await opts.storeOTP.hash(otp);
 88 | 		}
 89 | 		if (typeof opts.storeOTP === "object" && "encrypt" in opts.storeOTP) {
 90 | 			return await opts.storeOTP.encrypt(otp);
 91 | 		}
 92 | 		if (opts.storeOTP === "encrypted") {
 93 | 			return await symmetricEncrypt({
 94 | 				key: ctx.context.secret,
 95 | 				data: otp,
 96 | 			});
 97 | 		}
 98 | 		return otp;
 99 | 	}
100 | 
101 | 	async function decryptOTP(ctx: GenericEndpointContext, otp: string) {
102 | 		if (opts.storeOTP === "hashed") {
103 | 			return await defaultKeyHasher(otp);
104 | 		}
105 | 		if (opts.storeOTP === "encrypted") {
106 | 			return await symmetricDecrypt({
107 | 				key: ctx.context.secret,
108 | 				data: otp,
109 | 			});
110 | 		}
111 | 		if (typeof opts.storeOTP === "object" && "encrypt" in opts.storeOTP) {
112 | 			return await opts.storeOTP.decrypt(otp);
113 | 		}
114 | 		if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) {
115 | 			return await opts.storeOTP.hash(otp);
116 | 		}
117 | 		return otp;
118 | 	}
119 | 
120 | 	/**
121 | 	 * Generate OTP and send it to the user.
122 | 	 */
123 | 	const send2FaOTP = createAuthEndpoint(
124 | 		"/two-factor/send-otp",
125 | 		{
126 | 			method: "POST",
127 | 			body: z
128 | 				.object({
129 | 					/**
130 | 					 * if true, the device will be trusted
131 | 					 * for 30 days. It'll be refreshed on
132 | 					 * every sign in request within this time.
133 | 					 */
134 | 					trustDevice: z.boolean().optional().meta({
135 | 						description:
136 | 							"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
137 | 					}),
138 | 				})
139 | 				.optional(),
140 | 			metadata: {
141 | 				openapi: {
142 | 					summary: "Send two factor OTP",
143 | 					description: "Send two factor OTP to the user",
144 | 					responses: {
145 | 						200: {
146 | 							description: "Successful response",
147 | 							content: {
148 | 								"application/json": {
149 | 									schema: {
150 | 										type: "object",
151 | 										properties: {
152 | 											status: {
153 | 												type: "boolean",
154 | 											},
155 | 										},
156 | 									},
157 | 								},
158 | 							},
159 | 						},
160 | 					},
161 | 				},
162 | 			},
163 | 		},
164 | 		async (ctx) => {
165 | 			if (!options || !options.sendOTP) {
166 | 				ctx.context.logger.error(
167 | 					"send otp isn't configured. Please configure the send otp function on otp options.",
168 | 				);
169 | 				throw new APIError("BAD_REQUEST", {
170 | 					message: "otp isn't configured",
171 | 				});
172 | 			}
173 | 			const { session, key } = await verifyTwoFactor(ctx);
174 | 			if (!session.user.twoFactorEnabled) {
175 | 				throw new APIError("BAD_REQUEST", {
176 | 					message: TWO_FACTOR_ERROR_CODES.OTP_NOT_ENABLED,
177 | 				});
178 | 			}
179 | 			const code = generateRandomString(opts.digits, "0-9");
180 | 			const hashedCode = await storeOTP(ctx, code);
181 | 			await ctx.context.internalAdapter.createVerificationValue({
182 | 				value: `${hashedCode}:0`,
183 | 				identifier: `2fa-otp-${key}`,
184 | 				expiresAt: new Date(Date.now() + opts.period),
185 | 			});
186 | 			await options.sendOTP(
187 | 				{ user: session.user as UserWithTwoFactor, otp: code },
188 | 				ctx.request,
189 | 			);
190 | 			return ctx.json({ status: true });
191 | 		},
192 | 	);
193 | 
194 | 	const verifyOTP = createAuthEndpoint(
195 | 		"/two-factor/verify-otp",
196 | 		{
197 | 			method: "POST",
198 | 			body: z.object({
199 | 				code: z.string().meta({
200 | 					description: 'The otp code to verify. Eg: "012345"',
201 | 				}),
202 | 				/**
203 | 				 * if true, the device will be trusted
204 | 				 * for 30 days. It'll be refreshed on
205 | 				 * every sign in request within this time.
206 | 				 */
207 | 				trustDevice: z.boolean().optional().meta({
208 | 					description:
209 | 						"If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true",
210 | 				}),
211 | 			}),
212 | 			metadata: {
213 | 				openapi: {
214 | 					summary: "Verify two factor OTP",
215 | 					description: "Verify two factor OTP",
216 | 					responses: {
217 | 						"200": {
218 | 							description: "Two-factor OTP verified successfully",
219 | 							content: {
220 | 								"application/json": {
221 | 									schema: {
222 | 										type: "object",
223 | 										properties: {
224 | 											token: {
225 | 												type: "string",
226 | 												description:
227 | 													"Session token for the authenticated session",
228 | 											},
229 | 											user: {
230 | 												type: "object",
231 | 												properties: {
232 | 													id: {
233 | 														type: "string",
234 | 														description: "Unique identifier of the user",
235 | 													},
236 | 													email: {
237 | 														type: "string",
238 | 														format: "email",
239 | 														nullable: true,
240 | 														description: "User's email address",
241 | 													},
242 | 													emailVerified: {
243 | 														type: "boolean",
244 | 														nullable: true,
245 | 														description: "Whether the email is verified",
246 | 													},
247 | 													name: {
248 | 														type: "string",
249 | 														nullable: true,
250 | 														description: "User's name",
251 | 													},
252 | 													image: {
253 | 														type: "string",
254 | 														format: "uri",
255 | 														nullable: true,
256 | 														description: "User's profile image URL",
257 | 													},
258 | 													createdAt: {
259 | 														type: "string",
260 | 														format: "date-time",
261 | 														description: "Timestamp when the user was created",
262 | 													},
263 | 													updatedAt: {
264 | 														type: "string",
265 | 														format: "date-time",
266 | 														description:
267 | 															"Timestamp when the user was last updated",
268 | 													},
269 | 												},
270 | 												required: ["id", "createdAt", "updatedAt"],
271 | 												description: "The authenticated user object",
272 | 											},
273 | 										},
274 | 										required: ["token", "user"],
275 | 									},
276 | 								},
277 | 							},
278 | 						},
279 | 					},
280 | 				},
281 | 			},
282 | 		},
283 | 		async (ctx) => {
284 | 			const { session, key, valid, invalid } = await verifyTwoFactor(ctx);
285 | 			const toCheckOtp =
286 | 				await ctx.context.internalAdapter.findVerificationValue(
287 | 					`2fa-otp-${key}`,
288 | 				);
289 | 			const [otp, counter] = toCheckOtp?.value?.split(":") ?? [];
290 | 			const decryptedOtp = await decryptOTP(ctx, otp!);
291 | 			if (!toCheckOtp || toCheckOtp.expiresAt < new Date()) {
292 | 				if (toCheckOtp) {
293 | 					await ctx.context.internalAdapter.deleteVerificationValue(
294 | 						toCheckOtp.id,
295 | 					);
296 | 				}
297 | 				throw new APIError("BAD_REQUEST", {
298 | 					message: TWO_FACTOR_ERROR_CODES.OTP_HAS_EXPIRED,
299 | 				});
300 | 			}
301 | 			const allowedAttempts = options?.allowedAttempts || 5;
302 | 			if (parseInt(counter!) >= allowedAttempts) {
303 | 				await ctx.context.internalAdapter.deleteVerificationValue(
304 | 					toCheckOtp.id,
305 | 				);
306 | 				throw new APIError("BAD_REQUEST", {
307 | 					message: TWO_FACTOR_ERROR_CODES.TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE,
308 | 				});
309 | 			}
310 | 			if (decryptedOtp === ctx.body.code) {
311 | 				if (!session.user.twoFactorEnabled) {
312 | 					if (!session.session) {
313 | 						throw new APIError("BAD_REQUEST", {
314 | 							message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
315 | 						});
316 | 					}
317 | 					const updatedUser = await ctx.context.internalAdapter.updateUser(
318 | 						session.user.id,
319 | 						{
320 | 							twoFactorEnabled: true,
321 | 						},
322 | 					);
323 | 					const newSession = await ctx.context.internalAdapter.createSession(
324 | 						session.user.id,
325 | 						false,
326 | 						session.session,
327 | 					);
328 | 					await ctx.context.internalAdapter.deleteSession(
329 | 						session.session.token,
330 | 					);
331 | 					await setSessionCookie(ctx, {
332 | 						session: newSession,
333 | 						user: updatedUser,
334 | 					});
335 | 					return ctx.json({
336 | 						token: newSession.token,
337 | 						user: {
338 | 							id: updatedUser.id,
339 | 							email: updatedUser.email,
340 | 							emailVerified: updatedUser.emailVerified,
341 | 							name: updatedUser.name,
342 | 							image: updatedUser.image,
343 | 							createdAt: updatedUser.createdAt,
344 | 							updatedAt: updatedUser.updatedAt,
345 | 						},
346 | 					});
347 | 				}
348 | 				return valid(ctx);
349 | 			} else {
350 | 				await ctx.context.internalAdapter.updateVerificationValue(
351 | 					toCheckOtp.id,
352 | 					{
353 | 						value: `${otp}:${(parseInt(counter!, 10) || 0) + 1}`,
354 | 					},
355 | 				);
356 | 				return invalid("INVALID_CODE");
357 | 			}
358 | 		},
359 | 	);
360 | 
361 | 	return {
362 | 		id: "otp",
363 | 		endpoints: {
364 | 			/**
365 | 			 * ### Endpoint
366 | 			 *
367 | 			 * POST `/two-factor/send-otp`
368 | 			 *
369 | 			 * ### API Methods
370 | 			 *
371 | 			 * **server:**
372 | 			 * `auth.api.send2FaOTP`
373 | 			 *
374 | 			 * **client:**
375 | 			 * `authClient.twoFactor.sendOtp`
376 | 			 *
377 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-send-otp)
378 | 			 */
379 | 			sendTwoFactorOTP: send2FaOTP,
380 | 			/**
381 | 			 * ### Endpoint
382 | 			 *
383 | 			 * POST `/two-factor/verify-otp`
384 | 			 *
385 | 			 * ### API Methods
386 | 			 *
387 | 			 * **server:**
388 | 			 * `auth.api.verifyOTP`
389 | 			 *
390 | 			 * **client:**
391 | 			 * `authClient.twoFactor.verifyOtp`
392 | 			 *
393 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/2fa#api-method-two-factor-verify-otp)
394 | 			 */
395 | 			verifyTwoFactorOTP: verifyOTP,
396 | 		},
397 | 	} satisfies TwoFactorProvider;
398 | };
399 | 
```

--------------------------------------------------------------------------------
/packages/expo/test/expo.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createAuthClient } from "better-auth/react";
  2 | import Database from "better-sqlite3";
  3 | import { beforeAll, afterAll, describe, expect, it, vi } from "vitest";
  4 | import { expo } from "../src";
  5 | import { expoClient, storageAdapter } from "../src/client";
  6 | import { betterAuth } from "better-auth";
  7 | import { getMigrations } from "better-auth/db";
  8 | import { oAuthProxy } from "better-auth/plugins";
  9 | 
 10 | vi.mock("expo-web-browser", async () => {
 11 | 	return {
 12 | 		openAuthSessionAsync: vi.fn(async (...args) => {
 13 | 			fn(...args);
 14 | 			return {
 15 | 				type: "success",
 16 | 				url: "better-auth://?cookie=better-auth.session_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYxMzQwZj",
 17 | 			};
 18 | 		}),
 19 | 	};
 20 | });
 21 | 
 22 | vi.mock("react-native", async () => {
 23 | 	return {
 24 | 		Platform: {
 25 | 			OS: "android",
 26 | 		},
 27 | 	};
 28 | });
 29 | 
 30 | vi.mock("expo-constants", async () => {
 31 | 	return {
 32 | 		default: {
 33 | 			platform: {
 34 | 				scheme: "better-auth",
 35 | 			},
 36 | 		},
 37 | 	};
 38 | });
 39 | 
 40 | vi.mock("expo-linking", async () => {
 41 | 	return {
 42 | 		createURL: vi.fn((url) => `better-auth://${url}`),
 43 | 	};
 44 | });
 45 | 
 46 | const fn = vi.fn();
 47 | 
 48 | function testUtils(extraOpts?: Parameters<typeof betterAuth>[0]) {
 49 | 	const storage = new Map<string, string>();
 50 | 
 51 | 	const auth = betterAuth({
 52 | 		baseURL: "http://localhost:3000",
 53 | 		database: new Database(":memory:"),
 54 | 		emailAndPassword: {
 55 | 			enabled: true,
 56 | 		},
 57 | 		socialProviders: {
 58 | 			google: {
 59 | 				clientId: "test",
 60 | 				clientSecret: "test",
 61 | 			},
 62 | 		},
 63 | 		plugins: [expo(), oAuthProxy()],
 64 | 		trustedOrigins: ["better-auth://"],
 65 | 		...extraOpts,
 66 | 	});
 67 | 
 68 | 	const client = createAuthClient({
 69 | 		baseURL: "http://localhost:3000",
 70 | 		fetchOptions: {
 71 | 			customFetchImpl: (url, init) => {
 72 | 				const req = new Request(url.toString(), init);
 73 | 				return auth.handler(req);
 74 | 			},
 75 | 		},
 76 | 		plugins: [
 77 | 			expoClient({
 78 | 				storage: {
 79 | 					getItem: (key) => storage.get(key) || null,
 80 | 					setItem: async (key, value) => storage.set(key, value),
 81 | 				},
 82 | 			}),
 83 | 		],
 84 | 	});
 85 | 
 86 | 	return { storage, auth, client };
 87 | }
 88 | 
 89 | describe("expo", async () => {
 90 | 	const { auth, client, storage } = testUtils();
 91 | 
 92 | 	beforeAll(async () => {
 93 | 		const { runMigrations } = await getMigrations(auth.options);
 94 | 		await runMigrations();
 95 | 		vi.useFakeTimers();
 96 | 	});
 97 | 	afterAll(() => {
 98 | 		vi.useRealTimers();
 99 | 	});
100 | 
101 | 	it("should store cookie with expires date", async () => {
102 | 		const testUser = {
103 | 			email: "[email protected]",
104 | 			password: "password",
105 | 			name: "Test User",
106 | 		};
107 | 		await client.signUp.email(testUser);
108 | 		const storedCookie = storage.get("better-auth_cookie");
109 | 		expect(storedCookie).toBeDefined();
110 | 		const parsedCookie = JSON.parse(storedCookie || "");
111 | 		expect(parsedCookie["better-auth.session_token"]).toMatchObject({
112 | 			value: expect.stringMatching(/.+/),
113 | 			expires: expect.any(String),
114 | 		});
115 | 	});
116 | 
117 | 	it("should send cookie and get session", async () => {
118 | 		const { data } = await client.getSession();
119 | 		expect(data).toMatchObject({
120 | 			session: expect.any(Object),
121 | 			user: expect.any(Object),
122 | 		});
123 | 	});
124 | 
125 | 	it("should use the scheme to open the browser", async () => {
126 | 		const { data: res } = await client.signIn.social({
127 | 			provider: "google",
128 | 			callbackURL: "/dashboard",
129 | 		});
130 | 		const stateId = res?.url?.split("state=")[1]!.split("&")[0];
131 | 		const ctx = await auth.$context;
132 | 		if (!stateId) {
133 | 			throw new Error("State ID not found");
134 | 		}
135 | 		const state = await ctx.internalAdapter.findVerificationValue(stateId);
136 | 		const callbackURL = JSON.parse(state?.value || "{}").callbackURL;
137 | 		expect(callbackURL).toBe("better-auth:///dashboard");
138 | 		expect(res).toMatchObject({
139 | 			url: expect.stringContaining("accounts.google"),
140 | 		});
141 | 		expect(fn).toHaveBeenCalledWith(
142 | 			expect.stringContaining("accounts.google"),
143 | 			"better-auth:///dashboard",
144 | 		);
145 | 	});
146 | 
147 | 	it("should get cookies", async () => {
148 | 		const c = client.getCookie();
149 | 		expect(c).includes("better-auth.session_token");
150 | 	});
151 | 	it("should correctly parse multiple Set-Cookie headers with Expires commas", async () => {
152 | 		const header =
153 | 			"better-auth.session_token=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/, better-auth.session_data=xyz; Expires=Thu, 22 Oct 2015 07:28:00 GMT; Path=/";
154 | 		const map = (await import("../src/client")).parseSetCookieHeader(header);
155 | 		expect(map.get("better-auth.session_token")?.value).toBe("abc");
156 | 		expect(map.get("better-auth.session_data")?.value).toBe("xyz");
157 | 	});
158 | 
159 | 	it("should not trigger infinite refetch with non-better-auth cookies", async () => {
160 | 		const { hasBetterAuthCookies } = await import("../src/client");
161 | 
162 | 		const betterAuthOnlyHeader = "better-auth.session_token=abc; Path=/";
163 | 		expect(hasBetterAuthCookies(betterAuthOnlyHeader, "better-auth")).toBe(
164 | 			true,
165 | 		);
166 | 
167 | 		const sessionDataHeader = "better-auth.session_data=xyz; Path=/";
168 | 		expect(hasBetterAuthCookies(sessionDataHeader, "better-auth")).toBe(true);
169 | 
170 | 		const secureBetterAuthHeader =
171 | 			"__Secure-better-auth.session_token=abc; Path=/";
172 | 		expect(hasBetterAuthCookies(secureBetterAuthHeader, "better-auth")).toBe(
173 | 			true,
174 | 		);
175 | 
176 | 		const secureSessionDataHeader =
177 | 			"__Secure-better-auth.session_data=xyz; Path=/";
178 | 		expect(hasBetterAuthCookies(secureSessionDataHeader, "better-auth")).toBe(
179 | 			true,
180 | 		);
181 | 
182 | 		const nonBetterAuthHeader = "__cf_bm=abc123; Path=/; HttpOnly; Secure";
183 | 		expect(hasBetterAuthCookies(nonBetterAuthHeader, "better-auth")).toBe(
184 | 			false,
185 | 		);
186 | 
187 | 		const mixedHeader =
188 | 			"__cf_bm=abc123; Path=/; HttpOnly; Secure, better-auth.session_token=xyz; Path=/";
189 | 		expect(hasBetterAuthCookies(mixedHeader, "better-auth")).toBe(true);
190 | 
191 | 		const customPrefixHeader = "my-app.session_token=abc; Path=/";
192 | 		expect(hasBetterAuthCookies(customPrefixHeader, "my-app")).toBe(true);
193 | 		expect(hasBetterAuthCookies(customPrefixHeader, "better-auth")).toBe(false);
194 | 
195 | 		const customPrefixDataHeader = "my-app.session_data=abc; Path=/";
196 | 		expect(hasBetterAuthCookies(customPrefixDataHeader, "my-app")).toBe(true);
197 | 
198 | 		const emptyPrefixHeader = "session_token=abc; Path=/";
199 | 		expect(hasBetterAuthCookies(emptyPrefixHeader, "")).toBe(true);
200 | 
201 | 		const customFullNameHeader = "my_custom_session_token=abc; Path=/";
202 | 		expect(hasBetterAuthCookies(customFullNameHeader, "")).toBe(true);
203 | 
204 | 		const customFullDataHeader = "my_custom_session_data=xyz; Path=/";
205 | 		expect(hasBetterAuthCookies(customFullDataHeader, "")).toBe(true);
206 | 
207 | 		const multipleNonBetterAuthHeader =
208 | 			"__cf_bm=abc123; Path=/, _ga=GA1.2.123456789.1234567890; Path=/";
209 | 		expect(
210 | 			hasBetterAuthCookies(multipleNonBetterAuthHeader, "better-auth"),
211 | 		).toBe(false);
212 | 
213 | 		const nonSessionBetterAuthHeader = "better-auth.other_cookie=abc; Path=/";
214 | 		expect(
215 | 			hasBetterAuthCookies(nonSessionBetterAuthHeader, "better-auth"),
216 | 		).toBe(false);
217 | 	});
218 | 
219 | 	it("should preserve unchanged client store session properties on signout", async () => {
220 | 		const before = client.$store.atoms.session!.get();
221 | 		await client.signOut();
222 | 		const after = client.$store.atoms.session!.get();
223 | 
224 | 		expect(after).toMatchObject({
225 | 			...before,
226 | 			data: null,
227 | 			error: null,
228 | 			isPending: false,
229 | 		});
230 | 	});
231 | });
232 | 
233 | describe("expo with cookieCache", async () => {
234 | 	const { auth, client, storage } = testUtils({
235 | 		session: {
236 | 			expiresIn: 5,
237 | 			cookieCache: {
238 | 				enabled: true,
239 | 				maxAge: 1,
240 | 			},
241 | 		},
242 | 	});
243 | 	beforeAll(async () => {
244 | 		const { runMigrations } = await getMigrations(auth.options);
245 | 		await runMigrations();
246 | 		vi.useFakeTimers();
247 | 	});
248 | 	afterAll(() => {
249 | 		vi.useRealTimers();
250 | 	});
251 | 
252 | 	it("should store cookie with expires date", async () => {
253 | 		const testUser = {
254 | 			email: "[email protected]",
255 | 			password: "password",
256 | 			name: "Test User",
257 | 		};
258 | 		await client.signUp.email(testUser);
259 | 		const storedCookie = storage.get("better-auth_cookie");
260 | 		expect(storedCookie).toBeDefined();
261 | 		const parsedCookie = JSON.parse(storedCookie || "");
262 | 		expect(parsedCookie["better-auth.session_token"]).toMatchObject({
263 | 			value: expect.stringMatching(/.+/),
264 | 			expires: expect.any(String),
265 | 		});
266 | 		expect(parsedCookie["better-auth.session_data"]).toMatchObject({
267 | 			value: expect.stringMatching(/.+/),
268 | 			expires: expect.any(String),
269 | 		});
270 | 	});
271 | 	it("should refresh session_data when it expired without erasing session_token", async () => {
272 | 		vi.advanceTimersByTime(1000);
273 | 		const { data } = await client.getSession();
274 | 		expect(data).toMatchObject({
275 | 			session: expect.any(Object),
276 | 			user: expect.any(Object),
277 | 		});
278 | 		const storedCookie = storage.get("better-auth_cookie");
279 | 		expect(storedCookie).toBeDefined();
280 | 		const parsedCookie = JSON.parse(storedCookie || "");
281 | 		expect(parsedCookie["better-auth.session_token"]).toMatchObject({
282 | 			value: expect.any(String),
283 | 			expires: expect.any(String),
284 | 		});
285 | 		expect(parsedCookie["better-auth.session_data"]).toMatchObject({
286 | 			value: expect.any(String),
287 | 			expires: expect.any(String),
288 | 		});
289 | 	});
290 | 
291 | 	it("should erase both session_data and session_token when token expired", async () => {
292 | 		vi.advanceTimersByTime(5000);
293 | 		const { data } = await client.getSession();
294 | 		expect(data).toBeNull();
295 | 		const storedCookie = storage.get("better-auth_cookie");
296 | 		expect(storedCookie).toBeDefined();
297 | 		const parsedCookie = JSON.parse(storedCookie || "");
298 | 		expect(parsedCookie["better-auth.session_token"]).toMatchObject({
299 | 			value: expect.any(String),
300 | 			expires: expect.any(String),
301 | 		});
302 | 		expect(parsedCookie["better-auth.session_data"]).toMatchObject({
303 | 			value: expect.any(String),
304 | 			expires: expect.any(String),
305 | 		});
306 | 	});
307 | 
308 | 	it("should add `exp://` to trusted origins", async () => {
309 | 		vi.stubEnv("NODE_ENV", "development");
310 | 		const auth = betterAuth({
311 | 			plugins: [expo()],
312 | 			trustedOrigins: ["http://localhost:3000"],
313 | 		});
314 | 		const ctx = await auth.$context;
315 | 		expect(ctx.options.trustedOrigins).toContain("exp://");
316 | 		expect(ctx.options.trustedOrigins).toContain("http://localhost:3000");
317 | 	});
318 | 
319 | 	it("should allow independent cookiePrefix configuration", async () => {
320 | 		const { hasBetterAuthCookies } = await import("../src/client");
321 | 
322 | 		const customCookieHeader = "my-app.session_token=abc; Path=/";
323 | 
324 | 		expect(hasBetterAuthCookies(customCookieHeader, "my-app")).toBe(true);
325 | 
326 | 		expect(hasBetterAuthCookies(customCookieHeader, "better-auth")).toBe(false);
327 | 	});
328 | 
329 | 	it("should normalize colons in secure storage name via storage adapter", async () => {
330 | 		const map = new Map<string, string>();
331 | 		const storage = storageAdapter({
332 | 			getItem(name) {
333 | 				return map.get(name) || null;
334 | 			},
335 | 			setItem(name, value) {
336 | 				map.set(name, value);
337 | 			},
338 | 		});
339 | 		storage.setItem("better-auth:session_token", "123");
340 | 		expect(map.has("better-auth_session_token")).toBe(true);
341 | 		expect(map.has("better-auth:session_token")).toBe(false);
342 | 	});
343 | });
344 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/concepts/oauth.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: OAuth
  3 | description: How Better Auth handles OAuth
  4 | ---
  5 | 
  6 | Better Auth comes with built-in support for OAuth 2.0 and OpenID Connect. This allows you to authenticate users via popular OAuth providers like Google, Facebook, GitHub, and more.
  7 | 
  8 | If your desired provider isn't directly supported, you can use the [Generic OAuth Plugin](/docs/plugins/generic-oauth) for custom integrations.
  9 | 
 10 | ## Configuring Social Providers
 11 | 
 12 | To enable a social provider, you need to provide `clientId` and `clientSecret` for the provider.
 13 | 
 14 | Here's an example of how to configure Google as a provider:
 15 | 
 16 | ```ts title="auth.ts"
 17 | import { betterAuth } from "better-auth";
 18 | 
 19 | export const auth = betterAuth({
 20 |   // Other configurations...
 21 |   socialProviders: {
 22 |     google: {
 23 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
 24 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
 25 |     },
 26 |   },
 27 | });
 28 | ```
 29 | 
 30 | ## Usage
 31 | 
 32 | ### Sign In
 33 | 
 34 | To sign in with a social provider, you can use the `signIn.social` function with the `authClient` or `auth.api` for server-side usage.
 35 | 
 36 | ```ts
 37 | // client-side usage
 38 | await authClient.signIn.social({
 39 |   provider: "google", // or any other provider id
 40 | })
 41 | ```
 42 | 
 43 | ```ts
 44 | // server-side usage
 45 | await auth.api.signInSocial({
 46 |   body: {
 47 |     provider: "google", // or any other provider id
 48 |   },
 49 | });
 50 | ```
 51 | 
 52 | ### Link account
 53 | 
 54 | To link an account to a social provider, you can use the `linkAccount` function with the `authClient` or `auth.api` for server-side usage.
 55 | 
 56 | ```ts
 57 | await authClient.linkSocial({
 58 |   provider: "google", // or any other provider id
 59 | })
 60 | ```
 61 | 
 62 | server-side usage:
 63 | 
 64 | ```ts
 65 | await auth.api.linkSocialAccount({
 66 |   body: {
 67 |     provider: "google", // or any other provider id
 68 |   },
 69 |   headers: // pass headers with authenticated token
 70 | });
 71 | ```
 72 | 
 73 | ### Get Access Token
 74 | 
 75 | To get the access token for a social provider, you can use the `getAccessToken` function with the `authClient` or `auth.api` for server-side usage. When you use this endpoint, if the access token is expired, it will be refreshed.
 76 | 
 77 | ```ts
 78 | const { accessToken } = await authClient.getAccessToken({
 79 |   providerId: "google", // or any other provider id
 80 |   accountId: "accountId", // optional, if you want to get the access token for a specific account
 81 | })
 82 | ```
 83 | 
 84 | server-side usage:
 85 | 
 86 | ```ts
 87 | await auth.api.getAccessToken({
 88 |   body: {
 89 |     providerId: "google", // or any other provider id
 90 |     accountId: "accountId", // optional, if you want to get the access token for a specific account
 91 |     userId: "userId", // optional, if you don't provide headers with authenticated token
 92 |   },
 93 |   headers: // pass headers with authenticated token
 94 | });
 95 | ```
 96 | 
 97 | ### Get Account Info Provided by the provider
 98 | 
 99 | To get provider specific account info you can use the `accountInfo` function with the `authClient` or `auth.api` for server-side usage.
100 | 
101 | ```ts
102 | const info = await authClient.accountInfo({
103 |   accountId: "accountId", // here you pass in the provider given account id, the provider is automatically detected from the account id
104 | })
105 | ```
106 | 
107 | server-side usage:
108 | 
109 | ```ts
110 | await auth.api.accountInfo({
111 |   body: { accountId: "accountId" },
112 |   headers: // pass headers with authenticated token
113 | });
114 | ```
115 | 
116 | ### Requesting Additional Scopes
117 | 
118 | Sometimes your application may need additional OAuth scopes after the user has already signed up (e.g., for accessing GitHub repositories or Google Drive). Users may not want to grant extensive permissions initially, preferring to start with minimal permissions and grant additional access as needed.
119 | 
120 | You can request additional scopes by using the `linkSocial` method with the same provider. This will trigger a new OAuth flow that requests the additional scopes while maintaining the existing account connection.
121 | 
122 | ```ts
123 | const requestAdditionalScopes = async () => {
124 |     await authClient.linkSocial({
125 |         provider: "google",
126 |         scopes: ["https://www.googleapis.com/auth/drive.file"],
127 |     });
128 | };
129 | ```
130 | 
131 | <Callout>
132 | Make sure you're running Better Auth version 1.2.7 or later. Earlier versions (like 1.2.2) may show a "Social account already linked" error when trying to link with an existing provider for additional scopes.
133 | </Callout>
134 | 
135 | ## Provider Options
136 | 
137 | ### scope
138 | 
139 | The scope of the access request. For example, `email` or `profile`.
140 | 
141 | ```ts title="auth.ts"
142 | import { betterAuth } from "better-auth";
143 | 
144 | export const auth = betterAuth({
145 |   // Other configurations...
146 |   socialProviders: {
147 |     google: {
148 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
149 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
150 |       scope: ["email", "profile"],
151 |     },
152 |   },
153 | });
154 | ```
155 | 
156 | ### redirectURI
157 | 
158 | Custom redirect URI for the provider. By default, it uses `/api/auth/callback/${providerName}`
159 | 
160 | ```ts title="auth.ts"
161 | 
162 | export const auth = betterAuth({
163 |   // Other configurations...
164 |   socialProviders: {
165 |     google: {
166 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
167 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
168 |       redirectURI: "https://your-app.com/auth/callback",
169 |     },
170 |   },
171 | });
172 | ```
173 | 
174 | ### disableSignUp
175 | 
176 | Disables sign-up for new users.
177 | 
178 | ### disableIdTokenSignIn
179 | 
180 | Disables the use of the ID token for sign-in. By default, it's enabled for some providers like Google and Apple.
181 | 
182 | ### verifyIdToken
183 | 
184 | A custom function to verify the ID token.
185 | 
186 | ### overrideUserInfoOnSignIn
187 | 
188 | A boolean value that determines whether to override the user information in the database when signing in. By default, it is set to `false`, meaning that the user information will not be overridden during sign-in. If you want to update the user information every time they sign in, set this to `true`.
189 | 
190 | ### mapProfileToUser
191 | 
192 | A custom function to map the user profile returned from the provider to the user object in your database.
193 | 
194 | Useful, if you have additional fields in your user object you want to populate from the provider's profile. Or if you want to change how by default the user object is mapped.
195 | 
196 | ```ts title="auth.ts"
197 | import { betterAuth } from "better-auth";
198 | 
199 | export const auth = betterAuth({
200 |   // Other configurations...
201 |   socialProviders: {
202 |     google: {
203 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
204 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
205 |       mapProfileToUser: (profile) => {
206 |         return {
207 |           firstName: profile.given_name,
208 |           lastName: profile.family_name,
209 |         };
210 |       },
211 |     },
212 |   },
213 | });
214 | ```
215 | 
216 | ### refreshAccessToken
217 | 
218 | A custom function to refresh the token. This feature is only supported for built-in social providers (Google, Facebook, GitHub, etc.) and is not currently supported for custom OAuth providers configured through the Generic OAuth Plugin. For built-in providers, you can provide a custom function to refresh the token if needed.
219 | 
220 | ```ts title="auth.ts"
221 | import { betterAuth } from "better-auth";
222 | 
223 | export const auth = betterAuth({
224 |   // Other configurations...
225 |   socialProviders: {
226 |     google: {
227 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
228 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
229 |       refreshAccessToken: async (token) => {
230 |         return {
231 |           accessToken: "new-access-token",
232 |           refreshToken: "new-refresh-token",
233 |         };
234 |       },
235 |     },
236 |   },
237 | });
238 | ```
239 | 
240 | ### clientKey
241 | 
242 | The client key of your application. This is used by TikTok Social Provider instead of `clientId`.
243 | 
244 | ```ts title="auth.ts"
245 | import { betterAuth } from "better-auth";
246 | 
247 | export const auth = betterAuth({
248 |   // Other configurations...
249 |   socialProviders: {
250 |     tiktok: {
251 |       clientKey: "YOUR_TIKTOK_CLIENT_KEY",
252 |       clientSecret: "YOUR_TIKTOK_CLIENT_SECRET",
253 |     },
254 |   },
255 | });
256 | ```
257 | 
258 | ### getUserInfo
259 | 
260 | A custom function to get user info from the provider. This allows you to override the default user info retrieval process.
261 | 
262 | ```ts title="auth.ts"
263 | import { betterAuth } from "better-auth";
264 | 
265 | export const auth = betterAuth({
266 |   // Other configurations...
267 |   socialProviders: {
268 |     google: {
269 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
270 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
271 |       getUserInfo: async (token) => {
272 |         // Custom implementation to get user info
273 |         const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
274 |           headers: {
275 |             Authorization: `Bearer ${token.accessToken}`,
276 |           },
277 |         });
278 |         const profile = await response.json();
279 |         return {
280 |           user: {
281 |             id: profile.id,
282 |             name: profile.name,
283 |             email: profile.email,
284 |             image: profile.picture,
285 |             emailVerified: profile.verified_email,
286 |           },
287 |           data: profile,
288 |         };
289 |       },
290 |     },
291 |   },
292 | });
293 | ```
294 | 
295 | ### disableImplicitSignUp
296 | 
297 | Disables implicit sign up for new users. When set to true for the provider, sign-in needs to be called with `requestSignUp` as true to create new users.
298 | 
299 | ```ts title="auth.ts"
300 | import { betterAuth } from "better-auth";
301 | 
302 | export const auth = betterAuth({
303 |   // Other configurations...
304 |   socialProviders: {
305 |     google: {
306 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
307 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
308 |       disableImplicitSignUp: true,
309 |     },
310 |   },
311 | });
312 | ```
313 | 
314 | ### prompt
315 | 
316 | The prompt to use for the authorization code request. This controls the authentication flow behavior.
317 | 
318 | ```ts title="auth.ts"
319 | import { betterAuth } from "better-auth";
320 | 
321 | export const auth = betterAuth({
322 |   // Other configurations...
323 |   socialProviders: {
324 |     google: {
325 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
326 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
327 |       prompt: "select_account", // or "consent", "login", "none", "select_account+consent"
328 |     },
329 |   },
330 | });
331 | ```
332 | 
333 | ### responseMode
334 | 
335 | The response mode to use for the authorization code request. This determines how the authorization response is returned.
336 | 
337 | ```ts title="auth.ts"
338 | import { betterAuth } from "better-auth";
339 | 
340 | export const auth = betterAuth({
341 |   // Other configurations...
342 |   socialProviders: {
343 |     google: {
344 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
345 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
346 |       responseMode: "query", // or "form_post"
347 |     },
348 |   },
349 | });
350 | ```
351 | 
352 | ### disableDefaultScope
353 | 
354 | Removes the default scopes of the provider. By default, providers include certain scopes like `email` and `profile`. Set this to `true` to remove these default scopes and use only the scopes you specify.
355 | 
356 | ```ts title="auth.ts"
357 | import { betterAuth } from "better-auth";
358 | 
359 | export const auth = betterAuth({
360 |   // Other configurations...
361 |   socialProviders: {
362 |     google: {
363 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
364 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
365 |       disableDefaultScope: true,
366 |       scope: ["https://www.googleapis.com/auth/userinfo.email"], // Only this scope will be used
367 |     },
368 |   },
369 | });
370 | ```
371 | 
372 | ### Other Provider Configurations
373 | 
374 | Each provider may have additional options, check the specific provider documentation for more details.
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/routes/crud-org.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, vi } from "vitest";
  2 | import { getTestInstance } from "../../../test-utils/test-instance";
  3 | import { organization } from "../organization";
  4 | import { createAuthClient } from "../../../client";
  5 | import { organizationClient } from "../client";
  6 | import { ORGANIZATION_ERROR_CODES } from "../error-codes";
  7 | 
  8 | describe("get-full-organization", async () => {
  9 | 	const { auth, signInWithTestUser, cookieSetter } = await getTestInstance({
 10 | 		plugins: [organization()],
 11 | 	});
 12 | 	const { headers } = await signInWithTestUser();
 13 | 	const client = createAuthClient({
 14 | 		plugins: [organizationClient()],
 15 | 		baseURL: "http://localhost:3000/api/auth",
 16 | 		fetchOptions: {
 17 | 			customFetchImpl: async (url, init) => {
 18 | 				return auth.handler(new Request(url, init));
 19 | 			},
 20 | 		},
 21 | 	});
 22 | 	const org = await client.organization.create({
 23 | 		name: "test",
 24 | 		slug: "test",
 25 | 		metadata: {
 26 | 			test: "test",
 27 | 		},
 28 | 		fetchOptions: {
 29 | 			headers,
 30 | 		},
 31 | 	});
 32 | 	const secondOrg = await client.organization.create({
 33 | 		name: "test-second",
 34 | 		slug: "test-second",
 35 | 		metadata: {
 36 | 			test: "second-org",
 37 | 		},
 38 | 		fetchOptions: {
 39 | 			headers,
 40 | 		},
 41 | 	});
 42 | 
 43 | 	it("should get organization by organizationId", async () => {
 44 | 		const { headers } = await signInWithTestUser();
 45 | 
 46 | 		//set the second org as active
 47 | 		await client.organization.setActive({
 48 | 			organizationId: secondOrg.data?.id as string,
 49 | 			fetchOptions: {
 50 | 				headers,
 51 | 			},
 52 | 		});
 53 | 		const orgById = await client.organization.getFullOrganization({
 54 | 			query: {
 55 | 				// get the first org
 56 | 				organizationId: org.data?.id as string,
 57 | 			},
 58 | 			fetchOptions: {
 59 | 				headers,
 60 | 			},
 61 | 		});
 62 | 		expect(orgById.data?.name).toBe("test");
 63 | 	});
 64 | 
 65 | 	it("should get organization by organizationSlug", async () => {
 66 | 		const { headers } = await signInWithTestUser();
 67 | 		const orgBySlug = await client.organization.getFullOrganization({
 68 | 			query: {
 69 | 				organizationSlug: "test",
 70 | 			},
 71 | 			fetchOptions: {
 72 | 				headers,
 73 | 			},
 74 | 		});
 75 | 		expect(orgBySlug.data?.name).toBe("test");
 76 | 	});
 77 | 
 78 | 	it("should return null when no active organization and no query params", async () => {
 79 | 		await client.organization.setActive({
 80 | 			organizationId: null,
 81 | 			fetchOptions: {
 82 | 				headers,
 83 | 			},
 84 | 		});
 85 | 		const result = await client.organization.getFullOrganization({
 86 | 			fetchOptions: {
 87 | 				headers: headers,
 88 | 			},
 89 | 		});
 90 | 		expect(result.data).toBeNull();
 91 | 		expect(result.error).toBeNull();
 92 | 	});
 93 | 
 94 | 	it("should throw FORBIDDEN when user is not a member of the organization", async () => {
 95 | 		const newHeaders = new Headers();
 96 | 		await client.signUp.email(
 97 | 			{
 98 | 				email: "[email protected]",
 99 | 				password: "password",
100 | 				name: "test3",
101 | 			},
102 | 			{
103 | 				onSuccess: cookieSetter(newHeaders),
104 | 			},
105 | 		);
106 | 		const result = await client.organization.getFullOrganization({
107 | 			query: {
108 | 				organizationId: org.data?.id as string,
109 | 			},
110 | 			fetchOptions: {
111 | 				headers: newHeaders,
112 | 			},
113 | 		});
114 | 		expect(result.error?.status).toBe(403);
115 | 		expect(result.error?.message).toContain(
116 | 			ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
117 | 		);
118 | 	});
119 | 
120 | 	it("should throw BAD_REQUEST when organization doesn't exist", async () => {
121 | 		const result = await client.organization.getFullOrganization({
122 | 			query: {
123 | 				organizationId: "non-existent-org-id",
124 | 			},
125 | 			fetchOptions: {
126 | 				headers,
127 | 			},
128 | 		});
129 | 		expect(result.error?.status).toBe(400);
130 | 		expect(result.error?.message).toContain(
131 | 			ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
132 | 		);
133 | 	});
134 | 
135 | 	it("should include invitations in the response", async () => {
136 | 		await client.organization.setActive({
137 | 			organizationId: org.data?.id as string,
138 | 			fetchOptions: {
139 | 				headers,
140 | 			},
141 | 		});
142 | 
143 | 		// Create an invitation
144 | 		await client.organization.inviteMember({
145 | 			email: "[email protected]",
146 | 			role: "member",
147 | 			fetchOptions: {
148 | 				headers,
149 | 			},
150 | 		});
151 | 
152 | 		const fullOrg = await client.organization.getFullOrganization({
153 | 			fetchOptions: {
154 | 				headers,
155 | 			},
156 | 		});
157 | 
158 | 		expect(fullOrg.data?.invitations).toBeDefined();
159 | 		expect(Array.isArray(fullOrg.data?.invitations)).toBe(true);
160 | 		const invitation = fullOrg.data?.invitations.find(
161 | 			(inv: any) => inv.email === "[email protected]",
162 | 		);
163 | 		expect(invitation).toBeDefined();
164 | 		expect(invitation?.role).toBe("member");
165 | 	});
166 | 
167 | 	it("should prioritize organizationSlug over organizationId when both are provided", async () => {
168 | 		const result = await client.organization.getFullOrganization({
169 | 			query: {
170 | 				organizationId: org.data?.id as string,
171 | 				organizationSlug: secondOrg.data?.slug as string,
172 | 			},
173 | 			fetchOptions: {
174 | 				headers,
175 | 			},
176 | 		});
177 | 		expect(result.data).toBeTruthy();
178 | 		expect(result.data?.name).toBe(secondOrg.data?.name);
179 | 	});
180 | 
181 | 	it("should allow listing members with membersLimit", async () => {
182 | 		const { headers } = await signInWithTestUser();
183 | 		await client.organization.setActive({
184 | 			organizationId: org.data?.id as string,
185 | 			fetchOptions: {
186 | 				headers,
187 | 			},
188 | 		});
189 | 		const newUser = await auth.api.signUpEmail({
190 | 			body: {
191 | 				email: "[email protected]",
192 | 				password: "password",
193 | 				name: "test2",
194 | 			},
195 | 		});
196 | 		await auth.api.addMember({
197 | 			body: {
198 | 				userId: newUser.user.id,
199 | 				role: "member",
200 | 				organizationId: org.data?.id as string,
201 | 			},
202 | 		});
203 | 		const FullOrganization = await client.organization.getFullOrganization({
204 | 			fetchOptions: {
205 | 				headers,
206 | 			},
207 | 		});
208 | 		expect(FullOrganization.data?.members.length).toBe(2);
209 | 
210 | 		const limitedMembers = await client.organization.getFullOrganization({
211 | 			query: {
212 | 				membersLimit: 1,
213 | 			},
214 | 			fetchOptions: {
215 | 				headers,
216 | 			},
217 | 		});
218 | 		expect(limitedMembers.data?.members.length).toBe(1);
219 | 	});
220 | 
221 | 	it("should use default membershipLimit when no membersLimit is specified", async () => {
222 | 		await client.organization.setActive({
223 | 			organizationId: org.data?.id as string,
224 | 			fetchOptions: {
225 | 				headers,
226 | 			},
227 | 		});
228 | 		for (let i = 3; i <= 5; i++) {
229 | 			const newUser = await auth.api.signUpEmail({
230 | 				body: {
231 | 					email: `test-${i}@test.com`,
232 | 					password: "password",
233 | 					name: `test${i}`,
234 | 				},
235 | 			});
236 | 			await auth.api.addMember({
237 | 				body: {
238 | 					userId: newUser.user.id,
239 | 					role: "member",
240 | 					organizationId: org.data?.id as string,
241 | 				},
242 | 			});
243 | 		}
244 | 
245 | 		const fullOrg = await client.organization.getFullOrganization({
246 | 			fetchOptions: {
247 | 				headers,
248 | 			},
249 | 		});
250 | 
251 | 		expect(fullOrg.data?.members.length).toBeGreaterThan(3);
252 | 		expect(fullOrg.data?.members.length).toBeLessThanOrEqual(6);
253 | 	});
254 | });
255 | 
256 | describe("organization hooks", async () => {
257 | 	it("should apply beforeCreateOrganization hook", async () => {
258 | 		const beforeCreateOrganization = vi.fn();
259 | 		const { auth, signInWithTestUser } = await getTestInstance(
260 | 			{
261 | 				plugins: [
262 | 					organization({
263 | 						organizationHooks: {
264 | 							beforeCreateOrganization: async (data) => {
265 | 								beforeCreateOrganization();
266 | 								return {
267 | 									data: {
268 | 										...data.organization,
269 | 										metadata: {
270 | 											hookCalled: true,
271 | 										},
272 | 										name: "changed-name",
273 | 									},
274 | 								};
275 | 							},
276 | 						},
277 | 					}),
278 | 				],
279 | 			},
280 | 			{
281 | 				clientOptions: {
282 | 					plugins: [organizationClient()],
283 | 				},
284 | 			},
285 | 		);
286 | 		const { headers } = await signInWithTestUser();
287 | 		const result = await auth.api.createOrganization({
288 | 			body: {
289 | 				name: "test",
290 | 				slug: "test",
291 | 			},
292 | 			headers,
293 | 		});
294 | 		expect(beforeCreateOrganization).toHaveBeenCalled();
295 | 		expect(result?.name).toBe("changed-name");
296 | 		expect(result?.metadata).toEqual({
297 | 			hookCalled: true,
298 | 		});
299 | 	});
300 | 
301 | 	it("should apply afterCreateOrganization hook", async () => {
302 | 		const afterCreateOrganization = vi.fn();
303 | 		const { auth, signInWithTestUser } = await getTestInstance({
304 | 			plugins: [
305 | 				organization({
306 | 					organizationHooks: {
307 | 						afterCreateOrganization: async (data) => {
308 | 							afterCreateOrganization();
309 | 						},
310 | 					},
311 | 				}),
312 | 			],
313 | 		});
314 | 		const { headers } = await signInWithTestUser();
315 | 		const result = await auth.api.createOrganization({
316 | 			body: {
317 | 				name: "test",
318 | 				slug: "test",
319 | 			},
320 | 			headers,
321 | 		});
322 | 		expect(afterCreateOrganization).toHaveBeenCalled();
323 | 	});
324 | 
325 | 	it("should apply beforeAddMember hook", async () => {
326 | 		const beforeAddMember = vi.fn();
327 | 		const { auth, signInWithTestUser } = await getTestInstance({
328 | 			plugins: [
329 | 				organization({
330 | 					organizationHooks: {
331 | 						beforeAddMember: async (data) => {
332 | 							beforeAddMember();
333 | 							return {
334 | 								data: {
335 | 									role: "changed-role",
336 | 								},
337 | 							};
338 | 						},
339 | 					},
340 | 				}),
341 | 			],
342 | 		});
343 | 		const { headers } = await signInWithTestUser();
344 | 		await auth.api.createOrganization({
345 | 			body: {
346 | 				name: "test",
347 | 				slug: "test",
348 | 			},
349 | 			headers,
350 | 		});
351 | 		expect(beforeAddMember).toHaveBeenCalled();
352 | 		const member = await auth.api.getActiveMember({
353 | 			headers,
354 | 		});
355 | 		expect(member?.role).toBe("changed-role");
356 | 	});
357 | 
358 | 	it("should apply afterAddMember hook", async () => {
359 | 		const afterAddMember = vi.fn();
360 | 		const { auth, signInWithTestUser } = await getTestInstance({
361 | 			plugins: [
362 | 				organization({
363 | 					organizationHooks: {
364 | 						afterAddMember: async (data) => {
365 | 							afterAddMember();
366 | 						},
367 | 					},
368 | 				}),
369 | 			],
370 | 		});
371 | 		const { headers } = await signInWithTestUser();
372 | 		await auth.api.createOrganization({
373 | 			body: {
374 | 				name: "test",
375 | 				slug: "test",
376 | 			},
377 | 			headers,
378 | 		});
379 | 		expect(afterAddMember).toHaveBeenCalled();
380 | 	});
381 | 
382 | 	it("should apply beforeCreateTeam hook", async () => {
383 | 		const beforeCreateTeam = vi.fn();
384 | 		const { auth, signInWithTestUser } = await getTestInstance({
385 | 			plugins: [
386 | 				organization({
387 | 					teams: {
388 | 						enabled: true,
389 | 					},
390 | 					organizationHooks: {
391 | 						beforeCreateTeam: async (data) => {
392 | 							beforeCreateTeam();
393 | 							return {
394 | 								data: {
395 | 									name: "changed-name",
396 | 								},
397 | 							};
398 | 						},
399 | 					},
400 | 				}),
401 | 			],
402 | 		});
403 | 		const { headers } = await signInWithTestUser();
404 | 		const result = await auth.api.createOrganization({
405 | 			body: {
406 | 				name: "test",
407 | 				slug: "test",
408 | 			},
409 | 			headers,
410 | 		});
411 | 		expect(beforeCreateTeam).toHaveBeenCalled();
412 | 		const team = await auth.api.listOrganizationTeams({
413 | 			headers,
414 | 			query: {
415 | 				organizationId: result?.id,
416 | 			},
417 | 		});
418 | 		expect(team[0]?.name).toBe("changed-name");
419 | 	});
420 | 
421 | 	it("should apply afterCreateTeam hook", async () => {
422 | 		const afterCreateTeam = vi.fn();
423 | 		const { auth, signInWithTestUser } = await getTestInstance({
424 | 			plugins: [
425 | 				organization({
426 | 					teams: {
427 | 						enabled: true,
428 | 					},
429 | 					organizationHooks: {
430 | 						afterCreateTeam: async (data) => {
431 | 							afterCreateTeam();
432 | 						},
433 | 					},
434 | 				}),
435 | 			],
436 | 		});
437 | 		const { headers } = await signInWithTestUser();
438 | 		await auth.api.createOrganization({
439 | 			body: {
440 | 				name: "test",
441 | 				slug: "test",
442 | 			},
443 | 			headers,
444 | 		});
445 | 		expect(afterCreateTeam).toHaveBeenCalled();
446 | 	});
447 | });
448 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/integrations/convex.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Convex Integration
  3 | description: Integrate Better Auth with Convex.
  4 | ---
  5 | 
  6 | <Callout>
  7 |   This documentation comes from the [Convex documentation](https://convex-better-auth.netlify.app/),
  8 |   for more information, please refer to their documentation.
  9 | </Callout>
 10 | 
 11 | ## Prerequisites
 12 | 
 13 | <Steps>
 14 | <Step>
 15 | ### Create a Convex project
 16 | 
 17 | To use Convex + Better Auth, you'll first need a [Convex](https://www.convex.dev/) project.
 18 | If you don't have one, run the following command to get started.
 19 | ```package-install
 20 | npm create convex@latest
 21 | ```
 22 | 
 23 | Check out the [Convex docs](https://docs.convex.dev/home) to learn more about Convex.
 24 | 
 25 | </Step>
 26 | 
 27 | <Step>
 28 | 
 29 | ### Run `convex dev`
 30 | 
 31 | Running the CLI during setup will initialize your Convex deployment
 32 | if it doesn't already exist, and keeps generated types current through the process. Keep it running.
 33 | 
 34 | ```package-install
 35 | npx convex dev
 36 | ```
 37 | </Step>
 38 | </Steps>
 39 | 
 40 | 
 41 | 
 42 | ## Installation of Convex + Better Auth
 43 | 
 44 | The following documentation assumes you're using Next.js.
 45 | 
 46 | If you're not using Next.js, support for other frameworks is documented in the [installation guide by Convex](https://convex-better-auth.netlify.app/#select-your-framework).
 47 | 
 48 | <Callout>
 49 |     For a complete example, check out Convex + Better Auth example with Next.js [on GitHub](https://github.com/get-convex/better-auth/tree/main/examples/next).
 50 | </Callout>
 51 | 
 52 | ### Installation
 53 | 
 54 | <Steps>
 55 |     <Step>
 56 |         #### Install packages
 57 | 
 58 |         Install the component, a pinned version of Better Auth, and ensure the latest version of Convex.
 59 | 
 60 | 
 61 |         <Callout>
 62 |             This component requires Convex `1.25.0` or later.
 63 |         </Callout>
 64 | 
 65 |         ```package-install
 66 |         npm install [email protected] --save-exact
 67 |         npm install convex@latest @convex-dev/better-auth
 68 |         ```
 69 |     </Step>
 70 |     <Step>
 71 |         #### Register the component
 72 | 
 73 |         Register the Better Auth component in your Convex project.
 74 | 
 75 |         ```ts title="convex/convex.config.ts"
 76 |         import { defineApp } from "convex/server";
 77 |         import betterAuth from "@convex-dev/better-auth/convex.config";
 78 | 
 79 |         const app = defineApp();
 80 |         app.use(betterAuth);
 81 | 
 82 |         export default app;
 83 |         ```
 84 |     </Step>
 85 |     <Step>
 86 |         #### Add Convex auth config
 87 | 
 88 |         Add a `convex/auth.config.ts` file to configure Better Auth as an authentication provider.
 89 | 
 90 | 
 91 | 
 92 |         ```ts title="convex/auth.config.ts"
 93 |         export default {
 94 |             providers: [
 95 |                 {
 96 |                     domain: process.env.CONVEX_SITE_URL,
 97 |                     applicationID: "convex",
 98 |                 },
 99 |             ],
100 |         };
101 |         ```
102 |     </Step>
103 |     <Step>
104 |         #### Set environment variables
105 | 
106 |         Generate a secret for encryption and generating hashes. Use the command below if you have openssl installed, or generate your own however you like.
107 | 
108 |         ```package-install
109 |         npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
110 |         ```
111 | 
112 |         Add your site URL to your Convex deployment.
113 | 
114 |         ```package-install
115 |         npx convex env set SITE_URL http://localhost:3000
116 |         ```
117 | 
118 |         Add environment variables to the `.env.local` file created by `npx convex dev`.
119 |         It will be picked up by your framework dev server.
120 | 
121 |         ```shell title=".env.local" tab="Cloud"
122 |         # Deployment used by \`npx convex dev\`
123 |         CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name
124 | 
125 |         NEXT_PUBLIC_CONVEX_URL=https://adjective-animal-123.convex.cloud
126 | 
127 |         # Same as NEXT_PUBLIC_CONVEX_URL but ends in .site // [!code ++]
128 |         NEXT_PUBLIC_CONVEX_SITE_URL=https://adjective-animal-123.convex.site # [!code ++]
129 | 
130 |         # Your local site URL // [!code ++]
131 |         SITE_URL=http://localhost:3000 # [!code ++]
132 |         ```
133 | 
134 |         ```shell title=".env.local" tab="Self hosted"
135 |         # Deployment used by \`npx convex dev\`
136 |         CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name
137 | 
138 |         NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
139 | 
140 |         # Will generally be one number higher than NEXT_PUBLIC_CONVEX_URL,
141 |         # so if your convex url is :3212, your site url will be :3213
142 |         NEXT_PUBLIC_CONVEX_SITE_URL=http://127.0.0.1:3211 # [!code ++]
143 | 
144 |         # Your local site URL // [!code ++]
145 |         SITE_URL=http://localhost:3000 # [!code ++]
146 |         ```
147 |     </Step>
148 |     <Step>
149 |         ### Create a Better Auth instance
150 |         Create a Better Auth instance and initialize the component.
151 | 
152 |         <Callout>Some TypeScript errors will show until you save the file.</Callout>
153 | 
154 |         ```ts title="convex/auth.ts"
155 |         import { createClient, type GenericCtx } from "@convex-dev/better-auth";
156 |         import { convex } from "@convex-dev/better-auth/plugins";
157 |         import { components } from "./_generated/api";
158 |         import { DataModel } from "./_generated/dataModel";
159 |         import { query } from "./_generated/server";
160 |         import { betterAuth } from "better-auth";
161 | 
162 |         const siteUrl = process.env.SITE_URL!;
163 | 
164 |         // The component client has methods needed for integrating Convex with Better Auth,
165 |         // as well as helper methods for general use.
166 |         export const authComponent = createClient<DataModel>(components.betterAuth);
167 | 
168 |         export const createAuth = (
169 |             ctx: GenericCtx<DataModel>,
170 |             { optionsOnly } = { optionsOnly: false },
171 |         ) => {
172 |             return betterAuth({
173 |                 // disable logging when createAuth is called just to generate options.
174 |                 // this is not required, but there's a lot of noise in logs without it.
175 |                 logger: {
176 |                     disabled: optionsOnly,
177 |                 },
178 |                 baseURL: siteUrl,
179 |                 database: authComponent.adapter(ctx),
180 |                 // Configure simple, non-verified email/password to get started
181 |                 emailAndPassword: {
182 |                     enabled: true,
183 |                     requireEmailVerification: false,
184 |                 },
185 |                 plugins: [
186 |                     // The Convex plugin is required for Convex compatibility
187 |                     convex(),
188 |                 ],
189 |             });
190 |         };
191 | 
192 |         // Example function for getting the current user
193 |         // Feel free to edit, omit, etc.
194 |         export const getCurrentUser = query({
195 |             args: {},
196 |             handler: async (ctx) => {
197 |                 return authComponent.getAuthUser(ctx);
198 |             },
199 |         });
200 |         ```
201 |     </Step>
202 |     <Step>
203 |         ### Create a Better Auth client instance
204 | 
205 |         Create a Better Auth client instance for interacting with the Better Auth server from your client.
206 | 
207 |         ```ts title="src/lib/auth-client.ts"
208 |         import { createAuthClient } from "better-auth/react";
209 |         import { convexClient } from "@convex-dev/better-auth/client/plugins";
210 | 
211 |         export const authClient = createAuthClient({
212 |             plugins: [convexClient()],
213 |         });
214 |         ```
215 |     </Step>
216 |     <Step>
217 |     ### Mount handlers
218 | 
219 |     Register Better Auth route handlers on your Convex deployment.
220 | 
221 |     ```ts title="convex/http.ts"
222 |     import { httpRouter } from "convex/server";
223 |     import { authComponent, createAuth } from "./auth";
224 | 
225 |     const http = httpRouter();
226 | 
227 |     authComponent.registerRoutes(http, createAuth);
228 | 
229 |     export default http;
230 |     ```
231 | 
232 |     Set up route handlers to proxy auth requests from your framework server to your Convex deployment.
233 | 
234 |     ```ts title="app/api/auth/[...all]/route.ts"
235 |     import { nextJsHandler } from "@convex-dev/better-auth/nextjs";
236 | 
237 |     export const { GET, POST } = nextJsHandler();
238 |     ```
239 |     </Step>
240 |     <Step>
241 |     ### Set up Convex client provider
242 | 
243 |     Wrap your app with the `ConvexBetterAuthProvider` component.
244 | 
245 |     ```ts title="app/ConvexClientProvider.tsx"
246 |     "use client";
247 | 
248 |     import { ReactNode } from "react";
249 |     import { ConvexReactClient } from "convex/react";
250 |     import { authClient } from "@/lib/auth-client"; // [!code ++]
251 |     import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; // [!code ++]
252 | 
253 |     const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!, {
254 |       // Optionally pause queries until the user is authenticated // [!code ++]
255 |       expectAuth: true, // [!code ++]
256 |     });
257 | 
258 |     export function ConvexClientProvider({ children }: { children: ReactNode }) {
259 |       return (
260 |         <ConvexBetterAuthProvider client={convex} authClient={authClient}> // [!code ++]
261 |           {children}
262 |         </ConvexBetterAuthProvider> // [!code ++]
263 |       );
264 |     }
265 |     ```
266 |     </Step>
267 | </Steps>
268 | 
269 | 
270 | ### You're done!
271 | 
272 | You're now ready to start using Better Auth with Convex.
273 | 
274 | ## Usage
275 | 
276 | ### Using Better Auth from the server
277 | 
278 | To use Better Auth's [server
279 | methods](https://www.better-auth.com/docs/concepts/api) in server rendering,
280 | server functions, or any other Next.js server code, use Convex functions
281 | and call the function from your server code.
282 | 
283 | First, a token helper for calling Convex functions from your server code.
284 | 
285 | ```ts title="src/lib/auth-server.ts"
286 | import { createAuth } from "@/convex/auth";
287 | import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
288 | 
289 | export const getToken = () => {
290 |   return getTokenNextjs(createAuth);
291 | };
292 | ```
293 | 
294 | Here's an example Convex function that uses Better Auth's server methods, and
295 | a server action that calls the Convex function.
296 | 
297 | ```ts title="convex/users.ts"
298 | import { mutation } from "./_generated/server";
299 | import { v } from "convex/values";
300 | import { createAuth, authComponent } from "./auth";
301 | 
302 | export const updateUserPassword = mutation({
303 |   args: {
304 |     currentPassword: v.string(),
305 |     newPassword: v.string(),
306 |   },
307 |   handler: async (ctx, args) => {
308 |     const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
309 |     await auth.api.changePassword({
310 |       body: {
311 |         currentPassword: args.currentPassword,
312 |         newPassword: args.newPassword,
313 |       },
314 |       headers,
315 |     });
316 |   },
317 | });
318 | ```
319 | 
320 | ```ts title="app/actions.ts"
321 | "use server";
322 | 
323 | import { fetchMutation } from "convex/nextjs";
324 | import { api } from "../convex/_generated/api";
325 | import { getToken } from "../lib/auth-server";
326 | 
327 | // Authenticated mutation via server function
328 | export async function updatePassword({
329 |   currentPassword,
330 |   newPassword,
331 | }: {
332 |   currentPassword: string;
333 |   newPassword: string;
334 | }) {
335 |   const token = await getToken();
336 |   await fetchMutation(
337 |     api.users.updatePassword,
338 |     { currentPassword, newPassword },
339 |     { token }
340 |   );
341 | }
342 | ```
343 | 
344 | <Callout>
345 |   This documentation comes from the [Convex documentation](https://convex-better-auth.netlify.app/),
346 |   for more information, please refer to their documentation.
347 | </Callout>
348 | 
```

--------------------------------------------------------------------------------
/packages/core/src/db/adapter/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { BetterAuthDBSchema, DBFieldAttribute } from "../type";
  2 | import type { BetterAuthOptions } from "../../types";
  3 | 
  4 | export type DBAdapterDebugLogOption =
  5 | 	| boolean
  6 | 	| {
  7 | 			/**
  8 | 			 * Useful when you want to log only certain conditions.
  9 | 			 */
 10 | 			logCondition?: (() => boolean) | undefined;
 11 | 			create?: boolean;
 12 | 			update?: boolean;
 13 | 			updateMany?: boolean;
 14 | 			findOne?: boolean;
 15 | 			findMany?: boolean;
 16 | 			delete?: boolean;
 17 | 			deleteMany?: boolean;
 18 | 			count?: boolean;
 19 | 	  }
 20 | 	| {
 21 | 			/**
 22 | 			 * Only used for adapter tests to show debug logs if a test fails.
 23 | 			 *
 24 | 			 * @deprecated Not actually deprecated. Doing this for IDEs to show this option at the very bottom and stop end-users from using this.
 25 | 			 */
 26 | 			isRunningAdapterTests: boolean;
 27 | 	  };
 28 | 
 29 | export type DBAdapterSchemaCreation = {
 30 | 	/**
 31 | 	 * Code to be inserted into the file
 32 | 	 */
 33 | 	code: string;
 34 | 	/**
 35 | 	 * Path to the file, including the file name and extension.
 36 | 	 * Relative paths are supported, with the current working directory of the developer's project as the base.
 37 | 	 */
 38 | 	path: string;
 39 | 	/**
 40 | 	 * Append the file if it already exists.
 41 | 	 * Note: This will not apply if `overwrite` is set to true.
 42 | 	 */
 43 | 	append?: boolean;
 44 | 	/**
 45 | 	 * Overwrite the file if it already exists
 46 | 	 */
 47 | 	overwrite?: boolean;
 48 | };
 49 | 
 50 | export interface DBAdapterFactoryConfig<
 51 | 	Options extends BetterAuthOptions = BetterAuthOptions,
 52 | > {
 53 | 	/**
 54 | 	 * Use plural table names.
 55 | 	 *
 56 | 	 * All tables will be named with an `s` at the end.
 57 | 	 *
 58 | 	 * @default false
 59 | 	 */
 60 | 	usePlural?: boolean;
 61 | 	/**
 62 | 	 * Enable debug logs.
 63 | 	 *
 64 | 	 * @default false
 65 | 	 */
 66 | 	debugLogs?: DBAdapterDebugLogOption;
 67 | 	/**
 68 | 	 * Name of the adapter.
 69 | 	 *
 70 | 	 * This is used to identify the adapter in the debug logs.
 71 | 	 *
 72 | 	 * @default `adapterId`
 73 | 	 */
 74 | 	adapterName?: string;
 75 | 	/**
 76 | 	 * Adapter id
 77 | 	 */
 78 | 	adapterId: string;
 79 | 	/**
 80 | 	 * If the database supports numeric ids, set this to `true`.
 81 | 	 *
 82 | 	 * @default true
 83 | 	 */
 84 | 	supportsNumericIds?: boolean;
 85 | 	/**
 86 | 	 * If the database doesn't support JSON columns, set this to `false`.
 87 | 	 *
 88 | 	 * We will handle the translation between using `JSON` columns, and saving `string`s to the database.
 89 | 	 *
 90 | 	 * @default false
 91 | 	 */
 92 | 	supportsJSON?: boolean;
 93 | 	/**
 94 | 	 * If the database doesn't support dates, set this to `false`.
 95 | 	 *
 96 | 	 * We will handle the translation between using `Date` objects, and saving `string`s to the database.
 97 | 	 *
 98 | 	 * @default true
 99 | 	 */
100 | 	supportsDates?: boolean;
101 | 	/**
102 | 	 * If the database doesn't support booleans, set this to `false`.
103 | 	 *
104 | 	 * We will handle the translation between using `boolean`s, and saving `0`s and `1`s to the database.
105 | 	 *
106 | 	 * @default true
107 | 	 */
108 | 	supportsBooleans?: boolean;
109 | 	/**
110 | 	 * Execute multiple operations in a transaction.
111 | 	 *
112 | 	 * If the database doesn't support transactions, set this to `false` and operations will be executed sequentially.
113 | 	 *
114 | 	 * @default false
115 | 	 */
116 | 	transaction?:
117 | 		| false
118 | 		| (<R>(
119 | 				callback: (trx: DBTransactionAdapter<Options>) => Promise<R>,
120 | 		  ) => Promise<R>);
121 | 	/**
122 | 	 * Disable id generation for the `create` method.
123 | 	 *
124 | 	 * This is useful for databases that don't support custom id values and would auto-generate them for you.
125 | 	 *
126 | 	 * @default false
127 | 	 */
128 | 	disableIdGeneration?: boolean;
129 | 	/**
130 | 	 * Map the keys of the input data.
131 | 	 *
132 | 	 * This is useful for databases that expect a different key name for a given situation.
133 | 	 *
134 | 	 * For example, MongoDB uses `_id` while in Better-Auth we use `id`.
135 | 	 *
136 | 	 *
137 | 	 * @example
138 | 	 * Each key represents the old key to replace.
139 | 	 * The value represents the new key
140 | 	 *
141 | 	 * This can be a partial object that only transforms some keys.
142 | 	 *
143 | 	 * ```ts
144 | 	 * mapKeysTransformInput: {
145 | 	 *  id: "_id" // We want to replace `id` to `_id` to save into MongoDB
146 | 	 * }
147 | 	 * ```
148 | 	 */
149 | 	mapKeysTransformInput?: Record<string, string>;
150 | 	/**
151 | 	 * Map the keys of the output data.
152 | 	 *
153 | 	 * This is useful for databases that expect a different key name for a given situation.
154 | 	 *
155 | 	 * For example, MongoDB uses `_id` while in Better-Auth we use `id`.
156 | 	 *
157 | 	 * @example
158 | 	 * Each key represents the old key to replace.
159 | 	 * The value represents the new key
160 | 	 *
161 | 	 * This can be a partial object that only transforms some keys.
162 | 	 *
163 | 	 * ```ts
164 | 	 * mapKeysTransformOutput: {
165 | 	 *  _id: "id" // In MongoDB, we save `id` as `_id`. So we want to replace `_id` with `id` when we get the data back.
166 | 	 * }
167 | 	 * ```
168 | 	 */
169 | 	mapKeysTransformOutput?: Record<string, string>;
170 | 	/**
171 | 	 * Custom transform input function.
172 | 	 *
173 | 	 * This function is used to transform the input data before it is saved to the database.
174 | 	 */
175 | 	customTransformInput?: (props: {
176 | 		data: any;
177 | 		/**
178 | 		 * The fields of the model.
179 | 		 */
180 | 		fieldAttributes: DBFieldAttribute;
181 | 		/**
182 | 		 * The field to transform.
183 | 		 */
184 | 		field: string;
185 | 		/**
186 | 		 * The action which was called from the adapter.
187 | 		 */
188 | 		action: "create" | "update";
189 | 		/**
190 | 		 * The model name.
191 | 		 */
192 | 		model: string;
193 | 		/**
194 | 		 * The schema of the user's Better-Auth instance.
195 | 		 */
196 | 		schema: BetterAuthDBSchema;
197 | 		/**
198 | 		 * The options of the user's Better-Auth instance.
199 | 		 */
200 | 		options: Options;
201 | 	}) => any;
202 | 	/**
203 | 	 * Custom transform output function.
204 | 	 *
205 | 	 * This function is used to transform the output data before it is returned to the user.
206 | 	 */
207 | 	customTransformOutput?: (props: {
208 | 		data: any;
209 | 		/**
210 | 		 * The fields of the model.
211 | 		 */
212 | 		fieldAttributes: DBFieldAttribute;
213 | 		/**
214 | 		 * The field to transform.
215 | 		 */
216 | 		field: string;
217 | 		/**
218 | 		 * The fields to select.
219 | 		 */
220 | 		select: string[];
221 | 		/**
222 | 		 * The model name.
223 | 		 */
224 | 		model: string;
225 | 		/**
226 | 		 * The schema of the user's Better-Auth instance.
227 | 		 */
228 | 		schema: BetterAuthDBSchema;
229 | 		/**
230 | 		 * The options of the user's Better-Auth instance.
231 | 		 */
232 | 		options: Options;
233 | 	}) => any;
234 | 	/**
235 | 	 * Custom ID generator function.
236 | 	 *
237 | 	 * By default, we can handle ID generation for you, however if the database your adapter is for only supports a specific custom id generation,
238 | 	 * then you can use this function to generate your own IDs.
239 | 	 *
240 | 	 *
241 | 	 * Notes:
242 | 	 * - If the user enabled `useNumberId`, then this option will be ignored. Unless this adapter config has `supportsNumericIds` set to `false`.
243 | 	 * - If `generateId` is `false` in the user's Better-Auth config, then this option will be ignored.
244 | 	 * - If `generateId` is a function, then it will override this option.
245 | 	 *
246 | 	 * @example
247 | 	 *
248 | 	 * ```ts
249 | 	 * customIdGenerator: ({ model }) => {
250 | 	 *  return "my-super-unique-id";
251 | 	 * }
252 | 	 * ```
253 | 	 */
254 | 	customIdGenerator?: (props: { model: string }) => string;
255 | 	/**
256 | 	 * Whether to disable the transform output.
257 | 	 * Do not use this option unless you know what you are doing.
258 | 	 * @default false
259 | 	 */
260 | 	disableTransformOutput?: boolean;
261 | 	/**
262 | 	 * Whether to disable the transform input.
263 | 	 * Do not use this option unless you know what you are doing.
264 | 	 * @default false
265 | 	 */
266 | 	disableTransformInput?: boolean;
267 | }
268 | 
269 | export type Where = {
270 | 	/**
271 | 	 * @default eq
272 | 	 */
273 | 	operator?:
274 | 		| "eq"
275 | 		| "ne"
276 | 		| "lt"
277 | 		| "lte"
278 | 		| "gt"
279 | 		| "gte"
280 | 		| "in"
281 | 		| "not_in"
282 | 		| "contains"
283 | 		| "starts_with"
284 | 		| "ends_with";
285 | 	value: string | number | boolean | string[] | number[] | Date | null;
286 | 	field: string;
287 | 	/**
288 | 	 * @default AND
289 | 	 */
290 | 	connector?: "AND" | "OR";
291 | };
292 | 
293 | export type DBTransactionAdapter<
294 | 	Options extends BetterAuthOptions = BetterAuthOptions,
295 | > = Omit<DBAdapter<Options>, "transaction">;
296 | 
297 | export type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
298 | 	id: string;
299 | 	create: <T extends Record<string, any>, R = T>(data: {
300 | 		model: string;
301 | 		data: Omit<T, "id">;
302 | 		select?: string[];
303 | 		/**
304 | 		 * By default, any `id` provided in `data` will be ignored.
305 | 		 *
306 | 		 * If you want to force the `id` to be the same as the `data.id`, set this to `true`.
307 | 		 */
308 | 		forceAllowId?: boolean;
309 | 	}) => Promise<R>;
310 | 	findOne: <T>(data: {
311 | 		model: string;
312 | 		where: Where[];
313 | 		select?: string[];
314 | 	}) => Promise<T | null>;
315 | 	findMany: <T>(data: {
316 | 		model: string;
317 | 		where?: Where[];
318 | 		limit?: number;
319 | 		sortBy?: {
320 | 			field: string;
321 | 			direction: "asc" | "desc";
322 | 		};
323 | 		offset?: number;
324 | 	}) => Promise<T[]>;
325 | 	count: (data: { model: string; where?: Where[] }) => Promise<number>;
326 | 	/**
327 | 	 * ⚠︎ Update may not return the updated data
328 | 	 * if multiple where clauses are provided
329 | 	 */
330 | 	update: <T>(data: {
331 | 		model: string;
332 | 		where: Where[];
333 | 		update: Record<string, any>;
334 | 	}) => Promise<T | null>;
335 | 	updateMany: (data: {
336 | 		model: string;
337 | 		where: Where[];
338 | 		update: Record<string, any>;
339 | 	}) => Promise<number>;
340 | 	delete: <T>(data: { model: string; where: Where[] }) => Promise<void>;
341 | 	deleteMany: (data: { model: string; where: Where[] }) => Promise<number>;
342 | 	/**
343 | 	 * Execute multiple operations in a transaction.
344 | 	 * If the adapter doesn't support transactions, operations will be executed sequentially.
345 | 	 */
346 | 	transaction: <R>(
347 | 		callback: (trx: DBTransactionAdapter<Options>) => Promise<R>,
348 | 	) => Promise<R>;
349 | 	/**
350 | 	 *
351 | 	 * @param options
352 | 	 * @param file - file path if provided by the user
353 | 	 */
354 | 	createSchema?: (
355 | 		options: Options,
356 | 		file?: string,
357 | 	) => Promise<DBAdapterSchemaCreation>;
358 | 	options?: {
359 | 		adapterConfig: DBAdapterFactoryConfig<Options>;
360 | 	} & CustomAdapter["options"];
361 | };
362 | 
363 | export type CleanedWhere = Required<Where>;
364 | 
365 | export interface CustomAdapter {
366 | 	create: <T extends Record<string, any>>({
367 | 		data,
368 | 		model,
369 | 		select,
370 | 	}: {
371 | 		model: string;
372 | 		data: T;
373 | 		select?: string[];
374 | 	}) => Promise<T>;
375 | 	update: <T>(data: {
376 | 		model: string;
377 | 		where: CleanedWhere[];
378 | 		update: T;
379 | 	}) => Promise<T | null>;
380 | 	updateMany: (data: {
381 | 		model: string;
382 | 		where: CleanedWhere[];
383 | 		update: Record<string, any>;
384 | 	}) => Promise<number>;
385 | 	findOne: <T>({
386 | 		model,
387 | 		where,
388 | 		select,
389 | 	}: {
390 | 		model: string;
391 | 		where: CleanedWhere[];
392 | 		select?: string[];
393 | 	}) => Promise<T | null>;
394 | 	findMany: <T>({
395 | 		model,
396 | 		where,
397 | 		limit,
398 | 		sortBy,
399 | 		offset,
400 | 	}: {
401 | 		model: string;
402 | 		where?: CleanedWhere[];
403 | 		limit: number;
404 | 		sortBy?: { field: string; direction: "asc" | "desc" };
405 | 		offset?: number;
406 | 	}) => Promise<T[]>;
407 | 	delete: ({
408 | 		model,
409 | 		where,
410 | 	}: {
411 | 		model: string;
412 | 		where: CleanedWhere[];
413 | 	}) => Promise<void>;
414 | 	deleteMany: ({
415 | 		model,
416 | 		where,
417 | 	}: {
418 | 		model: string;
419 | 		where: CleanedWhere[];
420 | 	}) => Promise<number>;
421 | 	count: ({
422 | 		model,
423 | 		where,
424 | 	}: {
425 | 		model: string;
426 | 		where?: CleanedWhere[];
427 | 	}) => Promise<number>;
428 | 	createSchema?: (props: {
429 | 		/**
430 | 		 * The file the user may have passed in to the `generate` command as the expected schema file output path.
431 | 		 */
432 | 		file?: string;
433 | 		/**
434 | 		 * The tables from the user's Better-Auth instance schema.
435 | 		 */
436 | 		tables: BetterAuthDBSchema;
437 | 	}) => Promise<DBAdapterSchemaCreation>;
438 | 	/**
439 | 	 * Your adapter's options.
440 | 	 */
441 | 	options?: Record<string, any> | undefined;
442 | }
443 | 
444 | export interface DBAdapterInstance<
445 | 	Options extends BetterAuthOptions = BetterAuthOptions,
446 | > {
447 | 	(options: BetterAuthOptions): DBAdapter<Options>;
448 | }
449 | 
```
Page 31/70FirstPrevNextLast