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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/docs/components/nav-bar.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import Link from "next/link";
  2 | import { MobileSearchIcon } from "@/components/mobile-search-icon";
  3 | import { ThemeToggle } from "@/components/theme-toggler";
  4 | import DarkPng from "../public/branding/better-auth-logo-dark.png";
  5 | import WhitePng from "../public/branding/better-auth-logo-light.png";
  6 | import { Logo } from "./logo";
  7 | import LogoContextMenu from "./logo-context-menu";
  8 | import { NavLink } from "./nav-link";
  9 | import { NavbarMobile, NavbarMobileBtn } from "./nav-mobile";
 10 | export const Navbar = () => {
 11 | 	const logoAssets = {
 12 | 		darkSvg: `
 13 |     <svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
 14 |     <rect width="500" height="500" fill="black"/>
 15 |     <rect x="69" y="121" width="86.9879" height="259" fill="white"/>
 16 |     <rect x="337.575" y="121" width="92.4247" height="259" fill="white"/>
 17 |     <rect x="427.282" y="121" width="83.4555" height="174.52" transform="rotate(90 427.282 121)" fill="white"/>
 18 |     <rect x="430" y="296.544" width="83.4555" height="177.238" transform="rotate(90 430 296.544)" fill="white"/>
 19 |     <rect x="252.762" y="204.455" width="92.0888" height="96.7741" transform="rotate(90 252.762 204.455)" fill="white"/>
 20 |     </svg>
 21 |     `,
 22 | 		whiteSvg: `
 23 |     <svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
 24 |     <rect width="500" height="500" fill="white"/>
 25 |     <rect x="69" y="121" width="86.9879" height="259" fill="black"/>
 26 |     <rect x="337.575" y="121" width="92.4247" height="259" fill="black"/>
 27 |     <rect x="427.282" y="121" width="83.4555" height="174.52" transform="rotate(90 427.282 121)" fill="black"/>
 28 |     <rect x="430" y="296.544" width="83.4555" height="177.238" transform="rotate(90 430 296.544)" fill="black"/>
 29 |     <rect x="252.762" y="204.455" width="92.0888" height="96.7741" transform="rotate(90 252.762 204.455)" fill="black"/>
 30 |     </svg>
 31 |     `,
 32 | 		darkWordmark: `
 33 |     <svg width="1024" height="256" viewBox="0 0 1024 256" fill="none" xmlns="http://www.w3.org/2000/svg">
 34 |     <rect width="1024" height="256" fill="black"/>
 35 |     <rect x="96" y="79" width="34.6988" height="97.5904" fill="white"/>
 36 |     <rect x="203.133" y="79" width="36.8675" height="97.5904" fill="white"/>
 37 |     <rect x="238.916" y="79" width="31.4458" height="69.6144" transform="rotate(90 238.916 79)" fill="white"/>
 38 |     <rect x="240" y="145.145" width="31.4458" height="70.6988" transform="rotate(90 240 145.145)" fill="white"/>
 39 |     <rect x="169.301" y="110.446" width="34.6988" height="38.6024" transform="rotate(90 169.301 110.446)" fill="white"/>
 40 |     <path d="M281.832 162V93.84H305.256C313.32 93.84 319.368 95.312 323.4 98.256C327.432 101.2 329.448 105.84 329.448 112.176C329.448 116.016 328.36 119.248 326.184 121.872C324.072 124.432 321.128 126.064 317.352 126.768C322.024 127.408 325.672 129.232 328.296 132.24C330.984 135.184 332.328 138.864 332.328 143.28C332.328 149.488 330.312 154.16 326.28 157.296C322.248 160.432 316.52 162 309.096 162H281.832ZM290.088 123.312H305.256C310.248 123.312 314.088 122.384 316.776 120.528C319.464 118.608 320.808 115.952 320.808 112.56C320.808 105.456 315.624 101.904 305.256 101.904H290.088V123.312ZM290.088 153.936H309.096C313.768 153.936 317.352 152.976 319.848 151.056C322.408 149.136 323.688 146.384 323.688 142.8C323.688 139.216 322.408 136.432 319.848 134.448C317.352 132.4 313.768 131.376 309.096 131.376H290.088V153.936ZM345.301 162V93.84H388.117V101.904H353.557V123.888H386.965V131.76H353.557V153.936H388.885V162H345.301ZM416.681 162V101.904H395.465V93.84H446.153V101.904H424.937V162H416.681ZM470.587 162V101.904H449.371V93.84H500.059V101.904H478.843V162H470.587ZM507.113 162V93.84H549.929V101.904H515.369V123.888H548.777V131.76H515.369V153.936H550.697V162H507.113ZM564.02 162V93.84H589.844C597.012 93.84 602.676 95.696 606.836 99.408C610.996 103.12 613.076 108.144 613.076 114.48C613.076 117.104 612.532 119.504 611.444 121.68C610.356 123.792 608.948 125.584 607.22 127.056C605.492 128.528 603.604 129.552 601.556 130.128C604.564 130.64 606.932 131.856 608.66 133.776C610.452 135.696 611.508 138.416 611.828 141.936L613.748 162H605.396L603.667 142.8C603.412 139.984 602.388 137.904 600.596 136.56C598.868 135.216 596.02 134.544 592.052 134.544H572.276V162H564.02ZM572.276 126.48H590.9C595.06 126.48 598.356 125.424 600.788 123.312C603.22 121.2 604.436 118.192 604.436 114.288C604.436 110.32 603.188 107.28 600.692 105.168C598.196 102.992 594.58 101.904 589.844 101.904H572.276V126.48ZM623.912 137.808V130.224H655.688V137.808H623.912ZM661.826 162L686.402 93.84H697.538L722.114 162H713.09L706.274 142.608H677.666L670.85 162H661.826ZM680.45 134.544H703.49L691.97 101.04L680.45 134.544ZM755.651 163.536C750.403 163.536 745.827 162.512 741.923 160.464C738.083 158.416 735.107 155.504 732.995 151.728C730.947 147.888 729.923 143.376 729.923 138.192V93.744H738.179V138.192C738.179 143.696 739.683 147.952 742.691 150.96C745.763 153.968 750.083 155.472 755.651 155.472C761.155 155.472 765.411 153.968 768.419 150.96C771.491 147.952 773.027 143.696 773.027 138.192V93.744H781.283V138.192C781.283 143.376 780.227 147.888 778.115 151.728C776.067 155.504 773.123 158.416 769.283 160.464C765.443 162.512 760.899 163.536 755.651 163.536ZM811.087 162V101.904H789.871V93.84H840.559V101.904H819.343V162H811.087ZM847.613 162V93.84H855.869V123.696H890.141V93.84H898.397V162H890.141V131.76H855.869V162H847.613ZM911.443 162V151.152H922.291V162H911.443Z" fill="white"/>
 41 |     </svg>
 42 |     `,
 43 | 		whiteWordmark: `
 44 |       <svg width="1024" height="256" viewBox="0 0 1024 256" fill="none" xmlns="http://www.w3.org/2000/svg">
 45 |       <rect width="1024" height="256" fill="#FFEAEA"/>
 46 |       <rect x="96" y="79" width="34.6988" height="97.5904" fill="black"/>
 47 |       <rect x="203.133" y="79" width="36.8675" height="97.5904" fill="black"/>
 48 |       <rect x="238.916" y="79" width="31.4458" height="69.6144" transform="rotate(90 238.916 79)" fill="black"/>
 49 |       <rect x="240" y="145.145" width="31.4458" height="70.6988" transform="rotate(90 240 145.145)" fill="black"/>
 50 |       <rect x="169.301" y="110.446" width="34.6988" height="38.6024" transform="rotate(90 169.301 110.446)" fill="black"/>
 51 |       <path d="M281.832 162V93.84H305.256C313.32 93.84 319.368 95.312 323.4 98.256C327.432 101.2 329.448 105.84 329.448 112.176C329.448 116.016 328.36 119.248 326.184 121.872C324.072 124.432 321.128 126.064 317.352 126.768C322.024 127.408 325.672 129.232 328.296 132.24C330.984 135.184 332.328 138.864 332.328 143.28C332.328 149.488 330.312 154.16 326.28 157.296C322.248 160.432 316.52 162 309.096 162H281.832ZM290.088 123.312H305.256C310.248 123.312 314.088 122.384 316.776 120.528C319.464 118.608 320.808 115.952 320.808 112.56C320.808 105.456 315.624 101.904 305.256 101.904H290.088V123.312ZM290.088 153.936H309.096C313.768 153.936 317.352 152.976 319.848 151.056C322.408 149.136 323.688 146.384 323.688 142.8C323.688 139.216 322.408 136.432 319.848 134.448C317.352 132.4 313.768 131.376 309.096 131.376H290.088V153.936ZM345.301 162V93.84H388.117V101.904H353.557V123.888H386.965V131.76H353.557V153.936H388.885V162H345.301ZM416.681 162V101.904H395.465V93.84H446.153V101.904H424.937V162H416.681ZM470.587 162V101.904H449.371V93.84H500.059V101.904H478.843V162H470.587ZM507.113 162V93.84H549.929V101.904H515.369V123.888H548.777V131.76H515.369V153.936H550.697V162H507.113ZM564.02 162V93.84H589.844C597.012 93.84 602.676 95.696 606.836 99.408C610.996 103.12 613.076 108.144 613.076 114.48C613.076 117.104 612.532 119.504 611.444 121.68C610.356 123.792 608.948 125.584 607.22 127.056C605.492 128.528 603.604 129.552 601.556 130.128C604.564 130.64 606.932 131.856 608.66 133.776C610.452 135.696 611.508 138.416 611.828 141.936L613.748 162H605.396L603.667 142.8C603.412 139.984 602.388 137.904 600.596 136.56C598.868 135.216 596.02 134.544 592.052 134.544H572.276V162H564.02ZM572.276 126.48H590.9C595.06 126.48 598.356 125.424 600.788 123.312C603.22 121.2 604.436 118.192 604.436 114.288C604.436 110.32 603.188 107.28 600.692 105.168C598.196 102.992 594.58 101.904 589.844 101.904H572.276V126.48ZM623.912 137.808V130.224H655.688V137.808H623.912ZM661.826 162L686.402 93.84H697.538L722.114 162H713.09L706.274 142.608H677.666L670.85 162H661.826ZM680.45 134.544H703.49L691.97 101.04L680.45 134.544ZM755.651 163.536C750.403 163.536 745.827 162.512 741.923 160.464C738.083 158.416 735.107 155.504 732.995 151.728C730.947 147.888 729.923 143.376 729.923 138.192V93.744H738.179V138.192C738.179 143.696 739.683 147.952 742.691 150.96C745.763 153.968 750.083 155.472 755.651 155.472C761.155 155.472 765.411 153.968 768.419 150.96C771.491 147.952 773.027 143.696 773.027 138.192V93.744H781.283V138.192C781.283 143.376 780.227 147.888 778.115 151.728C776.067 155.504 773.123 158.416 769.283 160.464C765.443 162.512 760.899 163.536 755.651 163.536ZM811.087 162V101.904H789.871V93.84H840.559V101.904H819.343V162H811.087ZM847.613 162V93.84H855.869V123.696H890.141V93.84H898.397V162H890.141V131.76H855.869V162H847.613ZM911.443 162V151.152H922.291V162H911.443Z" fill="black"/>
 52 |       </svg>
 53 |       `,
 54 | 		darkPng: DarkPng,
 55 | 		whitePng: WhitePng,
 56 | 	};
 57 | 	return (
 58 | 		<div className="flex flex-col sticky top-0 bg-background backdrop-blur-md z-30">
 59 | 			<nav className="md:grid grid-cols-12 md:border-b top-0 flex items-center justify-between">
 60 | 				<Link
 61 | 					href="/"
 62 | 					className="md:border-r md:px-5 px-2.5 py-4 text-foreground md:col-span-2 shrink-0 transition-colors md:w-[268px] lg:w-[286px]"
 63 | 				>
 64 | 					<div className="flex flex-col gap-2 w-full">
 65 | 						<LogoContextMenu
 66 | 							logo={
 67 | 								<div className="flex items-center gap-2">
 68 | 									<Logo />
 69 | 									<p className="select-none">BETTER-AUTH.</p>
 70 | 								</div>
 71 | 							}
 72 | 							logoAssets={logoAssets}
 73 | 						/>
 74 | 					</div>
 75 | 				</Link>
 76 | 				<div className="md:col-span-10 flex items-center justify-end relative">
 77 | 					<ul className="md:flex items-center divide-x w-max hidden shrink-0">
 78 | 						{navMenu.map((menu, i) => (
 79 | 							<NavLink key={menu.name} href={menu.path}>
 80 | 								{menu.name}
 81 | 							</NavLink>
 82 | 						))}
 83 | 						<NavLink
 84 | 							href="https://github.com/better-auth/better-auth"
 85 | 							className=" bg-muted/20"
 86 | 							external
 87 | 						>
 88 | 							<svg
 89 | 								xmlns="http://www.w3.org/2000/svg"
 90 | 								width="1.4em"
 91 | 								height="1.4em"
 92 | 								viewBox="0 0 496 512"
 93 | 							>
 94 | 								<path
 95 | 									fill="currentColor"
 96 | 									d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2"
 97 | 								></path>
 98 | 							</svg>
 99 | 						</NavLink>
100 | 					</ul>
101 | 					<MobileSearchIcon />
102 | 					<ThemeToggle />
103 | 					<NavbarMobileBtn />
104 | 				</div>
105 | 			</nav>
106 | 			<NavbarMobile />
107 | 		</div>
108 | 	);
109 | };
110 | 
111 | export const navMenu = [
112 | 	{
113 | 		name: "helo_",
114 | 		path: "/",
115 | 	},
116 | 	{
117 | 		name: "docs",
118 | 
119 | 		path: "/docs",
120 | 	},
121 | 	{
122 | 		name: "changelogs",
123 | 		path: "/changelogs",
124 | 	},
125 | 	{
126 | 		name: "blogs",
127 | 		path: "/blog",
128 | 	},
129 | 	{
130 | 		name: "community",
131 | 		path: "/community",
132 | 	},
133 | ];
134 | 
```

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

```markdown
  1 | ---
  2 | title: Passkey
  3 | description: Passkey
  4 | ---
  5 | 
  6 | Passkeys are a secure, passwordless authentication method using cryptographic key pairs, supported by WebAuthn and FIDO2 standards in web browsers. They replace passwords with unique key pairs: a private key stored on the user's device and a public key shared with the website. Users can log in using biometrics, PINs, or security keys, providing strong, phishing-resistant authentication without traditional passwords.
  7 | 
  8 | The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplewebauthn.dev/) behind the scenes.
  9 | 
 10 | ## Installation
 11 | 
 12 | <Steps>
 13 |     <Step>
 14 |         ### Add the plugin to your auth config
 15 |         To add the passkey plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
 16 | 
 17 |         **Options**
 18 | 
 19 |         `rpID`: A unique identifier for your website based on your auth server origin. 'localhost' is okay for local dev. RP ID can be formed by discarding zero or more labels from the left of its effective domain
 20 |         until it hits an effective TLD. So `www.example.com` can use the RP IDs `www.example.com` or `example.com`. But not `com`, because that's an eTLD.
 21 | 
 22 |         `rpName`: Human-readable title for your website
 23 | 
 24 |         `origin`: The origin URL at which your better-auth server is hosted. `http://localhost` and `http://localhost:PORT` are also valid. Do **NOT** include any trailing /
 25 | 
 26 |         `authenticatorSelection`: Allows customization of WebAuthn authenticator selection criteria. Leave unspecified for default settings.
 27 |             - `authenticatorAttachment`: Specifies the type of authenticator
 28 |                 - `platform`: Authenticator is attached to the platform (e.g., fingerprint reader)
 29 |                 - `cross-platform`: Authenticator is not attached to the platform (e.g., security key)
 30 |                 - Default: `not set` (both platform and cross-platform allowed, with platform preferred)
 31 |             - `residentKey`: Determines credential storage behavior.
 32 |                 - `required`: User MUST store credentials on the authenticator (highest security)
 33 |                 - `preferred`: Encourages credential storage but not mandatory
 34 |                 - `discouraged`: No credential storage required (fastest experience)
 35 |                 - Default: `preferred`
 36 |             - `userVerification`: Controls biometric/PIN verification during authentication:
 37 |                 - `required`: User MUST verify identity (highest security)
 38 |                 - `preferred`: Verification encouraged but not mandatory
 39 |                 - `discouraged`: No verification required (fastest experience)
 40 |                 - Default: `preferred`
 41 |             
 42 |            
 43 |         ```ts title="auth.ts"
 44 |         import { betterAuth } from "better-auth"
 45 |         import { passkey } from "better-auth/plugins/passkey" // [!code highlight]
 46 | 
 47 |         export const auth = betterAuth({
 48 |             plugins: [ // [!code highlight]
 49 |                 passkey(), // [!code highlight]
 50 |             ], // [!code highlight]
 51 |         })
 52 |         ```
 53 |     </Step>
 54 |       <Step>
 55 |         ### Migrate the database
 56 | 
 57 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 58 | 
 59 |         <Tabs items={["migrate", "generate"]}>
 60 |             <Tab value="migrate">
 61 |             ```bash
 62 |             npx @better-auth/cli migrate
 63 |             ```
 64 |             </Tab>
 65 |             <Tab value="generate">
 66 |             ```bash
 67 |             npx @better-auth/cli generate
 68 |             ```
 69 |             </Tab>
 70 |         </Tabs>
 71 |         See the [Schema](#schema) section to add the fields manually.
 72 |     </Step>
 73 |     <Step>
 74 |         ### Add the client plugin
 75 | 
 76 |         ```ts title="auth-client.ts"
 77 |         import { createAuthClient } from "better-auth/client"
 78 |         import { passkeyClient } from "better-auth/client/plugins"
 79 | 
 80 |         export const authClient = createAuthClient({
 81 |             plugins: [ // [!code highlight]
 82 |                 passkeyClient() // [!code highlight]
 83 |             ] // [!code highlight]
 84 |         })
 85 |         ```
 86 |     </Step>
 87 | 
 88 | </Steps>
 89 | 
 90 | ## Usage
 91 | 
 92 | ### Add/Register a passkey
 93 | 
 94 | To add or register a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
 95 | 
 96 | <APIMethod path="/passkey/add-passkey" method="POST" isClientOnly>
 97 | ```ts
 98 | type addPasskey = {
 99 |     /**
100 |      * An optional name to label the authenticator account being registered. If not provided, it will default to the user's email address or user ID
101 |     */
102 |     name?: string = "example-passkey-name"
103 |     /**
104 |      * You can also specify the type of authenticator you want to register. Default behavior allows both platform and cross-platform passkeys
105 |     */
106 |     authenticatorAttachment?: "platform" | "cross-platform" = "cross-platform"
107 | }
108 | ```
109 | </APIMethod>
110 | 
111 | <Callout>
112 |  Setting `throw: true` in the fetch options has no effect for the register and sign-in passkey responses — they will always return a data object containing the error object.
113 | </Callout>
114 | 
115 | 
116 | ### Sign in with a passkey
117 | 
118 | To sign in with a passkey you can use the `signIn.passkey` method. This will prompt the user to sign in with their passkey.
119 | 
120 | 
121 | <APIMethod path="/sign-in/passkey" method="POST" isClientOnly>
122 | ```ts
123 | type signInPasskey = {
124 |     /**
125 |      * Browser autofill, a.k.a. Conditional UI. Read more: https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui
126 |     */
127 |     autoFill?: boolean = true
128 | }
129 | ```
130 | </APIMethod>
131 | 
132 | #### Example Usage
133 | ```ts
134 | // With post authentication redirect
135 | await authClient.signIn.passkey({
136 |     autoFill: true,
137 |     fetchOptions: {
138 |         onSuccess(context) {
139 |             // Redirect to dashboard after successful authentication
140 |             window.location.href = "/dashboard";
141 |         },
142 |         onError(context) {
143 |             // Handle authentication errors
144 |             console.error("Authentication failed:", context.error.message);
145 |         }
146 |     }
147 | });
148 | ```
149 | 
150 | ### List passkeys
151 | 
152 | You can list all of the passkeys for the authenticated user by calling `passkey.listUserPasskeys`:
153 | 
154 | <APIMethod
155 |   path="/passkey/list-user-passkeys"
156 |   method="GET"
157 |   requireSession
158 |   resultVariable="passkeys"
159 | >
160 | ```ts
161 | type listPasskeys = {
162 | }
163 | ```
164 | </APIMethod>
165 | 
166 | ### Deleting passkeys
167 | 
168 | You can delete a passkey by calling `passkey.delete` and providing the passkey ID.
169 | 
170 | <APIMethod
171 |   path="/passkey/delete-passkey"
172 |   method="POST"
173 |   requireSession
174 | >
175 | ```ts
176 | type deletePasskey = {
177 |     /**
178 |      * The ID of the passkey to delete. 
179 |      */
180 |     id: string = "some-passkey-id"
181 | }
182 | ```
183 | </APIMethod>
184 | 
185 | ### Updating passkey names
186 | 
187 | <APIMethod
188 |   path="/passkey/update-passkey"
189 |   method="POST"
190 |   requireSession
191 | >
192 | ```ts
193 | type updatePasskey = {
194 |     /**
195 |      * The ID of the passkey which you want to update.
196 |      */
197 |     id: string = "id of passkey"
198 |     /**
199 |      * The new name which the passkey will be updated to. 
200 |      */
201 |     name: string = "my-new-passkey-name"
202 | }
203 | ```
204 | </APIMethod>
205 | 
206 | ### Conditional UI
207 | 
208 | The plugin supports conditional UI, which allows the browser to autofill the passkey if the user has already registered a passkey.
209 | 
210 | There are two requirements for conditional UI to work:
211 | 
212 | <Steps>
213 |     <Step>
214 |         #### Update input fields
215 | 
216 |         Add the `autocomplete` attribute with the value `webauthn` to your input fields. You can add this attribute to multiple input fields, but at least one is required for conditional UI to work.
217 | 
218 |         The `webauthn` value should also be the last entry of the `autocomplete` attribute.
219 | 
220 |         ```html
221 |         <label for="name">Username:</label>
222 |         <input type="text" name="name" autocomplete="username webauthn">
223 |         <label for="password">Password:</label>
224 |         <input type="password" name="password" autocomplete="current-password webauthn">
225 |         ```
226 |     </Step>
227 |     <Step>
228 |         #### Preload the passkeys
229 | 
230 |         When your component mounts, you can preload the user's passkeys by calling the `authClient.signIn.passkey` method with the `autoFill` option set to `true`.
231 | 
232 |         To prevent unnecessary calls, we will also add a check to see if the browser supports conditional UI.
233 | 
234 |         <Tabs items={["React"]}>
235 |             <Tab value="React">
236 |             ```ts
237 |             useEffect(() => {
238 |                if (!PublicKeyCredential.isConditionalMediationAvailable ||
239 |                    !PublicKeyCredential.isConditionalMediationAvailable()) {
240 |                  return;
241 |                }
242 | 
243 |               void authClient.signIn.passkey({ autoFill: true })
244 |             }, [])
245 |             ```
246 |             </Tab>
247 |         </Tabs>
248 | 
249 |     </Step>
250 | 
251 |    </Steps>
252 | 
253 | Depending on the browser, a prompt will appear to autofill the passkey. If the user has multiple passkeys, they can select the one they want to use.
254 | 
255 | Some browsers also require the user to first interact with the input field before the autofill prompt appears.
256 | 
257 | ### Debugging
258 | 
259 | To test your passkey implementation you can use [emulated authenticators](https://developer.chrome.com/docs/devtools/webauthn). This way you can test the registration and sign-in process without even owning a physical device.
260 | 
261 | ## Schema
262 | 
263 | The plugin require a new table in the database to store passkey data.
264 | 
265 | Table Name: `passkey`
266 | 
267 | <DatabaseTable
268 |     fields={[
269 |         { 
270 |             name: "id", 
271 |             type: "string", 
272 |             description: "Unique identifier for each passkey",
273 |             isPrimaryKey: true
274 |         },
275 |         {
276 |             name: "name",
277 |             type: "string",
278 |             description: "The name of the passkey",
279 |             isOptional: true
280 |         },
281 |         { 
282 |             name: "publicKey", 
283 |             type: "string", 
284 |             description: "The public key of the passkey",
285 |         },
286 |         { 
287 |             name: "userId", 
288 |             type: "string", 
289 |             description: "The ID of the user",
290 |             isForeignKey: true
291 |         },
292 |         {
293 |             name: "credentialID",
294 |             type: "string",
295 |             description: "The unique identifier of the registered credential",
296 |         },
297 |         { 
298 |             name: "counter", 
299 |             type: "number", 
300 |             description: "The counter of the passkey",
301 |         },
302 |         {
303 |             name: "deviceType",
304 |             type: "string",
305 |             description: "The type of device used to register the passkey",
306 |         },
307 |         {
308 |             name: "backedUp",
309 |             type: "boolean",
310 |             description: "Whether the passkey is backed up",
311 |         },
312 |         {
313 |             name: "transports",
314 |             type: "string",
315 |             description: "The transports used to register the passkey",
316 |             isOptional: true
317 |         },
318 |         { 
319 |             name: "createdAt", 
320 |             type: "Date", 
321 |             description: "The time when the passkey was created",
322 |             isOptional: true
323 |         },
324 |         {
325 |                 name: "aaguid",
326 |                 type: "string",
327 |                 description: "Authenticator's Attestation GUID indicating the type of the authenticator",
328 |                 isOptional: true
329 |         },
330 |     ]}
331 |     />
332 | 
333 | ## Options
334 | 
335 | **rpID**: A unique identifier for your website based on your auth server origin.
336 | `'localhost'` is okay for local dev. RP ID can be formed by discarding zero or more labels from the left of its effective domain
337 | until it hits an effective TLD. So `www.example.com` can use the RP IDs `www.example.com` or `example.com`. But not `com`, because that's an eTLD.
338 | 
339 | **rpName**: Human-readable title for your website.
340 | 
341 | **origin**: The origin URL at which your better-auth server is hosted. `http://localhost` and `http://localhost:PORT` are also valid. Do NOT include any trailing /.
342 | 
343 | **authenticatorSelection**: Allows customization of WebAuthn authenticator selection criteria. When unspecified, both platform and cross-platform authenticators are allowed with `preferred` settings for `residentKey` and `userVerification`.
344 | 
345 | **aaguid**: (optional) Authenticator Attestation GUID. This is a unique identifier for the passkey provider (device or authenticator type) and can be used to identify the type of passkey device used during registration or authentication.
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/api-key/routes/update-api-key.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { AuthContext } from "@better-auth/core";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import * as z from "zod";
  4 | import { APIError, getSessionFromCtx } from "../../../api";
  5 | import { getDate } from "../../../utils/date";
  6 | import { safeJSONParse } from "../../../utils/json";
  7 | import { API_KEY_TABLE_NAME, ERROR_CODES } from "..";
  8 | import type { apiKeySchema } from "../schema";
  9 | import type { ApiKey } from "../types";
 10 | import type { PredefinedApiKeyOptions } from ".";
 11 | export function updateApiKey({
 12 | 	opts,
 13 | 	schema,
 14 | 	deleteAllExpiredApiKeys,
 15 | }: {
 16 | 	opts: PredefinedApiKeyOptions;
 17 | 	schema: ReturnType<typeof apiKeySchema>;
 18 | 	deleteAllExpiredApiKeys(
 19 | 		ctx: AuthContext,
 20 | 		byPassLastCheckTime?: boolean,
 21 | 	): void;
 22 | }) {
 23 | 	return createAuthEndpoint(
 24 | 		"/api-key/update",
 25 | 		{
 26 | 			method: "POST",
 27 | 			body: z.object({
 28 | 				keyId: z.string().meta({
 29 | 					description: "The id of the Api Key",
 30 | 				}),
 31 | 				userId: z.coerce
 32 | 					.string()
 33 | 					.meta({
 34 | 						description:
 35 | 							'The id of the user which the api key belongs to. server-only. Eg: "some-user-id"',
 36 | 					})
 37 | 					.optional(),
 38 | 				name: z
 39 | 					.string()
 40 | 					.meta({
 41 | 						description: "The name of the key",
 42 | 					})
 43 | 					.optional(),
 44 | 				enabled: z
 45 | 					.boolean()
 46 | 					.meta({
 47 | 						description: "Whether the Api Key is enabled or not",
 48 | 					})
 49 | 					.optional(),
 50 | 				remaining: z
 51 | 					.number()
 52 | 					.meta({
 53 | 						description: "The number of remaining requests",
 54 | 					})
 55 | 					.min(1)
 56 | 					.optional(),
 57 | 				refillAmount: z
 58 | 					.number()
 59 | 					.meta({
 60 | 						description: "The refill amount",
 61 | 					})
 62 | 					.optional(),
 63 | 				refillInterval: z
 64 | 					.number()
 65 | 					.meta({
 66 | 						description: "The refill interval",
 67 | 					})
 68 | 					.optional(),
 69 | 				metadata: z.any().optional(),
 70 | 				expiresIn: z
 71 | 					.number()
 72 | 					.meta({
 73 | 						description: "Expiration time of the Api Key in seconds",
 74 | 					})
 75 | 					.min(1)
 76 | 					.optional()
 77 | 					.nullable(),
 78 | 				rateLimitEnabled: z
 79 | 					.boolean()
 80 | 					.meta({
 81 | 						description: "Whether the key has rate limiting enabled.",
 82 | 					})
 83 | 					.optional(),
 84 | 				rateLimitTimeWindow: z
 85 | 					.number()
 86 | 					.meta({
 87 | 						description:
 88 | 							"The duration in milliseconds where each request is counted. server-only. Eg: 1000",
 89 | 					})
 90 | 					.optional(),
 91 | 				rateLimitMax: z
 92 | 					.number()
 93 | 					.meta({
 94 | 						description:
 95 | 							"Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100",
 96 | 					})
 97 | 					.optional(),
 98 | 				permissions: z
 99 | 					.record(z.string(), z.array(z.string()))
100 | 					.meta({
101 | 						description: "Update the permissions on the API Key. server-only.",
102 | 					})
103 | 					.optional()
104 | 					.nullable(),
105 | 			}),
106 | 			metadata: {
107 | 				openapi: {
108 | 					description: "Update an existing API key by ID",
109 | 					responses: {
110 | 						"200": {
111 | 							description: "API key updated successfully",
112 | 							content: {
113 | 								"application/json": {
114 | 									schema: {
115 | 										type: "object",
116 | 										properties: {
117 | 											id: {
118 | 												type: "string",
119 | 												description: "ID",
120 | 											},
121 | 											name: {
122 | 												type: "string",
123 | 												nullable: true,
124 | 												description: "The name of the key",
125 | 											},
126 | 											start: {
127 | 												type: "string",
128 | 												nullable: true,
129 | 												description:
130 | 													"Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key.",
131 | 											},
132 | 											prefix: {
133 | 												type: "string",
134 | 												nullable: true,
135 | 												description:
136 | 													"The API Key prefix. Stored as plain text.",
137 | 											},
138 | 											userId: {
139 | 												type: "string",
140 | 												description: "The owner of the user id",
141 | 											},
142 | 											refillInterval: {
143 | 												type: "number",
144 | 												nullable: true,
145 | 												description:
146 | 													"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
147 | 											},
148 | 											refillAmount: {
149 | 												type: "number",
150 | 												nullable: true,
151 | 												description: "The amount to refill",
152 | 											},
153 | 											lastRefillAt: {
154 | 												type: "string",
155 | 												format: "date-time",
156 | 												nullable: true,
157 | 												description: "The last refill date",
158 | 											},
159 | 											enabled: {
160 | 												type: "boolean",
161 | 												description: "Sets if key is enabled or disabled",
162 | 												default: true,
163 | 											},
164 | 											rateLimitEnabled: {
165 | 												type: "boolean",
166 | 												description:
167 | 													"Whether the key has rate limiting enabled",
168 | 											},
169 | 											rateLimitTimeWindow: {
170 | 												type: "number",
171 | 												nullable: true,
172 | 												description: "The duration in milliseconds",
173 | 											},
174 | 											rateLimitMax: {
175 | 												type: "number",
176 | 												nullable: true,
177 | 												description:
178 | 													"Maximum amount of requests allowed within a window",
179 | 											},
180 | 											requestCount: {
181 | 												type: "number",
182 | 												description:
183 | 													"The number of requests made within the rate limit time window",
184 | 											},
185 | 											remaining: {
186 | 												type: "number",
187 | 												nullable: true,
188 | 												description:
189 | 													"Remaining requests (every time api key is used this should updated and should be updated on refill as well)",
190 | 											},
191 | 											lastRequest: {
192 | 												type: "string",
193 | 												format: "date-time",
194 | 												nullable: true,
195 | 												description: "When last request occurred",
196 | 											},
197 | 											expiresAt: {
198 | 												type: "string",
199 | 												format: "date-time",
200 | 												nullable: true,
201 | 												description: "Expiry date of a key",
202 | 											},
203 | 											createdAt: {
204 | 												type: "string",
205 | 												format: "date-time",
206 | 												description: "created at",
207 | 											},
208 | 											updatedAt: {
209 | 												type: "string",
210 | 												format: "date-time",
211 | 												description: "updated at",
212 | 											},
213 | 											metadata: {
214 | 												type: "object",
215 | 												nullable: true,
216 | 												additionalProperties: true,
217 | 												description: "Extra metadata about the apiKey",
218 | 											},
219 | 											permissions: {
220 | 												type: "string",
221 | 												nullable: true,
222 | 												description:
223 | 													"Permissions for the api key (stored as JSON string)",
224 | 											},
225 | 										},
226 | 										required: [
227 | 											"id",
228 | 											"userId",
229 | 											"enabled",
230 | 											"rateLimitEnabled",
231 | 											"requestCount",
232 | 											"createdAt",
233 | 											"updatedAt",
234 | 										],
235 | 									},
236 | 								},
237 | 							},
238 | 						},
239 | 					},
240 | 				},
241 | 			},
242 | 		},
243 | 		async (ctx) => {
244 | 			const {
245 | 				keyId,
246 | 				expiresIn,
247 | 				enabled,
248 | 				metadata,
249 | 				refillAmount,
250 | 				refillInterval,
251 | 				remaining,
252 | 				name,
253 | 				permissions,
254 | 				rateLimitEnabled,
255 | 				rateLimitTimeWindow,
256 | 				rateLimitMax,
257 | 			} = ctx.body;
258 | 
259 | 			const session = await getSessionFromCtx(ctx);
260 | 			const authRequired = ctx.request || ctx.headers;
261 | 			const user =
262 | 				authRequired && !session
263 | 					? null
264 | 					: session?.user || { id: ctx.body.userId };
265 | 
266 | 			if (!user?.id) {
267 | 				throw new APIError("UNAUTHORIZED", {
268 | 					message: ERROR_CODES.UNAUTHORIZED_SESSION,
269 | 				});
270 | 			}
271 | 
272 | 			if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) {
273 | 				throw new APIError("UNAUTHORIZED", {
274 | 					message: ERROR_CODES.UNAUTHORIZED_SESSION,
275 | 				});
276 | 			}
277 | 
278 | 			if (authRequired) {
279 | 				// if this endpoint was being called from the client,
280 | 				// we must make sure they can't use server-only properties.
281 | 				if (
282 | 					refillAmount !== undefined ||
283 | 					refillInterval !== undefined ||
284 | 					rateLimitMax !== undefined ||
285 | 					rateLimitTimeWindow !== undefined ||
286 | 					rateLimitEnabled !== undefined ||
287 | 					remaining !== undefined ||
288 | 					permissions !== undefined
289 | 				) {
290 | 					throw new APIError("BAD_REQUEST", {
291 | 						message: ERROR_CODES.SERVER_ONLY_PROPERTY,
292 | 					});
293 | 				}
294 | 			}
295 | 
296 | 			const apiKey = await ctx.context.adapter.findOne<ApiKey>({
297 | 				model: API_KEY_TABLE_NAME,
298 | 				where: [
299 | 					{
300 | 						field: "id",
301 | 						value: keyId,
302 | 					},
303 | 					{
304 | 						field: "userId",
305 | 						value: user.id,
306 | 					},
307 | 				],
308 | 			});
309 | 
310 | 			if (!apiKey) {
311 | 				throw new APIError("NOT_FOUND", {
312 | 					message: ERROR_CODES.KEY_NOT_FOUND,
313 | 				});
314 | 			}
315 | 
316 | 			let newValues: Partial<ApiKey> = {};
317 | 
318 | 			if (name !== undefined) {
319 | 				if (name.length < opts.minimumNameLength) {
320 | 					throw new APIError("BAD_REQUEST", {
321 | 						message: ERROR_CODES.INVALID_NAME_LENGTH,
322 | 					});
323 | 				} else if (name.length > opts.maximumNameLength) {
324 | 					throw new APIError("BAD_REQUEST", {
325 | 						message: ERROR_CODES.INVALID_NAME_LENGTH,
326 | 					});
327 | 				}
328 | 				newValues.name = name;
329 | 			}
330 | 
331 | 			if (enabled !== undefined) {
332 | 				newValues.enabled = enabled;
333 | 			}
334 | 			if (expiresIn !== undefined) {
335 | 				if (opts.keyExpiration.disableCustomExpiresTime === true) {
336 | 					throw new APIError("BAD_REQUEST", {
337 | 						message: ERROR_CODES.KEY_DISABLED_EXPIRATION,
338 | 					});
339 | 				}
340 | 				if (expiresIn !== null) {
341 | 					// if expires is not null, check if it's under the valid range
342 | 					// if it IS null, this means the user wants to disable expiration time on the key
343 | 					const expiresIn_in_days = expiresIn / (60 * 60 * 24);
344 | 
345 | 					if (expiresIn_in_days < opts.keyExpiration.minExpiresIn) {
346 | 						throw new APIError("BAD_REQUEST", {
347 | 							message: ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL,
348 | 						});
349 | 					} else if (expiresIn_in_days > opts.keyExpiration.maxExpiresIn) {
350 | 						throw new APIError("BAD_REQUEST", {
351 | 							message: ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE,
352 | 						});
353 | 					}
354 | 				}
355 | 				newValues.expiresAt = expiresIn ? getDate(expiresIn, "sec") : null;
356 | 			}
357 | 			if (metadata !== undefined) {
358 | 				if (typeof metadata !== "object") {
359 | 					throw new APIError("BAD_REQUEST", {
360 | 						message: ERROR_CODES.INVALID_METADATA_TYPE,
361 | 					});
362 | 				}
363 | 				//@ts-expect-error - we need this to be a string to save into DB.
364 | 				newValues.metadata =
365 | 					schema.apikey.fields.metadata.transform.input(metadata);
366 | 			}
367 | 			if (remaining !== undefined) {
368 | 				newValues.remaining = remaining;
369 | 			}
370 | 			if (refillAmount !== undefined || refillInterval !== undefined) {
371 | 				if (refillAmount !== undefined && refillInterval === undefined) {
372 | 					throw new APIError("BAD_REQUEST", {
373 | 						message: ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED,
374 | 					});
375 | 				} else if (refillInterval !== undefined && refillAmount === undefined) {
376 | 					throw new APIError("BAD_REQUEST", {
377 | 						message: ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED,
378 | 					});
379 | 				}
380 | 				newValues.refillAmount = refillAmount;
381 | 				newValues.refillInterval = refillInterval;
382 | 			}
383 | 
384 | 			if (rateLimitEnabled !== undefined) {
385 | 				newValues.rateLimitEnabled = rateLimitEnabled;
386 | 			}
387 | 			if (rateLimitTimeWindow !== undefined) {
388 | 				newValues.rateLimitTimeWindow = rateLimitTimeWindow;
389 | 			}
390 | 			if (rateLimitMax !== undefined) {
391 | 				newValues.rateLimitMax = rateLimitMax;
392 | 			}
393 | 
394 | 			if (permissions !== undefined) {
395 | 				//@ts-expect-error - we need this to be a string to save into DB.
396 | 				newValues.permissions = JSON.stringify(permissions);
397 | 			}
398 | 
399 | 			if (Object.keys(newValues).length === 0) {
400 | 				throw new APIError("BAD_REQUEST", {
401 | 					message: ERROR_CODES.NO_VALUES_TO_UPDATE,
402 | 				});
403 | 			}
404 | 
405 | 			let newApiKey: ApiKey = apiKey;
406 | 			try {
407 | 				let result = await ctx.context.adapter.update<ApiKey>({
408 | 					model: API_KEY_TABLE_NAME,
409 | 					where: [
410 | 						{
411 | 							field: "id",
412 | 							value: apiKey.id,
413 | 						},
414 | 					],
415 | 					update: {
416 | 						...newValues,
417 | 					},
418 | 				});
419 | 				if (result) newApiKey = result;
420 | 			} catch (error: any) {
421 | 				throw new APIError("INTERNAL_SERVER_ERROR", {
422 | 					message: error?.message,
423 | 				});
424 | 			}
425 | 
426 | 			deleteAllExpiredApiKeys(ctx.context);
427 | 
428 | 			// transform metadata from string back to object
429 | 			newApiKey.metadata = schema.apikey.fields.metadata.transform.output(
430 | 				newApiKey.metadata as never as string,
431 | 			);
432 | 
433 | 			const { key, ...returningApiKey } = newApiKey;
434 | 
435 | 			return ctx.json({
436 | 				...returningApiKey,
437 | 				permissions: returningApiKey.permissions
438 | 					? safeJSONParse<{
439 | 							[key: string]: string[];
440 | 						}>(returningApiKey.permissions)
441 | 					: null,
442 | 			});
443 | 		},
444 | 	);
445 | }
446 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/open-api/generator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { AuthContext, BetterAuthOptions } from "@better-auth/core";
  2 | import type {
  3 | 	DBFieldAttribute,
  4 | 	DBFieldAttributeConfig,
  5 | 	DBFieldType,
  6 | } from "@better-auth/core/db";
  7 | import type {
  8 | 	Endpoint,
  9 | 	EndpointOptions,
 10 | 	OpenAPIParameter,
 11 | 	OpenAPISchemaType,
 12 | } from "better-call";
 13 | import * as z from "zod";
 14 | import { getEndpoints } from "../../api";
 15 | import { getAuthTables } from "../../db";
 16 | 
 17 | export interface Path {
 18 | 	get?: {
 19 | 		tags?: string[];
 20 | 		operationId?: string;
 21 | 		description?: string;
 22 | 		security?: [{ bearerAuth: string[] }];
 23 | 		parameters?: OpenAPIParameter[];
 24 | 		responses?: {
 25 | 			[key in string]: {
 26 | 				description?: string;
 27 | 				content: {
 28 | 					"application/json": {
 29 | 						schema: {
 30 | 							type?: OpenAPISchemaType;
 31 | 							properties?: Record<string, any>;
 32 | 							required?: string[];
 33 | 							$ref?: string;
 34 | 						};
 35 | 					};
 36 | 				};
 37 | 			};
 38 | 		};
 39 | 	};
 40 | 	post?: {
 41 | 		tags?: string[];
 42 | 		operationId?: string;
 43 | 		description?: string;
 44 | 		security?: [{ bearerAuth: string[] }];
 45 | 		parameters?: OpenAPIParameter[];
 46 | 		requestBody?: {
 47 | 			content: {
 48 | 				"application/json": {
 49 | 					schema: {
 50 | 						type?: OpenAPISchemaType;
 51 | 						properties?: Record<string, any>;
 52 | 						required?: string[];
 53 | 						$ref?: string;
 54 | 					};
 55 | 				};
 56 | 			};
 57 | 		};
 58 | 		responses?: {
 59 | 			[key in string]: {
 60 | 				description?: string;
 61 | 				content: {
 62 | 					"application/json": {
 63 | 						schema: {
 64 | 							type?: OpenAPISchemaType;
 65 | 							properties?: Record<string, any>;
 66 | 							required?: string[];
 67 | 							$ref?: string;
 68 | 						};
 69 | 					};
 70 | 				};
 71 | 			};
 72 | 		};
 73 | 	};
 74 | }
 75 | 
 76 | type AllowedType = "string" | "number" | "boolean" | "array" | "object";
 77 | const allowedType = new Set(["string", "number", "boolean", "array", "object"]);
 78 | function getTypeFromZodType(zodType: z.ZodType<any>) {
 79 | 	const type = zodType.type;
 80 | 	return allowedType.has(type) ? (type as AllowedType) : "string";
 81 | }
 82 | 
 83 | export type FieldSchema = {
 84 | 	type: DBFieldType;
 85 | 	default?: DBFieldAttributeConfig["defaultValue"] | "Generated at runtime";
 86 | 	readOnly?: boolean;
 87 | };
 88 | 
 89 | export type OpenAPIModelSchema = {
 90 | 	type: "object";
 91 | 	properties: Record<string, FieldSchema>;
 92 | 	required?: string[];
 93 | };
 94 | 
 95 | function getFieldSchema(field: DBFieldAttribute) {
 96 | 	const schema: FieldSchema = {
 97 | 		type: field.type === "date" ? "string" : field.type,
 98 | 	};
 99 | 
100 | 	if (field.defaultValue !== undefined) {
101 | 		schema.default =
102 | 			typeof field.defaultValue === "function"
103 | 				? "Generated at runtime"
104 | 				: field.defaultValue;
105 | 	}
106 | 
107 | 	if (field.input === false) {
108 | 		schema.readOnly = true;
109 | 	}
110 | 
111 | 	return schema;
112 | }
113 | 
114 | function getParameters(options: EndpointOptions) {
115 | 	const parameters: OpenAPIParameter[] = [];
116 | 	if (options.metadata?.openapi?.parameters) {
117 | 		parameters.push(...options.metadata.openapi.parameters);
118 | 		return parameters;
119 | 	}
120 | 	if (options.query instanceof z.ZodObject) {
121 | 		Object.entries(options.query.shape).forEach(([key, value]) => {
122 | 			if (value instanceof z.ZodType) {
123 | 				parameters.push({
124 | 					name: key,
125 | 					in: "query",
126 | 					schema: {
127 | 						...processZodType(value as z.ZodType<any>),
128 | 						...("minLength" in value && (value as any).minLength
129 | 							? {
130 | 									minLength: (value as any).minLength as number,
131 | 								}
132 | 							: {}),
133 | 					},
134 | 				});
135 | 			}
136 | 		});
137 | 	}
138 | 	return parameters;
139 | }
140 | 
141 | function getRequestBody(options: EndpointOptions): any {
142 | 	if (options.metadata?.openapi?.requestBody) {
143 | 		return options.metadata.openapi.requestBody;
144 | 	}
145 | 	if (!options.body) return undefined;
146 | 	if (
147 | 		options.body instanceof z.ZodObject ||
148 | 		options.body instanceof z.ZodOptional
149 | 	) {
150 | 		// @ts-expect-error
151 | 		const shape = options.body.shape;
152 | 		if (!shape) return undefined;
153 | 		const properties: Record<string, any> = {};
154 | 		const required: string[] = [];
155 | 		Object.entries(shape).forEach(([key, value]) => {
156 | 			if (value instanceof z.ZodType) {
157 | 				properties[key] = processZodType(value as z.ZodType<any>);
158 | 				if (!(value instanceof z.ZodOptional)) {
159 | 					required.push(key);
160 | 				}
161 | 			}
162 | 		});
163 | 		return {
164 | 			required:
165 | 				options.body instanceof z.ZodOptional
166 | 					? false
167 | 					: options.body
168 | 						? true
169 | 						: false,
170 | 			content: {
171 | 				"application/json": {
172 | 					schema: {
173 | 						type: "object",
174 | 						properties,
175 | 						required,
176 | 					},
177 | 				},
178 | 			},
179 | 		};
180 | 	}
181 | 	return undefined;
182 | }
183 | 
184 | function processZodType(zodType: z.ZodType<any>): any {
185 | 	// optional unwrapping
186 | 	if (zodType instanceof z.ZodOptional) {
187 | 		const innerType = (zodType as any)._def.innerType;
188 | 		const innerSchema = processZodType(innerType);
189 | 		return {
190 | 			...innerSchema,
191 | 			nullable: true,
192 | 		};
193 | 	}
194 | 	// object unwrapping
195 | 	if (zodType instanceof z.ZodObject) {
196 | 		const shape = (zodType as any).shape;
197 | 		if (shape) {
198 | 			const properties: Record<string, any> = {};
199 | 			const required: string[] = [];
200 | 			Object.entries(shape).forEach(([key, value]) => {
201 | 				if (value instanceof z.ZodType) {
202 | 					properties[key] = processZodType(value as z.ZodType<any>);
203 | 					if (!(value instanceof z.ZodOptional)) {
204 | 						required.push(key);
205 | 					}
206 | 				}
207 | 			});
208 | 			return {
209 | 				type: "object",
210 | 				properties,
211 | 				...(required.length > 0 ? { required } : {}),
212 | 				description: (zodType as any).description,
213 | 			};
214 | 		}
215 | 	}
216 | 
217 | 	// For primitive types, get the correct type from the unwrapped ZodType
218 | 	const baseSchema = {
219 | 		type: getTypeFromZodType(zodType),
220 | 		description: (zodType as any).description,
221 | 	};
222 | 
223 | 	return baseSchema;
224 | }
225 | 
226 | function getResponse(responses?: Record<string, any>) {
227 | 	return {
228 | 		"400": {
229 | 			content: {
230 | 				"application/json": {
231 | 					schema: {
232 | 						type: "object",
233 | 						properties: {
234 | 							message: {
235 | 								type: "string",
236 | 							},
237 | 						},
238 | 						required: ["message"],
239 | 					},
240 | 				},
241 | 			},
242 | 			description:
243 | 				"Bad Request. Usually due to missing parameters, or invalid parameters.",
244 | 		},
245 | 		"401": {
246 | 			content: {
247 | 				"application/json": {
248 | 					schema: {
249 | 						type: "object",
250 | 						properties: {
251 | 							message: {
252 | 								type: "string",
253 | 							},
254 | 						},
255 | 						required: ["message"],
256 | 					},
257 | 				},
258 | 			},
259 | 			description: "Unauthorized. Due to missing or invalid authentication.",
260 | 		},
261 | 		"403": {
262 | 			content: {
263 | 				"application/json": {
264 | 					schema: {
265 | 						type: "object",
266 | 						properties: {
267 | 							message: {
268 | 								type: "string",
269 | 							},
270 | 						},
271 | 					},
272 | 				},
273 | 			},
274 | 			description:
275 | 				"Forbidden. You do not have permission to access this resource or to perform this action.",
276 | 		},
277 | 		"404": {
278 | 			content: {
279 | 				"application/json": {
280 | 					schema: {
281 | 						type: "object",
282 | 						properties: {
283 | 							message: {
284 | 								type: "string",
285 | 							},
286 | 						},
287 | 					},
288 | 				},
289 | 			},
290 | 			description: "Not Found. The requested resource was not found.",
291 | 		},
292 | 		"429": {
293 | 			content: {
294 | 				"application/json": {
295 | 					schema: {
296 | 						type: "object",
297 | 						properties: {
298 | 							message: {
299 | 								type: "string",
300 | 							},
301 | 						},
302 | 					},
303 | 				},
304 | 			},
305 | 			description:
306 | 				"Too Many Requests. You have exceeded the rate limit. Try again later.",
307 | 		},
308 | 		"500": {
309 | 			content: {
310 | 				"application/json": {
311 | 					schema: {
312 | 						type: "object",
313 | 						properties: {
314 | 							message: {
315 | 								type: "string",
316 | 							},
317 | 						},
318 | 					},
319 | 				},
320 | 			},
321 | 			description:
322 | 				"Internal Server Error. This is a problem with the server that you cannot fix.",
323 | 		},
324 | 		...responses,
325 | 	} as any;
326 | }
327 | 
328 | function toOpenApiPath(path: string) {
329 | 	// /reset-password/:token -> /reset-password/{token}
330 | 	// replace all : with {}
331 | 	return path
332 | 		.split("/")
333 | 		.map((part) => (part.startsWith(":") ? `{${part.slice(1)}}` : part))
334 | 		.join("/");
335 | }
336 | 
337 | export async function generator(ctx: AuthContext, options: BetterAuthOptions) {
338 | 	const baseEndpoints = getEndpoints(ctx, {
339 | 		...options,
340 | 		plugins: [],
341 | 	});
342 | 
343 | 	const tables = getAuthTables(options);
344 | 	const models = Object.entries(tables).reduce<
345 | 		Record<string, OpenAPIModelSchema>
346 | 	>((acc, [key, value]) => {
347 | 		const modelName = key.charAt(0).toUpperCase() + key.slice(1);
348 | 		const fields = value.fields;
349 | 		const required: string[] = [];
350 | 		const properties: Record<string, FieldSchema> = {
351 | 			id: { type: "string" },
352 | 		};
353 | 		Object.entries(fields).forEach(([fieldKey, fieldValue]) => {
354 | 			if (!fieldValue) return;
355 | 			properties[fieldKey] = getFieldSchema(fieldValue);
356 | 			if (fieldValue.required && fieldValue.input !== false) {
357 | 				required.push(fieldKey);
358 | 			}
359 | 		});
360 | 
361 | 		acc[modelName] = {
362 | 			type: "object",
363 | 			properties,
364 | 			...(required.length > 0 ? { required } : {}),
365 | 		};
366 | 		return acc;
367 | 	}, {});
368 | 
369 | 	const components = {
370 | 		schemas: {
371 | 			...models,
372 | 		},
373 | 	};
374 | 
375 | 	const paths: Record<string, Path> = {};
376 | 
377 | 	Object.entries(baseEndpoints.api).forEach(([_, value]) => {
378 | 		if (ctx.options.disabledPaths?.includes(value.path)) return;
379 | 		const options = value.options as EndpointOptions;
380 | 		if (options.metadata?.SERVER_ONLY) return;
381 | 		const path = toOpenApiPath(value.path);
382 | 		if (options.method === "GET") {
383 | 			paths[path] = {
384 | 				get: {
385 | 					tags: ["Default", ...(options.metadata?.openapi?.tags || [])],
386 | 					description: options.metadata?.openapi?.description,
387 | 					operationId: options.metadata?.openapi?.operationId,
388 | 					security: [
389 | 						{
390 | 							bearerAuth: [],
391 | 						},
392 | 					],
393 | 					parameters: getParameters(options),
394 | 					responses: getResponse(options.metadata?.openapi?.responses),
395 | 				},
396 | 			};
397 | 		}
398 | 
399 | 		if (options.method === "POST") {
400 | 			const body = getRequestBody(options);
401 | 			paths[path] = {
402 | 				post: {
403 | 					tags: ["Default", ...(options.metadata?.openapi?.tags || [])],
404 | 					description: options.metadata?.openapi?.description,
405 | 					operationId: options.metadata?.openapi?.operationId,
406 | 					security: [
407 | 						{
408 | 							bearerAuth: [],
409 | 						},
410 | 					],
411 | 					parameters: getParameters(options),
412 | 					...(body
413 | 						? { requestBody: body }
414 | 						: {
415 | 								requestBody: {
416 | 									//set body none
417 | 									content: {
418 | 										"application/json": {
419 | 											schema: {
420 | 												type: "object",
421 | 												properties: {},
422 | 											},
423 | 										},
424 | 									},
425 | 								},
426 | 							}),
427 | 					responses: getResponse(options.metadata?.openapi?.responses),
428 | 				},
429 | 			};
430 | 		}
431 | 	});
432 | 
433 | 	for (const plugin of options.plugins || []) {
434 | 		if (plugin.id === "open-api") {
435 | 			continue;
436 | 		}
437 | 		const pluginEndpoints = getEndpoints(ctx, {
438 | 			...options,
439 | 			plugins: [plugin],
440 | 		});
441 | 		const api = Object.keys(pluginEndpoints.api)
442 | 			.map((key) => {
443 | 				if (
444 | 					baseEndpoints.api[key as keyof typeof baseEndpoints.api] === undefined
445 | 				) {
446 | 					return pluginEndpoints.api[key as keyof typeof pluginEndpoints.api];
447 | 				}
448 | 				return null;
449 | 			})
450 | 			.filter((x) => x !== null) as Endpoint[];
451 | 		Object.entries(api).forEach(([key, value]) => {
452 | 			if (ctx.options.disabledPaths?.includes(value.path)) return;
453 | 			const options = value.options as EndpointOptions;
454 | 			if (options.metadata?.SERVER_ONLY) return;
455 | 			const path = toOpenApiPath(value.path);
456 | 			if (options.method === "GET") {
457 | 				paths[path] = {
458 | 					get: {
459 | 						tags: options.metadata?.openapi?.tags || [
460 | 							plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1),
461 | 						],
462 | 						description: options.metadata?.openapi?.description,
463 | 						operationId: options.metadata?.openapi?.operationId,
464 | 						security: [
465 | 							{
466 | 								bearerAuth: [],
467 | 							},
468 | 						],
469 | 						parameters: getParameters(options),
470 | 						responses: getResponse(options.metadata?.openapi?.responses),
471 | 					},
472 | 				};
473 | 			}
474 | 			if (options.method === "POST") {
475 | 				paths[path] = {
476 | 					post: {
477 | 						tags: options.metadata?.openapi?.tags || [
478 | 							plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1),
479 | 						],
480 | 						description: options.metadata?.openapi?.description,
481 | 						operationId: options.metadata?.openapi?.operationId,
482 | 						security: [
483 | 							{
484 | 								bearerAuth: [],
485 | 							},
486 | 						],
487 | 						parameters: getParameters(options),
488 | 						requestBody: getRequestBody(options),
489 | 						responses: getResponse(options.metadata?.openapi?.responses),
490 | 					},
491 | 				};
492 | 			}
493 | 		});
494 | 	}
495 | 
496 | 	const res = {
497 | 		openapi: "3.1.1",
498 | 		info: {
499 | 			title: "Better Auth",
500 | 			description: "API Reference for your Better Auth Instance",
501 | 			version: "1.1.0",
502 | 		},
503 | 		components: {
504 | 			...components,
505 | 			securitySchemes: {
506 | 				apiKeyCookie: {
507 | 					type: "apiKey",
508 | 					in: "cookie",
509 | 					name: "apiKeyCookie",
510 | 					description: "API Key authentication via cookie",
511 | 				},
512 | 				bearerAuth: {
513 | 					type: "http",
514 | 					scheme: "bearer",
515 | 					description: "Bearer token authentication",
516 | 				},
517 | 			},
518 | 		},
519 | 		security: [
520 | 			{
521 | 				apiKeyCookie: [],
522 | 				bearerAuth: [],
523 | 			},
524 | 		],
525 | 		servers: [
526 | 			{
527 | 				url: ctx.baseURL,
528 | 			},
529 | 		],
530 | 		tags: [
531 | 			{
532 | 				name: "Default",
533 | 				description:
534 | 					"Default endpoints that are included with Better Auth by default. These endpoints are not part of any plugin.",
535 | 			},
536 | 		],
537 | 		paths,
538 | 	};
539 | 	return res;
540 | }
541 | 
```

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

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

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

```typescript
  1 | import type { BetterAuthOptions } from "@better-auth/core";
  2 | import type { DBFieldAttribute, DBFieldType } from "@better-auth/core/db";
  3 | import { createLogger } from "@better-auth/core/env";
  4 | import type {
  5 | 	AlterTableColumnAlteringBuilder,
  6 | 	CreateTableBuilder,
  7 | 	Kysely,
  8 | } from "kysely";
  9 | import { sql } from "kysely";
 10 | import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect";
 11 | import type { KyselyDatabaseType } from "../adapters/kysely-adapter/types";
 12 | import { getSchema } from "./get-schema";
 13 | 
 14 | const postgresMap = {
 15 | 	string: ["character varying", "varchar", "text"],
 16 | 	number: [
 17 | 		"int4",
 18 | 		"integer",
 19 | 		"bigint",
 20 | 		"smallint",
 21 | 		"numeric",
 22 | 		"real",
 23 | 		"double precision",
 24 | 	],
 25 | 	boolean: ["bool", "boolean"],
 26 | 	date: ["timestamptz", "timestamp", "date"],
 27 | 	json: ["json", "jsonb"],
 28 | };
 29 | const mysqlMap = {
 30 | 	string: ["varchar", "text"],
 31 | 	number: [
 32 | 		"integer",
 33 | 		"int",
 34 | 		"bigint",
 35 | 		"smallint",
 36 | 		"decimal",
 37 | 		"float",
 38 | 		"double",
 39 | 	],
 40 | 	boolean: ["boolean", "tinyint"],
 41 | 	date: ["timestamp", "datetime", "date"],
 42 | 	json: ["json"],
 43 | };
 44 | 
 45 | const sqliteMap = {
 46 | 	string: ["TEXT"],
 47 | 	number: ["INTEGER", "REAL"],
 48 | 	boolean: ["INTEGER", "BOOLEAN"], // 0 or 1
 49 | 	date: ["DATE", "INTEGER"],
 50 | 	json: ["TEXT"],
 51 | };
 52 | 
 53 | const mssqlMap = {
 54 | 	string: ["varchar", "nvarchar"],
 55 | 	number: ["int", "bigint", "smallint", "decimal", "float", "double"],
 56 | 	boolean: ["bit", "smallint"],
 57 | 	date: ["datetime2", "date", "datetime"],
 58 | 	json: ["varchar", "nvarchar"],
 59 | };
 60 | 
 61 | const map = {
 62 | 	postgres: postgresMap,
 63 | 	mysql: mysqlMap,
 64 | 	sqlite: sqliteMap,
 65 | 	mssql: mssqlMap,
 66 | };
 67 | 
 68 | export function matchType(
 69 | 	columnDataType: string,
 70 | 	fieldType: DBFieldType,
 71 | 	dbType: KyselyDatabaseType,
 72 | ) {
 73 | 	function normalize(type: string) {
 74 | 		return type.toLowerCase().split("(")[0]!.trim();
 75 | 	}
 76 | 	if (fieldType === "string[]" || fieldType === "number[]") {
 77 | 		return columnDataType.toLowerCase().includes("json");
 78 | 	}
 79 | 	const types = map[dbType]!;
 80 | 	const expected = Array.isArray(fieldType)
 81 | 		? types["string"].map((t) => t.toLowerCase())
 82 | 		: types[fieldType]!.map((t) => t.toLowerCase());
 83 | 	return expected.includes(normalize(columnDataType));
 84 | }
 85 | 
 86 | /**
 87 |  * Get the current PostgreSQL schema (search_path) for the database connection
 88 |  * Returns the first schema in the search_path, defaulting to 'public' if not found
 89 |  */
 90 | async function getPostgresSchema(db: Kysely<unknown>): Promise<string> {
 91 | 	try {
 92 | 		const result = await sql<{ search_path: string }>`SHOW search_path`.execute(
 93 | 			db,
 94 | 		);
 95 | 		if (result.rows[0]?.search_path) {
 96 | 			// search_path can be a comma-separated list like "$user, public" or '"$user", public'
 97 | 			// We want the first non-variable schema
 98 | 			const schemas = result.rows[0].search_path
 99 | 				.split(",")
100 | 				.map((s) => s.trim())
101 | 				// Remove quotes and filter out variables like $user
102 | 				.map((s) => s.replace(/^["']|["']$/g, ""))
103 | 				.filter((s) => !s.startsWith("$"));
104 | 			return schemas[0] || "public";
105 | 		}
106 | 	} catch (error) {
107 | 		// If query fails, fall back to public schema
108 | 	}
109 | 	return "public";
110 | }
111 | 
112 | export async function getMigrations(config: BetterAuthOptions) {
113 | 	const betterAuthSchema = getSchema(config);
114 | 	const logger = createLogger(config.logger);
115 | 
116 | 	let { kysely: db, databaseType: dbType } = await createKyselyAdapter(config);
117 | 
118 | 	if (!dbType) {
119 | 		logger.warn(
120 | 			"Could not determine database type, defaulting to sqlite. Please provide a type in the database options to avoid this.",
121 | 		);
122 | 		dbType = "sqlite";
123 | 	}
124 | 
125 | 	if (!db) {
126 | 		logger.error(
127 | 			"Only kysely adapter is supported for migrations. You can use `generate` command to generate the schema, if you're using a different adapter.",
128 | 		);
129 | 		process.exit(1);
130 | 	}
131 | 
132 | 	// For PostgreSQL, detect and log the current schema being used
133 | 	let currentSchema = "public";
134 | 	if (dbType === "postgres") {
135 | 		currentSchema = await getPostgresSchema(db);
136 | 		logger.debug(
137 | 			`PostgreSQL migration: Using schema '${currentSchema}' (from search_path)`,
138 | 		);
139 | 
140 | 		// Verify the schema exists
141 | 		try {
142 | 			const schemaCheck = await sql<{ schema_name: string }>`
143 | 				SELECT schema_name 
144 | 				FROM information_schema.schemata 
145 | 				WHERE schema_name = ${currentSchema}
146 | 			`.execute(db);
147 | 
148 | 			if (!schemaCheck.rows[0]) {
149 | 				logger.warn(
150 | 					`Schema '${currentSchema}' does not exist. Tables will be inspected from available schemas. Consider creating the schema first or checking your database configuration.`,
151 | 				);
152 | 			}
153 | 		} catch (error) {
154 | 			logger.debug(
155 | 				`Could not verify schema existence: ${error instanceof Error ? error.message : String(error)}`,
156 | 			);
157 | 		}
158 | 	}
159 | 
160 | 	const allTableMetadata = await db.introspection.getTables();
161 | 
162 | 	// For PostgreSQL, filter tables to only those in the target schema
163 | 	let tableMetadata = allTableMetadata;
164 | 	if (dbType === "postgres") {
165 | 		// Get tables with their schema information
166 | 		try {
167 | 			const tablesInSchema = await sql<{
168 | 				table_name: string;
169 | 			}>`
170 | 				SELECT table_name 
171 | 				FROM information_schema.tables 
172 | 				WHERE table_schema = ${currentSchema}
173 | 				AND table_type = 'BASE TABLE'
174 | 			`.execute(db);
175 | 
176 | 			const tableNamesInSchema = new Set(
177 | 				tablesInSchema.rows.map((row) => row.table_name),
178 | 			);
179 | 
180 | 			// Filter to only tables that exist in the target schema
181 | 			tableMetadata = allTableMetadata.filter(
182 | 				(table) =>
183 | 					table.schema === currentSchema && tableNamesInSchema.has(table.name),
184 | 			);
185 | 
186 | 			logger.debug(
187 | 				`Found ${tableMetadata.length} table(s) in schema '${currentSchema}': ${tableMetadata.map((t) => t.name).join(", ") || "(none)"}`,
188 | 			);
189 | 		} catch (error) {
190 | 			logger.warn(
191 | 				`Could not filter tables by schema. Using all discovered tables. Error: ${error instanceof Error ? error.message : String(error)}`,
192 | 			);
193 | 			// Fall back to using all tables if schema filtering fails
194 | 		}
195 | 	}
196 | 	const toBeCreated: {
197 | 		table: string;
198 | 		fields: Record<string, DBFieldAttribute>;
199 | 		order: number;
200 | 	}[] = [];
201 | 	const toBeAdded: {
202 | 		table: string;
203 | 		fields: Record<string, DBFieldAttribute>;
204 | 		order: number;
205 | 	}[] = [];
206 | 
207 | 	for (const [key, value] of Object.entries(betterAuthSchema)) {
208 | 		const table = tableMetadata.find((t) => t.name === key);
209 | 		if (!table) {
210 | 			const tIndex = toBeCreated.findIndex((t) => t.table === key);
211 | 			const tableData = {
212 | 				table: key,
213 | 				fields: value.fields,
214 | 				order: value.order || Infinity,
215 | 			};
216 | 
217 | 			const insertIndex = toBeCreated.findIndex(
218 | 				(t) => (t.order || Infinity) > tableData.order,
219 | 			);
220 | 
221 | 			if (insertIndex === -1) {
222 | 				if (tIndex === -1) {
223 | 					toBeCreated.push(tableData);
224 | 				} else {
225 | 					toBeCreated[tIndex]!.fields = {
226 | 						...toBeCreated[tIndex]!.fields,
227 | 						...value.fields,
228 | 					};
229 | 				}
230 | 			} else {
231 | 				toBeCreated.splice(insertIndex, 0, tableData);
232 | 			}
233 | 			continue;
234 | 		}
235 | 		let toBeAddedFields: Record<string, DBFieldAttribute> = {};
236 | 		for (const [fieldName, field] of Object.entries(value.fields)) {
237 | 			const column = table.columns.find((c) => c.name === fieldName);
238 | 			if (!column) {
239 | 				toBeAddedFields[fieldName] = field;
240 | 				continue;
241 | 			}
242 | 
243 | 			if (matchType(column.dataType, field.type, dbType)) {
244 | 				continue;
245 | 			} else {
246 | 				logger.warn(
247 | 					`Field ${fieldName} in table ${key} has a different type in the database. Expected ${field.type} but got ${column.dataType}.`,
248 | 				);
249 | 			}
250 | 		}
251 | 		if (Object.keys(toBeAddedFields).length > 0) {
252 | 			toBeAdded.push({
253 | 				table: key,
254 | 				fields: toBeAddedFields,
255 | 				order: value.order || Infinity,
256 | 			});
257 | 		}
258 | 	}
259 | 
260 | 	const migrations: (
261 | 		| AlterTableColumnAlteringBuilder
262 | 		| CreateTableBuilder<string, string>
263 | 	)[] = [];
264 | 
265 | 	function getType(field: DBFieldAttribute, fieldName: string) {
266 | 		const type = field.type;
267 | 		const typeMap = {
268 | 			string: {
269 | 				sqlite: "text",
270 | 				postgres: "text",
271 | 				mysql: field.unique
272 | 					? "varchar(255)"
273 | 					: field.references
274 | 						? "varchar(36)"
275 | 						: "text",
276 | 				mssql:
277 | 					field.unique || field.sortable
278 | 						? "varchar(255)"
279 | 						: field.references
280 | 							? "varchar(36)"
281 | 							: // mssql deprecated `text`, and the alternative is `varchar(max)`.
282 | 								// Kysely type interface doesn't support `text`, so we set this to `varchar(8000)` as
283 | 								// that's the max length for `varchar`
284 | 								"varchar(8000)",
285 | 			},
286 | 			boolean: {
287 | 				sqlite: "integer",
288 | 				postgres: "boolean",
289 | 				mysql: "boolean",
290 | 				mssql: "smallint",
291 | 			},
292 | 			number: {
293 | 				sqlite: field.bigint ? "bigint" : "integer",
294 | 				postgres: field.bigint ? "bigint" : "integer",
295 | 				mysql: field.bigint ? "bigint" : "integer",
296 | 				mssql: field.bigint ? "bigint" : "integer",
297 | 			},
298 | 			date: {
299 | 				sqlite: "date",
300 | 				postgres: "timestamptz",
301 | 				mysql: "timestamp(3)",
302 | 				mssql: sql`datetime2(3)`,
303 | 			},
304 | 			json: {
305 | 				sqlite: "text",
306 | 				postgres: "jsonb",
307 | 				mysql: "json",
308 | 				mssql: "varchar(8000)",
309 | 			},
310 | 			id: {
311 | 				postgres: config.advanced?.database?.useNumberId ? "serial" : "text",
312 | 				mysql: config.advanced?.database?.useNumberId
313 | 					? "integer"
314 | 					: "varchar(36)",
315 | 				mssql: config.advanced?.database?.useNumberId
316 | 					? "integer"
317 | 					: "varchar(36)",
318 | 				sqlite: config.advanced?.database?.useNumberId ? "integer" : "text",
319 | 			},
320 | 			foreignKeyId: {
321 | 				postgres: config.advanced?.database?.useNumberId ? "integer" : "text",
322 | 				mysql: config.advanced?.database?.useNumberId
323 | 					? "integer"
324 | 					: "varchar(36)",
325 | 				mssql: config.advanced?.database?.useNumberId
326 | 					? "integer"
327 | 					: "varchar(36)",
328 | 				sqlite: config.advanced?.database?.useNumberId ? "integer" : "text",
329 | 			},
330 | 		} as const;
331 | 		if (fieldName === "id" || field.references?.field === "id") {
332 | 			if (fieldName === "id") {
333 | 				return typeMap.id[dbType!];
334 | 			}
335 | 			return typeMap.foreignKeyId[dbType!];
336 | 		}
337 | 		if (dbType === "sqlite" && (type === "string[]" || type === "number[]")) {
338 | 			return "text";
339 | 		}
340 | 		if (type === "string[]" || type === "number[]") {
341 | 			return "jsonb";
342 | 		}
343 | 		if (Array.isArray(type)) {
344 | 			return "text";
345 | 		}
346 | 		return typeMap[type]![dbType || "sqlite"];
347 | 	}
348 | 	if (toBeAdded.length) {
349 | 		for (const table of toBeAdded) {
350 | 			for (const [fieldName, field] of Object.entries(table.fields)) {
351 | 				const type = getType(field, fieldName);
352 | 				const exec = db.schema
353 | 					.alterTable(table.table)
354 | 					.addColumn(fieldName, type, (col) => {
355 | 						col = field.required !== false ? col.notNull() : col;
356 | 						if (field.references) {
357 | 							col = col
358 | 								.references(
359 | 									`${field.references.model}.${field.references.field}`,
360 | 								)
361 | 								.onDelete(field.references.onDelete || "cascade");
362 | 						}
363 | 						if (field.unique) {
364 | 							col = col.unique();
365 | 						}
366 | 						if (
367 | 							field.type === "date" &&
368 | 							typeof field.defaultValue === "function" &&
369 | 							(dbType === "postgres" ||
370 | 								dbType === "mysql" ||
371 | 								dbType === "mssql")
372 | 						) {
373 | 							if (dbType === "mysql") {
374 | 								col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
375 | 							} else {
376 | 								col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
377 | 							}
378 | 						}
379 | 						return col;
380 | 					});
381 | 				migrations.push(exec);
382 | 			}
383 | 		}
384 | 	}
385 | 	if (toBeCreated.length) {
386 | 		for (const table of toBeCreated) {
387 | 			let dbT = db.schema
388 | 				.createTable(table.table)
389 | 				.addColumn(
390 | 					"id",
391 | 					config.advanced?.database?.useNumberId
392 | 						? dbType === "postgres"
393 | 							? "serial"
394 | 							: "integer"
395 | 						: dbType === "mysql" || dbType === "mssql"
396 | 							? "varchar(36)"
397 | 							: "text",
398 | 					(col) => {
399 | 						if (config.advanced?.database?.useNumberId) {
400 | 							if (dbType === "postgres" || dbType === "sqlite") {
401 | 								return col.primaryKey().notNull();
402 | 							} else if (dbType === "mssql") {
403 | 								return col.identity().primaryKey().notNull();
404 | 							}
405 | 							return col.autoIncrement().primaryKey().notNull();
406 | 						}
407 | 						return col.primaryKey().notNull();
408 | 					},
409 | 				);
410 | 
411 | 			for (const [fieldName, field] of Object.entries(table.fields)) {
412 | 				const type = getType(field, fieldName);
413 | 				dbT = dbT.addColumn(fieldName, type, (col) => {
414 | 					col = field.required !== false ? col.notNull() : col;
415 | 					if (field.references) {
416 | 						col = col
417 | 							.references(`${field.references.model}.${field.references.field}`)
418 | 							.onDelete(field.references.onDelete || "cascade");
419 | 					}
420 | 
421 | 					if (field.unique) {
422 | 						col = col.unique();
423 | 					}
424 | 					if (
425 | 						field.type === "date" &&
426 | 						typeof field.defaultValue === "function" &&
427 | 						(dbType === "postgres" || dbType === "mysql" || dbType === "mssql")
428 | 					) {
429 | 						if (dbType === "mysql") {
430 | 							col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
431 | 						} else {
432 | 							col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
433 | 						}
434 | 					}
435 | 					return col;
436 | 				});
437 | 			}
438 | 			migrations.push(dbT);
439 | 		}
440 | 	}
441 | 	async function runMigrations() {
442 | 		for (const migration of migrations) {
443 | 			await migration.execute();
444 | 		}
445 | 	}
446 | 	async function compileMigrations() {
447 | 		const compiled = migrations.map((m) => m.compile().sql);
448 | 		return compiled.join(";\n\n") + ";";
449 | 	}
450 | 	return { toBeCreated, toBeAdded, runMigrations, compileMigrations };
451 | }
452 | 
```
Page 34/71FirstPrevNextLast