#
tokens: 47453/50000 7/1119 files (page 32/70)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 32 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

--------------------------------------------------------------------------------
/packages/cli/src/generators/drizzle.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 | 	getAuthTables,
  3 | 	type BetterAuthDBSchema,
  4 | 	type DBFieldAttribute,
  5 | } from "better-auth/db";
  6 | import type { BetterAuthOptions } from "better-auth/types";
  7 | import { existsSync } from "fs";
  8 | import type { SchemaGenerator } from "./types";
  9 | import prettier from "prettier";
 10 | 
 11 | export function convertToSnakeCase(str: string, camelCase?: boolean) {
 12 | 	if (camelCase) {
 13 | 		return str;
 14 | 	}
 15 | 	// Handle consecutive capitals (like ID, URL, API) by treating them as a single word
 16 | 	return str
 17 | 		.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // Handle AABb -> AA_Bb
 18 | 		.replace(/([a-z\d])([A-Z])/g, "$1_$2") // Handle aBb -> a_Bb
 19 | 		.toLowerCase();
 20 | }
 21 | 
 22 | export const generateDrizzleSchema: SchemaGenerator = async ({
 23 | 	options,
 24 | 	file,
 25 | 	adapter,
 26 | }) => {
 27 | 	const tables = getAuthTables(options);
 28 | 	const filePath = file || "./auth-schema.ts";
 29 | 	const databaseType: "sqlite" | "mysql" | "pg" | undefined =
 30 | 		adapter.options?.provider;
 31 | 
 32 | 	if (!databaseType) {
 33 | 		throw new Error(
 34 | 			`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`,
 35 | 		);
 36 | 	}
 37 | 	const fileExist = existsSync(filePath);
 38 | 
 39 | 	let code: string = generateImport({ databaseType, tables, options });
 40 | 
 41 | 	for (const tableKey in tables) {
 42 | 		const table = tables[tableKey]!;
 43 | 		const modelName = getModelName(table.modelName, adapter.options);
 44 | 		const fields = table.fields;
 45 | 
 46 | 		function getType(name: string, field: DBFieldAttribute) {
 47 | 			// Not possible to reach, it's here to make typescript happy
 48 | 			if (!databaseType) {
 49 | 				throw new Error(
 50 | 					`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`,
 51 | 				);
 52 | 			}
 53 | 			name = convertToSnakeCase(name, adapter.options?.camelCase);
 54 | 			if (field.references?.field === "id") {
 55 | 				if (options.advanced?.database?.useNumberId) {
 56 | 					if (databaseType === "pg") {
 57 | 						return `integer('${name}')`;
 58 | 					} else if (databaseType === "mysql") {
 59 | 						return `int('${name}')`;
 60 | 					} else {
 61 | 						// using sqlite
 62 | 						return `integer('${name}')`;
 63 | 					}
 64 | 				}
 65 | 				if (field.references.field) {
 66 | 					if (databaseType === "mysql") {
 67 | 						return `varchar('${name}', { length: 36 })`;
 68 | 					}
 69 | 				}
 70 | 				return `text('${name}')`;
 71 | 			}
 72 | 			const type = field.type;
 73 | 			if (typeof type !== "string") {
 74 | 				if (Array.isArray(type) && type.every((x) => typeof x === "string")) {
 75 | 					return {
 76 | 						sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
 77 | 						pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
 78 | 						mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`,
 79 | 					}[databaseType];
 80 | 				} else {
 81 | 					throw new TypeError(
 82 | 						`Invalid field type for field ${name} in model ${modelName}`,
 83 | 					);
 84 | 				}
 85 | 			}
 86 | 			const typeMap: Record<
 87 | 				typeof type,
 88 | 				Record<typeof databaseType, string>
 89 | 			> = {
 90 | 				string: {
 91 | 					sqlite: `text('${name}')`,
 92 | 					pg: `text('${name}')`,
 93 | 					mysql: field.unique
 94 | 						? `varchar('${name}', { length: 255 })`
 95 | 						: field.references
 96 | 							? `varchar('${name}', { length: 36 })`
 97 | 							: `text('${name}')`,
 98 | 				},
 99 | 				boolean: {
100 | 					sqlite: `integer('${name}', { mode: 'boolean' })`,
101 | 					pg: `boolean('${name}')`,
102 | 					mysql: `boolean('${name}')`,
103 | 				},
104 | 				number: {
105 | 					sqlite: `integer('${name}')`,
106 | 					pg: field.bigint
107 | 						? `bigint('${name}', { mode: 'number' })`
108 | 						: `integer('${name}')`,
109 | 					mysql: field.bigint
110 | 						? `bigint('${name}', { mode: 'number' })`
111 | 						: `int('${name}')`,
112 | 				},
113 | 				date: {
114 | 					sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
115 | 					pg: `timestamp('${name}')`,
116 | 					mysql: `timestamp('${name}', { fsp: 3 })`,
117 | 				},
118 | 				"number[]": {
119 | 					sqlite: `integer('${name}').array()`,
120 | 					pg: field.bigint
121 | 						? `bigint('${name}', { mode: 'number' }).array()`
122 | 						: `integer('${name}').array()`,
123 | 					mysql: field.bigint
124 | 						? `bigint('${name}', { mode: 'number' }).array()`
125 | 						: `int('${name}').array()`,
126 | 				},
127 | 				"string[]": {
128 | 					sqlite: `text('${name}').array()`,
129 | 					pg: `text('${name}').array()`,
130 | 					mysql: `text('${name}').array()`,
131 | 				},
132 | 				json: {
133 | 					sqlite: `text('${name}')`,
134 | 					pg: `jsonb('${name}')`,
135 | 					mysql: `json('${name}')`,
136 | 				},
137 | 			} as const;
138 | 			return typeMap[type][databaseType];
139 | 		}
140 | 
141 | 		let id: string = "";
142 | 
143 | 		if (options.advanced?.database?.useNumberId) {
144 | 			if (databaseType === "pg") {
145 | 				id = `serial("id").primaryKey()`;
146 | 			} else if (databaseType === "sqlite") {
147 | 				id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
148 | 			} else {
149 | 				id = `int("id").autoincrement().primaryKey()`;
150 | 			}
151 | 		} else {
152 | 			if (databaseType === "mysql") {
153 | 				id = `varchar('id', { length: 36 }).primaryKey()`;
154 | 			} else if (databaseType === "pg") {
155 | 				id = `text('id').primaryKey()`;
156 | 			} else {
157 | 				id = `text('id').primaryKey()`;
158 | 			}
159 | 		}
160 | 
161 | 		const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(
162 | 			modelName,
163 | 			adapter.options?.camelCase,
164 | 		)}", {
165 | 					id: ${id},
166 | 					${Object.keys(fields)
167 | 						.map((field) => {
168 | 							const attr = fields[field]!;
169 | 							const fieldName = attr.fieldName || field;
170 | 							let type = getType(fieldName, attr);
171 | 
172 | 							if (
173 | 								attr.defaultValue !== null &&
174 | 								typeof attr.defaultValue !== "undefined"
175 | 							) {
176 | 								if (typeof attr.defaultValue === "function") {
177 | 									if (
178 | 										attr.type === "date" &&
179 | 										attr.defaultValue.toString().includes("new Date()")
180 | 									) {
181 | 										if (databaseType === "sqlite") {
182 | 											type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
183 | 										} else {
184 | 											type += `.defaultNow()`;
185 | 										}
186 | 									} else {
187 | 										// we are intentionally not adding .$defaultFn(${attr.defaultValue})
188 | 										// this is because if the defaultValue is a function, it could have
189 | 										// custom logic within that function that might not work in drizzle's context.
190 | 									}
191 | 								} else if (typeof attr.defaultValue === "string") {
192 | 									type += `.default("${attr.defaultValue}")`;
193 | 								} else {
194 | 									type += `.default(${attr.defaultValue})`;
195 | 								}
196 | 							}
197 | 							// Add .$onUpdate() for fields with onUpdate property
198 | 							// Supported for all database types: PostgreSQL, MySQL, and SQLite
199 | 							if (attr.onUpdate && attr.type === "date") {
200 | 								if (typeof attr.onUpdate === "function") {
201 | 									type += `.$onUpdate(${attr.onUpdate})`;
202 | 								}
203 | 							}
204 | 
205 | 							return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${
206 | 								attr.unique ? ".unique()" : ""
207 | 							}${
208 | 								attr.references
209 | 									? `.references(()=> ${getModelName(
210 | 											tables[attr.references.model]?.modelName ||
211 | 												attr.references.model,
212 | 											adapter.options,
213 | 										)}.${fields[attr.references.field]?.fieldName || attr.references.field}, { onDelete: '${
214 | 											attr.references.onDelete || "cascade"
215 | 										}' })`
216 | 									: ""
217 | 							}`;
218 | 						})
219 | 						.join(",\n ")}
220 | 				});`;
221 | 		code += `\n${schema}\n`;
222 | 	}
223 | 	const formattedCode = await prettier.format(code, {
224 | 		parser: "typescript",
225 | 	});
226 | 	return {
227 | 		code: formattedCode,
228 | 		fileName: filePath,
229 | 		overwrite: fileExist,
230 | 	};
231 | };
232 | 
233 | function generateImport({
234 | 	databaseType,
235 | 	tables,
236 | 	options,
237 | }: {
238 | 	databaseType: "sqlite" | "mysql" | "pg";
239 | 	tables: BetterAuthDBSchema;
240 | 	options: BetterAuthOptions;
241 | }) {
242 | 	const rootImports: string[] = [];
243 | 	const coreImports: string[] = [];
244 | 
245 | 	let hasBigint = false;
246 | 	let hasJson = false;
247 | 
248 | 	for (const table of Object.values(tables)) {
249 | 		for (const field of Object.values(table.fields)) {
250 | 			if (field.bigint) hasBigint = true;
251 | 			if (field.type === "json") hasJson = true;
252 | 		}
253 | 		if (hasJson && hasBigint) break;
254 | 	}
255 | 
256 | 	const useNumberId = options.advanced?.database?.useNumberId;
257 | 
258 | 	coreImports.push(`${databaseType}Table`);
259 | 	coreImports.push(
260 | 		databaseType === "mysql"
261 | 			? "varchar, text"
262 | 			: databaseType === "pg"
263 | 				? "text"
264 | 				: "text",
265 | 	);
266 | 	coreImports.push(
267 | 		hasBigint ? (databaseType !== "sqlite" ? "bigint" : "") : "",
268 | 	);
269 | 	coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
270 | 	if (databaseType === "mysql") {
271 | 		// Only include int for MySQL if actually needed
272 | 		const hasNonBigintNumber = Object.values(tables).some((table) =>
273 | 			Object.values(table.fields).some(
274 | 				(field) =>
275 | 					(field.type === "number" || field.type === "number[]") &&
276 | 					!field.bigint,
277 | 			),
278 | 		);
279 | 		const needsInt = !!useNumberId || hasNonBigintNumber;
280 | 		if (needsInt) {
281 | 			coreImports.push("int");
282 | 		}
283 | 		const hasEnum = Object.values(tables).some((table) =>
284 | 			Object.values(table.fields).some(
285 | 				(field) =>
286 | 					typeof field.type !== "string" &&
287 | 					Array.isArray(field.type) &&
288 | 					field.type.every((x) => typeof x === "string"),
289 | 			),
290 | 		);
291 | 		if (hasEnum) {
292 | 			coreImports.push("mysqlEnum");
293 | 		}
294 | 	} else if (databaseType === "pg") {
295 | 		// Only include integer for PG if actually needed
296 | 		const hasNonBigintNumber = Object.values(tables).some((table) =>
297 | 			Object.values(table.fields).some(
298 | 				(field) =>
299 | 					(field.type === "number" || field.type === "number[]") &&
300 | 					!field.bigint,
301 | 			),
302 | 		);
303 | 		const hasFkToId = Object.values(tables).some((table) =>
304 | 			Object.values(table.fields).some(
305 | 				(field) => field.references?.field === "id",
306 | 			),
307 | 		);
308 | 		// handles the references field with useNumberId
309 | 		const needsInteger =
310 | 			hasNonBigintNumber ||
311 | 			(options.advanced?.database?.useNumberId && hasFkToId);
312 | 		if (needsInteger) {
313 | 			coreImports.push("integer");
314 | 		}
315 | 	} else {
316 | 		coreImports.push("integer");
317 | 	}
318 | 	coreImports.push(useNumberId ? (databaseType === "pg" ? "serial" : "") : "");
319 | 
320 | 	//handle json last on the import order
321 | 	if (hasJson) {
322 | 		if (databaseType === "pg") coreImports.push("jsonb");
323 | 		if (databaseType === "mysql") coreImports.push("json");
324 | 		// sqlite uses text for JSON, so there's no need to handle this case
325 | 	}
326 | 
327 | 	// Add sql import for SQLite timestamps with defaultNow
328 | 	const hasSQLiteTimestamp =
329 | 		databaseType === "sqlite" &&
330 | 		Object.values(tables).some((table) =>
331 | 			Object.values(table.fields).some(
332 | 				(field) =>
333 | 					field.type === "date" &&
334 | 					field.defaultValue &&
335 | 					typeof field.defaultValue === "function" &&
336 | 					field.defaultValue.toString().includes("new Date()"),
337 | 			),
338 | 		);
339 | 
340 | 	if (hasSQLiteTimestamp) {
341 | 		rootImports.push("sql");
342 | 	}
343 | 
344 | 	return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports
345 | 		.map((x) => x.trim())
346 | 		.filter((x) => x !== "")
347 | 		.join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
348 | }
349 | 
350 | function getModelName(
351 | 	modelName: string,
352 | 	options: Record<string, any> | undefined,
353 | ) {
354 | 	return options?.usePlural ? `${modelName}s` : modelName;
355 | }
356 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/adapters/mongodb-adapter/mongodb-adapter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ClientSession, ObjectId, type Db, type MongoClient } from "mongodb";
  2 | import type { BetterAuthOptions } from "@better-auth/core";
  3 | import {
  4 | 	createAdapterFactory,
  5 | 	type AdapterFactoryOptions,
  6 | 	type AdapterFactoryCustomizeAdapterCreator,
  7 | } from "../adapter-factory";
  8 | import type {
  9 | 	DBAdapterDebugLogOption,
 10 | 	DBAdapter,
 11 | 	Where,
 12 | } from "@better-auth/core/db/adapter";
 13 | 
 14 | export interface MongoDBAdapterConfig {
 15 | 	/**
 16 | 	 * MongoDB client instance
 17 | 	 * If not provided, Database transactions won't be enabled.
 18 | 	 */
 19 | 	client?: MongoClient;
 20 | 	/**
 21 | 	 * Enable debug logs for the adapter
 22 | 	 *
 23 | 	 * @default false
 24 | 	 */
 25 | 	debugLogs?: DBAdapterDebugLogOption;
 26 | 	/**
 27 | 	 * Use plural table names
 28 | 	 *
 29 | 	 * @default false
 30 | 	 */
 31 | 	usePlural?: boolean;
 32 | 	/**
 33 | 	 * Whether to execute multiple operations in a transaction.
 34 | 	 *
 35 | 	 * If the database doesn't support transactions,
 36 | 	 * set this to `false` and operations will be executed sequentially.
 37 | 	 * @default false
 38 | 	 */
 39 | 	transaction?: boolean;
 40 | }
 41 | 
 42 | export const mongodbAdapter = (db: Db, config?: MongoDBAdapterConfig) => {
 43 | 	let lazyOptions: BetterAuthOptions | null;
 44 | 
 45 | 	const createCustomAdapter =
 46 | 		(db: Db, session?: ClientSession): AdapterFactoryCustomizeAdapterCreator =>
 47 | 		({ options, getFieldName, schema, getDefaultModelName }) => {
 48 | 			function serializeID({
 49 | 				field,
 50 | 				value,
 51 | 				model,
 52 | 			}: {
 53 | 				field: string;
 54 | 				value: any;
 55 | 				model: string;
 56 | 			}) {
 57 | 				model = getDefaultModelName(model);
 58 | 				if (
 59 | 					field === "id" ||
 60 | 					field === "_id" ||
 61 | 					schema[model]!.fields[field]?.references?.field === "id"
 62 | 				) {
 63 | 					if (typeof value !== "string") {
 64 | 						if (value instanceof ObjectId) {
 65 | 							return value;
 66 | 						}
 67 | 						if (Array.isArray(value)) {
 68 | 							return value.map((v) => {
 69 | 								if (typeof v === "string") {
 70 | 									try {
 71 | 										return new ObjectId(v);
 72 | 									} catch (e) {
 73 | 										return v;
 74 | 									}
 75 | 								}
 76 | 								if (v instanceof ObjectId) {
 77 | 									return v;
 78 | 								}
 79 | 								throw new Error("Invalid id value");
 80 | 							});
 81 | 						}
 82 | 						throw new Error("Invalid id value");
 83 | 					}
 84 | 					try {
 85 | 						return new ObjectId(value);
 86 | 					} catch (e) {
 87 | 						return value;
 88 | 					}
 89 | 				}
 90 | 				return value;
 91 | 			}
 92 | 
 93 | 			function convertWhereClause({
 94 | 				where,
 95 | 				model,
 96 | 			}: {
 97 | 				where: Where[];
 98 | 				model: string;
 99 | 			}) {
100 | 				if (!where.length) return {};
101 | 				const conditions = where.map((w) => {
102 | 					const {
103 | 						field: field_,
104 | 						value,
105 | 						operator = "eq",
106 | 						connector = "AND",
107 | 					} = w;
108 | 					let condition: any;
109 | 					let field = getFieldName({ model, field: field_ });
110 | 					if (field === "id") field = "_id";
111 | 					switch (operator.toLowerCase()) {
112 | 						case "eq":
113 | 							condition = {
114 | 								[field]: serializeID({
115 | 									field,
116 | 									value,
117 | 									model,
118 | 								}),
119 | 							};
120 | 							break;
121 | 						case "in":
122 | 							condition = {
123 | 								[field]: {
124 | 									$in: Array.isArray(value)
125 | 										? value.map((v) => serializeID({ field, value: v, model }))
126 | 										: [serializeID({ field, value, model })],
127 | 								},
128 | 							};
129 | 							break;
130 | 						case "not_in":
131 | 							condition = {
132 | 								[field]: {
133 | 									$nin: Array.isArray(value)
134 | 										? value.map((v) => serializeID({ field, value: v, model }))
135 | 										: [serializeID({ field, value, model })],
136 | 								},
137 | 							};
138 | 							break;
139 | 						case "gt":
140 | 							condition = { [field]: { $gt: value } };
141 | 							break;
142 | 						case "gte":
143 | 							condition = { [field]: { $gte: value } };
144 | 							break;
145 | 						case "lt":
146 | 							condition = { [field]: { $lt: value } };
147 | 							break;
148 | 						case "lte":
149 | 							condition = { [field]: { $lte: value } };
150 | 							break;
151 | 						case "ne":
152 | 							condition = { [field]: { $ne: value } };
153 | 							break;
154 | 						case "contains":
155 | 							condition = {
156 | 								[field]: {
157 | 									$regex: `.*${escapeForMongoRegex(value as string)}.*`,
158 | 								},
159 | 							};
160 | 							break;
161 | 						case "starts_with":
162 | 							condition = {
163 | 								[field]: { $regex: `^${escapeForMongoRegex(value as string)}` },
164 | 							};
165 | 							break;
166 | 						case "ends_with":
167 | 							condition = {
168 | 								[field]: { $regex: `${escapeForMongoRegex(value as string)}$` },
169 | 							};
170 | 							break;
171 | 						default:
172 | 							throw new Error(`Unsupported operator: ${operator}`);
173 | 					}
174 | 					return { condition, connector };
175 | 				});
176 | 				if (conditions.length === 1) {
177 | 					return conditions[0]!.condition;
178 | 				}
179 | 				const andConditions = conditions
180 | 					.filter((c) => c.connector === "AND")
181 | 					.map((c) => c.condition);
182 | 				const orConditions = conditions
183 | 					.filter((c) => c.connector === "OR")
184 | 					.map((c) => c.condition);
185 | 
186 | 				let clause = {};
187 | 				if (andConditions.length) {
188 | 					clause = { ...clause, $and: andConditions };
189 | 				}
190 | 				if (orConditions.length) {
191 | 					clause = { ...clause, $or: orConditions };
192 | 				}
193 | 				return clause;
194 | 			}
195 | 
196 | 			return {
197 | 				async create({ model, data: values }) {
198 | 					const res = await db.collection(model).insertOne(values, { session });
199 | 					const insertedData = { _id: res.insertedId.toString(), ...values };
200 | 					return insertedData as any;
201 | 				},
202 | 				async findOne({ model, where, select }) {
203 | 					const clause = convertWhereClause({ where, model });
204 | 					const projection = select
205 | 						? Object.fromEntries(
206 | 								select.map((field) => [getFieldName({ field, model }), 1]),
207 | 							)
208 | 						: undefined;
209 | 					const res = await db
210 | 						.collection(model)
211 | 						.findOne(clause, { session, projection });
212 | 					if (!res) return null;
213 | 					return res as any;
214 | 				},
215 | 				async findMany({ model, where, limit, offset, sortBy }) {
216 | 					const clause = where ? convertWhereClause({ where, model }) : {};
217 | 					const cursor = db.collection(model).find(clause, { session });
218 | 					if (limit) cursor.limit(limit);
219 | 					if (offset) cursor.skip(offset);
220 | 					if (sortBy)
221 | 						cursor.sort(
222 | 							getFieldName({ field: sortBy.field, model }),
223 | 							sortBy.direction === "desc" ? -1 : 1,
224 | 						);
225 | 					const res = await cursor.toArray();
226 | 					return res as any;
227 | 				},
228 | 				async count({ model, where }) {
229 | 					const clause = where ? convertWhereClause({ where, model }) : {};
230 | 					const res = await db
231 | 						.collection(model)
232 | 						.countDocuments(clause, { session });
233 | 					return res;
234 | 				},
235 | 				async update({ model, where, update: values }) {
236 | 					const clause = convertWhereClause({ where, model });
237 | 
238 | 					const res = await db.collection(model).findOneAndUpdate(
239 | 						clause,
240 | 						{ $set: values as any },
241 | 						{
242 | 							session,
243 | 							returnDocument: "after",
244 | 						},
245 | 					);
246 | 					if (!res) return null;
247 | 					return res as any;
248 | 				},
249 | 				async updateMany({ model, where, update: values }) {
250 | 					const clause = convertWhereClause({ where, model });
251 | 
252 | 					const res = await db.collection(model).updateMany(
253 | 						clause,
254 | 						{
255 | 							$set: values as any,
256 | 						},
257 | 						{ session },
258 | 					);
259 | 					return res.modifiedCount;
260 | 				},
261 | 				async delete({ model, where }) {
262 | 					const clause = convertWhereClause({ where, model });
263 | 					await db.collection(model).deleteOne(clause, { session });
264 | 				},
265 | 				async deleteMany({ model, where }) {
266 | 					const clause = convertWhereClause({ where, model });
267 | 					const res = await db
268 | 						.collection(model)
269 | 						.deleteMany(clause, { session });
270 | 					return res.deletedCount;
271 | 				},
272 | 			};
273 | 		};
274 | 
275 | 	let lazyAdapter:
276 | 		| ((options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>)
277 | 		| null = null;
278 | 	let adapterOptions: AdapterFactoryOptions | null = null;
279 | 	adapterOptions = {
280 | 		config: {
281 | 			adapterId: "mongodb-adapter",
282 | 			adapterName: "MongoDB Adapter",
283 | 			usePlural: config?.usePlural ?? false,
284 | 			debugLogs: config?.debugLogs ?? false,
285 | 			mapKeysTransformInput: {
286 | 				id: "_id",
287 | 			},
288 | 			mapKeysTransformOutput: {
289 | 				_id: "id",
290 | 			},
291 | 			supportsNumericIds: false,
292 | 			transaction:
293 | 				config?.client && (config?.transaction ?? true)
294 | 					? async (cb) => {
295 | 							if (!config.client) {
296 | 								return cb(lazyAdapter!(lazyOptions!));
297 | 							}
298 | 
299 | 							const session = config.client.startSession();
300 | 
301 | 							try {
302 | 								session.startTransaction();
303 | 
304 | 								const adapter = createAdapterFactory({
305 | 									config: adapterOptions!.config,
306 | 									adapter: createCustomAdapter(db, session),
307 | 								})(lazyOptions!);
308 | 
309 | 								const result = await cb(adapter);
310 | 
311 | 								await session.commitTransaction();
312 | 								return result;
313 | 							} catch (err) {
314 | 								await session.abortTransaction();
315 | 								throw err;
316 | 							} finally {
317 | 								await session.endSession();
318 | 							}
319 | 						}
320 | 					: false,
321 | 			customTransformInput({
322 | 				action,
323 | 				data,
324 | 				field,
325 | 				fieldAttributes,
326 | 				schema,
327 | 				model,
328 | 				options,
329 | 			}) {
330 | 				if (field === "_id" || fieldAttributes.references?.field === "id") {
331 | 					if (action === "update") {
332 | 						if (typeof data === "string") {
333 | 							try {
334 | 								return new ObjectId(data);
335 | 							} catch (error) {
336 | 								return data;
337 | 							}
338 | 						}
339 | 						return data;
340 | 					}
341 | 					if (Array.isArray(data)) {
342 | 						return data.map((v) => {
343 | 							if (typeof v === "string") {
344 | 								try {
345 | 									return new ObjectId(v);
346 | 								} catch (error) {
347 | 									return v;
348 | 								}
349 | 							}
350 | 							return v;
351 | 						});
352 | 					}
353 | 					if (typeof data === "string") {
354 | 						try {
355 | 							return new ObjectId(data);
356 | 						} catch (error) {
357 | 							return new ObjectId();
358 | 						}
359 | 					}
360 | 					if (data === null && fieldAttributes.references?.field === "id") {
361 | 						return null;
362 | 					}
363 | 					return new ObjectId();
364 | 				}
365 | 				return data;
366 | 			},
367 | 			customTransformOutput({ data, field, fieldAttributes }) {
368 | 				if (field === "id" || fieldAttributes.references?.field === "id") {
369 | 					if (data instanceof ObjectId) {
370 | 						return data.toHexString();
371 | 					}
372 | 					if (Array.isArray(data)) {
373 | 						return data.map((v) => {
374 | 							if (v instanceof ObjectId) {
375 | 								return v.toHexString();
376 | 							}
377 | 							return v;
378 | 						});
379 | 					}
380 | 					return data;
381 | 				}
382 | 				return data;
383 | 			},
384 | 			customIdGenerator(props) {
385 | 				return new ObjectId().toString();
386 | 			},
387 | 		},
388 | 		adapter: createCustomAdapter(db),
389 | 	};
390 | 	lazyAdapter = createAdapterFactory(adapterOptions);
391 | 
392 | 	return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => {
393 | 		lazyOptions = options;
394 | 		return lazyAdapter(options);
395 | 	};
396 | };
397 | 
398 | /**
399 |  * Safely escape user input for use in a MongoDB regex.
400 |  * This ensures the resulting pattern is treated as literal text,
401 |  * and not as a regex with special syntax.
402 |  *
403 |  * @param input - The input string to escape. Any type that isn't a string will be converted to an empty string.
404 |  * @param maxLength - The maximum length of the input string to escape. Defaults to 256. This is to prevent DOS attacks.
405 |  * @returns The escaped string.
406 |  */
407 | function escapeForMongoRegex(input: string, maxLength = 256): string {
408 | 	if (typeof input !== "string") return "";
409 | 
410 | 	// Escape all PCRE special characters
411 | 	// Source: PCRE docs — https://www.pcre.org/original/doc/html/pcrepattern.html
412 | 	return input.slice(0, maxLength).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
413 | }
414 | 
```

--------------------------------------------------------------------------------
/e2e/smoke/test/fixtures/tsconfig-declaration/src/demo.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { betterAuth } from "better-auth";
  2 | import {
  3 | 	bearer,
  4 | 	admin,
  5 | 	multiSession,
  6 | 	organization,
  7 | 	twoFactor,
  8 | 	oneTap,
  9 | 	oAuthProxy,
 10 | 	openAPI,
 11 | 	customSession,
 12 | 	deviceAuthorization,
 13 | 	lastLoginMethod,
 14 | } from "better-auth/plugins";
 15 | import { nextCookies } from "better-auth/next-js";
 16 | import { passkey } from "better-auth/plugins/passkey";
 17 | import { stripe } from "@better-auth/stripe";
 18 | import { sso } from "@better-auth/sso";
 19 | import { Stripe } from "stripe";
 20 | 
 21 | export const auth = betterAuth({
 22 | 	appName: "Better Auth Demo",
 23 | 	plugins: [
 24 | 		organization({}),
 25 | 		twoFactor({}),
 26 | 		passkey(),
 27 | 		openAPI(),
 28 | 		bearer(),
 29 | 		admin({ adminUserIds: ["EXD5zjob2SD6CBWcEQ6OpLRHcyoUbnaB"] }),
 30 | 		multiSession(),
 31 | 		oAuthProxy(),
 32 | 		nextCookies(),
 33 | 		oneTap(),
 34 | 		customSession(async (session) => {
 35 | 			return {
 36 | 				...session,
 37 | 				user: {
 38 | 					...session.user,
 39 | 					dd: "test",
 40 | 				},
 41 | 			};
 42 | 		}),
 43 | 		stripe({
 44 | 			stripeClient: new Stripe(process.env.STRIPE_KEY || "sk_test_"),
 45 | 			stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
 46 | 			subscription: {
 47 | 				enabled: true,
 48 | 				allowReTrialsForDifferentPlans: true,
 49 | 				plans: () => {
 50 | 					const PRO_PRICE_ID = {
 51 | 						default:
 52 | 							process.env.STRIPE_PRO_PRICE_ID ??
 53 | 							"price_1RoxnRHmTADgihIt4y8c0lVE",
 54 | 						annual:
 55 | 							process.env.STRIPE_PRO_ANNUAL_PRICE_ID ??
 56 | 							"price_1RoxnoHmTADgihItzFvVP8KT",
 57 | 					};
 58 | 					const PLUS_PRICE_ID = {
 59 | 						default:
 60 | 							process.env.STRIPE_PLUS_PRICE_ID ??
 61 | 							"price_1RoxnJHmTADgihIthZTLmrPn",
 62 | 						annual:
 63 | 							process.env.STRIPE_PLUS_ANNUAL_PRICE_ID ??
 64 | 							"price_1Roxo5HmTADgihItEbJu5llL",
 65 | 					};
 66 | 
 67 | 					return [
 68 | 						{
 69 | 							name: "Plus",
 70 | 							priceId: PLUS_PRICE_ID.default,
 71 | 							annualDiscountPriceId: PLUS_PRICE_ID.annual,
 72 | 							freeTrial: {
 73 | 								days: 7,
 74 | 							},
 75 | 						},
 76 | 						{
 77 | 							name: "Pro",
 78 | 							priceId: PRO_PRICE_ID.default,
 79 | 							annualDiscountPriceId: PRO_PRICE_ID.annual,
 80 | 							freeTrial: {
 81 | 								days: 7,
 82 | 							},
 83 | 						},
 84 | 					];
 85 | 				},
 86 | 			},
 87 | 		}),
 88 | 		sso({
 89 | 			defaultSSO: [
 90 | 				{
 91 | 					domain: "http://localhost:3000",
 92 | 					providerId: "sso",
 93 | 					samlConfig: {
 94 | 						issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata",
 95 | 						entryPoint:
 96 | 							"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435",
 97 | 						cert: `-----BEGIN CERTIFICATE-----
 98 | 	  MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL
 99 | 	  BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx
100 | 	  MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD
101 | 	  ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ
102 | 	  HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj
103 | 	  yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g
104 | 	  0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7
105 | 	  mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO
106 | 	  aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE
107 | 	  FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm
108 | 	  RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z
109 | 	  FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm
110 | 	  2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx
111 | 	  UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J
112 | 	  QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9
113 | 	  vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+
114 | 	  jzGhYL6m9gFTm/8=
115 | 	  -----END CERTIFICATE-----`,
116 | 						spMetadata: {
117 | 							metadata: `
118 | 				  <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/api/auth/sso/saml2/sp/metadata">
119 | 		  <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
120 | 			  <md:KeyDescriptor use="signing">
121 | 			  <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
122 | 				  <ds:X509Data>
123 | 				  <ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
124 | 				  </ds:X509Data>
125 | 			  </ds:KeyInfo>
126 | 			  </md:KeyDescriptor>
127 | 			  <md:KeyDescriptor use="encryption">
128 | 			  <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
129 | 				  <ds:X509Data>
130 | 				  <ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
131 | 				  </ds:X509Data>
132 | 			  </ds:KeyInfo>
133 | 			  </md:KeyDescriptor>
134 | 			  <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/sls"/>
135 | 			  <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
136 | 			  <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/>
137 | 			  <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/>
138 | 			  </md:SPSSODescriptor>
139 | 		  <md:Organization>
140 | 			  <md:OrganizationName xml:lang="en-US">Organization Name</md:OrganizationName>
141 | 			  <md:OrganizationDisplayName xml:lang="en-US">Organization DisplayName</md:OrganizationDisplayName>
142 | 			  <md:OrganizationURL xml:lang="en-US">http://localhost:3000/</md:OrganizationURL>
143 | 		  </md:Organization>
144 | 		  <md:ContactPerson contactType="technical">
145 | 			  <md:GivenName>Technical Contact Name</md:GivenName>
146 | 			  <md:EmailAddress>[email protected]</md:EmailAddress>
147 | 		  </md:ContactPerson>
148 | 		  <md:ContactPerson contactType="support">
149 | 			  <md:GivenName>Support Contact Name</md:GivenName>
150 | 			  <md:EmailAddress>[email protected]</md:EmailAddress>
151 | 		  </md:ContactPerson>
152 | 		  </md:EntityDescriptor>
153 | 		  `,
154 | 						},
155 | 						idpMetadata: {
156 | 							entityURL:
157 | 								"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/metadata",
158 | 							entityID:
159 | 								"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435",
160 | 							redirectURL:
161 | 								"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso",
162 | 							singleSignOnService: [
163 | 								{
164 | 									Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
165 | 									Location:
166 | 										"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso",
167 | 								},
168 | 							],
169 | 							cert: `-----BEGIN CERTIFICATE-----
170 | 		MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL
171 | 		BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx
172 | 		MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD
173 | 		ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ
174 | 		HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj
175 | 		yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g
176 | 		0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7
177 | 		mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO
178 | 		aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE
179 | 		FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm
180 | 		RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z
181 | 		FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm
182 | 		2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx
183 | 		UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J
184 | 		QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9
185 | 		vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+
186 | 		jzGhYL6m9gFTm/8=
187 | 		-----END CERTIFICATE-----`,
188 | 						},
189 | 						callbackUrl: "/dashboard",
190 | 					},
191 | 				},
192 | 			],
193 | 		}),
194 | 		deviceAuthorization({
195 | 			expiresIn: "3min",
196 | 			interval: "5s",
197 | 		}),
198 | 		lastLoginMethod(),
199 | 	],
200 | });
201 | 
202 | auth.api
203 | 	.createOrganization({
204 | 		body: {
205 | 			name: "My Org",
206 | 			slug: "my-org",
207 | 		},
208 | 	})
209 | 	.catch();
210 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/cookies/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { CookieOptions } from "better-call";
  2 | import { BetterAuthError } from "@better-auth/core/error";
  3 | import type { Session, User } from "../types";
  4 | import type { BetterAuthOptions } from "@better-auth/core";
  5 | import { getDate } from "../utils/date";
  6 | import { env, isProduction } from "@better-auth/core/env";
  7 | import { base64Url } from "@better-auth/utils/base64";
  8 | import { ms } from "ms";
  9 | import { createHMAC } from "@better-auth/utils/hmac";
 10 | import { safeJSONParse } from "../utils/json";
 11 | import { getBaseURL } from "../utils/url";
 12 | import { binary } from "@better-auth/utils/binary";
 13 | import type {
 14 | 	BetterAuthCookies,
 15 | 	GenericEndpointContext,
 16 | } from "@better-auth/core";
 17 | import { parseUserOutput } from "../db/schema";
 18 | import { symmetricEncodeJWT, symmetricDecodeJWT } from "../crypto/jwt";
 19 | 
 20 | export function createCookieGetter(options: BetterAuthOptions) {
 21 | 	const secure =
 22 | 		options.advanced?.useSecureCookies !== undefined
 23 | 			? options.advanced?.useSecureCookies
 24 | 			: options.baseURL !== undefined
 25 | 				? options.baseURL.startsWith("https://")
 26 | 					? true
 27 | 					: false
 28 | 				: isProduction;
 29 | 	const secureCookiePrefix = secure ? "__Secure-" : "";
 30 | 	const crossSubdomainEnabled =
 31 | 		!!options.advanced?.crossSubDomainCookies?.enabled;
 32 | 	const domain = crossSubdomainEnabled
 33 | 		? options.advanced?.crossSubDomainCookies?.domain ||
 34 | 			(options.baseURL ? new URL(options.baseURL).hostname : undefined)
 35 | 		: undefined;
 36 | 	if (crossSubdomainEnabled && !domain) {
 37 | 		throw new BetterAuthError(
 38 | 			"baseURL is required when crossSubdomainCookies are enabled",
 39 | 		);
 40 | 	}
 41 | 	function createCookie(
 42 | 		cookieName: string,
 43 | 		overrideAttributes: Partial<CookieOptions> = {},
 44 | 	) {
 45 | 		const prefix = options.advanced?.cookiePrefix || "better-auth";
 46 | 		const name =
 47 | 			options.advanced?.cookies?.[cookieName as "session_token"]?.name ||
 48 | 			`${prefix}.${cookieName}`;
 49 | 
 50 | 		const attributes =
 51 | 			options.advanced?.cookies?.[cookieName as "session_token"]?.attributes;
 52 | 
 53 | 		return {
 54 | 			name: `${secureCookiePrefix}${name}`,
 55 | 			attributes: {
 56 | 				secure: !!secureCookiePrefix,
 57 | 				sameSite: "lax",
 58 | 				path: "/",
 59 | 				httpOnly: true,
 60 | 				...(crossSubdomainEnabled ? { domain } : {}),
 61 | 				...options.advanced?.defaultCookieAttributes,
 62 | 				...overrideAttributes,
 63 | 				...attributes,
 64 | 			} as CookieOptions,
 65 | 		};
 66 | 	}
 67 | 	return createCookie;
 68 | }
 69 | 
 70 | export function getCookies(options: BetterAuthOptions) {
 71 | 	const createCookie = createCookieGetter(options);
 72 | 	const sessionMaxAge = options.session?.expiresIn || ms("7d") / 1000;
 73 | 	const sessionToken = createCookie("session_token", {
 74 | 		maxAge: sessionMaxAge,
 75 | 	});
 76 | 	const sessionData = createCookie("session_data", {
 77 | 		maxAge: options.session?.cookieCache?.maxAge || 60 * 5,
 78 | 	});
 79 | 	const dontRememberToken = createCookie("dont_remember");
 80 | 	return {
 81 | 		sessionToken: {
 82 | 			name: sessionToken.name,
 83 | 			options: sessionToken.attributes,
 84 | 		},
 85 | 		/**
 86 | 		 * This cookie is used to store the session data in the cookie
 87 | 		 * This is useful for when you want to cache the session in the cookie
 88 | 		 */
 89 | 		sessionData: {
 90 | 			name: sessionData.name,
 91 | 			options: sessionData.attributes,
 92 | 		},
 93 | 		dontRememberToken: {
 94 | 			name: dontRememberToken.name,
 95 | 			options: dontRememberToken.attributes,
 96 | 		},
 97 | 	};
 98 | }
 99 | 
100 | export async function setCookieCache(
101 | 	ctx: GenericEndpointContext,
102 | 	session: {
103 | 		session: Session & Record<string, any>;
104 | 		user: User;
105 | 	},
106 | 	dontRememberMe: boolean,
107 | ) {
108 | 	const shouldStoreSessionDataInCookie =
109 | 		ctx.context.options.session?.cookieCache?.enabled;
110 | 
111 | 	if (shouldStoreSessionDataInCookie) {
112 | 		const filteredSession = Object.entries(session.session).reduce(
113 | 			(acc, [key, value]) => {
114 | 				const fieldConfig =
115 | 					ctx.context.options.session?.additionalFields?.[key];
116 | 				if (!fieldConfig || fieldConfig.returned !== false) {
117 | 					acc[key] = value;
118 | 				}
119 | 				return acc;
120 | 			},
121 | 			{} as Record<string, any>,
122 | 		);
123 | 
124 | 		// Apply field filtering to user data
125 | 		const filteredUser = parseUserOutput(ctx.context.options, session.user);
126 | 		const sessionData = {
127 | 			session: filteredSession,
128 | 			user: filteredUser,
129 | 			updatedAt: Date.now(),
130 | 		};
131 | 
132 | 		const options = {
133 | 			...ctx.context.authCookies.sessionData.options,
134 | 			maxAge: dontRememberMe
135 | 				? undefined
136 | 				: ctx.context.authCookies.sessionData.options.maxAge,
137 | 		};
138 | 
139 | 		const expiresAtDate = getDate(options.maxAge || 60, "sec").getTime();
140 | 		const strategy =
141 | 			ctx.context.options.session?.cookieCache?.strategy || "base64-hmac";
142 | 
143 | 		let data: string;
144 | 
145 | 		if (strategy === "jwt") {
146 | 			// Use JWT strategy with JWE (A256CBC-HS512 + HKDF)
147 | 			data = await symmetricEncodeJWT(
148 | 				sessionData,
149 | 				ctx.context.secret,
150 | 				"better-auth-session",
151 | 				options.maxAge || 60 * 5,
152 | 			);
153 | 		} else {
154 | 			// Use base64-hmac strategy (legacy)
155 | 			data = base64Url.encode(
156 | 				JSON.stringify({
157 | 					session: sessionData,
158 | 					expiresAt: expiresAtDate,
159 | 					signature: await createHMAC("SHA-256", "base64urlnopad").sign(
160 | 						ctx.context.secret,
161 | 						JSON.stringify({
162 | 							...sessionData,
163 | 							expiresAt: expiresAtDate,
164 | 						}),
165 | 					),
166 | 				}),
167 | 				{
168 | 					padding: false,
169 | 				},
170 | 			);
171 | 		}
172 | 
173 | 		if (data.length > 4093) {
174 | 			ctx.context?.logger?.error(
175 | 				`Session data exceeds cookie size limit (${data.length} bytes > 4093 bytes). Consider reducing session data size or disabling cookie cache. Session will not be cached in cookie.`,
176 | 			);
177 | 			return;
178 | 		}
179 | 		ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options);
180 | 	}
181 | }
182 | 
183 | export async function setSessionCookie(
184 | 	ctx: GenericEndpointContext,
185 | 	session: {
186 | 		session: Session & Record<string, any>;
187 | 		user: User;
188 | 	},
189 | 	dontRememberMe?: boolean,
190 | 	overrides?: Partial<CookieOptions>,
191 | ) {
192 | 	const dontRememberMeCookie = await ctx.getSignedCookie(
193 | 		ctx.context.authCookies.dontRememberToken.name,
194 | 		ctx.context.secret,
195 | 	);
196 | 	// if dontRememberMe is not set, use the cookie value
197 | 	dontRememberMe =
198 | 		dontRememberMe !== undefined ? dontRememberMe : !!dontRememberMeCookie;
199 | 
200 | 	const options = ctx.context.authCookies.sessionToken.options;
201 | 	const maxAge = dontRememberMe
202 | 		? undefined
203 | 		: ctx.context.sessionConfig.expiresIn;
204 | 	await ctx.setSignedCookie(
205 | 		ctx.context.authCookies.sessionToken.name,
206 | 		session.session.token,
207 | 		ctx.context.secret,
208 | 		{
209 | 			...options,
210 | 			maxAge,
211 | 			...overrides,
212 | 		},
213 | 	);
214 | 
215 | 	if (dontRememberMe) {
216 | 		await ctx.setSignedCookie(
217 | 			ctx.context.authCookies.dontRememberToken.name,
218 | 			"true",
219 | 			ctx.context.secret,
220 | 			ctx.context.authCookies.dontRememberToken.options,
221 | 		);
222 | 	}
223 | 	await setCookieCache(ctx, session, dontRememberMe);
224 | 	ctx.context.setNewSession(session);
225 | 	/**
226 | 	 * If secondary storage is enabled, store the session data in the secondary storage
227 | 	 * This is useful if the session got updated and we want to update the session data in the
228 | 	 * secondary storage
229 | 	 */
230 | 	if (ctx.context.options.secondaryStorage) {
231 | 		await ctx.context.secondaryStorage?.set(
232 | 			session.session.token,
233 | 			JSON.stringify({
234 | 				user: session.user,
235 | 				session: session.session,
236 | 			}),
237 | 			Math.floor(
238 | 				(new Date(session.session.expiresAt).getTime() - Date.now()) / 1000,
239 | 			),
240 | 		);
241 | 	}
242 | }
243 | 
244 | export function deleteSessionCookie(
245 | 	ctx: GenericEndpointContext,
246 | 	skipDontRememberMe?: boolean,
247 | ) {
248 | 	ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
249 | 		...ctx.context.authCookies.sessionToken.options,
250 | 		maxAge: 0,
251 | 	});
252 | 	ctx.setCookie(ctx.context.authCookies.sessionData.name, "", {
253 | 		...ctx.context.authCookies.sessionData.options,
254 | 		maxAge: 0,
255 | 	});
256 | 	if (!skipDontRememberMe) {
257 | 		ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", {
258 | 			...ctx.context.authCookies.dontRememberToken.options,
259 | 			maxAge: 0,
260 | 		});
261 | 	}
262 | }
263 | 
264 | export function parseCookies(cookieHeader: string) {
265 | 	const cookies = cookieHeader.split("; ");
266 | 	const cookieMap = new Map<string, string>();
267 | 
268 | 	cookies.forEach((cookie) => {
269 | 		const [name, value] = cookie.split("=");
270 | 		cookieMap.set(name!, value!);
271 | 	});
272 | 	return cookieMap;
273 | }
274 | 
275 | export type EligibleCookies = (string & {}) | (keyof BetterAuthCookies & {});
276 | 
277 | export const getSessionCookie = (
278 | 	request: Request | Headers,
279 | 	config?: {
280 | 		cookiePrefix?: string;
281 | 		cookieName?: string;
282 | 		path?: string;
283 | 	},
284 | ) => {
285 | 	if (config?.cookiePrefix) {
286 | 		if (config.cookieName) {
287 | 			config.cookiePrefix = `${config.cookiePrefix}-`;
288 | 		} else {
289 | 			config.cookiePrefix = `${config.cookiePrefix}.`;
290 | 		}
291 | 	}
292 | 	const headers = "headers" in request ? request.headers : request;
293 | 	const req = request instanceof Request ? request : undefined;
294 | 	const url = getBaseURL(req?.url, config?.path, req);
295 | 	const cookies = headers.get("cookie");
296 | 	if (!cookies) {
297 | 		return null;
298 | 	}
299 | 	const { cookieName = "session_token", cookiePrefix = "better-auth." } =
300 | 		config || {};
301 | 	const name = `${cookiePrefix}${cookieName}`;
302 | 	const secureCookieName = `__Secure-${name}`;
303 | 	const parsedCookie = parseCookies(cookies);
304 | 	const sessionToken =
305 | 		parsedCookie.get(name) || parsedCookie.get(secureCookieName);
306 | 	if (sessionToken) {
307 | 		return sessionToken;
308 | 	}
309 | 
310 | 	return null;
311 | };
312 | 
313 | export const getCookieCache = async <
314 | 	S extends {
315 | 		session: Session & Record<string, any>;
316 | 		user: User & Record<string, any>;
317 | 		updatedAt: number;
318 | 	},
319 | >(
320 | 	request: Request | Headers,
321 | 	config?: {
322 | 		cookiePrefix?: string;
323 | 		cookieName?: string;
324 | 		isSecure?: boolean;
325 | 		secret?: string;
326 | 		strategy?: "base64-hmac" | "jwt";
327 | 	},
328 | ) => {
329 | 	const headers = request instanceof Headers ? request : request.headers;
330 | 	const cookies = headers.get("cookie");
331 | 	if (!cookies) {
332 | 		return null;
333 | 	}
334 | 	const { cookieName = "session_data", cookiePrefix = "better-auth" } =
335 | 		config || {};
336 | 	const name =
337 | 		config?.isSecure !== undefined
338 | 			? config.isSecure
339 | 				? `__Secure-${cookiePrefix}.${cookieName}`
340 | 				: `${cookiePrefix}.${cookieName}`
341 | 			: isProduction
342 | 				? `__Secure-${cookiePrefix}.${cookieName}`
343 | 				: `${cookiePrefix}.${cookieName}`;
344 | 	const parsedCookie = parseCookies(cookies);
345 | 	const sessionData = parsedCookie.get(name);
346 | 	if (sessionData) {
347 | 		const secret = config?.secret || env.BETTER_AUTH_SECRET;
348 | 		if (!secret) {
349 | 			throw new BetterAuthError(
350 | 				"getCookieCache requires a secret to be provided. Either pass it as an option or set the BETTER_AUTH_SECRET environment variable",
351 | 			);
352 | 		}
353 | 
354 | 		const strategy = config?.strategy || "base64-hmac";
355 | 
356 | 		if (strategy === "jwt") {
357 | 			// Use JWT strategy with JWE (A256CBC-HS512 + HKDF)
358 | 			const payload = await symmetricDecodeJWT<S>(
359 | 				sessionData,
360 | 				secret,
361 | 				"better-auth-session",
362 | 			);
363 | 
364 | 			if (payload && payload.session && payload.user) {
365 | 				return payload;
366 | 			}
367 | 			return null;
368 | 		} else {
369 | 			// Use base64-hmac strategy (legacy)
370 | 			const sessionDataPayload = safeJSONParse<{
371 | 				session: S;
372 | 				expiresAt: number;
373 | 				signature: string;
374 | 			}>(binary.decode(base64Url.decode(sessionData)));
375 | 			if (!sessionDataPayload) {
376 | 				return null;
377 | 			}
378 | 			const isValid = await createHMAC("SHA-256", "base64urlnopad").verify(
379 | 				secret,
380 | 				JSON.stringify({
381 | 					...sessionDataPayload.session,
382 | 					expiresAt: sessionDataPayload.expiresAt,
383 | 				}),
384 | 				sessionDataPayload.signature,
385 | 			);
386 | 			if (!isValid) {
387 | 				return null;
388 | 			}
389 | 			return sessionDataPayload.session;
390 | 		}
391 | 	}
392 | 	return null;
393 | };
394 | 
395 | export * from "./cookie-utils";
396 | 
```

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

```markdown
  1 | ---
  2 | title: Client
  3 | description: Better Auth client library for authentication.
  4 | ---
  5 | 
  6 | Better Auth offers a client library compatible with popular frontend frameworks like React, Vue, Svelte, and more. This client library includes a set of functions for interacting with the Better Auth server. Each framework's client library is built on top of a core client library that is framework-agnostic, so that all methods and hooks are consistently available across all client libraries.
  7 | 
  8 | ## Installation
  9 | 
 10 | If you haven't already, install better-auth.
 11 | 
 12 | ```package-install 
 13 | npm i better-auth
 14 | ```
 15 | 
 16 | ## Create Client Instance
 17 | 
 18 | Import `createAuthClient` from the package for your framework (e.g., "better-auth/react" for React). Call the function to create your client. Pass the base URL of your auth server. If the auth server is running on the same domain as your client, you can skip this step.
 19 | 
 20 | <Callout type="info">
 21 | If you're using a different base path other than `/api/auth`, make sure to pass the whole URL, including the path. (e.g., `http://localhost:3000/custom-path/auth`)
 22 | </Callout>
 23 | 
 24 | 
 25 | <Tabs items={["react", "vue", "svelte", "solid", 
 26 |   "vanilla"]} defaultValue="react">
 27 |     <Tab value="vanilla">
 28 |             ```ts  title="lib/auth-client.ts" 
 29 |             import { createAuthClient } from "better-auth/client"
 30 |             export const authClient = createAuthClient({
 31 |                 baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
 32 |             })
 33 |             ```
 34 |     </Tab>
 35 |     <Tab value="react" title="lib/auth-client.ts">
 36 |             ```ts  title="lib/auth-client.ts"  
 37 |             import { createAuthClient } from "better-auth/react"
 38 |             export const authClient = createAuthClient({
 39 |                 baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
 40 |             })
 41 |             ```
 42 |     </Tab>
 43 |     <Tab value="vue" title="lib/auth-client.ts">
 44 |             ```ts  title="lib/auth-client.ts" 
 45 |             import { createAuthClient } from "better-auth/vue"
 46 |             export const authClient = createAuthClient({
 47 |                 baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
 48 |             })
 49 |             ```
 50 |     </Tab>
 51 |     <Tab value="svelte" title="lib/auth-client.ts"> 
 52 |             ```ts  title="lib/auth-client.ts" 
 53 |             import { createAuthClient } from "better-auth/svelte"
 54 |             export const authClient = createAuthClient({
 55 |                 baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
 56 |             })
 57 |             ```
 58 |     </Tab>
 59 |     <Tab value="solid" title="lib/auth-client.ts">
 60 |             ```ts title="lib/auth-client.ts" 
 61 |             import { createAuthClient } from "better-auth/solid"
 62 |             export const authClient = createAuthClient({
 63 |                 baseURL: "http://localhost:3000" // The base URL of your auth server // [!code highlight]
 64 |             })
 65 |             ```
 66 |     </Tab>
 67 | </Tabs>
 68 | 
 69 | ## Usage
 70 | 
 71 | Once you've created your client instance, you can use the client to interact with the Better Auth server. The client provides a set of functions by default and they can be extended with plugins.
 72 | 
 73 | **Example: Sign In**
 74 | 
 75 | ```ts title="auth-client.ts"
 76 | import { createAuthClient } from "better-auth/client"
 77 | const authClient = createAuthClient()
 78 | 
 79 | await authClient.signIn.email({
 80 |     email: "[email protected]",
 81 |     password: "password1234"
 82 | })
 83 | ```
 84 | 
 85 | ### Hooks
 86 | 
 87 | In addition to the standard methods, the client provides hooks to easily access different reactive data. Every hook is available in the root object of the client and they all start with `use`.
 88 | 
 89 | **Example: useSession**
 90 | 
 91 | 
 92 | <Tabs items={["React", "Vue","Svelte", "Solid"]} defaultValue="react">
 93 |     <Tab value="React">
 94 |         ```tsx title="user.tsx"
 95 |         //make sure you're using the react client
 96 |         import { createAuthClient } from "better-auth/react"
 97 |         const { useSession } = createAuthClient() // [!code highlight]
 98 |     
 99 |         export function User() {
100 |             const {
101 |                 data: session,
102 |                 isPending, //loading state
103 |                 error, //error object 
104 |                 refetch //refetch the session
105 |             } = useSession()
106 |             return (
107 |                 //...
108 |             )
109 |         }
110 |         ```
111 |     </Tab>
112 | 
113 |     <Tab value="Vue">
114 |         ```vue title="user.vue"
115 |         <script lang="ts" setup>
116 |         import { authClient } from '@/lib/auth-client'
117 |         const session = authClient.useSession()
118 |         </script>
119 |         <template>
120 |             <div>
121 |                 <button v-if="!session.data" @click="() => authClient.signIn.social({
122 |                     provider: 'github'
123 |                 })">
124 |                     Continue with GitHub
125 |                 </button>
126 |                 <div>
127 |                     <pre>{{ session.data }}</pre>
128 |                     <button v-if="session.data" @click="authClient.signOut()">
129 |                         Sign out
130 |                     </button>
131 |                 </div>
132 |             </div>
133 |         </template>
134 |         ```
135 |         </Tab>
136 | 
137 |         <Tab value="Svelte">
138 |             ```svelte title="user.svelte"
139 |             <script lang="ts">
140 |             import { client } from "$lib/client";
141 |             const session = client.useSession();
142 |             </script>
143 | 
144 |             <div
145 |                 style="display: flex; flex-direction: column; gap: 10px; border-radius: 10px; border: 1px solid #4B453F; padding: 20px; margin-top: 10px;"
146 |             >
147 |                 <div>
148 |                 {#if $session.data}
149 |                     <div>
150 |                     <p>
151 |                         {$session.data.user.name}
152 |                     </p>
153 |                     <p>
154 |                         {$session.data.user.email}
155 |                     </p>
156 |                     <button
157 |                         onclick={async () => {
158 |                         await authClient.signOut();
159 |                         }}
160 |                     >
161 |                         Signout
162 |                     </button>
163 |                     </div>
164 |                 {:else}
165 |                     <button
166 |                     onclick={async () => {
167 |                         await authClient.signIn.social({
168 |                         provider: "github",
169 |                         });
170 |                     }}
171 |                     >
172 |                     Continue with GitHub
173 |                     </button>
174 |                 {/if}
175 |                 </div>
176 |             </div>
177 |             ```
178 |         </Tab>
179 | 
180 |         <Tab value="Solid">
181 |             ```tsx title="user.tsx"
182 |             import { client } from "~/lib/client";
183 |             import { Show } from 'solid-js';
184 | 
185 |             export default function Home() {
186 |                 const session = client.useSession()
187 |                 return (
188 |                     <Show
189 |                         when={session()}
190 |                         fallback={<button onClick={toggle}>Log in</button>}
191 |                     >
192 |                         <button onClick={toggle}>Log out</button>
193 |                     </Show>
194 |                 ); 
195 |             }
196 |             ```
197 |             </Tab>
198 | </Tabs>
199 | 
200 | ### Fetch Options
201 | 
202 | The client uses a library called [better fetch](https://better-fetch.vercel.app) to make requests to the server. 
203 | 
204 | Better fetch is a wrapper around the native fetch API that provides a more convenient way to make requests. It's created by the same team behind Better Auth and is designed to work seamlessly with it.
205 | 
206 | You can pass any default fetch options to the client by passing `fetchOptions` object to the `createAuthClient`.
207 | 
208 | ```ts title="auth-client.ts"
209 | import { createAuthClient } from "better-auth/client"
210 | 
211 | const authClient = createAuthClient({
212 |     fetchOptions: {
213 |         //any better-fetch options
214 |     },
215 | })
216 | ```         
217 | 
218 | You can also pass fetch options to most of the client functions. Either as the second argument or as a property in the object.
219 | 
220 | ```ts title="auth-client.ts"
221 | await authClient.signIn.email({
222 |     email: "[email protected]",
223 |     password: "password1234",
224 | }, {
225 |     onSuccess(ctx) {
226 |             //      
227 |     }
228 | })
229 | 
230 | //or
231 | 
232 | await authClient.signIn.email({
233 |     email: "[email protected]",
234 |     password: "password1234",
235 |     fetchOptions: {
236 |         onSuccess(ctx) {
237 |             //      
238 |         }
239 |     },
240 | })
241 | ```
242 | 
243 | 
244 | ### Handling Errors
245 | 
246 | Most of the client functions return a response object with the following properties:
247 | - `data`: The response data.
248 | - `error`: The error object if there was an error.
249 | 
250 | The error object contains the following properties:
251 | - `message`: The error message. (e.g., "Invalid email or password")
252 | - `status`: The HTTP status code.
253 | - `statusText`: The HTTP status text.
254 | 
255 | ```ts title="auth-client.ts"
256 | const { data, error } = await authClient.signIn.email({
257 |     email: "[email protected]",
258 |     password: "password1234"
259 | })
260 | if (error) {
261 |     //handle error
262 | }
263 | ```
264 | 
265 | If the action accepts a `fetchOptions` option, you can pass an `onError` callback to handle errors.
266 | 
267 | ```ts title="auth-client.ts"
268 | 
269 | await authClient.signIn.email({
270 |     email: "[email protected]",
271 |     password: "password1234",
272 | }, {
273 |     onError(ctx) {
274 |         //handle error
275 |     }
276 | })
277 | 
278 | //or
279 | await authClient.signIn.email({
280 |     email: "[email protected]",
281 |     password: "password1234",
282 |     fetchOptions: {
283 |         onError(ctx) {
284 |             //handle error
285 |         }
286 |     }
287 | })
288 | ```
289 | 
290 | Hooks like `useSession` also return an error object if there was an error fetching the session. On top of that, they also return an `isPending` property to indicate if the request is still pending.
291 | 
292 | ```ts title="auth-client.ts"
293 | const { data, error, isPending } = useSession()
294 | if (error) {
295 |     //handle error
296 | }
297 | ```
298 | 
299 | #### Error Codes
300 | 
301 | The client instance contains $ERROR_CODES object that contains all the error codes returned by the server. You can use this to handle error translations or custom error messages.
302 | 
303 | ```ts title="auth-client.ts"
304 | const authClient = createAuthClient();
305 | 
306 | type ErrorTypes = Partial<
307 | 	Record<
308 | 		keyof typeof authClient.$ERROR_CODES,
309 | 		{
310 | 			en: string;
311 | 			es: string;
312 | 		}
313 | 	>
314 | >;
315 | 
316 | const errorCodes = {
317 | 	USER_ALREADY_EXISTS: {
318 | 		en: "user already registered",
319 | 		es: "usuario ya registrada",
320 | 	},
321 | } satisfies ErrorTypes;
322 | 
323 | const getErrorMessage = (code: string, lang: "en" | "es") => {
324 | 	if (code in errorCodes) {
325 | 		return errorCodes[code as keyof typeof errorCodes][lang];
326 | 	}
327 | 	return "";
328 | };
329 | 
330 | 
331 | const { error } = await authClient.signUp.email({
332 | 	email: "[email protected]",
333 | 	password: "password",
334 | 	name: "User",
335 | });
336 | if(error?.code){
337 |     alert(getErrorMessage(error.code, "en"));
338 | }
339 | ```
340 | 
341 | ### Plugins
342 | 
343 | You can extend the client with plugins to add more functionality. Plugins can add new functions to the client or modify existing ones. 
344 | 
345 | **Example: Magic Link Plugin**
346 | ```ts title="auth-client.ts"
347 | import { createAuthClient } from "better-auth/client"
348 | import { magicLinkClient } from "better-auth/client/plugins"
349 | 
350 | const authClient = createAuthClient({
351 |     plugins: [
352 |         magicLinkClient()
353 |     ]
354 | })
355 | ```
356 | 
357 | once you've added the plugin, you can use the new functions provided by the plugin.
358 | 
359 | ```ts title="auth-client.ts"
360 | await authClient.signIn.magicLink({
361 |     email: "[email protected]"
362 | })
363 | ```
364 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/routes/email-verification.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as z from "zod";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import { APIError } from "better-call";
  4 | import { getSessionFromCtx } from "./session";
  5 | import { setSessionCookie } from "../../cookies";
  6 | import type { User } from "../../types";
  7 | import { jwtVerify, type JWTPayload, type JWTVerifyResult } from "jose";
  8 | import { signJWT } from "../../crypto/jwt";
  9 | import { originCheck } from "../middlewares";
 10 | import { JWTExpired } from "jose/errors";
 11 | import type { GenericEndpointContext } from "@better-auth/core";
 12 | 
 13 | export async function createEmailVerificationToken(
 14 | 	secret: string,
 15 | 	email: string,
 16 | 	/**
 17 | 	 * The email to update from
 18 | 	 */
 19 | 	updateTo?: string,
 20 | 	/**
 21 | 	 * The time in seconds for the token to expire
 22 | 	 */
 23 | 	expiresIn: number = 3600,
 24 | ) {
 25 | 	const token = await signJWT(
 26 | 		{
 27 | 			email: email.toLowerCase(),
 28 | 			updateTo,
 29 | 		},
 30 | 		secret,
 31 | 		expiresIn,
 32 | 	);
 33 | 	return token;
 34 | }
 35 | 
 36 | /**
 37 |  * A function to send a verification email to the user
 38 |  */
 39 | export async function sendVerificationEmailFn(
 40 | 	ctx: GenericEndpointContext,
 41 | 	user: User,
 42 | ) {
 43 | 	if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
 44 | 		ctx.context.logger.error("Verification email isn't enabled.");
 45 | 		throw new APIError("BAD_REQUEST", {
 46 | 			message: "Verification email isn't enabled",
 47 | 		});
 48 | 	}
 49 | 	const token = await createEmailVerificationToken(
 50 | 		ctx.context.secret,
 51 | 		user.email,
 52 | 		undefined,
 53 | 		ctx.context.options.emailVerification?.expiresIn,
 54 | 	);
 55 | 	const callbackURL = ctx.body.callbackURL
 56 | 		? encodeURIComponent(ctx.body.callbackURL)
 57 | 		: encodeURIComponent("/");
 58 | 	const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
 59 | 	await ctx.context.options.emailVerification.sendVerificationEmail(
 60 | 		{
 61 | 			user: user,
 62 | 			url,
 63 | 			token,
 64 | 		},
 65 | 		ctx.request,
 66 | 	);
 67 | }
 68 | export const sendVerificationEmail = createAuthEndpoint(
 69 | 	"/send-verification-email",
 70 | 	{
 71 | 		method: "POST",
 72 | 		body: z.object({
 73 | 			email: z.email().meta({
 74 | 				description: "The email to send the verification email to",
 75 | 			}),
 76 | 			callbackURL: z
 77 | 				.string()
 78 | 				.meta({
 79 | 					description: "The URL to use for email verification callback",
 80 | 				})
 81 | 				.optional(),
 82 | 		}),
 83 | 		metadata: {
 84 | 			openapi: {
 85 | 				description: "Send a verification email to the user",
 86 | 				requestBody: {
 87 | 					content: {
 88 | 						"application/json": {
 89 | 							schema: {
 90 | 								type: "object",
 91 | 								properties: {
 92 | 									email: {
 93 | 										type: "string",
 94 | 										description: "The email to send the verification email to",
 95 | 										example: "[email protected]",
 96 | 									},
 97 | 									callbackURL: {
 98 | 										type: "string",
 99 | 										description:
100 | 											"The URL to use for email verification callback",
101 | 										example: "https://example.com/callback",
102 | 										nullable: true,
103 | 									},
104 | 								},
105 | 								required: ["email"],
106 | 							},
107 | 						},
108 | 					},
109 | 				},
110 | 				responses: {
111 | 					"200": {
112 | 						description: "Success",
113 | 						content: {
114 | 							"application/json": {
115 | 								schema: {
116 | 									type: "object",
117 | 									properties: {
118 | 										status: {
119 | 											type: "boolean",
120 | 											description:
121 | 												"Indicates if the email was sent successfully",
122 | 											example: true,
123 | 										},
124 | 									},
125 | 								},
126 | 							},
127 | 						},
128 | 					},
129 | 					"400": {
130 | 						description: "Bad Request",
131 | 						content: {
132 | 							"application/json": {
133 | 								schema: {
134 | 									type: "object",
135 | 									properties: {
136 | 										message: {
137 | 											type: "string",
138 | 											description: "Error message",
139 | 											example: "Verification email isn't enabled",
140 | 										},
141 | 									},
142 | 								},
143 | 							},
144 | 						},
145 | 					},
146 | 				},
147 | 			},
148 | 		},
149 | 	},
150 | 	async (ctx) => {
151 | 		if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
152 | 			ctx.context.logger.error("Verification email isn't enabled.");
153 | 			throw new APIError("BAD_REQUEST", {
154 | 				message: "Verification email isn't enabled",
155 | 			});
156 | 		}
157 | 		const { email } = ctx.body;
158 | 		const session = await getSessionFromCtx(ctx);
159 | 		if (!session) {
160 | 			const user = await ctx.context.internalAdapter.findUserByEmail(email);
161 | 			if (!user) {
162 | 				//we're returning true to avoid leaking information about the user
163 | 				return ctx.json({
164 | 					status: true,
165 | 				});
166 | 			}
167 | 			await sendVerificationEmailFn(ctx, user.user);
168 | 			return ctx.json({
169 | 				status: true,
170 | 			});
171 | 		}
172 | 		if (session?.user.emailVerified) {
173 | 			throw new APIError("BAD_REQUEST", {
174 | 				message:
175 | 					"You can only send a verification email to an unverified email",
176 | 			});
177 | 		}
178 | 		if (session?.user.email !== email) {
179 | 			throw new APIError("BAD_REQUEST", {
180 | 				message: "You can only send a verification email to your own email",
181 | 			});
182 | 		}
183 | 		await sendVerificationEmailFn(ctx, session.user);
184 | 		return ctx.json({
185 | 			status: true,
186 | 		});
187 | 	},
188 | );
189 | 
190 | export const verifyEmail = createAuthEndpoint(
191 | 	"/verify-email",
192 | 	{
193 | 		method: "GET",
194 | 		query: z.object({
195 | 			token: z.string().meta({
196 | 				description: "The token to verify the email",
197 | 			}),
198 | 			callbackURL: z
199 | 				.string()
200 | 				.meta({
201 | 					description: "The URL to redirect to after email verification",
202 | 				})
203 | 				.optional(),
204 | 		}),
205 | 		use: [originCheck((ctx) => ctx.query.callbackURL)],
206 | 		metadata: {
207 | 			openapi: {
208 | 				description: "Verify the email of the user",
209 | 				parameters: [
210 | 					{
211 | 						name: "token",
212 | 						in: "query",
213 | 						description: "The token to verify the email",
214 | 						required: true,
215 | 						schema: {
216 | 							type: "string",
217 | 						},
218 | 					},
219 | 					{
220 | 						name: "callbackURL",
221 | 						in: "query",
222 | 						description: "The URL to redirect to after email verification",
223 | 						required: false,
224 | 						schema: {
225 | 							type: "string",
226 | 						},
227 | 					},
228 | 				],
229 | 				responses: {
230 | 					"200": {
231 | 						description: "Success",
232 | 						content: {
233 | 							"application/json": {
234 | 								schema: {
235 | 									type: "object",
236 | 									properties: {
237 | 										user: {
238 | 											type: "object",
239 | 											properties: {
240 | 												id: {
241 | 													type: "string",
242 | 													description: "User ID",
243 | 												},
244 | 												email: {
245 | 													type: "string",
246 | 													description: "User email",
247 | 												},
248 | 												name: {
249 | 													type: "string",
250 | 													description: "User name",
251 | 												},
252 | 												image: {
253 | 													type: "string",
254 | 													description: "User image URL",
255 | 												},
256 | 												emailVerified: {
257 | 													type: "boolean",
258 | 													description:
259 | 														"Indicates if the user email is verified",
260 | 												},
261 | 												createdAt: {
262 | 													type: "string",
263 | 													description: "User creation date",
264 | 												},
265 | 												updatedAt: {
266 | 													type: "string",
267 | 													description: "User update date",
268 | 												},
269 | 											},
270 | 											required: [
271 | 												"id",
272 | 												"email",
273 | 												"name",
274 | 												"image",
275 | 												"emailVerified",
276 | 												"createdAt",
277 | 												"updatedAt",
278 | 											],
279 | 										},
280 | 										status: {
281 | 											type: "boolean",
282 | 											description:
283 | 												"Indicates if the email was verified successfully",
284 | 										},
285 | 									},
286 | 									required: ["user", "status"],
287 | 								},
288 | 							},
289 | 						},
290 | 					},
291 | 				},
292 | 			},
293 | 		},
294 | 	},
295 | 	async (ctx) => {
296 | 		function redirectOnError(error: string) {
297 | 			if (ctx.query.callbackURL) {
298 | 				if (ctx.query.callbackURL.includes("?")) {
299 | 					throw ctx.redirect(`${ctx.query.callbackURL}&error=${error}`);
300 | 				}
301 | 				throw ctx.redirect(`${ctx.query.callbackURL}?error=${error}`);
302 | 			}
303 | 			throw new APIError("UNAUTHORIZED", {
304 | 				message: error,
305 | 			});
306 | 		}
307 | 		const { token } = ctx.query;
308 | 		let jwt: JWTVerifyResult<JWTPayload>;
309 | 		try {
310 | 			jwt = await jwtVerify(
311 | 				token,
312 | 				new TextEncoder().encode(ctx.context.secret),
313 | 				{
314 | 					algorithms: ["HS256"],
315 | 				},
316 | 			);
317 | 		} catch (e) {
318 | 			if (e instanceof JWTExpired) {
319 | 				return redirectOnError("token_expired");
320 | 			}
321 | 			return redirectOnError("invalid_token");
322 | 		}
323 | 		const schema = z.object({
324 | 			email: z.string().email(),
325 | 			updateTo: z.string().optional(),
326 | 		});
327 | 		const parsed = schema.parse(jwt.payload);
328 | 		const user = await ctx.context.internalAdapter.findUserByEmail(
329 | 			parsed.email,
330 | 		);
331 | 		if (!user) {
332 | 			return redirectOnError("user_not_found");
333 | 		}
334 | 		if (parsed.updateTo) {
335 | 			const session = await getSessionFromCtx(ctx);
336 | 			if (!session) {
337 | 				if (ctx.query.callbackURL) {
338 | 					throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
339 | 				}
340 | 				return redirectOnError("unauthorized");
341 | 			}
342 | 			if (session.user.email !== parsed.email) {
343 | 				if (ctx.query.callbackURL) {
344 | 					throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
345 | 				}
346 | 				return redirectOnError("unauthorized");
347 | 			}
348 | 
349 | 			const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(
350 | 				parsed.email,
351 | 				{
352 | 					email: parsed.updateTo,
353 | 					emailVerified: false,
354 | 				},
355 | 			);
356 | 
357 | 			const newToken = await createEmailVerificationToken(
358 | 				ctx.context.secret,
359 | 				parsed.updateTo,
360 | 			);
361 | 
362 | 			//send verification email to the new email
363 | 			const updateCallbackURL = ctx.query.callbackURL
364 | 				? encodeURIComponent(ctx.query.callbackURL)
365 | 				: encodeURIComponent("/");
366 | 			await ctx.context.options.emailVerification?.sendVerificationEmail?.(
367 | 				{
368 | 					user: updatedUser,
369 | 					url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
370 | 					token: newToken,
371 | 				},
372 | 				ctx.request,
373 | 			);
374 | 
375 | 			await setSessionCookie(ctx, {
376 | 				session: session.session,
377 | 				user: {
378 | 					...session.user,
379 | 					email: parsed.updateTo,
380 | 					emailVerified: false,
381 | 				},
382 | 			});
383 | 
384 | 			if (ctx.query.callbackURL) {
385 | 				throw ctx.redirect(ctx.query.callbackURL);
386 | 			}
387 | 			return ctx.json({
388 | 				status: true,
389 | 				user: {
390 | 					id: updatedUser.id,
391 | 					email: updatedUser.email,
392 | 					name: updatedUser.name,
393 | 					image: updatedUser.image,
394 | 					emailVerified: updatedUser.emailVerified,
395 | 					createdAt: updatedUser.createdAt,
396 | 					updatedAt: updatedUser.updatedAt,
397 | 				},
398 | 			});
399 | 		}
400 | 		if (ctx.context.options.emailVerification?.onEmailVerification) {
401 | 			await ctx.context.options.emailVerification.onEmailVerification(
402 | 				user.user,
403 | 				ctx.request,
404 | 			);
405 | 		}
406 | 		const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(
407 | 			parsed.email,
408 | 			{
409 | 				emailVerified: true,
410 | 			},
411 | 		);
412 | 		if (ctx.context.options.emailVerification?.afterEmailVerification) {
413 | 			await ctx.context.options.emailVerification.afterEmailVerification(
414 | 				updatedUser,
415 | 				ctx.request,
416 | 			);
417 | 		}
418 | 		if (ctx.context.options.emailVerification?.autoSignInAfterVerification) {
419 | 			const currentSession = await getSessionFromCtx(ctx);
420 | 			if (!currentSession || currentSession.user.email !== parsed.email) {
421 | 				const session = await ctx.context.internalAdapter.createSession(
422 | 					user.user.id,
423 | 				);
424 | 				if (!session) {
425 | 					throw new APIError("INTERNAL_SERVER_ERROR", {
426 | 						message: "Failed to create session",
427 | 					});
428 | 				}
429 | 				await setSessionCookie(ctx, {
430 | 					session,
431 | 					user: {
432 | 						...user.user,
433 | 						emailVerified: true,
434 | 					},
435 | 				});
436 | 			} else {
437 | 				await setSessionCookie(ctx, {
438 | 					session: currentSession.session,
439 | 					user: {
440 | 						...currentSession.user,
441 | 						emailVerified: true,
442 | 					},
443 | 				});
444 | 			}
445 | 		}
446 | 
447 | 		if (ctx.query.callbackURL) {
448 | 			throw ctx.redirect(ctx.query.callbackURL);
449 | 		}
450 | 		return ctx.json({
451 | 			status: true,
452 | 			user: null,
453 | 		});
454 | 	},
455 | );
456 | 
```

--------------------------------------------------------------------------------
/packages/cli/test/generate.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from "vitest";
  2 | import { prismaAdapter } from "better-auth/adapters/prisma";
  3 | import { generatePrismaSchema } from "../src/generators/prisma";
  4 | import { organization, twoFactor, username } from "better-auth/plugins";
  5 | import { generateDrizzleSchema } from "../src/generators/drizzle";
  6 | import { drizzleAdapter } from "better-auth/adapters/drizzle";
  7 | import { generateMigrations } from "../src/generators/kysely";
  8 | import Database from "better-sqlite3";
  9 | import type { BetterAuthOptions } from "better-auth";
 10 | import { generateAuthConfig } from "../src/generators/auth-config";
 11 | import type { SupportedPlugin } from "../src/commands/init";
 12 | 
 13 | describe("generate", async () => {
 14 | 	it("should generate prisma schema", async () => {
 15 | 		const schema = await generatePrismaSchema({
 16 | 			file: "test.prisma",
 17 | 			adapter: prismaAdapter(
 18 | 				{},
 19 | 				{
 20 | 					provider: "postgresql",
 21 | 				},
 22 | 			)({} as BetterAuthOptions),
 23 | 			options: {
 24 | 				database: prismaAdapter(
 25 | 					{},
 26 | 					{
 27 | 						provider: "postgresql",
 28 | 					},
 29 | 				),
 30 | 				plugins: [twoFactor(), username()],
 31 | 			},
 32 | 		});
 33 | 		expect(schema.code).toMatchFileSnapshot("./__snapshots__/schema.prisma");
 34 | 	});
 35 | 
 36 | 	it("should generate prisma schema with number id", async () => {
 37 | 		const schema = await generatePrismaSchema({
 38 | 			file: "test.prisma",
 39 | 			adapter: prismaAdapter(
 40 | 				{},
 41 | 				{
 42 | 					provider: "postgresql",
 43 | 				},
 44 | 			)({} as BetterAuthOptions),
 45 | 			options: {
 46 | 				database: prismaAdapter(
 47 | 					{},
 48 | 					{
 49 | 						provider: "postgresql",
 50 | 					},
 51 | 				),
 52 | 				plugins: [twoFactor(), username()],
 53 | 				advanced: {
 54 | 					database: {
 55 | 						useNumberId: true,
 56 | 					},
 57 | 				},
 58 | 			},
 59 | 		});
 60 | 		expect(schema.code).toMatchFileSnapshot(
 61 | 			"./__snapshots__/schema-numberid.prisma",
 62 | 		);
 63 | 	});
 64 | 
 65 | 	it("should generate prisma schema for mongodb", async () => {
 66 | 		const schema = await generatePrismaSchema({
 67 | 			file: "test.prisma",
 68 | 			adapter: prismaAdapter(
 69 | 				{},
 70 | 				{
 71 | 					provider: "mongodb",
 72 | 				},
 73 | 			)({} as BetterAuthOptions),
 74 | 			options: {
 75 | 				database: prismaAdapter(
 76 | 					{},
 77 | 					{
 78 | 						provider: "mongodb",
 79 | 					},
 80 | 				),
 81 | 				plugins: [twoFactor(), username()],
 82 | 			},
 83 | 		});
 84 | 		expect(schema.code).toMatchFileSnapshot(
 85 | 			"./__snapshots__/schema-mongodb.prisma",
 86 | 		);
 87 | 	});
 88 | 
 89 | 	it("should generate prisma schema for mysql", async () => {
 90 | 		const schema = await generatePrismaSchema({
 91 | 			file: "test.prisma",
 92 | 			adapter: prismaAdapter(
 93 | 				{},
 94 | 				{
 95 | 					provider: "mysql",
 96 | 				},
 97 | 			)({} as BetterAuthOptions),
 98 | 			options: {
 99 | 				database: prismaAdapter(
100 | 					{},
101 | 					{
102 | 						provider: "mongodb",
103 | 					},
104 | 				),
105 | 				plugins: [twoFactor(), username()],
106 | 			},
107 | 		});
108 | 		expect(schema.code).toMatchFileSnapshot(
109 | 			"./__snapshots__/schema-mysql.prisma",
110 | 		);
111 | 	});
112 | 
113 | 	it("should generate prisma schema for mysql with custom model names", async () => {
114 | 		const schema = await generatePrismaSchema({
115 | 			file: "test.prisma",
116 | 			adapter: prismaAdapter(
117 | 				{},
118 | 				{
119 | 					provider: "mysql",
120 | 				},
121 | 			)({} as BetterAuthOptions),
122 | 			options: {
123 | 				database: prismaAdapter(
124 | 					{},
125 | 					{
126 | 						provider: "mongodb",
127 | 					},
128 | 				),
129 | 				plugins: [
130 | 					twoFactor(),
131 | 					username(),
132 | 					organization({
133 | 						schema: {
134 | 							organization: {
135 | 								modelName: "workspace",
136 | 							},
137 | 							invitation: {
138 | 								modelName: "workspaceInvitation",
139 | 							},
140 | 						},
141 | 					}),
142 | 				],
143 | 			},
144 | 		});
145 | 		expect(schema.code).toMatchFileSnapshot(
146 | 			"./__snapshots__/schema-mysql-custom.prisma",
147 | 		);
148 | 	});
149 | 
150 | 	it("should generate drizzle schema", async () => {
151 | 		const schema = await generateDrizzleSchema({
152 | 			file: "test.drizzle",
153 | 			adapter: drizzleAdapter(
154 | 				{},
155 | 				{
156 | 					provider: "pg",
157 | 					schema: {},
158 | 				},
159 | 			)({} as BetterAuthOptions),
160 | 			options: {
161 | 				database: drizzleAdapter(
162 | 					{},
163 | 					{
164 | 						provider: "pg",
165 | 						schema: {},
166 | 					},
167 | 				),
168 | 				plugins: [twoFactor(), username()],
169 | 				user: {
170 | 					modelName: "custom_user",
171 | 				},
172 | 				account: {
173 | 					modelName: "custom_account",
174 | 				},
175 | 				session: {
176 | 					modelName: "custom_session",
177 | 				},
178 | 				verification: {
179 | 					modelName: "custom_verification",
180 | 				},
181 | 			},
182 | 		});
183 | 		expect(schema.code).toMatchFileSnapshot("./__snapshots__/auth-schema.txt");
184 | 	});
185 | 
186 | 	it("should generate drizzle schema with number id", async () => {
187 | 		const schema = await generateDrizzleSchema({
188 | 			file: "test.drizzle",
189 | 			adapter: drizzleAdapter(
190 | 				{},
191 | 				{
192 | 					provider: "pg",
193 | 					schema: {},
194 | 				},
195 | 			)({} as BetterAuthOptions),
196 | 			options: {
197 | 				database: drizzleAdapter(
198 | 					{},
199 | 					{
200 | 						provider: "pg",
201 | 						schema: {},
202 | 					},
203 | 				),
204 | 				plugins: [twoFactor(), username()],
205 | 				advanced: {
206 | 					database: {
207 | 						useNumberId: true,
208 | 					},
209 | 				},
210 | 				user: {
211 | 					modelName: "custom_user",
212 | 				},
213 | 				account: {
214 | 					modelName: "custom_account",
215 | 				},
216 | 				session: {
217 | 					modelName: "custom_session",
218 | 				},
219 | 				verification: {
220 | 					modelName: "custom_verification",
221 | 				},
222 | 			},
223 | 		});
224 | 		expect(schema.code).toMatchFileSnapshot(
225 | 			"./__snapshots__/auth-schema-number-id.txt",
226 | 		);
227 | 	});
228 | 
229 | 	it("should generate kysely schema", async () => {
230 | 		const schema = await generateMigrations({
231 | 			file: "test.sql",
232 | 			options: {
233 | 				database: new Database(":memory:"),
234 | 			},
235 | 			adapter: {} as any,
236 | 		});
237 | 		expect(schema.code).toMatchFileSnapshot("./__snapshots__/migrations.sql");
238 | 	});
239 | 
240 | 	it("should add plugin to empty plugins array without leading comma", async () => {
241 | 		const initialConfig = `export const auth = betterAuth({
242 | 			plugins: []
243 | 		});`;
244 | 
245 | 		const mockFormat = (code: string) => Promise.resolve(code);
246 | 		const mockSpinner = { stop: () => {} };
247 | 		const plugins: SupportedPlugin[] = [
248 | 			{
249 | 				id: "next-cookies",
250 | 				name: "nextCookies",
251 | 				path: "better-auth/next-js",
252 | 				clientName: undefined,
253 | 				clientPath: undefined,
254 | 			},
255 | 		];
256 | 
257 | 		const result = await generateAuthConfig({
258 | 			format: mockFormat,
259 | 			current_user_config: initialConfig,
260 | 			spinner: mockSpinner as any,
261 | 			plugins,
262 | 			database: null,
263 | 		});
264 | 
265 | 		expect(result.generatedCode).toContain(`plugins: [nextCookies()]`);
266 | 		expect(result.generatedCode).not.toContain(`plugins: [, nextCookies()]`);
267 | 	});
268 | });
269 | 
270 | describe("JSON field support in CLI generators", () => {
271 | 	it("should generate Drizzle schema with JSON fields for PostgreSQL", async () => {
272 | 		const schema = await generateDrizzleSchema({
273 | 			file: "test.drizzle",
274 | 			adapter: {
275 | 				id: "drizzle",
276 | 				options: {
277 | 					provider: "pg",
278 | 					schema: {},
279 | 				},
280 | 			} as any,
281 | 			options: {
282 | 				database: {} as any,
283 | 				user: {
284 | 					additionalFields: {
285 | 						preferences: {
286 | 							type: "json",
287 | 						},
288 | 					},
289 | 				},
290 | 			} as BetterAuthOptions,
291 | 		});
292 | 		expect(schema.code).toContain("preferences: jsonb(");
293 | 	});
294 | 
295 | 	it("should generate Drizzle schema with JSON fields for MySQL", async () => {
296 | 		const schema = await generateDrizzleSchema({
297 | 			file: "test.drizzle",
298 | 			adapter: {
299 | 				id: "drizzle",
300 | 				options: {
301 | 					provider: "mysql",
302 | 					schema: {},
303 | 				},
304 | 			} as any,
305 | 			options: {
306 | 				database: {} as any,
307 | 				user: {
308 | 					additionalFields: {
309 | 						preferences: {
310 | 							type: "json",
311 | 						},
312 | 					},
313 | 				},
314 | 			} as BetterAuthOptions,
315 | 		});
316 | 		expect(schema.code).toContain("preferences: json(");
317 | 	});
318 | 
319 | 	it("should generate Drizzle schema with JSON fields for SQLite", async () => {
320 | 		const schema = await generateDrizzleSchema({
321 | 			file: "test.drizzle",
322 | 			adapter: {
323 | 				id: "drizzle",
324 | 				options: {
325 | 					provider: "sqlite",
326 | 					schema: {},
327 | 				},
328 | 			} as any,
329 | 			options: {
330 | 				database: {} as any,
331 | 				user: {
332 | 					additionalFields: {
333 | 						preferences: {
334 | 							type: "json",
335 | 						},
336 | 					},
337 | 				},
338 | 			} as BetterAuthOptions,
339 | 		});
340 | 		expect(schema.code).toContain("preferences: text(");
341 | 	});
342 | 
343 | 	it("should generate Prisma schema with JSON fields", async () => {
344 | 		const schema = await generatePrismaSchema({
345 | 			file: "test.prisma",
346 | 			adapter: {
347 | 				id: "prisma",
348 | 				options: {},
349 | 			} as any,
350 | 			options: {
351 | 				database: {} as any,
352 | 				user: {
353 | 					additionalFields: {
354 | 						preferences: {
355 | 							type: "json",
356 | 						},
357 | 					},
358 | 				},
359 | 			} as BetterAuthOptions,
360 | 		});
361 | 		expect(schema.code).toContain("preferences   Json?");
362 | 	});
363 | });
364 | 
365 | describe("Enum field support in Drizzle schemas", () => {
366 | 	it("should generate Drizzle schema with enum fields for PostgreSQL", async () => {
367 | 		const schema = await generateDrizzleSchema({
368 | 			file: "test.drizzle",
369 | 			adapter: {
370 | 				id: "drizzle",
371 | 				options: {
372 | 					provider: "pg",
373 | 					schema: {},
374 | 				},
375 | 			} as any,
376 | 			options: {
377 | 				database: {} as any,
378 | 				user: {
379 | 					additionalFields: {
380 | 						role: {
381 | 							type: ["admin", "user", "guest"],
382 | 							required: true,
383 | 						},
384 | 					},
385 | 				},
386 | 			} as BetterAuthOptions,
387 | 		});
388 | 		expect(schema.code).toContain(
389 | 			'role: text("role", { enum: ["admin", "user", "guest"] })',
390 | 		);
391 | 		await expect(schema.code).toMatchFileSnapshot(
392 | 			"./__snapshots__/auth-schema-pg-enum.txt",
393 | 		);
394 | 	});
395 | 
396 | 	it("should generate Drizzle schema with enum fields for MySQL", async () => {
397 | 		const schema = await generateDrizzleSchema({
398 | 			file: "test.drizzle",
399 | 			adapter: {
400 | 				id: "drizzle",
401 | 				options: {
402 | 					provider: "mysql",
403 | 					schema: {},
404 | 				},
405 | 			} as any,
406 | 			options: {
407 | 				database: {} as any,
408 | 				user: {
409 | 					additionalFields: {
410 | 						status: {
411 | 							type: ["active", "inactive", "pending"],
412 | 							required: false,
413 | 						},
414 | 					},
415 | 				},
416 | 			} as BetterAuthOptions,
417 | 		});
418 | 		expect(schema.code).toContain("mysqlEnum");
419 | 		expect(schema.code).toContain(
420 | 			'status: mysqlEnum(["active", "inactive", "pending"])',
421 | 		);
422 | 		await expect(schema.code).toMatchFileSnapshot(
423 | 			"./__snapshots__/auth-schema-mysql-enum.txt",
424 | 		);
425 | 	});
426 | 
427 | 	it("should generate Drizzle schema with enum fields for SQLite", async () => {
428 | 		const schema = await generateDrizzleSchema({
429 | 			file: "test.drizzle",
430 | 			adapter: {
431 | 				id: "drizzle",
432 | 				options: {
433 | 					provider: "sqlite",
434 | 					schema: {},
435 | 				},
436 | 			} as any,
437 | 			options: {
438 | 				database: {} as any,
439 | 				user: {
440 | 					additionalFields: {
441 | 						priority: {
442 | 							type: ["high", "medium", "low"],
443 | 						},
444 | 					},
445 | 				},
446 | 			} as BetterAuthOptions,
447 | 		});
448 | 		expect(schema.code).toContain("text({ enum: [");
449 | 		expect(schema.code).toContain(
450 | 			'priority: text({ enum: ["high", "medium", "low"] })',
451 | 		);
452 | 		await expect(schema.code).toMatchFileSnapshot(
453 | 			"./__snapshots__/auth-schema-sqlite-enum.txt",
454 | 		);
455 | 	});
456 | 
457 | 	it("should include correct imports for enum fields in MySQL", async () => {
458 | 		const schema = await generateDrizzleSchema({
459 | 			file: "test.drizzle",
460 | 			adapter: {
461 | 				id: "drizzle",
462 | 				options: {
463 | 					provider: "mysql",
464 | 					schema: {},
465 | 				},
466 | 			} as any,
467 | 			options: {
468 | 				database: {} as any,
469 | 				user: {
470 | 					additionalFields: {
471 | 						status: {
472 | 							type: ["active", "inactive"],
473 | 						},
474 | 					},
475 | 				},
476 | 			} as BetterAuthOptions,
477 | 		});
478 | 		expect(schema.code).toMatch(
479 | 			/import.*mysqlEnum.*from.*drizzle-orm\/mysql-core/s,
480 | 		);
481 | 	});
482 | 
483 | 	it("should not include enum imports when no enum fields are present", async () => {
484 | 		const schema = await generateDrizzleSchema({
485 | 			file: "test.drizzle",
486 | 			adapter: {
487 | 				id: "drizzle",
488 | 				options: {
489 | 					provider: "pg",
490 | 					schema: {},
491 | 				},
492 | 			} as any,
493 | 			options: {
494 | 				database: {} as any,
495 | 				user: {
496 | 					additionalFields: {
497 | 						name: {
498 | 							type: "string",
499 | 						},
500 | 					},
501 | 				},
502 | 			} as BetterAuthOptions,
503 | 		});
504 | 		expect(schema.code).not.toContain("enum");
505 | 	});
506 | });
507 | 
```
Page 32/70FirstPrevNextLast