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

# Directory Structure

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

# Files

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

```typescript
  1 | import * as z from "zod";
  2 | import { createAuthEndpoint } from "@better-auth/core/middleware";
  3 | import { APIError } from "better-call";
  4 | import type { OAuth2Tokens } from "@better-auth/core/oauth2";
  5 | import {
  6 | 	freshSessionMiddleware,
  7 | 	getSessionFromCtx,
  8 | 	sessionMiddleware,
  9 | } from "./session";
 10 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
 11 | import { SocialProviderListEnum } from "@better-auth/core/social-providers";
 12 | import { generateState } from "../../oauth2/state";
 13 | import { decryptOAuthToken, setTokenUtil } from "../../oauth2/utils";
 14 | 
 15 | export const listUserAccounts = createAuthEndpoint(
 16 | 	"/list-accounts",
 17 | 	{
 18 | 		method: "GET",
 19 | 		use: [sessionMiddleware],
 20 | 		metadata: {
 21 | 			openapi: {
 22 | 				description: "List all accounts linked to the user",
 23 | 				responses: {
 24 | 					"200": {
 25 | 						description: "Success",
 26 | 						content: {
 27 | 							"application/json": {
 28 | 								schema: {
 29 | 									type: "array",
 30 | 									items: {
 31 | 										type: "object",
 32 | 										properties: {
 33 | 											id: {
 34 | 												type: "string",
 35 | 											},
 36 | 											providerId: {
 37 | 												type: "string",
 38 | 											},
 39 | 											createdAt: {
 40 | 												type: "string",
 41 | 												format: "date-time",
 42 | 											},
 43 | 											updatedAt: {
 44 | 												type: "string",
 45 | 												format: "date-time",
 46 | 											},
 47 | 											accountId: {
 48 | 												type: "string",
 49 | 											},
 50 | 											scopes: {
 51 | 												type: "array",
 52 | 												items: {
 53 | 													type: "string",
 54 | 												},
 55 | 											},
 56 | 										},
 57 | 										required: [
 58 | 											"id",
 59 | 											"providerId",
 60 | 											"createdAt",
 61 | 											"updatedAt",
 62 | 											"accountId",
 63 | 											"scopes",
 64 | 										],
 65 | 									},
 66 | 								},
 67 | 							},
 68 | 						},
 69 | 					},
 70 | 				},
 71 | 			},
 72 | 		},
 73 | 	},
 74 | 	async (c) => {
 75 | 		const session = c.context.session;
 76 | 		const accounts = await c.context.internalAdapter.findAccounts(
 77 | 			session.user.id,
 78 | 		);
 79 | 		return c.json(
 80 | 			accounts.map((a) => ({
 81 | 				id: a.id,
 82 | 				providerId: a.providerId,
 83 | 				createdAt: a.createdAt,
 84 | 				updatedAt: a.updatedAt,
 85 | 				accountId: a.accountId,
 86 | 				scopes: a.scope?.split(",") || [],
 87 | 			})),
 88 | 		);
 89 | 	},
 90 | );
 91 | 
 92 | export const linkSocialAccount = createAuthEndpoint(
 93 | 	"/link-social",
 94 | 	{
 95 | 		method: "POST",
 96 | 		requireHeaders: true,
 97 | 		body: z.object({
 98 | 			/**
 99 | 			 * Callback URL to redirect to after the user has signed in.
100 | 			 */
101 | 			callbackURL: z
102 | 				.string()
103 | 				.meta({
104 | 					description: "The URL to redirect to after the user has signed in",
105 | 				})
106 | 				.optional(),
107 | 			/**
108 | 			 * OAuth2 provider to use
109 | 			 */
110 | 			provider: SocialProviderListEnum,
111 | 			/**
112 | 			 * ID Token for direct authentication without redirect
113 | 			 */
114 | 			idToken: z
115 | 				.object({
116 | 					token: z.string(),
117 | 					nonce: z.string().optional(),
118 | 					accessToken: z.string().optional(),
119 | 					refreshToken: z.string().optional(),
120 | 					scopes: z.array(z.string()).optional(),
121 | 				})
122 | 				.optional(),
123 | 			/**
124 | 			 * Whether to allow sign up for new users
125 | 			 */
126 | 			requestSignUp: z.boolean().optional(),
127 | 			/**
128 | 			 * Additional scopes to request when linking the account.
129 | 			 * This is useful for requesting additional permissions when
130 | 			 * linking a social account compared to the initial authentication.
131 | 			 */
132 | 			scopes: z
133 | 				.array(z.string())
134 | 				.meta({
135 | 					description: "Additional scopes to request from the provider",
136 | 				})
137 | 				.optional(),
138 | 			/**
139 | 			 * The URL to redirect to if there is an error during the link process.
140 | 			 */
141 | 			errorCallbackURL: z
142 | 				.string()
143 | 				.meta({
144 | 					description:
145 | 						"The URL to redirect to if there is an error during the link process",
146 | 				})
147 | 				.optional(),
148 | 			/**
149 | 			 * Disable automatic redirection to the provider
150 | 			 *
151 | 			 * This is useful if you want to handle the redirection
152 | 			 * yourself like in a popup or a different tab.
153 | 			 */
154 | 			disableRedirect: z
155 | 				.boolean()
156 | 				.meta({
157 | 					description:
158 | 						"Disable automatic redirection to the provider. Useful for handling the redirection yourself",
159 | 				})
160 | 				.optional(),
161 | 		}),
162 | 		use: [sessionMiddleware],
163 | 		metadata: {
164 | 			openapi: {
165 | 				description: "Link a social account to the user",
166 | 				responses: {
167 | 					"200": {
168 | 						description: "Success",
169 | 						content: {
170 | 							"application/json": {
171 | 								schema: {
172 | 									type: "object",
173 | 									properties: {
174 | 										url: {
175 | 											type: "string",
176 | 											description:
177 | 												"The authorization URL to redirect the user to",
178 | 										},
179 | 										redirect: {
180 | 											type: "boolean",
181 | 											description:
182 | 												"Indicates if the user should be redirected to the authorization URL",
183 | 										},
184 | 										status: {
185 | 											type: "boolean",
186 | 										},
187 | 									},
188 | 									required: ["redirect"],
189 | 								},
190 | 							},
191 | 						},
192 | 					},
193 | 				},
194 | 			},
195 | 		},
196 | 	},
197 | 	async (c) => {
198 | 		const session = c.context.session;
199 | 
200 | 		const provider = c.context.socialProviders.find(
201 | 			(p) => p.id === c.body.provider,
202 | 		);
203 | 
204 | 		if (!provider) {
205 | 			c.context.logger.error(
206 | 				"Provider not found. Make sure to add the provider in your auth config",
207 | 				{
208 | 					provider: c.body.provider,
209 | 				},
210 | 			);
211 | 			throw new APIError("NOT_FOUND", {
212 | 				message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
213 | 			});
214 | 		}
215 | 
216 | 		// Handle ID Token flow if provided
217 | 		if (c.body.idToken) {
218 | 			if (!provider.verifyIdToken) {
219 | 				c.context.logger.error(
220 | 					"Provider does not support id token verification",
221 | 					{
222 | 						provider: c.body.provider,
223 | 					},
224 | 				);
225 | 				throw new APIError("NOT_FOUND", {
226 | 					message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED,
227 | 				});
228 | 			}
229 | 
230 | 			const { token, nonce } = c.body.idToken;
231 | 			const valid = await provider.verifyIdToken(token, nonce);
232 | 			if (!valid) {
233 | 				c.context.logger.error("Invalid id token", {
234 | 					provider: c.body.provider,
235 | 				});
236 | 				throw new APIError("UNAUTHORIZED", {
237 | 					message: BASE_ERROR_CODES.INVALID_TOKEN,
238 | 				});
239 | 			}
240 | 
241 | 			const linkingUserInfo = await provider.getUserInfo({
242 | 				idToken: token,
243 | 				accessToken: c.body.idToken.accessToken,
244 | 				refreshToken: c.body.idToken.refreshToken,
245 | 			});
246 | 
247 | 			if (!linkingUserInfo || !linkingUserInfo?.user) {
248 | 				c.context.logger.error("Failed to get user info", {
249 | 					provider: c.body.provider,
250 | 				});
251 | 				throw new APIError("UNAUTHORIZED", {
252 | 					message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO,
253 | 				});
254 | 			}
255 | 
256 | 			const linkingUserId = String(linkingUserInfo.user.id);
257 | 
258 | 			if (!linkingUserInfo.user.email) {
259 | 				c.context.logger.error("User email not found", {
260 | 					provider: c.body.provider,
261 | 				});
262 | 				throw new APIError("UNAUTHORIZED", {
263 | 					message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND,
264 | 				});
265 | 			}
266 | 
267 | 			const existingAccounts = await c.context.internalAdapter.findAccounts(
268 | 				session.user.id,
269 | 			);
270 | 
271 | 			const hasBeenLinked = existingAccounts.find(
272 | 				(a) => a.providerId === provider.id && a.accountId === linkingUserId,
273 | 			);
274 | 
275 | 			if (hasBeenLinked) {
276 | 				return c.json({
277 | 					url: "", // this is for type inference
278 | 					status: true,
279 | 					redirect: false,
280 | 				});
281 | 			}
282 | 
283 | 			const trustedProviders =
284 | 				c.context.options.account?.accountLinking?.trustedProviders;
285 | 
286 | 			const isTrustedProvider = trustedProviders?.includes(provider.id);
287 | 			if (
288 | 				(!isTrustedProvider && !linkingUserInfo.user.emailVerified) ||
289 | 				c.context.options.account?.accountLinking?.enabled === false
290 | 			) {
291 | 				throw new APIError("UNAUTHORIZED", {
292 | 					message: "Account not linked - linking not allowed",
293 | 				});
294 | 			}
295 | 
296 | 			if (
297 | 				linkingUserInfo.user.email !== session.user.email &&
298 | 				c.context.options.account?.accountLinking?.allowDifferentEmails !== true
299 | 			) {
300 | 				throw new APIError("UNAUTHORIZED", {
301 | 					message: "Account not linked - different emails not allowed",
302 | 				});
303 | 			}
304 | 
305 | 			try {
306 | 				await c.context.internalAdapter.createAccount(
307 | 					{
308 | 						userId: session.user.id,
309 | 						providerId: provider.id,
310 | 						accountId: linkingUserId,
311 | 						accessToken: c.body.idToken.accessToken,
312 | 						idToken: token,
313 | 						refreshToken: c.body.idToken.refreshToken,
314 | 						scope: c.body.idToken.scopes?.join(","),
315 | 					},
316 | 					c,
317 | 				);
318 | 			} catch (e: any) {
319 | 				throw new APIError("EXPECTATION_FAILED", {
320 | 					message: "Account not linked - unable to create account",
321 | 				});
322 | 			}
323 | 
324 | 			if (
325 | 				c.context.options.account?.accountLinking?.updateUserInfoOnLink === true
326 | 			) {
327 | 				try {
328 | 					await c.context.internalAdapter.updateUser(session.user.id, {
329 | 						name: linkingUserInfo.user?.name,
330 | 						image: linkingUserInfo.user?.image,
331 | 					});
332 | 				} catch (e: any) {
333 | 					console.warn("Could not update user - " + e.toString());
334 | 				}
335 | 			}
336 | 
337 | 			return c.json({
338 | 				url: "", // this is for type inference
339 | 				status: true,
340 | 				redirect: false,
341 | 			});
342 | 		}
343 | 
344 | 		// Handle OAuth flow
345 | 		const state = await generateState(c, {
346 | 			userId: session.user.id,
347 | 			email: session.user.email,
348 | 		});
349 | 
350 | 		const url = await provider.createAuthorizationURL({
351 | 			state: state.state,
352 | 			codeVerifier: state.codeVerifier,
353 | 			redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
354 | 			scopes: c.body.scopes,
355 | 		});
356 | 
357 | 		return c.json({
358 | 			url: url.toString(),
359 | 			redirect: !c.body.disableRedirect,
360 | 		});
361 | 	},
362 | );
363 | export const unlinkAccount = createAuthEndpoint(
364 | 	"/unlink-account",
365 | 	{
366 | 		method: "POST",
367 | 		body: z.object({
368 | 			providerId: z.string(),
369 | 			accountId: z.string().optional(),
370 | 		}),
371 | 		use: [freshSessionMiddleware],
372 | 		metadata: {
373 | 			openapi: {
374 | 				description: "Unlink an account",
375 | 				responses: {
376 | 					"200": {
377 | 						description: "Success",
378 | 						content: {
379 | 							"application/json": {
380 | 								schema: {
381 | 									type: "object",
382 | 									properties: {
383 | 										status: {
384 | 											type: "boolean",
385 | 										},
386 | 									},
387 | 								},
388 | 							},
389 | 						},
390 | 					},
391 | 				},
392 | 			},
393 | 		},
394 | 	},
395 | 	async (ctx) => {
396 | 		const { providerId, accountId } = ctx.body;
397 | 		const accounts = await ctx.context.internalAdapter.findAccounts(
398 | 			ctx.context.session.user.id,
399 | 		);
400 | 		if (
401 | 			accounts.length === 1 &&
402 | 			!ctx.context.options.account?.accountLinking?.allowUnlinkingAll
403 | 		) {
404 | 			throw new APIError("BAD_REQUEST", {
405 | 				message: BASE_ERROR_CODES.FAILED_TO_UNLINK_LAST_ACCOUNT,
406 | 			});
407 | 		}
408 | 		const accountExist = accounts.find((account) =>
409 | 			accountId
410 | 				? account.accountId === accountId && account.providerId === providerId
411 | 				: account.providerId === providerId,
412 | 		);
413 | 		if (!accountExist) {
414 | 			throw new APIError("BAD_REQUEST", {
415 | 				message: BASE_ERROR_CODES.ACCOUNT_NOT_FOUND,
416 | 			});
417 | 		}
418 | 		await ctx.context.internalAdapter.deleteAccount(accountExist.id);
419 | 		return ctx.json({
420 | 			status: true,
421 | 		});
422 | 	},
423 | );
424 | 
425 | export const getAccessToken = createAuthEndpoint(
426 | 	"/get-access-token",
427 | 	{
428 | 		method: "POST",
429 | 		body: z.object({
430 | 			providerId: z.string().meta({
431 | 				description: "The provider ID for the OAuth provider",
432 | 			}),
433 | 			accountId: z
434 | 				.string()
435 | 				.meta({
436 | 					description: "The account ID associated with the refresh token",
437 | 				})
438 | 				.optional(),
439 | 			userId: z
440 | 				.string()
441 | 				.meta({
442 | 					description: "The user ID associated with the account",
443 | 				})
444 | 				.optional(),
445 | 		}),
446 | 		metadata: {
447 | 			openapi: {
448 | 				description: "Get a valid access token, doing a refresh if needed",
449 | 				responses: {
450 | 					200: {
451 | 						description: "A Valid access token",
452 | 						content: {
453 | 							"application/json": {
454 | 								schema: {
455 | 									type: "object",
456 | 									properties: {
457 | 										tokenType: {
458 | 											type: "string",
459 | 										},
460 | 										idToken: {
461 | 											type: "string",
462 | 										},
463 | 										accessToken: {
464 | 											type: "string",
465 | 										},
466 | 										refreshToken: {
467 | 											type: "string",
468 | 										},
469 | 										accessTokenExpiresAt: {
470 | 											type: "string",
471 | 											format: "date-time",
472 | 										},
473 | 										refreshTokenExpiresAt: {
474 | 											type: "string",
475 | 											format: "date-time",
476 | 										},
477 | 									},
478 | 								},
479 | 							},
480 | 						},
481 | 					},
482 | 					400: {
483 | 						description: "Invalid refresh token or provider configuration",
484 | 					},
485 | 				},
486 | 			},
487 | 		},
488 | 	},
489 | 	async (ctx) => {
490 | 		const { providerId, accountId, userId } = ctx.body;
491 | 		const req = ctx.request;
492 | 		const session = await getSessionFromCtx(ctx);
493 | 		if (req && !session) {
494 | 			throw ctx.error("UNAUTHORIZED");
495 | 		}
496 | 		let resolvedUserId = session?.user?.id || userId;
497 | 		if (!resolvedUserId) {
498 | 			throw new APIError("BAD_REQUEST", {
499 | 				message: `Either userId or session is required`,
500 | 			});
501 | 		}
502 | 		if (!ctx.context.socialProviders.find((p) => p.id === providerId)) {
503 | 			throw new APIError("BAD_REQUEST", {
504 | 				message: `Provider ${providerId} is not supported.`,
505 | 			});
506 | 		}
507 | 		const accounts =
508 | 			await ctx.context.internalAdapter.findAccounts(resolvedUserId);
509 | 		const account = accounts.find((acc) =>
510 | 			accountId
511 | 				? acc.id === accountId && acc.providerId === providerId
512 | 				: acc.providerId === providerId,
513 | 		);
514 | 		if (!account) {
515 | 			throw new APIError("BAD_REQUEST", {
516 | 				message: "Account not found",
517 | 			});
518 | 		}
519 | 		const provider = ctx.context.socialProviders.find(
520 | 			(p) => p.id === providerId,
521 | 		);
522 | 		if (!provider) {
523 | 			throw new APIError("BAD_REQUEST", {
524 | 				message: `Provider ${providerId} not found.`,
525 | 			});
526 | 		}
527 | 
528 | 		try {
529 | 			let newTokens: OAuth2Tokens | null = null;
530 | 			const accessTokenExpired =
531 | 				account.accessTokenExpiresAt &&
532 | 				new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5_000;
533 | 			if (
534 | 				account.refreshToken &&
535 | 				accessTokenExpired &&
536 | 				provider.refreshAccessToken
537 | 			) {
538 | 				const refreshToken = await decryptOAuthToken(
539 | 					account.refreshToken,
540 | 					ctx.context,
541 | 				);
542 | 				newTokens = await provider.refreshAccessToken(refreshToken);
543 | 				await ctx.context.internalAdapter.updateAccount(account.id, {
544 | 					accessToken: await setTokenUtil(newTokens.accessToken, ctx.context),
545 | 					accessTokenExpiresAt: newTokens.accessTokenExpiresAt,
546 | 					refreshToken: await setTokenUtil(newTokens.refreshToken, ctx.context),
547 | 					refreshTokenExpiresAt: newTokens.refreshTokenExpiresAt,
548 | 				});
549 | 			}
550 | 			const tokens = {
551 | 				accessToken:
552 | 					newTokens?.accessToken ??
553 | 					(await decryptOAuthToken(account.accessToken ?? "", ctx.context)),
554 | 				accessTokenExpiresAt:
555 | 					newTokens?.accessTokenExpiresAt ??
556 | 					account.accessTokenExpiresAt ??
557 | 					undefined,
558 | 				scopes: account.scope?.split(",") ?? [],
559 | 				idToken: newTokens?.idToken ?? account.idToken ?? undefined,
560 | 			};
561 | 			return ctx.json(tokens);
562 | 		} catch (error) {
563 | 			throw new APIError("BAD_REQUEST", {
564 | 				message: "Failed to get a valid access token",
565 | 				cause: error,
566 | 			});
567 | 		}
568 | 	},
569 | );
570 | 
571 | export const refreshToken = createAuthEndpoint(
572 | 	"/refresh-token",
573 | 	{
574 | 		method: "POST",
575 | 		body: z.object({
576 | 			providerId: z.string().meta({
577 | 				description: "The provider ID for the OAuth provider",
578 | 			}),
579 | 			accountId: z
580 | 				.string()
581 | 				.meta({
582 | 					description: "The account ID associated with the refresh token",
583 | 				})
584 | 				.optional(),
585 | 			userId: z
586 | 				.string()
587 | 				.meta({
588 | 					description: "The user ID associated with the account",
589 | 				})
590 | 				.optional(),
591 | 		}),
592 | 		metadata: {
593 | 			openapi: {
594 | 				description: "Refresh the access token using a refresh token",
595 | 				responses: {
596 | 					200: {
597 | 						description: "Access token refreshed successfully",
598 | 						content: {
599 | 							"application/json": {
600 | 								schema: {
601 | 									type: "object",
602 | 									properties: {
603 | 										tokenType: {
604 | 											type: "string",
605 | 										},
606 | 										idToken: {
607 | 											type: "string",
608 | 										},
609 | 										accessToken: {
610 | 											type: "string",
611 | 										},
612 | 										refreshToken: {
613 | 											type: "string",
614 | 										},
615 | 										accessTokenExpiresAt: {
616 | 											type: "string",
617 | 											format: "date-time",
618 | 										},
619 | 										refreshTokenExpiresAt: {
620 | 											type: "string",
621 | 											format: "date-time",
622 | 										},
623 | 									},
624 | 								},
625 | 							},
626 | 						},
627 | 					},
628 | 					400: {
629 | 						description: "Invalid refresh token or provider configuration",
630 | 					},
631 | 				},
632 | 			},
633 | 		},
634 | 	},
635 | 	async (ctx) => {
636 | 		const { providerId, accountId, userId } = ctx.body;
637 | 		const req = ctx.request;
638 | 		const session = await getSessionFromCtx(ctx);
639 | 		if (req && !session) {
640 | 			throw ctx.error("UNAUTHORIZED");
641 | 		}
642 | 		let resolvedUserId = session?.user?.id || userId;
643 | 		if (!resolvedUserId) {
644 | 			throw new APIError("BAD_REQUEST", {
645 | 				message: `Either userId or session is required`,
646 | 			});
647 | 		}
648 | 		const accounts =
649 | 			await ctx.context.internalAdapter.findAccounts(resolvedUserId);
650 | 		const account = accounts.find((acc) =>
651 | 			accountId
652 | 				? acc.id === accountId && acc.providerId === providerId
653 | 				: acc.providerId === providerId,
654 | 		);
655 | 		if (!account) {
656 | 			throw new APIError("BAD_REQUEST", {
657 | 				message: "Account not found",
658 | 			});
659 | 		}
660 | 		const provider = ctx.context.socialProviders.find(
661 | 			(p) => p.id === providerId,
662 | 		);
663 | 		if (!provider) {
664 | 			throw new APIError("BAD_REQUEST", {
665 | 				message: `Provider ${providerId} not found.`,
666 | 			});
667 | 		}
668 | 		if (!provider.refreshAccessToken) {
669 | 			throw new APIError("BAD_REQUEST", {
670 | 				message: `Provider ${providerId} does not support token refreshing.`,
671 | 			});
672 | 		}
673 | 		try {
674 | 			const tokens: OAuth2Tokens = await provider.refreshAccessToken(
675 | 				account.refreshToken as string,
676 | 			);
677 | 			await ctx.context.internalAdapter.updateAccount(account.id, {
678 | 				accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
679 | 				refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context),
680 | 				accessTokenExpiresAt: tokens.accessTokenExpiresAt,
681 | 				refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
682 | 			});
683 | 			return ctx.json(tokens);
684 | 		} catch (error) {
685 | 			throw new APIError("BAD_REQUEST", {
686 | 				message: "Failed to refresh access token",
687 | 				cause: error,
688 | 			});
689 | 		}
690 | 	},
691 | );
692 | 
693 | export const accountInfo = createAuthEndpoint(
694 | 	"/account-info",
695 | 	{
696 | 		method: "POST",
697 | 		use: [sessionMiddleware],
698 | 		metadata: {
699 | 			openapi: {
700 | 				description: "Get the account info provided by the provider",
701 | 				responses: {
702 | 					"200": {
703 | 						description: "Success",
704 | 						content: {
705 | 							"application/json": {
706 | 								schema: {
707 | 									type: "object",
708 | 									properties: {
709 | 										user: {
710 | 											type: "object",
711 | 											properties: {
712 | 												id: {
713 | 													type: "string",
714 | 												},
715 | 												name: {
716 | 													type: "string",
717 | 												},
718 | 												email: {
719 | 													type: "string",
720 | 												},
721 | 												image: {
722 | 													type: "string",
723 | 												},
724 | 												emailVerified: {
725 | 													type: "boolean",
726 | 												},
727 | 											},
728 | 											required: ["id", "emailVerified"],
729 | 										},
730 | 										data: {
731 | 											type: "object",
732 | 											properties: {},
733 | 											additionalProperties: true,
734 | 										},
735 | 									},
736 | 									required: ["user", "data"],
737 | 									additionalProperties: false,
738 | 								},
739 | 							},
740 | 						},
741 | 					},
742 | 				},
743 | 			},
744 | 		},
745 | 		body: z.object({
746 | 			accountId: z.string().meta({
747 | 				description:
748 | 					"The provider given account id for which to get the account info",
749 | 			}),
750 | 		}),
751 | 	},
752 | 	async (ctx) => {
753 | 		const account = await ctx.context.internalAdapter.findAccount(
754 | 			ctx.body.accountId,
755 | 		);
756 | 
757 | 		if (!account || account.userId !== ctx.context.session.user.id) {
758 | 			throw new APIError("BAD_REQUEST", {
759 | 				message: "Account not found",
760 | 			});
761 | 		}
762 | 
763 | 		const provider = ctx.context.socialProviders.find(
764 | 			(p) => p.id === account.providerId,
765 | 		);
766 | 
767 | 		if (!provider) {
768 | 			throw new APIError("INTERNAL_SERVER_ERROR", {
769 | 				message: `Provider account provider is ${account.providerId} but it is not configured`,
770 | 			});
771 | 		}
772 | 		const tokens = await getAccessToken({
773 | 			...ctx,
774 | 			body: {
775 | 				accountId: account.id,
776 | 				providerId: account.providerId,
777 | 			},
778 | 			returnHeaders: false,
779 | 		});
780 | 		if (!tokens.accessToken) {
781 | 			throw new APIError("BAD_REQUEST", {
782 | 				message: "Access token not found",
783 | 			});
784 | 		}
785 | 		const info = await provider.getUserInfo({
786 | 			...tokens,
787 | 			accessToken: tokens.accessToken as string,
788 | 		});
789 | 		return ctx.json(info);
790 | 	},
791 | );
792 | 
```

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

```markdown
  1 | ---
  2 | title: Database
  3 | description: Learn how to use a database with Better Auth.
  4 | ---
  5 | 
  6 | ## Adapters
  7 | 
  8 | Better Auth requires a database connection to store data. The database will be used to store data such as users, sessions, and more. Plugins can also define their own database tables to store data.
  9 | 
 10 | You can pass a database connection to Better Auth by passing a supported database instance in the database options. You can learn more about supported database adapters in the [Other relational databases](/docs/adapters/other-relational-databases) documentation.
 11 | 
 12 | ## CLI
 13 | 
 14 | Better Auth comes with a CLI tool to manage database migrations and generate schema.
 15 | 
 16 | ### Running Migrations
 17 | 
 18 | The cli checks your database and prompts you to add missing tables or update existing ones with new columns. This is only supported for the built-in Kysely adapter. For other adapters, you can use the `generate` command to create the schema and handle the migration through your ORM.
 19 | 
 20 | ```bash
 21 | npx @better-auth/cli migrate
 22 | ```
 23 | 
 24 | ### Generating Schema
 25 | 
 26 | Better Auth also provides a `generate` command to generate the schema required by Better Auth. The `generate` command creates the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database.
 27 | 
 28 | ```bash
 29 | npx @better-auth/cli generate
 30 | ```
 31 | 
 32 | See the [CLI](/docs/concepts/cli) documentation for more information on the CLI.
 33 | 
 34 | <Callout>
 35 |   If you prefer adding tables manually, you can do that as well. The core schema
 36 |   required by Better Auth is described below and you can find additional schema
 37 |   required by plugins in the plugin documentation.
 38 | </Callout>
 39 | 
 40 | ## Secondary Storage
 41 | 
 42 | Secondary storage in Better Auth allows you to use key-value stores for managing session data, rate limiting counters, etc. This can be useful when you want to offload the storage of this intensive records to a high performance storage or even RAM.
 43 | 
 44 | ### Implementation
 45 | 
 46 | To use secondary storage, implement the `SecondaryStorage` interface:
 47 | 
 48 | ```typescript
 49 | interface SecondaryStorage {
 50 |   get: (key: string) => Promise<unknown>; 
 51 |   set: (key: string, value: string, ttl?: number) => Promise<void>;
 52 |   delete: (key: string) => Promise<void>;
 53 | }
 54 | ```
 55 | 
 56 | Then, provide your implementation to the `betterAuth` function:
 57 | 
 58 | ```typescript
 59 | betterAuth({
 60 |   // ... other options
 61 |   secondaryStorage: {
 62 |     // Your implementation here
 63 |   },
 64 | });
 65 | ```
 66 | 
 67 | **Example: Redis Implementation**
 68 | 
 69 | Here's a basic example using Redis:
 70 | 
 71 | ```typescript
 72 | import { createClient } from "redis";
 73 | import { betterAuth } from "better-auth";
 74 | 
 75 | const redis = createClient();
 76 | await redis.connect();
 77 | 
 78 | export const auth = betterAuth({
 79 | 	// ... other options
 80 | 	secondaryStorage: {
 81 | 		get: async (key) => {
 82 | 			return await redis.get(key);
 83 | 		},
 84 | 		set: async (key, value, ttl) => {
 85 | 			if (ttl) await redis.set(key, value, { EX: ttl });
 86 | 			// or for ioredis:
 87 | 			// if (ttl) await redis.set(key, value, 'EX', ttl)
 88 | 			else await redis.set(key, value);
 89 | 		},
 90 | 		delete: async (key) => {
 91 | 			await redis.del(key);
 92 | 		}
 93 | 	}
 94 | });
 95 | ```
 96 | 
 97 | This implementation allows Better Auth to use Redis for storing session data and rate limiting counters. You can also add prefixes to the keys names.
 98 | 
 99 | ## Core Schema
100 | 
101 | Better Auth requires the following tables to be present in the database. The types are in `typescript` format. You can use corresponding types in your database.
102 | 
103 | ### User
104 | 
105 | Table Name: `user`
106 | 
107 | <DatabaseTable
108 |   fields={[
109 |     {
110 |       name: "id",
111 |       type: "string",
112 |       description: "Unique identifier for each user",
113 |       isPrimaryKey: true,
114 |     },
115 |     {
116 |       name: "name",
117 |       type: "string",
118 |       description: "User's chosen display name",
119 |     },
120 |     {
121 |       name: "email",
122 |       type: "string",
123 |       description: "User's email address for communication and login",
124 |     },
125 |     {
126 |       name: "emailVerified",
127 |       type: "boolean",
128 |       description: "Whether the user's email is verified",
129 |     },
130 |     {
131 |       name: "image",
132 |       type: "string",
133 |       description: "User's image url",
134 |       isOptional: true,
135 |     },
136 |     {
137 |       name: "createdAt",
138 |       type: "Date",
139 |       description: "Timestamp of when the user account was created",
140 |     },
141 |     {
142 |       name: "updatedAt",
143 |       type: "Date",
144 |       description: "Timestamp of the last update to the user's information",
145 |     },
146 |   ]}
147 | />
148 | 
149 | ### Session
150 | 
151 | Table Name: `session`
152 | 
153 | <DatabaseTable
154 |   fields={[
155 |     {
156 |       name: "id",
157 |       type: "string",
158 |       description: "Unique identifier for each session",
159 |       isPrimaryKey: true,
160 |     },
161 |     {
162 |       name: "userId",
163 |       type: "string",
164 |       description: "The ID of the user",
165 |       isForeignKey: true,
166 |     },
167 |     {
168 |       name: "token",
169 |       type: "string",
170 |       description: "The unique session token",
171 |       isUnique: true,
172 |     },
173 |     {
174 |       name: "expiresAt",
175 |       type: "Date",
176 |       description: "The time when the session expires",
177 |     },
178 |     {
179 |       name: "ipAddress",
180 |       type: "string",
181 |       description: "The IP address of the device",
182 |       isOptional: true,
183 |     },
184 |     {
185 |       name: "userAgent",
186 |       type: "string",
187 |       description: "The user agent information of the device",
188 |       isOptional: true,
189 |     },
190 |     {
191 |       name: "createdAt",
192 |       type: "Date",
193 |       description: "Timestamp of when the session was created",
194 |     },
195 |     {
196 |       name: "updatedAt",
197 |       type: "Date",
198 |       description: "Timestamp of when the session was updated",
199 |     },
200 |   ]}
201 | />
202 | 
203 | ### Account
204 | 
205 | Table Name: `account`
206 | 
207 | <DatabaseTable
208 |   fields={[
209 |     {
210 |       name: "id",
211 |       type: "string",
212 |       description: "Unique identifier for each account",
213 |       isPrimaryKey: true,
214 |     },
215 |     {
216 |       name: "userId",
217 |       type: "string",
218 |       description: "The ID of the user",
219 |       isForeignKey: true,
220 |     },
221 |     {
222 |       name: "accountId",
223 |       type: "string",
224 |       description:
225 |         "The ID of the account as provided by the SSO or equal to userId for credential accounts",
226 |     },
227 |     {
228 |       name: "providerId",
229 |       type: "string",
230 |       description: "The ID of the provider",
231 |     },
232 |     {
233 |       name: "accessToken",
234 |       type: "string",
235 |       description: "The access token of the account. Returned by the provider",
236 |       isOptional: true,
237 |     },
238 |     {
239 |       name: "refreshToken",
240 |       type: "string",
241 |       description: "The refresh token of the account. Returned by the provider",
242 |       isOptional: true,
243 |     },
244 |     {
245 |       name: "accessTokenExpiresAt",
246 |       type: "Date",
247 |       description: "The time when the access token expires",
248 |       isOptional: true,
249 |     },
250 |     {
251 |       name: "refreshTokenExpiresAt",
252 |       type: "Date",
253 |       description: "The time when the refresh token expires",
254 |       isOptional: true,
255 |     },
256 |     {
257 |       name: "scope",
258 |       type: "string",
259 |       description: "The scope of the account. Returned by the provider",
260 |       isOptional: true,
261 |     },
262 |     {
263 |       name: "idToken",
264 |       type: "string",
265 |       description: "The ID token returned from the provider",
266 |       isOptional: true,
267 |     },
268 |     {
269 |       name: "password",
270 |       type: "string",
271 |       description:
272 |         "The password of the account. Mainly used for email and password authentication",
273 |       isOptional: true,
274 |     },
275 |     {
276 |       name: "createdAt",
277 |       type: "Date",
278 |       description: "Timestamp of when the account was created",
279 |     },
280 |     {
281 |       name: "updatedAt",
282 |       type: "Date",
283 |       description: "Timestamp of when the account was updated",
284 |     },
285 |   ]}
286 | />
287 | 
288 | ### Verification
289 | 
290 | Table Name: `verification`
291 | 
292 | <DatabaseTable
293 |   fields={[
294 |     {
295 |       name: "id",
296 |       type: "string",
297 |       description: "Unique identifier for each verification",
298 |       isPrimaryKey: true,
299 |     },
300 |     {
301 |       name: "identifier",
302 |       type: "string",
303 |       description: "The identifier for the verification request",
304 |     },
305 |     {
306 |       name: "value",
307 |       type: "string",
308 |       description: "The value to be verified",
309 |     },
310 |     {
311 |       name: "expiresAt",
312 |       type: "Date",
313 |       description: "The time when the verification request expires",
314 |     },
315 |     {
316 |       name: "createdAt",
317 |       type: "Date",
318 |       description: "Timestamp of when the verification request was created",
319 |     },
320 |     {
321 |       name: "updatedAt",
322 |       type: "Date",
323 |       description: "Timestamp of when the verification request was updated",
324 |     },
325 |   ]}
326 | />
327 | 
328 | ## Custom Tables
329 | 
330 | Better Auth allows you to customize the table names and column names for the core schema. You can also extend the core schema by adding additional fields to the user and session tables.
331 | 
332 | ### Custom Table Names
333 | 
334 | You can customize the table names and column names for the core schema by using the `modelName` and `fields` properties in your auth config:
335 | 
336 | ```ts title="auth.ts"
337 | export const auth = betterAuth({
338 |   user: {
339 |     modelName: "users",
340 |     fields: {
341 |       name: "full_name",
342 |       email: "email_address",
343 |     },
344 |   },
345 |   session: {
346 |     modelName: "user_sessions",
347 |     fields: {
348 |       userId: "user_id",
349 |     },
350 |   },
351 | });
352 | ```
353 | 
354 | <Callout>
355 |   Type inference in your code will still use the original field names (e.g.,
356 |   `user.name`, not `user.full_name`).
357 | </Callout>
358 | 
359 | To customize table names and column name for plugins, you can use the `schema` property in the plugin config:
360 | 
361 | ```ts title="auth.ts"
362 | import { betterAuth } from "better-auth";
363 | import { twoFactor } from "better-auth/plugins";
364 | 
365 | export const auth = betterAuth({
366 |   plugins: [
367 |     twoFactor({
368 |       schema: {
369 |         user: {
370 |           fields: {
371 |             twoFactorEnabled: "two_factor_enabled",
372 |             secret: "two_factor_secret",
373 |           },
374 |         },
375 |       },
376 |     }),
377 |   ],
378 | });
379 | ```
380 | 
381 | ### Extending Core Schema
382 | 
383 | Better Auth provides a type-safe way to extend the `user` and `session` schemas. You can add custom fields to your auth config, and the CLI will automatically update the database schema. These additional fields will be properly inferred in functions like `useSession`, `signUp.email`, and other endpoints that work with user or session objects.
384 | 
385 | To add custom fields, use the `additionalFields` property in the `user` or `session` object of your auth config. The `additionalFields` object uses field names as keys, with each value being a `FieldAttributes` object containing:
386 | 
387 | - `type`: The data type of the field (e.g., "string", "number", "boolean").
388 | - `required`: A boolean indicating if the field is mandatory.
389 | - `defaultValue`: The default value for the field (note: this only applies in the JavaScript layer; in the database, the field will be optional).
390 | - `input`: This determines whether a value can be provided when creating a new record (default: `true`). If there are additional fields, like `role`, that should not be provided by the user during signup, you can set this to `false`.
391 | 
392 | Here's an example of how to extend the user schema with additional fields:
393 | 
394 | ```ts title="auth.ts"
395 | import { betterAuth } from "better-auth";
396 | 
397 | export const auth = betterAuth({
398 |   user: {
399 |     additionalFields: {
400 |       role: {
401 |         type: "string",
402 |         required: false,
403 |         defaultValue: "user",
404 |         input: false, // don't allow user to set role
405 |       },
406 |       lang: {
407 |         type: "string",
408 |         required: false,
409 |         defaultValue: "en",
410 |       },
411 |     },
412 |   },
413 | });
414 | ```
415 | 
416 | Now you can access the additional fields in your application logic.
417 | 
418 | ```ts
419 | //on signup
420 | const res = await auth.api.signUpEmail({
421 |   email: "[email protected]",
422 |   password: "password",
423 |   name: "John Doe",
424 |   lang: "fr",
425 | });
426 | 
427 | //user object
428 | res.user.role; // > "admin"
429 | res.user.lang; // > "fr"
430 | ```
431 | 
432 | <Callout>
433 |   See the
434 |   [TypeScript](/docs/concepts/typescript#inferring-additional-fields-on-client)
435 |   documentation for more information on how to infer additional fields on the
436 |   client side.
437 | </Callout>
438 | 
439 | If you're using social / OAuth providers, you may want to provide `mapProfileToUser` to map the profile data to the user object. So, you can populate additional fields from the provider's profile.
440 | 
441 | **Example: Mapping Profile to User For `firstName` and `lastName`**
442 | 
443 | ```ts title="auth.ts"
444 | import { betterAuth } from "better-auth";
445 | 
446 | export const auth = betterAuth({
447 |   socialProviders: {
448 |     github: {
449 |       clientId: "YOUR_GITHUB_CLIENT_ID",
450 |       clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
451 |       mapProfileToUser: (profile) => {
452 |         return {
453 |           firstName: profile.name.split(" ")[0],
454 |           lastName: profile.name.split(" ")[1],
455 |         };
456 |       },
457 |     },
458 |     google: {
459 |       clientId: "YOUR_GOOGLE_CLIENT_ID",
460 |       clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
461 |       mapProfileToUser: (profile) => {
462 |         return {
463 |           firstName: profile.given_name,
464 |           lastName: profile.family_name,
465 |         };
466 |       },
467 |     },
468 |   },
469 | });
470 | ```
471 | 
472 | ### ID Generation
473 | 
474 | Better Auth by default will generate unique IDs for users, sessions, and other entities. If you want to customize how IDs are generated, you can configure this in the `advanced.database.generateId` option in your auth config.
475 | 
476 | You can also disable ID generation by setting the `advanced.database.generateId` option to `false`. This will assume your database will generate the ID automatically.
477 | 
478 | **Example: Automatic Database IDs**
479 | 
480 | ```ts title="auth.ts"
481 | import { betterAuth } from "better-auth";
482 | import { db } from "./db";
483 | 
484 | export const auth = betterAuth({
485 |   database: db,
486 |   advanced: {
487 |     database: {
488 |       generateId: false,
489 |     },
490 |   },
491 | });
492 | ```
493 | 
494 | **Example: Using a Custom ID Generator**
495 | 
496 | ```ts title="auth.ts"
497 | import { betterAuth } from "better-auth";
498 | import { db } from "./db";
499 | 
500 | export const auth = betterAuth({
501 |   database: db,
502 |   advanced: {
503 |     database: {
504 |       generateId: () => crypto.randomUUID(),
505 |     },
506 |   },
507 | });
508 | ```
509 | 
510 | ### Numeric IDs
511 | 
512 | If you prefer auto-incrementing numeric IDs, you can set the `advanced.database.useNumberId` option to `true`.
513 | Doing this will disable Better-Auth from generating IDs for any table, and will assume your
514 | database will generate the numeric ID automatically.
515 | 
516 | When enabled, the Better-Auth CLI will generate or migrate the schema with the `id` field as a numeric type for your database
517 | with auto-incrementing attributes associated with it.
518 | 
519 | ```ts
520 | import { betterAuth } from "better-auth";
521 | import { db } from "./db";
522 | 
523 | export const auth = betterAuth({
524 |   database: db,
525 |   advanced: {
526 |     database: {
527 |       useNumberId: true,
528 |     },
529 |   },
530 | });
531 | ```
532 | 
533 | <Callout type="info">
534 |   Better-Auth will continue to infer the type of the `id` field as a `string` for the database, but will
535 |   automatically convert it to a numeric type when fetching or inserting data from the database.
536 | 
537 |   It's likely when grabbing `id` values returned from Better-Auth that you'll receive a string version of a number,
538 |   this is normal. It's also expected that all id values passed to Better-Auth (eg via an endpoint body) is expected to be a string.
539 | </Callout>
540 | 
541 | 
542 | ### Database Hooks
543 | 
544 | Database hooks allow you to define custom logic that can be executed during the lifecycle of core database operations in Better Auth. You can create hooks for the following models: **user**, **session**, and **account**.
545 | 
546 | <Callout type="warn">
547 |   Additional fields are supported, however full type inference for these fields isn't yet supported.
548 |   Improved type support is planned.
549 | </Callout>
550 | 
551 | There are two types of hooks you can define:
552 | 
553 | #### 1. Before Hook
554 | 
555 | - **Purpose**: This hook is called before the respective entity (user, session, or account) is created, updated, or deleted.
556 | - **Behavior**: If the hook returns `false`, the operation will be aborted. And If it returns a data object, it'll replace the original payload.
557 | 
558 | #### 2. After Hook
559 | 
560 | - **Purpose**: This hook is called after the respective entity is created or updated.
561 | - **Behavior**: You can perform additional actions or modifications after the entity has been successfully created or updated.
562 | 
563 | **Example Usage**
564 | 
565 | ```typescript title="auth.ts"
566 | import { betterAuth } from "better-auth";
567 | 
568 | export const auth = betterAuth({
569 |   databaseHooks: {
570 |     user: {
571 |       create: {
572 |         before: async (user, ctx) => {
573 |           // Modify the user object before it is created
574 |           return {
575 |             data: {
576 |               // Ensure to return Better-Auth named fields, not the original field names in your database.
577 |               ...user,
578 |               firstName: user.name.split(" ")[0],
579 |               lastName: user.name.split(" ")[1],
580 |             },
581 |           };
582 |         },
583 |         after: async (user) => {
584 |           //perform additional actions, like creating a stripe customer
585 |         },
586 |       },
587 |       delete: {
588 |         before: async (user, ctx) => {
589 |           console.log(`User ${user.email} is being deleted`);
590 |           if (user.email.includes("admin")) {
591 |             return false; // Abort deletion
592 |           }
593 |           
594 |           return true; // Allow deletion
595 |         },
596 |         after: async (user) => {
597 |           console.log(`User ${user.email} has been deleted`);
598 |         },
599 |       },
600 |     },
601 |     session: {
602 |       delete: {
603 |         before: async (session, ctx) => {
604 |           console.log(`Session ${session.token} is being deleted`);
605 |           if (session.userId === "admin-user-id") {
606 |             return false; // Abort deletion
607 |           }
608 |           return true; // Allow deletion
609 |         },
610 |         after: async (session) => {
611 |           console.log(`Session ${session.token} has been deleted`);
612 |         },
613 |       },
614 |     },
615 |   },
616 | });
617 | ```
618 | 
619 | #### Throwing Errors
620 | 
621 | If you want to stop the database hook from proceeding, you can throw errors using the `APIError` class imported from `better-auth/api`.
622 | 
623 | ```typescript title="auth.ts"
624 | import { betterAuth } from "better-auth";
625 | import { APIError } from "better-auth/api";
626 | 
627 | export const auth = betterAuth({
628 |   databaseHooks: {
629 |     user: {
630 |       create: {
631 |         before: async (user, ctx) => {
632 |           if (user.isAgreedToTerms === false) {
633 |             // Your special condition.
634 |             // Send the API error.
635 |             throw new APIError("BAD_REQUEST", {
636 |               message: "User must agree to the TOS before signing up.",
637 |             });
638 |           }
639 |           return {
640 |             data: user,
641 |           };
642 |         },
643 |       },
644 |     },
645 |   },
646 | });
647 | ```
648 | 
649 | #### Using the Context Object
650 | 
651 | The context object (`ctx`), passed as the second argument to the hook, contains useful information. For `update` hooks, this includes the current `session`, which you can use to access the logged-in user's details.
652 | 
653 | ```typescript title="auth.ts"
654 | import { betterAuth } from "better-auth";
655 | 
656 | export const auth = betterAuth({
657 |   databaseHooks: {
658 |     user: {
659 |       update: {
660 |         before: async (data, ctx) => {
661 |           // You can access the session from the context object.
662 |           if (ctx.context.session) {
663 |             console.log("User update initiated by:", ctx.context.session.userId);
664 |           }
665 |           return { data };
666 |         },
667 |       },
668 |     },
669 |   },
670 | });
671 | ```
672 | 
673 | Much like standard hooks, database hooks also provide a `ctx` object that offers a variety of useful properties. Learn more in the [Hooks Documentation](/docs/concepts/hooks#ctx).
674 | 
675 | ## Plugins Schema
676 | 
677 | Plugins can define their own tables in the database to store additional data. They can also add columns to the core tables to store additional data. For example, the two factor authentication plugin adds the following columns to the `user` table:
678 | 
679 | - `twoFactorEnabled`: Whether two factor authentication is enabled for the user.
680 | - `twoFactorSecret`: The secret key used to generate TOTP codes.
681 | - `twoFactorBackupCodes`: Encrypted backup codes for account recovery.
682 | 
683 | To add new tables and columns to your database, you have two options:
684 | 
685 | `CLI`: Use the migrate or generate command. These commands will scan your database and guide you through adding any missing tables or columns.
686 | `Manual Method`: Follow the instructions in the plugin documentation to manually add tables and columns.
687 | 
688 | Both methods ensure your database schema stays up to date with your plugins' requirements.
689 | 
```

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

```markdown
  1 | ---
  2 | title: Plugins
  3 | description: Learn how to use plugins with Better Auth.
  4 | ---
  5 | 
  6 | Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors.
  7 | 
  8 | Better Auth comes with many built-in plugins ready to use. Check the plugins section for details. You can also create your own plugins.
  9 | 
 10 | ## Using a Plugin
 11 | 
 12 | Plugins can be a server-side plugin, a client-side plugin, or both.
 13 | 
 14 | To add a plugin on the server, include it in the `plugins` array in your auth configuration. The plugin will initialize with the provided options.
 15 | 
 16 | ```ts title="server.ts"
 17 | import { betterAuth } from "better-auth";
 18 | 
 19 | export const auth = betterAuth({
 20 |     plugins: [
 21 |         // Add your plugins here
 22 |     ]
 23 | });
 24 | ```
 25 | 
 26 | Client plugins are added when creating the client. Most plugin require both server and client plugins to work correctly.
 27 | The Better Auth auth client on the frontend uses the `createAuthClient` function provided by `better-auth/client`.
 28 | 
 29 | ```ts title="auth-client.ts"
 30 | import { createAuthClient } from "better-auth/client";
 31 | 
 32 | const authClient =  createAuthClient({
 33 |     plugins: [
 34 |         // Add your client plugins here
 35 |     ]
 36 | });
 37 | ```
 38 | 
 39 | We recommend keeping the auth-client and your normal auth instance in separate files.
 40 | <Files>
 41 |   <Folder name="auth" defaultOpen>
 42 |     <File name="server.ts" />
 43 |     <File name="auth-client.ts" />
 44 |   </Folder>
 45 | </Files>
 46 | 
 47 | ## Creating a Plugin
 48 | 
 49 | To get started, you'll need a server plugin.
 50 | Server plugins are the backbone of all plugins, and client plugins are there to provide an interface with frontend APIs to easily work with your server plugins.
 51 | 
 52 | <Callout type="info">
 53 |     If your server plugins has endpoints that needs to be called from the client, you'll also need to create a client plugin.
 54 | </Callout>
 55 | 
 56 | ### What can a plugin do?
 57 | 
 58 | * Create custom `endpoint`s to perform any action you want.
 59 | * Extend database tables with custom `schemas`.
 60 | * Use a `middleware` to target a group of routes using it's route matcher, and run only when those routes are called through a request.
 61 | * Use `hooks` to target a specific route or request. And if you want to run the hook even if the endpoint is called directly.
 62 | * Use `onRequest` or `onResponse` if you want to do something that affects all requests or responses.
 63 | * Create custom `rate-limit` rule.
 64 | 
 65 | ## Create a Server plugin
 66 | 
 67 | To create a server plugin you need to pass an object that satisfies the `BetterAuthPlugin` interface.
 68 | 
 69 | The only required property is `id`, which is a unique identifier for the plugin.
 70 | Both server and client plugins can use the same `id`.
 71 | 
 72 | ```ts title="plugin.ts"
 73 | import type { BetterAuthPlugin } from "better-auth";
 74 | 
 75 | export const myPlugin = ()=>{
 76 |     return {
 77 |         id: "my-plugin",
 78 |     } satisfies BetterAuthPlugin
 79 | }
 80 | ```
 81 | <Callout>
 82 |     You don't have to make the plugin a function, but it's recommended to do so. This way you can pass options to the plugin and it's consistent with the built-in plugins.
 83 | </Callout>
 84 | 
 85 | ### Endpoints
 86 | 
 87 | To add endpoints to the server, you can pass `endpoints` which requires an object with the key being any `string` and the value being an `AuthEndpoint`.
 88 | 
 89 | To create an Auth Endpoint you'll need to import `createAuthEndpoint` from `better-auth`.
 90 | 
 91 | Better Auth uses wraps around another library called <Link href="https://github.com/bekacru/better-call"> Better Call </Link> to create endpoints. Better call is a simple ts web framework made by the same team behind Better Auth.
 92 | 
 93 | ```ts title="plugin.ts"
 94 | import { createAuthEndpoint } from "better-auth/api";
 95 | 
 96 | const myPlugin = ()=> {
 97 |     return {
 98 |         id: "my-plugin",
 99 |         endpoints: {
100 |             getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
101 |                 method: "GET",
102 |             }, async(ctx) => {
103 |                 return ctx.json({
104 |                     message: "Hello World"
105 |                 })
106 |             })
107 |         }
108 |     } satisfies BetterAuthPlugin
109 | }
110 | ```
111 | 
112 | Create Auth endpoints wraps around `createEndpoint` from Better Call. Inside the `ctx` object, it'll provide another object called `context` that give you access better-auth specific contexts including `options`, `db`, `baseURL` and more.
113 | 
114 | **Context Object**
115 | 
116 | - `appName`: The name of the application. Defaults to "Better Auth".
117 | - `options`: The options passed to the Better Auth instance.
118 | - `tables`:  Core tables definition. It is an object which has the table name as the key and the schema definition as the value.
119 | - `baseURL`: the baseURL of the auth server. This includes the path. For example, if the server is running on `http://localhost:3000`, the baseURL will be `http://localhost:3000/api/auth` by default unless changed by the user.
120 | - `session`: The session configuration. Includes `updateAge` and `expiresIn` values.
121 | - `secret`: The secret key used for various purposes. This is defined by the user.
122 | - `authCookie`: The default cookie configuration for core auth cookies.
123 | - `logger`: The logger instance used by Better Auth.
124 | - `db`: The Kysely instance used by Better Auth to interact with the database.
125 | - `adapter`: This is the same as db but it give you `orm` like functions to interact with the database. (we recommend using this over `db` unless you need raw sql queries or for performance reasons)
126 | - `internalAdapter`: These are internal db calls that are used by Better Auth. For example, you can use these calls to create a session instead of using `adapter` directly. `internalAdapter.createSession(userId)`
127 | - `createAuthCookie`: This is a helper function that lets you get a cookie `name` and `options` for either to `set` or `get` cookies. It implements things like `__Secure-` prefix for cookies based on whether the connection is secure (HTTPS) or the application is running in production mode.
128 | 
129 | For other properties, you can check the <Link href="https://github.com/bekacru/better-call">Better Call</Link> documentation and the <Link href="https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/init.ts">source code </Link>.
130 | 
131 | 
132 | **Rules for Endpoints**
133 | 
134 | - Makes sure you use kebab-case for the endpoint path
135 | - Make sure to only use `POST` or `GET` methods for the endpoints.
136 | - Any function that modifies a data should be a `POST` method.
137 | - Any function that fetches data should be a `GET` method.
138 | - Make sure to use the `createAuthEndpoint` function to create API endpoints.
139 | - Make sure your paths are unique to avoid conflicts with other plugins. If you're using a common path, add the plugin name as a prefix to the path. (`/my-plugin/hello-world` instead of `/hello-world`.)
140 | 
141 | ### Schema
142 | 
143 | You can define a database schema for your plugin by passing a `schema` object. The schema object should have the table name as the key and the schema definition as the value.
144 | 
145 | ```ts title="plugin.ts"
146 | import { BetterAuthPlugin } from "better-auth/plugins";
147 | 
148 | const myPlugin = ()=> {
149 |     return {
150 |         id: "my-plugin",
151 |         schema: {
152 |             myTable: {
153 |                 fields: {
154 |                     name: {
155 |                         type: "string"
156 |                     }
157 |                 },
158 |                 modelName: "myTable" // optional if you want to use a different name than the key
159 |             }
160 |         }
161 |     } satisfies BetterAuthPlugin
162 | }
163 | ```
164 | 
165 | **Fields**
166 | 
167 | By default better-auth will create an `id` field for each table. You can add additional fields to the table by adding them to the `fields` object.
168 | 
169 | The key is the column name and the value is the column definition. The column definition can have the following properties:
170 | 
171 | `type`: The type of the field. It can be `string`, `number`, `boolean`, `date`.
172 | 
173 | `required`:  if the field should be required on a new record. (default: `false`)
174 | 
175 | `unique`: if the field should be unique. (default: `false`)
176 | 
177 | `reference`: if the field is a reference to another table. (default: `null`) It takes an object with the following properties:
178 |     - `model`: The table name to reference.
179 |     - `field`: The field name to reference.
180 |     - `onDelete`: The action to take when the referenced record is deleted. (default: `null`)
181 | 
182 | **Other Schema Properties**
183 | 
184 | `disableMigration`: if the table should not be migrated. (default: `false`)
185 | 
186 | ```ts title="plugin.ts"
187 | const myPlugin = (opts: PluginOptions)=>{
188 |     return {
189 |         id: "my-plugin",
190 |         schema: {
191 |             rateLimit: {
192 |                 fields: {
193 |                     key: {
194 |                         type: "string",
195 |                     },
196 |                 },
197 |                 disableMigration: opts.storage.provider !== "database", // [!code highlight]
198 |             },
199 |         },
200 |     } satisfies BetterAuthPlugin
201 | }
202 | ```
203 | 
204 | if you add additional fields to a `user` or `session` table, the types will be inferred automatically on `getSession` and `signUpEmail` calls.
205 | 
206 | ```ts title="plugin.ts"
207 | 
208 | const myPlugin = ()=>{
209 |     return {
210 |         id: "my-plugin",
211 |         schema: {
212 |             user: {
213 |                 fields: {
214 |                     age: {
215 |                         type: "number",
216 |                     },
217 |                 },
218 |             },
219 |         },
220 |     } satisfies BetterAuthPlugin
221 | }
222 | ```
223 | 
224 | This will add an `age` field to the `user` table and all `user` returning endpoints will include the `age` field and it'll be inferred properly by typescript.
225 | 
226 | <Callout type="warn">
227 | Don't store sensitive information in `user` or `session` table. Crate a new table if you need to store sensitive information.
228 | </Callout>
229 | 
230 | ### Hooks
231 | 
232 | Hooks are used to run code before or after an action is performed, either from a client or directly on the server. You can add hooks to the server by passing a `hooks` object, which should contain `before` and `after` properties.
233 | 
234 | ```ts title="plugin.ts"
235 | import {  createAuthMiddleware } from "better-auth/plugins";
236 | 
237 | const myPlugin = ()=>{
238 |     return {
239 |         id: "my-plugin",
240 |         hooks: {
241 |             before: [{
242 |                     matcher: (context)=>{
243 |                         return context.headers.get("x-my-header") === "my-value"
244 |                     },
245 |                     handler: createAuthMiddleware(async (ctx)=>{
246 |                         //do something before the request
247 |                         return  {
248 |                             context: ctx // if you want to modify the context
249 |                         }
250 |                     })
251 |                 }],
252 |             after: [{
253 |                 matcher: (context)=>{
254 |                     return context.path === "/sign-up/email"
255 |                 },
256 |                 handler: createAuthMiddleware(async (ctx)=>{
257 |                     return ctx.json({
258 |                         message: "Hello World"
259 |                     }) // if you want to modify the response
260 |                 })
261 |             }]
262 |         }
263 |     } satisfies BetterAuthPlugin
264 | }
265 | ```
266 | 
267 | ### Middleware
268 | 
269 | You can add middleware to the server by passing a `middlewares` array. This array should contain middleware objects, each with a `path` and a `middleware` property. Unlike hooks, middleware only runs on `api` requests from a client. If the endpoint is invoked directly, the middleware will not run.
270 | 
271 | The `path` can be either a string or a path matcher, using the same path-matching system as `better-call`.
272 | 
273 | If you throw an `APIError` from the middleware or returned a `Response` object, the request will be stopped and the response will be sent to the client.
274 | 
275 | ```ts title="plugin.ts"
276 | const myPlugin = ()=>{
277 |     return {
278 |         id: "my-plugin",
279 |         middlewares: [
280 |             {
281 |                 path: "/my-plugin/hello-world",
282 |                 middleware: createAuthMiddleware(async(ctx)=>{
283 |                     //do something
284 |                 })
285 |             }
286 |         ]
287 |     } satisfies BetterAuthPlugin
288 | }
289 | ```
290 | 
291 | 
292 | ### On Request & On Response
293 | 
294 | Additional to middlewares, you can also hook into right before a request is made and right after a response is returned. This is mostly useful if you want to do something that affects all requests or responses.
295 | 
296 | #### On Request
297 | 
298 | The `onRequest` function is called right before the request is made. It takes two parameters: the `request` and the `context` object.
299 | 
300 | Here’s how it works:
301 | 
302 | - **Continue as Normal**: If you don't return anything, the request will proceed as usual.
303 | - **Interrupt the Request**: To stop the request and send a response, return an object with a `response` property that contains a `Response` object.
304 | - **Modify the Request**: You can also return a modified `request` object to change the request before it's sent.
305 | 
306 | ```ts title="plugin.ts"
307 | const myPlugin = ()=> {
308 |     return  {
309 |         id: "my-plugin",
310 |         onRequest: async (request, context) => {
311 |             //do something
312 |         },
313 |     } satisfies BetterAuthPlugin
314 | }
315 | ```
316 | 
317 | #### On Response
318 | 
319 | The `onResponse` function is executed immediately after a response is returned. It takes two parameters: the `response` and the `context` object.
320 | 
321 | Here’s how to use it:
322 | 
323 | - **Modify the Response**: You can return a modified response object to change the response before it is sent to the client.
324 | - **Continue Normally**: If you don't return anything, the response will be sent as is.
325 | 
326 | ```ts title="plugin.ts"
327 | const myPlugin = ()=>{
328 |     return {
329 |         id: "my-plugin",
330 |         onResponse: async (response, context) => {
331 |             //do something
332 |         },
333 |     } satisfies BetterAuthPlugin
334 | }
335 | ```
336 | 
337 | ### Rate Limit
338 | 
339 | You can define custom rate limit rules for your plugin by passing a `rateLimit` array. The rate limit array should contain an array of rate limit objects.
340 | 
341 | ```ts title="plugin.ts"
342 | const myPlugin = ()=>{
343 |     return {
344 |         id: "my-plugin",
345 |         rateLimit: [
346 |             {
347 |                 pathMatcher: (path)=>{
348 |                     return path === "/my-plugin/hello-world"
349 |                 },
350 |                 limit: 10,
351 |                 window: 60,
352 |             }
353 |         ]
354 |     } satisfies BetterAuthPlugin
355 | }
356 | ```
357 | 
358 | ### Server-plugin helper functions
359 | 
360 | Some additional helper functions for creating server plugins.
361 | 
362 | #### `getSessionFromCtx`
363 | 
364 | Allows you to get the client's session data by passing the auth middleware's `context`.
365 | 
366 | ```ts title="plugin.ts"
367 | import {  createAuthMiddleware } from "better-auth/plugins";
368 | import { getSessionFromCtx } from "better-auth/api";
369 | 
370 | const myPlugin = {
371 |     id: "my-plugin",
372 |     hooks: {
373 |         before: [{
374 |                 matcher: (context)=>{
375 |                     return context.headers.get("x-my-header") === "my-value"
376 |                 },
377 |                 handler: createAuthMiddleware(async (ctx) => {
378 |                     const session = await getSessionFromCtx(ctx);
379 |                     //do something with the client's session.
380 | 
381 |                     return  {
382 |                         context: ctx
383 |                     }
384 |                 })
385 |             }],
386 |     }
387 | } satisfies BetterAuthPlugin
388 | ```
389 | 
390 | #### `sessionMiddleware`
391 | 
392 | A middleware that checks if the client has a valid session. If the client has a valid session, it'll add the session data to the context object.
393 | 
394 | ```ts title="plugin.ts"
395 | import { createAuthMiddleware } from "better-auth/plugins";
396 | import { sessionMiddleware } from "better-auth/api";
397 | 
398 | const myPlugin = ()=>{
399 |     return {
400 |         id: "my-plugin",
401 |         endpoints: {
402 |             getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
403 |                 method: "GET",
404 |                 use: [sessionMiddleware], // [!code highlight]
405 |             }, async(ctx) => {
406 |                 const session = ctx.context.session;
407 |                 return ctx.json({
408 |                     message: "Hello World"
409 |                 })
410 |             })
411 |         }
412 |     } satisfies BetterAuthPlugin
413 | }
414 | ```
415 | 
416 | 
417 | ## Creating a client plugin
418 | 
419 | If your endpoints needs to be called from the client, you'll need to also create a client plugin. Better Auth clients can infer the endpoints from the server plugins. You can also add additional client side logic.
420 | 
421 | ```ts title="client-plugin.ts"
422 | import type { BetterAuthClientPlugin } from "better-auth";
423 | 
424 | export const myPluginClient = ()=>{
425 |     return {
426 |         id: "my-plugin",
427 |     } satisfies BetterAuthClientPlugin
428 | }
429 | ```
430 | 
431 | ### Endpoint Interface
432 | 
433 | Endpoints are inferred from the server plugin by adding a `$InferServerPlugin` key to the client plugin.
434 | 
435 | The client infers the `path` as an object and converts kebab-case to camelCase. For example, `/my-plugin/hello-world` becomes `myPlugin.helloWorld`.
436 | 
437 | ```ts title="client-plugin.ts"
438 | import type { BetterAuthClientPlugin } from "better-auth/client";
439 | import type { myPlugin } from "./plugin";
440 | 
441 | const myPluginClient = ()=> {
442 |     return  {
443 |         id: "my-plugin",
444 |         $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
445 |     } satisfies BetterAuthClientPlugin
446 | }
447 | ```
448 | 
449 | ### Get actions
450 | 
451 | If you need to add additional methods or what not to the client you can use the `getActions` function. This function is called with the `fetch` function from the client.
452 | 
453 | Better Auth uses <Link href="https://better-fetch.vercel.app"> Better fetch </Link> to make requests. Better fetch is a simple fetch wrapper made by the same author of Better Auth.
454 | 
455 | ```ts title="client-plugin.ts"
456 | import type { BetterAuthClientPlugin } from "better-auth/client";
457 | import type { myPlugin } from "./plugin";
458 | import type { BetterFetchOption } from "@better-fetch/fetch";
459 | 
460 | const myPluginClient = {
461 |     id: "my-plugin",
462 |     $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
463 |     getActions: ($fetch)=>{
464 |         return {
465 |             myCustomAction: async (data: {
466 |                 foo: string,
467 |             }, fetchOptions?: BetterFetchOption)=>{
468 |                 const res = $fetch("/custom/action", {
469 |                     method: "POST",
470 |                     body: {
471 |                         foo: data.foo
472 |                     },
473 |                     ...fetchOptions
474 |                 })
475 |                 return res
476 |             }
477 |         }
478 |     }
479 | } satisfies BetterAuthClientPlugin
480 | ```
481 | 
482 | <Callout>
483 | As a general guideline, ensure that each function accepts only one argument, with an optional second argument for fetchOptions to allow users to pass additional options to the fetch call. The function should return an object containing data and error keys.
484 | 
485 | If your use case involves actions beyond API calls, feel free to deviate from this rule.
486 | </Callout>
487 | 
488 | ### Get Atoms
489 | 
490 | This is only useful if you want to provide `hooks` like `useSession`.
491 | 
492 | Get atoms is called with the `fetch` function from better fetch and it should return an object with the atoms. The atoms should be created using <Link href="https://github.com/nanostores/nanostores">nanostores</Link>. The atoms will be resolved by each framework `useStore` hook provided by nanostores.
493 | 
494 | ```ts title="client-plugin.ts"
495 | import { atom } from "nanostores";
496 | import type { BetterAuthClientPlugin } from "better-auth/client";
497 | 
498 | const myPluginClient = {
499 |     id: "my-plugin",
500 |     $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
501 |     getAtoms: ($fetch)=>{
502 |         const myAtom = atom<null>()
503 |         return {
504 |             myAtom
505 |         }
506 |     }
507 | } satisfies BetterAuthClientPlugin
508 | ```
509 | 
510 | See built-in plugins for examples of how to use atoms properly.
511 | 
512 | ### Path methods
513 | 
514 | By default, inferred paths use `GET` method if they don't require a body and `POST` if they do. You can override this by passing a `pathMethods` object. The key should be the path and the value should be the method ("POST" | "GET").
515 | 
516 | ```ts title="client-plugin.ts"
517 | import type { BetterAuthClientPlugin } from "better-auth/client";
518 | import type { myPlugin } from "./plugin";
519 | 
520 | const myPluginClient = {
521 |     id: "my-plugin",
522 |     $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
523 |     pathMethods: {
524 |         "/my-plugin/hello-world": "POST"
525 |     }
526 | } satisfies BetterAuthClientPlugin
527 | ```
528 | 
529 | ### Fetch plugins
530 | 
531 | If you need to use better fetch plugins you can pass them to the `fetchPlugins` array. You can read more about better fetch plugins in the <Link href="https://better-fetch.vercel.app/docs/plugins">better fetch documentation</Link>.
532 | 
533 | ### Atom Listeners
534 | 
535 | This is only useful if you want to provide `hooks` like `useSession` and you want to listen to atoms and re-evaluate them when they change.
536 | 
537 | You can see how this is used in the built-in plugins.
```

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

```markdown
  1 | ---
  2 | title: Admin
  3 | description: Admin plugin for Better Auth
  4 | ---
  5 | 
  6 | The Admin plugin provides a set of administrative functions for user management in your application. It allows administrators to perform various operations such as creating users, managing user roles, banning/unbanning users, impersonating users, and more.
  7 | 
  8 | ## Installation
  9 | 
 10 | <Steps>
 11 |   <Step>
 12 |     ### Add the plugin to your auth config
 13 | 
 14 |     To use the Admin plugin, add it to your auth config.
 15 | 
 16 |     ```ts title="auth.ts"
 17 |     import { betterAuth } from "better-auth"
 18 |     import { admin } from "better-auth/plugins" // [!code highlight]
 19 | 
 20 |     export const auth = betterAuth({
 21 |         // ... other config options
 22 |         plugins: [
 23 |             admin() // [!code highlight]
 24 |         ]
 25 |     })
 26 |     ```
 27 | 
 28 |   </Step>
 29 | 
 30 |     <Step>
 31 |         ### Migrate the database
 32 | 
 33 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 34 | 
 35 |         <Tabs items={["migrate", "generate"]}>
 36 |             <Tab value="migrate">
 37 |             ```bash
 38 |             npx @better-auth/cli migrate
 39 |             ```
 40 |             </Tab>
 41 |             <Tab value="generate">
 42 |             ```bash
 43 |             npx @better-auth/cli generate
 44 |             ```
 45 |             </Tab>
 46 |         </Tabs>
 47 |         See the [Schema](#schema) section to add the fields manually.
 48 |     </Step>
 49 | 
 50 |   <Step>
 51 |     ### Add the client plugin
 52 | 
 53 |     Next, include the admin client plugin in your authentication client instance.
 54 | 
 55 |     ```ts title="auth-client.ts"
 56 |     import { createAuthClient } from "better-auth/client"
 57 |     import { adminClient } from "better-auth/client/plugins"
 58 | 
 59 |     export const authClient = createAuthClient({
 60 |         plugins: [
 61 |             adminClient()
 62 |         ]
 63 |     })
 64 |     ```
 65 | 
 66 |   </Step>
 67 | </Steps>
 68 | 
 69 | ## Usage
 70 | 
 71 | Before performing any admin operations, the user must be authenticated with an admin account. An admin is any user assigned the `admin` role or any user whose ID is included in the `adminUserIds` option.
 72 | 
 73 | ### Create User
 74 | 
 75 | Allows an admin to create a new user.
 76 | 
 77 | <APIMethod
 78 |   path="/admin/create-user"
 79 |   method="POST"
 80 |   resultVariable="newUser"
 81 | >
 82 | ```ts
 83 | type createUser = {
 84 |     /**
 85 |      * The email of the user. 
 86 |      */
 87 |     email: string = "[email protected]"
 88 |     /**
 89 |      * The password of the user. 
 90 |      */
 91 |     password: string = "some-secure-password"
 92 |     /**
 93 |      * The name of the user. 
 94 |      */
 95 |     name: string = "James Smith"
 96 |     /**
 97 |      * A string or array of strings representing the roles to apply to the new user. 
 98 |      */
 99 |     role?: string | string[] = "user"
100 |     /**
101 |      * Extra fields for the user. Including custom additional fields. 
102 |      */
103 |     data?: Record<string, any> = { customField: "customValue" }
104 | }
105 | ```
106 | </APIMethod>
107 | 
108 | ### List Users
109 | 
110 | Allows an admin to list all users in the database.
111 | 
112 | <APIMethod
113 |   path="/admin/list-users"
114 |   method="GET"
115 |   requireSession
116 |   note={"All properties are optional to configure. By default, 100 rows are returned, you can configure this by the `limit` property."}
117 |   resultVariable={"users"}
118 | >
119 | ```ts
120 | type listUsers = {
121 |   /**
122 |    * The value to search for. 
123 |    */
124 |   searchValue?: string = "some name"
125 |   /**
126 |    * The field to search in, defaults to email. Can be `email` or `name`. 
127 |    */
128 |   searchField?: "email" | "name" = "name"
129 |   /**
130 |    * The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. 
131 |    */
132 |   searchOperator?: "contains" | "starts_with" | "ends_with" = "contains"
133 |   /**
134 |    * The number of users to return. Defaults to 100.
135 |    */
136 |   limit?: string | number = 100
137 |   /**
138 |    * The offset to start from. 
139 |    */
140 |   offset?: string | number = 100
141 |   /**
142 |    * The field to sort by. 
143 |    */
144 |   sortBy?: string = "name"
145 |   /**
146 |    * The direction to sort by. 
147 |    */
148 |   sortDirection?: "asc" | "desc" = "desc"
149 |   /**
150 |    * The field to filter by. 
151 |    */
152 |   filterField?: string = "email"
153 |   /**
154 |    * The value to filter by. 
155 |    */
156 |   filterValue?: string | number | boolean = "[email protected]"
157 |   /**
158 |    * The operator to use for the filter. 
159 |    */
160 |   filterOperator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" = "eq"
161 | }
162 | ```
163 | </APIMethod>
164 | 
165 | 
166 | #### Query Filtering
167 | 
168 | The `listUsers` function supports various filter operators including `eq`, `contains`, `starts_with`, and `ends_with`.
169 | 
170 | #### Pagination
171 | 
172 | The `listUsers` function supports pagination by returning metadata alongside the user list. The response includes the following fields:
173 | 
174 | ```ts
175 | {
176 |   users: User[],   // Array of returned users
177 |   total: number,   // Total number of users after filters and search queries
178 |   limit: number | undefined,   // The limit provided in the query
179 |   offset: number | undefined   // The offset provided in the query
180 | }
181 | ```
182 | 
183 | ##### How to Implement Pagination
184 | 
185 | To paginate results, use the `total`, `limit`, and `offset` values to calculate:
186 | 
187 | - **Total pages:** `Math.ceil(total / limit)`
188 | - **Current page:** `(offset / limit) + 1`
189 | - **Next page offset:** `Math.min(offset + limit, (total - 1))` – The value to use as `offset` for the next page, ensuring it does not exceed the total number of pages.
190 | - **Previous page offset:** `Math.max(0, offset - limit)` – The value to use as `offset` for the previous page (ensuring it doesn’t go below zero).
191 | 
192 | ##### Example Usage
193 | 
194 | Fetching the second page with 10 users per page:
195 | 
196 | ```ts title="admin.ts"
197 | const pageSize = 10;
198 | const currentPage = 2;
199 | 
200 | const users = await authClient.admin.listUsers({
201 |     query: {
202 |         limit: pageSize,
203 |         offset: (currentPage - 1) * pageSize
204 |     }
205 | });
206 | 
207 | const totalUsers = users.total;
208 | const totalPages = Math.ceil(totalUsers / pageSize)
209 | ```
210 | 
211 | ### Set User Role
212 | 
213 | Changes the role of a user.
214 | 
215 | <APIMethod
216 |   path="/admin/set-role"
217 |   method="POST"
218 |   requireSession
219 | >
220 | ```ts
221 | type setRole = {
222 |     /**
223 |      * The user id which you want to set the role for.
224 |      */
225 |     userId?: string = "user-id"
226 |     /**
227 |      * The role to set, this can be a string or an array of strings. 
228 |      */
229 |     role: string | string[] = "admin"
230 | }
231 | ```
232 | </APIMethod>
233 | 
234 | ### Set User Password
235 | 
236 | Changes the password of a user.
237 | 
238 | <APIMethod
239 |   path="/admin/set-user-password"
240 |   method="POST"
241 |   requireSession
242 | >
243 | ```ts
244 | type setUserPassword = {
245 |     /**
246 |      * The new password. 
247 |      */
248 |     newPassword: string = 'new-password'
249 |     /**
250 |      * The user id which you want to set the password for.
251 |      */
252 |     userId: string = 'user-id'
253 | }
254 | ```
255 | </APIMethod>
256 | 
257 | ### Update user
258 | 
259 | Update a user's details.
260 | 
261 | <APIMethod
262 |   path="/admin/update-user"
263 |   method="POST"
264 |   requireSession
265 | >
266 | ```ts
267 | type adminUpdateUser = {
268 |     /**
269 |      * The user id which you want to update.
270 |      */
271 |     userId: string = "user-id"
272 |     /**
273 |      * The data to update.
274 |      */
275 |     data: Record<string, any> = { name: "John Doe" }
276 | }
277 | ```
278 | </APIMethod>
279 | 
280 | ### Ban User
281 | 
282 | Bans a user, preventing them from signing in and revokes all of their existing sessions.
283 | 
284 | 
285 | <APIMethod
286 |   path="/admin/ban-user"
287 |   method="POST"
288 |   requireSession
289 |   noResult
290 | >
291 | ```ts
292 | type banUser = {
293 |     /**
294 |      * The user id which you want to ban.
295 |      */
296 |     userId: string = "user-id"
297 |     /**
298 |      * The reason for the ban. 
299 |      */
300 |     banReason?: string = "Spamming"
301 |     /**
302 |      * The number of seconds until the ban expires. If not provided, the ban will never expire. 
303 |      */
304 |     banExpiresIn?: number = 60 * 60 * 24 * 7
305 | }
306 | ```
307 | </APIMethod>
308 | 
309 | ### Unban User
310 | 
311 | Removes the ban from a user, allowing them to sign in again.
312 | 
313 | <APIMethod
314 |   path="/admin/unban-user"
315 |   method="POST"
316 |   requireSession
317 |   noResult
318 | >
319 | ```ts
320 | type unbanUser = {
321 |     /**
322 |      * The user id which you want to unban.
323 |      */
324 |     userId: string = "user-id"
325 | }
326 | ```
327 | </APIMethod>
328 | 
329 | ### List User Sessions
330 | 
331 | Lists all sessions for a user.
332 | 
333 | <APIMethod
334 |   path="/admin/list-user-sessions"
335 |   method="POST"
336 |   requireSession
337 | >
338 | ```ts
339 | type listUserSessions = {
340 |     /**
341 |      * The user id. 
342 |      */
343 |     userId: string = "user-id"
344 | }
345 | ```
346 | </APIMethod>
347 | 
348 | ### Revoke User Session
349 | 
350 | Revokes a specific session for a user.
351 | 
352 | 
353 | <APIMethod
354 |   path="/admin/revoke-user-session"
355 |   method="POST"
356 |   requireSession
357 | >
358 | ```ts
359 | type revokeUserSession = {
360 |     /**
361 |      * The session token which you want to revoke. 
362 |      */
363 |     sessionToken: string = "session_token_here"
364 | }
365 | ```
366 | </APIMethod>
367 | 
368 | ### Revoke All Sessions for a User
369 | 
370 | Revokes all sessions for a user.
371 | 
372 | <APIMethod
373 |   path="/admin/revoke-user-sessions"
374 |   method="POST"
375 |   requireSession
376 | >
377 | ```ts
378 | type revokeUserSessions = {
379 |     /**
380 |      * The user id which you want to revoke all sessions for. 
381 |      */
382 |     userId: string = "user-id"
383 | }
384 | ```
385 | </APIMethod>
386 | 
387 | ### Impersonate User
388 | 
389 | This feature allows an admin to create a session that mimics the specified user. The session will remain active until either the browser session ends or it reaches 1 hour. You can change this duration by setting the `impersonationSessionDuration` option.
390 | 
391 | <APIMethod
392 |   path="/admin/impersonate-user"
393 |   method="POST"
394 |   requireSession
395 | >
396 | ```ts
397 | type impersonateUser = {
398 |     /**
399 |      * The user id which you want to impersonate. 
400 |      */
401 |     userId: string = "user-id"
402 | }
403 | ```
404 | </APIMethod>
405 | 
406 | ### Stop Impersonating User
407 | 
408 | To stop impersonating a user and continue with the admin account, you can use `stopImpersonating`
409 | 
410 | <APIMethod path="/admin/stop-impersonating" method="POST" noResult requireSession>
411 | ```ts
412 | type stopImpersonating = {
413 | }
414 | ```
415 | </APIMethod>
416 | 
417 | ### Remove User
418 | 
419 | Hard deletes a user from the database.
420 | 
421 | <APIMethod
422 |   path="/admin/remove-user"
423 |   method="POST"
424 |   requireSession
425 |   resultVariable="deletedUser"
426 | >
427 | ```ts
428 | type removeUser = {
429 |     /**
430 |      * The user id which you want to remove. 
431 |      */
432 |     userId: string = "user-id"
433 | }
434 | ```
435 | </APIMethod>
436 | 
437 | ## Access Control
438 | 
439 | The admin plugin offers a highly flexible access control system, allowing you to manage user permissions based on their role. You can define custom permission sets to fit your needs.
440 | 
441 | ### Roles
442 | 
443 | By default, there are two roles:
444 | 
445 | `admin`: Users with the admin role have full control over other users.
446 | 
447 | `user`: Users with the user role have no control over other users.
448 | 
449 | <Callout>
450 |   A user can have multiple roles. Multiple roles are stored as string separated by comma (",").
451 | </Callout>
452 | 
453 | ### Permissions
454 | 
455 | By default, there are two resources with up to six permissions.
456 | 
457 | **user**:
458 |   `create` `list` `set-role` `ban` `impersonate` `delete` `set-password`
459 | 
460 | **session**:
461 |   `list` `revoke` `delete`
462 | 
463 | Users with the admin role have full control over all the resources and actions. Users with the user role have no control over any of those actions.
464 | 
465 | ### Custom Permissions
466 | 
467 | The plugin provides an easy way to define your own set of permissions for each role.
468 | 
469 | <Steps>
470 |     <Step>
471 |     #### Create Access Control
472 | 
473 |     You first need to create an access controller by calling the `createAccessControl` function and passing the statement object. The statement object should have the resource name as the key and the array of actions as the value.
474 |     ```ts title="permissions.ts"
475 |     import { createAccessControl } from "better-auth/plugins/access";
476 | 
477 |     /**
478 |      * make sure to use `as const` so typescript can infer the type correctly
479 |      */
480 |     const statement = { // [!code highlight]
481 |         project: ["create", "share", "update", "delete"], // [!code highlight]
482 |     } as const; // [!code highlight]
483 | 
484 |     const ac = createAccessControl(statement); // [!code highlight]
485 |     ```
486 |     </Step>
487 | 
488 |     <Step>
489 |     #### Create Roles
490 | 
491 |     Once you have created the access controller you can create roles with the permissions you have defined.
492 | 
493 |     ```ts title="permissions.ts"
494 |     import { createAccessControl } from "better-auth/plugins/access";
495 | 
496 |     export const statement = {
497 |         project: ["create", "share", "update", "delete"], // <-- Permissions available for created roles
498 |     } as const;
499 | 
500 |     const ac = createAccessControl(statement);
501 | 
502 |     export const user = ac.newRole({ // [!code highlight]
503 |         project: ["create"], // [!code highlight]
504 |     }); // [!code highlight]
505 | 
506 |    export const admin = ac.newRole({ // [!code highlight]
507 |         project: ["create", "update"], // [!code highlight]
508 |     }); // [!code highlight]
509 | 
510 |     export const myCustomRole = ac.newRole({ // [!code highlight]
511 |         project: ["create", "update", "delete"], // [!code highlight]
512 |         user: ["ban"], // [!code highlight]
513 |     }); // [!code highlight]
514 |     ```
515 | 
516 |       When you create custom roles for existing roles, the predefined permissions for those roles will be overridden. To add the existing permissions to the custom role, you need to import `defaultStatements` and merge it with your new statement, plus merge the roles' permissions set with the default roles.
517 | 
518 |     ```ts title="permissions.ts"
519 |     import { createAccessControl } from "better-auth/plugins/access";
520 |     import { defaultStatements, adminAc } from "better-auth/plugins/admin/access";
521 | 
522 |     const statement = {
523 |         ...defaultStatements, // [!code highlight]
524 |         project: ["create", "share", "update", "delete"],
525 |     } as const;
526 | 
527 |     const ac = createAccessControl(statement);
528 | 
529 |     const admin = ac.newRole({
530 |         project: ["create", "update"],
531 |         ...adminAc.statements, // [!code highlight]
532 |     });
533 |     ```
534 | 
535 |     </Step>
536 | 
537 |     <Step>
538 |         #### Pass Roles to the Plugin
539 | 
540 |         Once you have created the roles you can pass them to the admin plugin both on the client and the server.
541 | 
542 |         ```ts title="auth.ts"
543 |         import { betterAuth } from "better-auth"
544 |         import { admin as adminPlugin } from "better-auth/plugins"
545 |         import { ac, admin, user } from "@/auth/permissions"
546 | 
547 |         export const auth = betterAuth({
548 |             plugins: [
549 |                 adminPlugin({
550 |                     ac,
551 |                     roles: {
552 |                         admin,
553 |                         user,
554 |                         myCustomRole
555 |                     }
556 |                 }),
557 |             ],
558 |         });
559 |         ```
560 | 
561 |         You also need to pass the access controller and the roles to the client plugin.
562 | 
563 |         ```ts title="auth-client.ts"
564 |         import { createAuthClient } from "better-auth/client"
565 |         import { adminClient } from "better-auth/client/plugins"
566 |         import { ac, admin, user, myCustomRole } from "@/auth/permissions"
567 | 
568 |         export const client = createAuthClient({
569 |             plugins: [
570 |                 adminClient({
571 |                     ac,
572 |                     roles: {
573 |                         admin,
574 |                         user,
575 |                         myCustomRole
576 |                     }
577 |                 })
578 |             ]
579 |         })
580 |         ```
581 |     </Step>
582 | 
583 | </Steps>
584 | 
585 | ### Access Control Usage
586 | 
587 | **Has Permission**:
588 | 
589 | To check a user's permissions, you can use the `hasPermission` function provided by the client.
590 | 
591 | 
592 | <APIMethod path="/admin/has-permission" method="POST">
593 | ```ts
594 | type userHasPermission = {
595 |     /**
596 |      * The user id which you want to check the permissions for. 
597 |      */
598 |     userId?: string = "user-id"
599 |     /**
600 |      * Check role permissions.
601 |      * @serverOnly
602 |      */
603 |     role?: string = "admin"
604 |     /**
605 |      * Optionally check if a single permission is granted. Must use this, or permissions. 
606 |      */
607 |     permission?: Record<string, string[]> = { "project": ["create", "update"] } /* Must use this, or permissions */,
608 |     /**
609 |      * Optionally check if multiple permissions are granted. Must use this, or permission. 
610 |      */
611 |     permissions?: Record<string, string[]>
612 | }
613 | ```
614 | </APIMethod>
615 | 
616 | Example usage:
617 | 
618 | ```ts title="auth-client.ts"
619 | const canCreateProject = await authClient.admin.hasPermission({
620 |   permissions: {
621 |     project: ["create"],
622 |   },
623 | });
624 | 
625 | // You can also check multiple resource permissions at the same time
626 | const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
627 |   permissions: {
628 |     project: ["create"],
629 |     sale: ["create"]
630 |   },
631 | });
632 | ```
633 | 
634 | If you want to check a user's permissions server-side, you can use the `userHasPermission` action provided by the `api` to check the user's permissions.
635 | 
636 | 
637 | 
638 | ```ts title="api.ts"
639 | import { auth } from "@/auth";
640 | 
641 | await auth.api.userHasPermission({
642 |   body: {
643 |     userId: 'id', //the user id
644 |     permissions: {
645 |       project: ["create"], // This must match the structure in your access control
646 |     },
647 |   },
648 | });
649 | 
650 | // You can also just pass the role directly
651 | await auth.api.userHasPermission({
652 |   body: {
653 |    role: "admin",
654 |     permissions: {
655 |       project: ["create"], // This must match the structure in your access control
656 |     },
657 |   },
658 | });
659 | 
660 | // You can also check multiple resource permissions at the same time
661 | await auth.api.userHasPermission({
662 |   body: {
663 |    role: "admin",
664 |     permissions: {
665 |       project: ["create"], // This must match the structure in your access control
666 |       sale: ["create"]
667 |     },
668 |   },
669 | });
670 | ```
671 | 
672 | 
673 | **Check Role Permission**:
674 | 
675 | Use the `checkRolePermission` function on the client side to verify whether a given **role** has a specific **permission**. This is helpful after defining roles and their permissions, as it allows you to perform permission checks without needing to contact the server.
676 | 
677 | Note that this function does **not** check the permissions of the currently logged-in user directly. Instead, it checks what permissions are assigned to a specified role. The function is synchronous, so you don't need to use `await` when calling it.
678 | 
679 | ```ts title="auth-client.ts"
680 | const canCreateProject = authClient.admin.checkRolePermission({
681 |   permissions: {
682 |     user: ["delete"],
683 |   },
684 |   role: "admin",
685 | });
686 | 
687 | // You can also check multiple resource permissions at the same time
688 | const canDeleteUserAndRevokeSession = authClient.admin.checkRolePermission({
689 |   permissions: {
690 |     user: ["delete"],
691 |     session: ["revoke"]
692 |   },
693 |   role: "admin",
694 | });
695 | ```
696 | 
697 | ## Schema
698 | 
699 | This plugin adds the following fields to the `user` table:
700 | 
701 | <DatabaseTable
702 |   fields={[
703 |     {
704 |       name: "role",
705 |       type: "string",
706 |       description:
707 |         "The user's role. Defaults to `user`. Admins will have the `admin` role.",
708 |       isOptional: true,
709 |     },
710 |     {
711 |       name: "banned",
712 |       type: "boolean",
713 |       description: "Indicates whether the user is banned.",
714 |       isOptional: true,
715 |     },
716 |     {
717 |       name: "banReason",
718 |       type: "string",
719 |       description: "The reason for the user's ban.",
720 |       isOptional: true,
721 |     },
722 |     {
723 |       name: "banExpires",
724 |       type: "date",
725 |       description: "The date when the user's ban will expire.",
726 |       isOptional: true,
727 |     },
728 |   ]}
729 | />
730 | 
731 | And adds one field in the `session` table:
732 | 
733 | <DatabaseTable
734 |   fields={[
735 |     {
736 |       name: "impersonatedBy",
737 |       type: "string",
738 |       description: "The ID of the admin that is impersonating this session.",
739 |       isOptional: true,
740 |     },
741 |   ]}
742 | />
743 | 
744 | ## Options
745 | 
746 | ### Default Role
747 | 
748 | The default role for a user. Defaults to `user`.
749 | 
750 | ```ts title="auth.ts"
751 | admin({
752 |   defaultRole: "regular",
753 | });
754 | ```
755 | ### Admin Roles
756 | 
757 | The roles that are considered admin roles. Defaults to `["admin"]`.
758 | 
759 | ```ts title="auth.ts"
760 | admin({
761 |   adminRoles: ["admin", "superadmin"],
762 | });
763 | ```
764 | 
765 | <Callout type="warning">
766 |   Any role that isn't in the `adminRoles` list, even if they have the permission,
767 |   will not be considered an admin.
768 | </Callout>
769 | 
770 | ### Admin userIds
771 | 
772 | You can pass an array of userIds that should be considered as admin. Default to `[]`
773 | 
774 | ```ts title="auth.ts"
775 | admin({
776 |     adminUserIds: ["user_id_1", "user_id_2"]
777 | })
778 | ```
779 | 
780 | If a user is in the `adminUserIds` list, they will be able to perform any admin operation.
781 | 
782 | ### impersonationSessionDuration
783 | 
784 | The duration of the impersonation session in seconds. Defaults to 1 hour.
785 | 
786 | ```ts title="auth.ts"
787 | admin({
788 |   impersonationSessionDuration: 60 * 60 * 24, // 1 day
789 | });
790 | ```
791 | 
792 | ### Default Ban Reason
793 | 
794 | The default ban reason for a user created by the admin. Defaults to `No reason`.
795 | 
796 | ```ts title="auth.ts"
797 | admin({
798 |   defaultBanReason: "Spamming",
799 | });
800 | ```
801 | 
802 | ### Default Ban Expires In
803 | 
804 | The default ban expires in for a user created by the admin in seconds. Defaults to `undefined` (meaning the ban never expires).
805 | 
806 | ```ts title="auth.ts"
807 | admin({
808 |   defaultBanExpiresIn: 60 * 60 * 24, // 1 day
809 | });
810 | ```
811 | 
812 | ### bannedUserMessage
813 | 
814 | The message to show when a banned user tries to sign in. Defaults to "You have been banned from this application. Please contact support if you believe this is an error."
815 | 
816 | ```ts title="auth.ts"
817 | admin({
818 |   bannedUserMessage: "Custom banned user message",
819 | });
820 | ```
821 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/social.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
  2 | import { setupServer } from "msw/node";
  3 | import { http, HttpResponse } from "msw";
  4 | import { getTestInstance } from "./test-utils/test-instance";
  5 | import { DEFAULT_SECRET } from "./utils/constants";
  6 | import type { GoogleProfile } from "@better-auth/core/social-providers";
  7 | import { parseSetCookieHeader } from "./cookies";
  8 | import { refreshAccessToken } from "@better-auth/core/oauth2";
  9 | import { signJWT } from "./crypto";
 10 | import { OAuth2Server } from "oauth2-mock-server";
 11 | import { betterFetch } from "@better-fetch/fetch";
 12 | import Database from "better-sqlite3";
 13 | import { getMigrations } from "./db";
 14 | 
 15 | let server = new OAuth2Server();
 16 | let port = 8005;
 17 | 
 18 | const mswServer = setupServer();
 19 | let shouldUseUpdatedProfile = false;
 20 | 
 21 | beforeAll(async () => {
 22 | 	mswServer.listen({ onUnhandledRequest: "bypass" });
 23 | 	mswServer.use(
 24 | 		http.post("https://oauth2.googleapis.com/token", async () => {
 25 | 			const data: GoogleProfile = shouldUseUpdatedProfile
 26 | 				? {
 27 | 						email: "[email protected]",
 28 | 						email_verified: true,
 29 | 						name: "Updated User",
 30 | 						picture: "https://test.com/picture.png",
 31 | 						exp: 1234567890,
 32 | 						sub: "1234567890",
 33 | 						iat: 1234567890,
 34 | 						aud: "test",
 35 | 						azp: "test",
 36 | 						nbf: 1234567890,
 37 | 						iss: "test",
 38 | 						locale: "en",
 39 | 						jti: "test",
 40 | 						given_name: "Updated",
 41 | 						family_name: "User",
 42 | 					}
 43 | 				: {
 44 | 						email: "[email protected]",
 45 | 						email_verified: true,
 46 | 						name: "First Last",
 47 | 						picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw",
 48 | 						exp: 1234567890,
 49 | 						sub: "1234567890",
 50 | 						iat: 1234567890,
 51 | 						aud: "test",
 52 | 						azp: "test",
 53 | 						nbf: 1234567890,
 54 | 						iss: "test",
 55 | 						locale: "en",
 56 | 						jti: "test",
 57 | 						given_name: "First",
 58 | 						family_name: "Last",
 59 | 					};
 60 | 			const testIdToken = await signJWT(data, DEFAULT_SECRET);
 61 | 			return HttpResponse.json({
 62 | 				access_token: "test",
 63 | 				refresh_token: "test",
 64 | 				id_token: testIdToken,
 65 | 			});
 66 | 		}),
 67 | 		http.post(`http://localhost:${port}/token`, async () => {
 68 | 			const data: GoogleProfile = {
 69 | 				email: "[email protected]",
 70 | 				email_verified: true,
 71 | 				name: "First Last",
 72 | 				picture: "https://lh3.googleusercontent.com/a-/AOh14GjQ4Z7Vw",
 73 | 				exp: 1234567890,
 74 | 				sub: "1234567890",
 75 | 				iat: 1234567890,
 76 | 				aud: "test",
 77 | 				azp: "test",
 78 | 				nbf: 1234567890,
 79 | 				iss: "test",
 80 | 				locale: "en",
 81 | 				jti: "test",
 82 | 				given_name: "First",
 83 | 				family_name: "Last",
 84 | 			};
 85 | 			const testIdToken = await signJWT(data, DEFAULT_SECRET);
 86 | 			return HttpResponse.json({
 87 | 				access_token: "new-access-token",
 88 | 				refresh_token: "new-refresh-token",
 89 | 				id_token: testIdToken,
 90 | 				token_type: "Bearer",
 91 | 				expires_in: 3600,
 92 | 			});
 93 | 		}),
 94 | 	);
 95 | });
 96 | 
 97 | afterEach(() => {
 98 | 	shouldUseUpdatedProfile = false;
 99 | });
100 | 
101 | afterAll(() => mswServer.close());
102 | 
103 | describe("Social Providers", async (c) => {
104 | 	const { client, cookieSetter } = await getTestInstance(
105 | 		{
106 | 			user: {
107 | 				additionalFields: {
108 | 					firstName: {
109 | 						type: "string",
110 | 					},
111 | 					lastName: {
112 | 						type: "string",
113 | 					},
114 | 					isOAuth: {
115 | 						type: "boolean",
116 | 					},
117 | 				},
118 | 			},
119 | 			socialProviders: {
120 | 				google: {
121 | 					clientId: "test",
122 | 					clientSecret: "test",
123 | 					enabled: true,
124 | 					mapProfileToUser(profile) {
125 | 						return {
126 | 							firstName: profile.given_name,
127 | 							lastName: profile.family_name,
128 | 							isOAuth: true,
129 | 						};
130 | 					},
131 | 				},
132 | 				apple: {
133 | 					clientId: "test",
134 | 					clientSecret: "test",
135 | 				},
136 | 			},
137 | 		},
138 | 		{
139 | 			disableTestUser: true,
140 | 		},
141 | 	);
142 | 
143 | 	beforeAll(async () => {
144 | 		await server.issuer.keys.generate("RS256");
145 | 		server.issuer.on;
146 | 		await server.start(port, "localhost");
147 | 		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:${port}
148 | 	});
149 | 	afterAll(async () => {
150 | 		await server.stop().catch(console.error);
151 | 	});
152 | 	server.service.on("beforeRsponse", (tokenResponse, req) => {
153 | 		tokenResponse.body = {
154 | 			accessToken: "access-token",
155 | 			refreshToken: "refresher-token",
156 | 		};
157 | 		tokenResponse.statusCode = 200;
158 | 	});
159 | 	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
160 | 		userInfoResponse.body = {
161 | 			email: "[email protected]",
162 | 			name: "OAuth2 Test",
163 | 			sub: "oauth2",
164 | 			picture: "https://test.com/picture.png",
165 | 			email_verified: true,
166 | 		};
167 | 		userInfoResponse.statusCode = 200;
168 | 	});
169 | 
170 | 	server.service.on("beforeTokenSigning", (token, req) => {
171 | 		token.payload.email = "sso-user@localhost:8000.com";
172 | 		token.payload.email_verified = true;
173 | 		token.payload.name = "Test User";
174 | 		token.payload.picture = "https://test.com/picture.png";
175 | 	});
176 | 
177 | 	const headers = new Headers();
178 | 	async function simulateOAuthFlowRefresh(
179 | 		authUrl: string,
180 | 		headers: Headers,
181 | 		fetchImpl?: (...args: any) => any,
182 | 	) {
183 | 		let location: string | null = null;
184 | 		await betterFetch(authUrl, {
185 | 			method: "GET",
186 | 			redirect: "manual",
187 | 			onError(context) {
188 | 				location = context.response.headers.get("location");
189 | 			},
190 | 		});
191 | 		if (!location) throw new Error("No redirect location found");
192 | 
193 | 		const tokens = await refreshAccessToken({
194 | 			refreshToken: "mock-refresh-token",
195 | 			options: {
196 | 				clientId: "test-client-id",
197 | 				clientKey: "test-client-key",
198 | 				clientSecret: "test-client-secret",
199 | 			},
200 | 			tokenEndpoint: `http://localhost:${port}/token`,
201 | 		});
202 | 		return tokens;
203 | 	}
204 | 	it("should be able to add social providers", async () => {
205 | 		const signInRes = await client.signIn.social({
206 | 			provider: "google",
207 | 			callbackURL: "/callback",
208 | 			newUserCallbackURL: "/welcome",
209 | 		});
210 | 		expect(signInRes.data).toMatchObject({
211 | 			url: expect.stringContaining("google.com"),
212 | 			redirect: true,
213 | 		});
214 | 	});
215 | 
216 | 	it("should be able to sign in with social providers", async () => {
217 | 		const headers = new Headers();
218 | 		const signInRes = await client.signIn.social({
219 | 			provider: "google",
220 | 			callbackURL: "/callback",
221 | 			newUserCallbackURL: "/welcome",
222 | 			fetchOptions: {
223 | 				onSuccess: cookieSetter(headers),
224 | 			},
225 | 		});
226 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
227 | 		await client.$fetch("/callback/google", {
228 | 			query: {
229 | 				state,
230 | 				code: "test",
231 | 			},
232 | 			headers,
233 | 			method: "GET",
234 | 			onError(context) {
235 | 				expect(context.response.status).toBe(302);
236 | 				const location = context.response.headers.get("location");
237 | 				expect(location).toBeDefined();
238 | 				expect(location).toContain("/welcome");
239 | 				const cookies = parseSetCookieHeader(
240 | 					context.response.headers.get("set-cookie") || "",
241 | 				);
242 | 				expect(cookies.get("better-auth.session_token")?.value).toBeDefined();
243 | 			},
244 | 		});
245 | 	});
246 | 
247 | 	it("Should use callback URL if the user is already registered", async () => {
248 | 		const headers = new Headers();
249 | 		const signInRes = await client.signIn.social({
250 | 			provider: "google",
251 | 			callbackURL: "/callback",
252 | 			newUserCallbackURL: "/welcome",
253 | 			fetchOptions: {
254 | 				onSuccess: cookieSetter(headers),
255 | 			},
256 | 		});
257 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
258 | 		expect(signInRes.data).toMatchObject({
259 | 			url: expect.stringContaining("google.com"),
260 | 			redirect: true,
261 | 		});
262 | 
263 | 		await client.$fetch("/callback/google", {
264 | 			query: {
265 | 				state,
266 | 				code: "test",
267 | 			},
268 | 			headers,
269 | 			method: "GET",
270 | 			onError(context) {
271 | 				expect(context.response.status).toBe(302);
272 | 				const location = context.response.headers.get("location");
273 | 				expect(location).toBeDefined();
274 | 				expect(location).toContain("/callback");
275 | 				const cookies = parseSetCookieHeader(
276 | 					context.response.headers.get("set-cookie") || "",
277 | 				);
278 | 				expect(cookies.get("better-auth.session_token")?.value).toBeDefined();
279 | 			},
280 | 		});
281 | 	});
282 | 
283 | 	it("should be able to map profile to user", async () => {
284 | 		const headers = new Headers();
285 | 		const signInRes = await client.signIn.social({
286 | 			provider: "google",
287 | 			callbackURL: "/callback",
288 | 			fetchOptions: {
289 | 				onSuccess: cookieSetter(headers),
290 | 			},
291 | 		});
292 | 		expect(signInRes.data).toMatchObject({
293 | 			url: expect.stringContaining("google.com"),
294 | 			redirect: true,
295 | 		});
296 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
297 | 		await client.$fetch("/callback/google", {
298 | 			query: {
299 | 				state,
300 | 				code: "test",
301 | 			},
302 | 			headers,
303 | 			method: "GET",
304 | 			onError: (c) => {
305 | 				//TODO: fix this
306 | 				cookieSetter(headers)(c as any);
307 | 			},
308 | 		});
309 | 		const session = await client.getSession({
310 | 			fetchOptions: {
311 | 				headers,
312 | 			},
313 | 		});
314 | 		expect(session.data?.user).toMatchObject({
315 | 			isOAuth: true,
316 | 			firstName: "First",
317 | 			lastName: "Last",
318 | 		});
319 | 	});
320 | 
321 | 	it("should be protected from callback URL attacks", async () => {
322 | 		const signInRes = await client.signIn.social(
323 | 			{
324 | 				provider: "google",
325 | 				callbackURL: "https://evil.com/callback",
326 | 			},
327 | 			{
328 | 				onSuccess(context) {
329 | 					const cookies = parseSetCookieHeader(
330 | 						context.response.headers.get("set-cookie") || "",
331 | 					);
332 | 					headers.set(
333 | 						"cookie",
334 | 						`better-auth.state=${cookies.get("better-auth.state")?.value}`,
335 | 					);
336 | 				},
337 | 			},
338 | 		);
339 | 
340 | 		expect(signInRes.error?.status).toBe(403);
341 | 		expect(signInRes.error?.message).toBe("Invalid callbackURL");
342 | 	});
343 | 
344 | 	it("should refresh the access token", async () => {
345 | 		const headers = new Headers();
346 | 		const signInRes = await client.signIn.social({
347 | 			provider: "google",
348 | 			callbackURL: "/callback",
349 | 			newUserCallbackURL: "/welcome",
350 | 			fetchOptions: {
351 | 				onSuccess: cookieSetter(headers),
352 | 			},
353 | 		});
354 | 
355 | 		expect(signInRes.data).toMatchObject({
356 | 			url: expect.stringContaining("google.com"),
357 | 			redirect: true,
358 | 		});
359 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
360 | 		await client.$fetch("/callback/google", {
361 | 			query: {
362 | 				state,
363 | 				code: "test",
364 | 			},
365 | 			headers,
366 | 			method: "GET",
367 | 			onError(context) {
368 | 				expect(context.response.status).toBe(302);
369 | 				const location = context.response.headers.get("location");
370 | 				expect(location).toBeDefined();
371 | 				expect(location).toContain("/callback");
372 | 				const cookies = parseSetCookieHeader(
373 | 					context.response.headers.get("set-cookie") || "",
374 | 				);
375 | 				cookieSetter(headers)(context as any);
376 | 				expect(cookies.get("better-auth.session_token")?.value).toBeDefined();
377 | 			},
378 | 		});
379 | 		const accounts = await client.listAccounts({
380 | 			fetchOptions: { headers },
381 | 		});
382 | 		await client.$fetch("/refresh-token", {
383 | 			body: {
384 | 				accountId: "test-id",
385 | 				providerId: "google",
386 | 			},
387 | 			headers,
388 | 			method: "POST",
389 | 			onError(context) {
390 | 				cookieSetter(headers)(context as any);
391 | 			},
392 | 		});
393 | 
394 | 		const authUrl = signInRes.data?.url;
395 | 		if (!authUrl) throw new Error("No auth url found");
396 | 		const mockEndpoint = authUrl.replace(
397 | 			"https://accounts.google.com/o/oauth2/auth",
398 | 			`http://localhost:${port}/authorize`,
399 | 		);
400 | 		const result = await simulateOAuthFlowRefresh(mockEndpoint, headers);
401 | 		const { accessToken, refreshToken } = result;
402 | 		expect({ accessToken, refreshToken }).toEqual({
403 | 			accessToken: "new-access-token",
404 | 			refreshToken: "new-refresh-token",
405 | 		});
406 | 	});
407 | });
408 | describe("Redirect URI", async () => {
409 | 	it("should infer redirect uri", async () => {
410 | 		const { client } = await getTestInstance({
411 | 			basePath: "/custom/path",
412 | 			socialProviders: {
413 | 				google: {
414 | 					clientId: "test",
415 | 					clientSecret: "test",
416 | 					enabled: true,
417 | 				},
418 | 			},
419 | 		});
420 | 
421 | 		await client.signIn.social(
422 | 			{
423 | 				provider: "google",
424 | 				callbackURL: "/callback",
425 | 			},
426 | 			{
427 | 				onSuccess(context) {
428 | 					const redirectURI = context.data.url;
429 | 					expect(redirectURI).toContain(
430 | 						"http%3A%2F%2Flocalhost%3A3000%2Fcustom%2Fpath%2Fcallback%2Fgoogle",
431 | 					);
432 | 				},
433 | 			},
434 | 		);
435 | 	});
436 | 
437 | 	it("should respect custom redirect uri", async () => {
438 | 		const { client } = await getTestInstance({
439 | 			socialProviders: {
440 | 				google: {
441 | 					clientId: "test",
442 | 					clientSecret: "test",
443 | 					enabled: true,
444 | 					redirectURI: "https://test.com/callback",
445 | 				},
446 | 			},
447 | 		});
448 | 
449 | 		await client.signIn.social(
450 | 			{
451 | 				provider: "google",
452 | 				callbackURL: "/callback",
453 | 			},
454 | 			{
455 | 				onSuccess(context) {
456 | 					const redirectURI = context.data.url;
457 | 					expect(redirectURI).toContain(
458 | 						"redirect_uri=https%3A%2F%2Ftest.com%2Fcallback",
459 | 					);
460 | 				},
461 | 			},
462 | 		);
463 | 	});
464 | });
465 | 
466 | describe("Disable implicit signup", async () => {
467 | 	it("Should not create user when implicit sign up is disabled", async () => {
468 | 		const { client, cookieSetter } = await getTestInstance({
469 | 			socialProviders: {
470 | 				google: {
471 | 					clientId: "test",
472 | 					clientSecret: "test",
473 | 					enabled: true,
474 | 					disableImplicitSignUp: true,
475 | 				},
476 | 			},
477 | 		});
478 | 		const headers = new Headers();
479 | 		const signInRes = await client.signIn.social({
480 | 			provider: "google",
481 | 			callbackURL: "/callback",
482 | 			newUserCallbackURL: "/welcome",
483 | 			fetchOptions: {
484 | 				onSuccess: cookieSetter(headers),
485 | 			},
486 | 		});
487 | 		expect(signInRes.data).toMatchObject({
488 | 			url: expect.stringContaining("google.com"),
489 | 			redirect: true,
490 | 		});
491 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
492 | 
493 | 		await client.$fetch("/callback/google", {
494 | 			query: {
495 | 				state,
496 | 				code: "test",
497 | 			},
498 | 			headers,
499 | 			method: "GET",
500 | 			onError(context) {
501 | 				expect(context.response.status).toBe(302);
502 | 				const location = context.response.headers.get("location");
503 | 				expect(location).toBeDefined();
504 | 				expect(location).toContain(
505 | 					"http://localhost:3000/api/auth/error?error=signup_disabled",
506 | 				);
507 | 			},
508 | 		});
509 | 	});
510 | 
511 | 	it("Should create user when implicit sign up is disabled but it is requested", async () => {
512 | 		const { client, cookieSetter } = await getTestInstance({
513 | 			socialProviders: {
514 | 				google: {
515 | 					clientId: "test",
516 | 					clientSecret: "test",
517 | 					enabled: true,
518 | 					disableImplicitSignUp: true,
519 | 				},
520 | 			},
521 | 		});
522 | 
523 | 		const headers = new Headers();
524 | 		const signInRes = await client.signIn.social({
525 | 			provider: "google",
526 | 			callbackURL: "/callback",
527 | 			newUserCallbackURL: "/welcome",
528 | 			requestSignUp: true,
529 | 			fetchOptions: {
530 | 				onSuccess: cookieSetter(headers),
531 | 			},
532 | 		});
533 | 		expect(signInRes.data).toMatchObject({
534 | 			url: expect.stringContaining("google.com"),
535 | 			redirect: true,
536 | 		});
537 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
538 | 
539 | 		await client.$fetch("/callback/google", {
540 | 			query: {
541 | 				state,
542 | 				code: "test",
543 | 			},
544 | 			headers,
545 | 			method: "GET",
546 | 			onError(context) {
547 | 				expect(context.response.status).toBe(302);
548 | 				const location = context.response.headers.get("location");
549 | 				expect(location).toBeDefined();
550 | 				expect(location).toContain("/welcome");
551 | 				const cookies = parseSetCookieHeader(
552 | 					context.response.headers.get("set-cookie") || "",
553 | 				);
554 | 				expect(cookies.get("better-auth.session_token")?.value).toBeDefined();
555 | 			},
556 | 		});
557 | 	});
558 | });
559 | 
560 | describe("Disable signup", async () => {
561 | 	it("Should not create user when sign up is disabled", async () => {
562 | 		const headers = new Headers();
563 | 		const { client, cookieSetter } = await getTestInstance({
564 | 			socialProviders: {
565 | 				google: {
566 | 					clientId: "test",
567 | 					clientSecret: "test",
568 | 					enabled: true,
569 | 					disableSignUp: true,
570 | 				},
571 | 			},
572 | 		});
573 | 
574 | 		const signInRes = await client.signIn.social({
575 | 			provider: "google",
576 | 			callbackURL: "/callback",
577 | 			newUserCallbackURL: "/welcome",
578 | 			fetchOptions: {
579 | 				onSuccess: cookieSetter(headers),
580 | 			},
581 | 		});
582 | 		expect(signInRes.data).toMatchObject({
583 | 			url: expect.stringContaining("google.com"),
584 | 			redirect: true,
585 | 		});
586 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
587 | 
588 | 		await client.$fetch("/callback/google", {
589 | 			query: {
590 | 				state,
591 | 				code: "test",
592 | 			},
593 | 			headers,
594 | 			method: "GET",
595 | 			onError(context) {
596 | 				expect(context.response.status).toBe(302);
597 | 				const location = context.response.headers.get("location");
598 | 				expect(location).toBeDefined();
599 | 				expect(location).toContain(
600 | 					"http://localhost:3000/api/auth/error?error=signup_disabled",
601 | 				);
602 | 			},
603 | 		});
604 | 	});
605 | });
606 | 
607 | describe("signin", async () => {
608 | 	const database = new Database(":memory:");
609 | 
610 | 	beforeAll(async () => {
611 | 		const migrations = await getMigrations({
612 | 			database,
613 | 		});
614 | 		await migrations.runMigrations();
615 | 	});
616 | 	it("should allow user info override during sign in", async () => {
617 | 		let state = "";
618 | 		const headers = new Headers();
619 | 		const { client, cookieSetter } = await getTestInstance({
620 | 			database,
621 | 			socialProviders: {
622 | 				google: {
623 | 					clientId: "test",
624 | 					clientSecret: "test",
625 | 					enabled: true,
626 | 				},
627 | 			},
628 | 		});
629 | 		const signInRes = await client.signIn.social({
630 | 			provider: "google",
631 | 			callbackURL: "/callback",
632 | 			fetchOptions: {
633 | 				onSuccess: cookieSetter(headers),
634 | 			},
635 | 		});
636 | 		expect(signInRes.data).toMatchObject({
637 | 			url: expect.stringContaining("google.com"),
638 | 			redirect: true,
639 | 		});
640 | 		state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
641 | 
642 | 		await client.$fetch("/callback/google", {
643 | 			query: {
644 | 				state,
645 | 				code: "test",
646 | 			},
647 | 			headers,
648 | 			method: "GET",
649 | 			onError: (c) => {
650 | 				cookieSetter(headers)(c as any);
651 | 			},
652 | 		});
653 | 
654 | 		const session = await client.getSession({
655 | 			fetchOptions: {
656 | 				headers,
657 | 			},
658 | 		});
659 | 		expect(session.data?.user).toMatchObject({
660 | 			name: "First Last",
661 | 		});
662 | 	});
663 | 
664 | 	it("should allow user info override during sign in", async () => {
665 | 		shouldUseUpdatedProfile = true;
666 | 		const headers = new Headers();
667 | 		let state = "";
668 | 		const { client, cookieSetter } = await getTestInstance(
669 | 			{
670 | 				database,
671 | 				socialProviders: {
672 | 					google: {
673 | 						clientId: "test",
674 | 						clientSecret: "test",
675 | 						enabled: true,
676 | 						overrideUserInfoOnSignIn: true,
677 | 					},
678 | 				},
679 | 			},
680 | 			{
681 | 				disableTestUser: true,
682 | 			},
683 | 		);
684 | 		const signInRes = await client.signIn.social({
685 | 			provider: "google",
686 | 			callbackURL: "/callback",
687 | 			fetchOptions: {
688 | 				onSuccess: cookieSetter(headers),
689 | 			},
690 | 		});
691 | 		expect(signInRes.data).toMatchObject({
692 | 			url: expect.stringContaining("google.com"),
693 | 			redirect: true,
694 | 		});
695 | 		state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
696 | 
697 | 		await client.$fetch("/callback/google", {
698 | 			query: {
699 | 				state,
700 | 				code: "test",
701 | 			},
702 | 			headers,
703 | 			method: "GET",
704 | 			onError: (c) => {
705 | 				cookieSetter(headers)(c as any);
706 | 			},
707 | 		});
708 | 
709 | 		const session = await client.getSession({
710 | 			fetchOptions: {
711 | 				headers,
712 | 			},
713 | 		});
714 | 		expect(session.data?.user).toMatchObject({
715 | 			name: "Updated User",
716 | 		});
717 | 	});
718 | });
719 | 
720 | describe("updateAccountOnSignIn", async () => {
721 | 	const { client, cookieSetter, auth } = await getTestInstance({
722 | 		account: {
723 | 			updateAccountOnSignIn: false,
724 | 		},
725 | 	});
726 | 	const ctx = await auth.$context;
727 | 	it("should not update account on sign in", async () => {
728 | 		const headers = new Headers();
729 | 		const signInRes = await client.signIn.social({
730 | 			provider: "google",
731 | 			callbackURL: "/callback",
732 | 			fetchOptions: {
733 | 				onSuccess: cookieSetter(headers),
734 | 			},
735 | 		});
736 | 		expect(signInRes.data).toMatchObject({
737 | 			url: expect.stringContaining("google.com"),
738 | 			redirect: true,
739 | 		});
740 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
741 | 
742 | 		await client.$fetch("/callback/google", {
743 | 			query: {
744 | 				state,
745 | 				code: "test",
746 | 			},
747 | 			method: "GET",
748 | 			headers,
749 | 			onError(context) {
750 | 				cookieSetter(headers)(context as any);
751 | 			},
752 | 		});
753 | 		const session = await client.getSession({
754 | 			fetchOptions: {
755 | 				headers,
756 | 			},
757 | 		});
758 | 		const userAccounts = await ctx.internalAdapter.findAccounts(
759 | 			session.data?.user.id!,
760 | 		);
761 | 		await ctx.internalAdapter.updateAccount(userAccounts[0]!.id, {
762 | 			accessToken: "new-access-token",
763 | 		});
764 | 
765 | 		//re-sign in
766 | 		const signInRes2 = await client.signIn.social({
767 | 			provider: "google",
768 | 			callbackURL: "/callback",
769 | 			fetchOptions: {
770 | 				onSuccess: cookieSetter(headers),
771 | 			},
772 | 		});
773 | 		expect(signInRes2.data).toMatchObject({
774 | 			url: expect.stringContaining("google.com"),
775 | 			redirect: true,
776 | 		});
777 | 		const state2 =
778 | 			new URL(signInRes2.data!.url!).searchParams.get("state") || "";
779 | 
780 | 		await client.$fetch("/callback/google", {
781 | 			query: {
782 | 				state: state2,
783 | 				code: "test",
784 | 			},
785 | 			headers,
786 | 			method: "GET",
787 | 			onError(context) {
788 | 				cookieSetter(headers)(context as any);
789 | 			},
790 | 		});
791 | 		const session2 = await client.getSession({
792 | 			fetchOptions: {
793 | 				headers,
794 | 			},
795 | 		});
796 | 		const userAccounts2 = await ctx.internalAdapter.findAccounts(
797 | 			session2.data?.user.id!,
798 | 		);
799 | 		expect(userAccounts2[0]!.accessToken).toBe("new-access-token");
800 | 	});
801 | });
802 | 
```
Page 42/67FirstPrevNextLast