This is page 20 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&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-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.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 │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── 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 -------------------------------------------------------------------------------- /docs/content/docs/plugins/siwe.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Sign In With Ethereum (SIWE) description: Sign in with Ethereum plugin for Better Auth --- The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the [ERC-4361 standard](https://eips.ethereum.org/EIPS/eip-4361). This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic. ## Installation <Steps> <Step> ### Add the Server Plugin Add the SIWE plugin to your auth configuration: ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { siwe } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ siwe({ domain: "example.com", emailDomainName: "example.com", // optional anonymous: false, // optional, default is true getNonce: async () => { // Implement your nonce generation logic here return "your-secure-random-nonce"; }, verifyMessage: async (args) => { // Implement your SIWE message verification logic here // This should verify the signature against the message return true; // return true if signature is valid }, ensLookup: async (args) => { // Optional: Implement ENS lookup for user names and avatars return { name: "user.eth", avatar: "https://example.com/avatar.png" }; }, }), ], }); ``` </Step> <Step> ### Migrate the database Run the migration or generate the schema to add the necessary fields and tables to the database. <Tabs items={["migrate", "generate"]}> <Tab value="migrate"> ```bash npx @better-auth/cli migrate ``` </Tab> <Tab value="generate"> ```bash npx @better-auth/cli generate ``` </Tab> </Tabs> See the [Schema](#schema) section to add the fields manually. </Step> <Step> ### Add the Client Plugin ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; import { siweClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ plugins: [siweClient()], }); ``` </Step> </Steps> ## Usage ### Generate a Nonce Before signing a SIWE message, you need to generate a nonce for the wallet address: ```ts title="generate-nonce.ts" const { data, error } = await authClient.siwe.nonce({ walletAddress: "0x1234567890abcdef1234567890abcdef12345678", chainId: 1, // optional for Ethereum mainnet, required for other chains. Defaults to 1 }); if (data) { console.log("Nonce:", data.nonce); } ``` ### Sign In with Ethereum After generating a nonce and creating a SIWE message, verify the signature to authenticate: ```ts title="sign-in-siwe.ts" const { data, error } = await authClient.siwe.verify({ message: "Your SIWE message string", signature: "0x...", // The signature from the user's wallet walletAddress: "0x1234567890abcdef1234567890abcdef12345678", chainId: 1, // optional for Ethereum mainnet, required for other chains. Must match Chain ID in SIWE message email: "[email protected]", // optional, required if anonymous is false }); if (data) { console.log("Authentication successful:", data.user); } ``` ### Chain-Specific Examples Here are examples for different blockchain networks: ```ts title="ethereum-mainnet.ts" // Ethereum Mainnet (chainId can be omitted, defaults to 1) const { data, error } = await authClient.siwe.verify({ message, signature, walletAddress, // chainId: 1 (default) }); ``` ```ts title="polygon.ts" // Polygon (chainId REQUIRED) const { data, error } = await authClient.siwe.verify({ message, signature, walletAddress, chainId: 137, // Required for Polygon }); ``` ```ts title="arbitrum.ts" // Arbitrum (chainId REQUIRED) const { data, error } = await authClient.siwe.verify({ message, signature, walletAddress, chainId: 42161, // Required for Arbitrum }); ``` ```ts title="base.ts" // Base (chainId REQUIRED) const { data, error } = await authClient.siwe.verify({ message, signature, walletAddress, chainId: 8453, // Required for Base }); ``` <Callout type="warning"> The `chainId` must match the Chain ID specified in your SIWE message. Verification will fail with a 401 error if there's a mismatch between the message's Chain ID and the `chainId` parameter. </Callout> ## Configuration Options ### Server Options The SIWE plugin accepts the following configuration options: - **domain**: The domain name of your application (required for SIWE message generation) - **emailDomainName**: The email domain name for creating user accounts when not using anonymous mode. Defaults to the domain from your base URL - **anonymous**: Whether to allow anonymous sign-ins without requiring an email. Default is `true` - **getNonce**: Function to generate a unique nonce for each sign-in attempt. You must implement this function to return a cryptographically secure random string. Must return a `Promise<string>` - **verifyMessage**: Function to verify the signed SIWE message. Receives message details and should return `Promise<boolean>` - **ensLookup**: Optional function to lookup ENS names and avatars for Ethereum addresses ### Client Options The SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility: ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; import { siweClient } from "better-auth/client/plugins"; export const authClient = createAuthClient({ plugins: [ siweClient({ // Optional client configuration can go here }), ], }); ``` ## Schema The SIWE plugin adds a `walletAddress` table to store user wallet associations: | Field | Type | Description | | --------- | ------- | ----------------------------------------- | | id | string | Primary key | | userId | string | Reference to user.id | | address | string | Ethereum wallet address | | chainId | number | Chain ID (e.g., 1 for Ethereum mainnet) | | isPrimary | boolean | Whether this is the user's primary wallet | | createdAt | date | Creation timestamp | ## Example Implementation Here's a complete example showing how to implement SIWE authentication: ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { siwe } from "better-auth/plugins"; import { generateRandomString } from "better-auth/crypto"; import { verifyMessage, createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; export const auth = betterAuth({ database: { // your database configuration }, plugins: [ siwe({ domain: "myapp.com", emailDomainName: "myapp.com", anonymous: false, getNonce: async () => { // Generate a cryptographically secure random nonce return generateRandomString(32); }, verifyMessage: async ({ message, signature, address }) => { try { // Verify the signature using viem (recommended) const isValid = await verifyMessage({ address: address as `0x${string}`, message, signature: signature as `0x${string}`, }); return isValid; } catch (error) { console.error("SIWE verification failed:", error); return false; } }, ensLookup: async ({ walletAddress }) => { try { // Optional: lookup ENS name and avatar using viem // You can use viem's ENS utilities here const client = createPublicClient({ chain: mainnet, transport: http(), }); const ensName = await client.getEnsName({ address: walletAddress as `0x${string}`, }); const ensAvatar = ensName ? await client.getEnsAvatar({ name: ensName, }) : null; return { name: ensName || walletAddress, avatar: ensAvatar || "", }; } catch { return { name: walletAddress, avatar: "", }; } }, }), ], }); ``` ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.mssql.test.ts: -------------------------------------------------------------------------------- ```typescript import { Kysely, MssqlDialect } from "kysely"; import { testAdapter } from "../../test-adapter"; import { kyselyAdapter } from "../kysely-adapter"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { getMigrations } from "../../../db"; import * as Tedious from "tedious"; import * as Tarn from "tarn"; import type { BetterAuthOptions } from "@better-auth/core"; // We are not allowed to handle the mssql connection // we must let kysely handle it. This is because if kysely is already // handling it, and we were to connect it ourselves, it will create bugs. const dialect = new MssqlDialect({ tarn: { ...Tarn, options: { min: 0, max: 10, }, }, tedious: { ...Tedious, connectionFactory: () => new Tedious.Connection({ authentication: { options: { password: "Password123!", userName: "sa", }, type: "default", }, options: { database: "master", // Start with master database, will create better_auth if needed port: 1433, trustServerCertificate: true, encrypt: false, }, server: "localhost", }), TYPES: { ...Tedious.TYPES, DateTime: Tedious.TYPES.DateTime2, }, }, }); const kyselyDB = new Kysely({ dialect: dialect, }); // Create better_auth database if it doesn't exist const ensureDatabaseExists = async () => { try { console.log("Ensuring better_auth database exists..."); await kyselyDB.getExecutor().executeQuery({ sql: ` IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'better_auth') BEGIN CREATE DATABASE better_auth; PRINT 'Database better_auth created successfully'; END ELSE BEGIN PRINT 'Database better_auth already exists'; END `, parameters: [], query: { kind: "SelectQueryNode" }, queryId: { queryId: "ensure-db" }, }); console.log("Database check/creation completed"); } catch (error) { console.error("Failed to ensure database exists:", error); throw error; } }; // Warm up connection for CI environments const warmupConnection = async () => { const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; if (isCI) { console.log("Warming up MSSQL connection for CI environment..."); console.log( `Environment: CI=${process.env.CI}, GITHUB_ACTIONS=${process.env.GITHUB_ACTIONS}`, ); try { await ensureDatabaseExists(); // Try a simple query to establish the connection await kyselyDB.getExecutor().executeQuery({ sql: "SELECT 1 as warmup, @@VERSION as version", parameters: [], query: { kind: "SelectQueryNode" }, queryId: { queryId: "warmup" }, }); console.log("Connection warmup successful"); } catch (error) { console.warn( "Connection warmup failed, will retry during validation:", error, ); // Log additional debugging info for CI if (isCI) { console.log("CI Debug Info:"); console.log("- MSSQL server may not be ready yet"); console.log("- Network connectivity issues possible"); console.log("- Database may not exist yet"); } } } else { // For local development, also ensure database exists await ensureDatabaseExists(); } }; // Add connection validation helper with CI-specific handling const validateConnection = async (retries: number = 10): Promise<boolean> => { const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const maxRetries = isCI ? 15 : retries; // More retries in CI const baseDelay = isCI ? 2000 : 1000; // Longer delays in CI console.log( `Validating connection (CI: ${isCI}, max retries: ${maxRetries})`, ); for (let i = 0; i < maxRetries; i++) { try { await query("SELECT 1 as test", isCI ? 10000 : 5000); console.log("Connection validated successfully"); return true; } catch (error) { console.warn( `Connection validation attempt ${i + 1}/${maxRetries} failed:`, error, ); if (i === maxRetries - 1) { console.error("All connection validation attempts failed"); return false; } // Exponential backoff with longer delays in CI const delay = baseDelay * Math.pow(1.5, i); console.log(`Waiting ${delay}ms before retry...`); await new Promise((resolve) => setTimeout(resolve, delay)); } } return false; }; const query = async (sql: string, timeoutMs: number = 30000) => { const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const actualTimeout = isCI ? Math.max(timeoutMs, 60000) : timeoutMs; // Minimum 60s timeout in CI try { console.log( `Executing SQL: ${sql.substring(0, 100)}... (timeout: ${actualTimeout}ms, CI: ${isCI})`, ); // Ensure we're using the better_auth database for queries const sqlWithContext = sql.includes("USE ") ? sql : `USE better_auth; ${sql}`; const result = (await Promise.race([ kyselyDB.getExecutor().executeQuery({ sql: sqlWithContext, parameters: [], query: { kind: "SelectQueryNode" }, queryId: { queryId: "" }, }), new Promise((_, reject) => setTimeout( () => reject(new Error(`Query timeout after ${actualTimeout}ms`)), actualTimeout, ), ), ])) as any; console.log(`Query completed successfully`); return { rows: result.rows, rowCount: result.rows.length }; } catch (error) { console.error(`Query failed: ${error}`); throw error; } }; const showDB = async () => { const DB = { users: await query("SELECT * FROM [user]"), sessions: await query("SELECT * FROM [session]"), accounts: await query("SELECT * FROM [account]"), verifications: await query("SELECT * FROM [verification]"), }; console.log(`DB`, DB); }; const resetDB = async (retryCount: number = 0) => { const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const maxRetries = isCI ? 3 : 1; // Allow retries in CI try { console.log( `Starting database reset... (attempt ${retryCount + 1}/${maxRetries + 1})`, ); // Warm up connection first (especially important for CI) await warmupConnection(); const isConnected = await validateConnection(); if (!isConnected) { throw new Error("Database connection validation failed"); } // First, try to disable foreign key checks and drop constraints await query( ` -- Disable all foreign key constraints EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"; `, 15000, ); // Drop foreign key constraints await query( ` DECLARE @sql NVARCHAR(MAX) = ''; SELECT @sql = @sql + 'ALTER TABLE [' + TABLE_SCHEMA + '].[' + TABLE_NAME + '] DROP CONSTRAINT [' + CONSTRAINT_NAME + '];' + CHAR(13) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' AND TABLE_CATALOG = DB_NAME(); IF LEN(@sql) > 0 EXEC sp_executesql @sql; `, 15000, ); // Then drop all tables await query( ` DECLARE @sql NVARCHAR(MAX) = ''; SELECT @sql = @sql + 'DROP TABLE [' + TABLE_NAME + '];' + CHAR(13) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG = DB_NAME() AND TABLE_SCHEMA = 'dbo'; IF LEN(@sql) > 0 EXEC sp_executesql @sql; `, 15000, ); console.log("Database reset completed successfully"); } catch (error) { console.error("Database reset failed:", error); // Retry logic for CI environments if (retryCount < maxRetries) { const delay = 5000 * (retryCount + 1); // Increasing delay console.log( `Retrying in ${delay}ms... (attempt ${retryCount + 2}/${maxRetries + 1})`, ); await new Promise((resolve) => setTimeout(resolve, delay)); return resetDB(retryCount + 1); } // Final fallback - try to recreate the database try { console.log("Attempting database recreation..."); // This would require a separate connection to master database // For now, just throw the error with better context throw new Error(`Database reset failed completely: ${error}`); } catch (finalError) { console.error("Final fallback also failed:", finalError); throw new Error( `Database reset failed: ${error}. All fallback attempts failed: ${finalError}`, ); } } }; const { execute } = await testAdapter({ adapter: () => { return kyselyAdapter(kyselyDB, { type: "mssql", debugLogs: { isRunningAdapterTests: true }, }); }, async runMigrations(betterAuthOptions) { await resetDB(); const opts = Object.assign(betterAuthOptions, { database: { db: kyselyDB, type: "mssql" }, } satisfies BetterAuthOptions); const { runMigrations } = await getMigrations(opts); await runMigrations(); }, prefixTests: "mssql", tests: [ normalTestSuite(), transactionsTestSuite({ disableTests: { ALL: true } }), authFlowTestSuite({ showDB }), numberIdTestSuite(), performanceTestSuite({ dialect: "mssql" }), ], async onFinish() { kyselyDB.destroy(); }, }); execute(); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/check-endpoint-conflicts.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi, beforeEach } from "vitest"; import { checkEndpointConflicts } from "./index"; import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core"; import { createEndpoint } from "better-call"; import type { InternalLogger, LogLevel } from "@better-auth/core/env"; export let mockLoggerLevel: LogLevel = "debug"; export const mockLogger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), success: vi.fn(), get level(): LogLevel { return mockLoggerLevel; }, } satisfies InternalLogger; describe("checkEndpointConflicts", () => { const endpoint = createEndpoint.create({}); beforeEach(() => { mockLoggerLevel = "debug"; mockLogger.error.mockReset(); mockLogger.warn.mockReset(); mockLogger.info.mockReset(); mockLogger.debug.mockReset(); mockLogger.success.mockReset(); }); it("should not log errors when there are no endpoint conflicts", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/endpoint1", { method: "GET", }, vi.fn(), ), endpoint2: endpoint( "/api/endpoint2", { method: "POST", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint3: endpoint( "/api/endpoint3", { method: "GET", }, vi.fn(), ), endpoint4: endpoint( "/api/endpoint4", { method: "POST", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should NOT log an error when two plugins use the same endpoint path with different methods", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/shared", { method: "GET", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint2: endpoint( "/api/shared", { method: "POST", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); // Should NOT report an error since methods are different expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should log an error when two plugins use the same endpoint path with the same method", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/shared", { method: "GET", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint2: endpoint( "/api/shared", { method: "GET", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("Endpoint path conflicts detected"), ); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining( '"/api/shared" [GET] used by plugins: plugin1, plugin2', ), ); }); it("should NOT detect conflicts when plugins use different methods on same paths", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/resource1", { method: "GET", }, vi.fn(), ), endpoint2: endpoint( "/api/resource2", { method: "POST", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint3: endpoint( "/api/resource1", { method: "POST", }, vi.fn(), ), }, }; const plugin3: BetterAuthPlugin = { id: "plugin3", endpoints: { endpoint4: endpoint( "/api/resource2", { method: "GET", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2, plugin3], }; checkEndpointConflicts(options, mockLogger); // Should not report errors since all methods are different expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should detect conflicts when plugins use the same method on the same path", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/conflict", { method: "GET", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint2: endpoint( "/api/conflict", { method: "GET", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).toHaveBeenCalledTimes(1); const errorCall = mockLogger.error.mock.calls[0]![0]; expect(errorCall).toContain( '"/api/conflict" [GET] used by plugins: plugin1, plugin2', ); }); it("should allow multiple endpoints from the same plugin using the same path with different methods", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/same", { method: "GET", }, vi.fn(), ), endpoint2: endpoint( "/api/same", { method: "POST", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1], }; checkEndpointConflicts(options, mockLogger); // Should not report error since methods are different expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should detect conflicts when same plugin has duplicate methods on same path", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/same", { method: "GET", }, vi.fn(), ), endpoint2: endpoint( "/api/same", { method: "GET", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('"/api/same" [GET] used by plugins: plugin1'), ); }); it("should allow three plugins on the same path with different methods", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/resource", { method: "GET", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint2: endpoint( "/api/resource", { method: "POST", }, vi.fn(), ), }, }; const plugin3: BetterAuthPlugin = { id: "plugin3", endpoints: { endpoint3: endpoint( "/api/resource", { method: "DELETE", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2, plugin3], }; checkEndpointConflicts(options, mockLogger); // Should not report error since all methods are different expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should detect conflicts when endpoints don't specify a method (wildcard)", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", endpoints: { endpoint1: endpoint( "/api/wildcard", { method: "*", }, vi.fn(), ), }, }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: { endpoint2: endpoint( "/api/wildcard", { method: "GET", }, vi.fn(), ), }, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('"/api/wildcard"'), ); }); it("should handle plugins with no endpoints", () => { const plugin1: BetterAuthPlugin = { id: "plugin1", }; const plugin2: BetterAuthPlugin = { id: "plugin2", endpoints: {}, }; const options: BetterAuthOptions = { plugins: [plugin1, plugin2], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should handle options with no plugins", () => { const options: BetterAuthOptions = {}; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).not.toHaveBeenCalled(); }); it("should handle options with empty plugins array", () => { const options: BetterAuthOptions = { plugins: [], }; checkEndpointConflicts(options, mockLogger); expect(mockLogger.error).not.toHaveBeenCalled(); }); }); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/sign-in.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { useState, useTransition } from "react"; import { Loader2 } from "lucide-react"; import { client, signIn } from "@/lib/auth-client"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { useRouter, useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { getCallbackURL } from "@/lib/shared"; export default function SignIn() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, startTransition] = useTransition(); const [rememberMe, setRememberMe] = useState(false); const router = useRouter(); const params = useSearchParams(); const LastUsedIndicator = () => ( <span className="ml-auto absolute top-0 right-0 px-2 py-1 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md font-medium"> Last Used </span> ); return ( <Card className="max-w-md rounded-none"> <CardHeader> <CardTitle className="text-lg md:text-xl">Sign In</CardTitle> <CardDescription className="text-xs md:text-sm"> Enter your email below to login to your account </CardDescription> </CardHeader> <CardContent> <div className="grid gap-4"> <div className="grid gap-2"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="[email protected]" required onChange={(e) => { setEmail(e.target.value); }} value={email} /> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="password">Password</Label> <Link href="/forget-password" className="ml-auto inline-block text-sm underline" > Forgot your password? </Link> </div> <Input id="password" type="password" placeholder="password" autoComplete="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> <div className="flex items-center gap-2"> <Checkbox id="remember" onClick={() => { setRememberMe(!rememberMe); }} /> <Label htmlFor="remember">Remember me</Label> </div> <Button type="submit" className="w-full flex items-center justify-center" disabled={loading} onClick={async () => { startTransition(async () => { await signIn.email( { email, password, rememberMe }, { onSuccess(context) { toast.success("Successfully signed in"); router.push(getCallbackURL(params)); }, onError(context) { toast.error(context.error.message); }, }, ); }); }} > <div className="flex items-center justify-center w-full relative"> {loading ? ( <Loader2 size={16} className="animate-spin" /> ) : ( "Login" )} {client.isLastUsedLoginMethod("email") && <LastUsedIndicator />} </div> </Button> <div className={cn( "w-full gap-2 flex items-center", "justify-between flex-col", )} > <Button variant="outline" className={cn("w-full gap-2 flex relative")} onClick={async () => { await signIn.social({ provider: "google", callbackURL: "/dashboard", }); }} > <svg xmlns="http://www.w3.org/2000/svg" width="0.98em" height="1em" viewBox="0 0 256 262" > <path fill="#4285F4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" ></path> <path fill="#34A853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" ></path> <path fill="#FBBC05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z" ></path> <path fill="#EB4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" ></path> </svg> <span>Sign in with Google</span> {client.isLastUsedLoginMethod("google") && <LastUsedIndicator />} </Button> <Button variant="outline" className={cn("w-full gap-2 flex relative")} onClick={async () => { await signIn.social({ provider: "twitch", callbackURL: "/dashboard", }); }} > <svg xmlns="http://www.w3.org/2000/svg" width="0.98em" height="1em" viewBox="0 0 256 262" > <path fill="#4285F4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" ></path> <path fill="#34A853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" ></path> <path fill="#FBBC05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z" ></path> <path fill="#EB4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" ></path> </svg> <span>Sign in with Twitch</span> {client.isLastUsedLoginMethod("apple") && <LastUsedIndicator />} </Button> <Button variant="outline" className={cn("w-full gap-2 flex items-center relative")} onClick={async () => { await signIn.social({ provider: "github", callbackURL: "/dashboard", }); }} > <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" > <path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" ></path> </svg> <span>Sign in with GitHub</span> {client.isLastUsedLoginMethod("github") && <LastUsedIndicator />} </Button> <Button variant="outline" className={cn("w-full gap-2 flex items-center relative")} onClick={async () => { await signIn.social({ provider: "microsoft", callbackURL: "/dashboard", }); }} > <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" > <path fill="currentColor" d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z" ></path> </svg> <span>Sign in with Microsoft</span> {client.isLastUsedLoginMethod("microsoft") && ( <LastUsedIndicator /> )} </Button> </div> </div> </CardContent> <CardFooter> <div className="flex justify-center w-full border-t pt-4"> <p className="text-center text-xs text-neutral-500"> built with{" "} <Link href="https://better-auth.com" className="underline" target="_blank" > <span className="dark:text-white/70 cursor-pointer"> better-auth. </span> </Link> </p> </div> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/email-otp.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Email OTP description: Email OTP plugin for Better Auth. --- The Email OTP plugin allows user to sign in, verify their email, or reset their password using a one-time password (OTP) sent to their email address. ## Installation <Steps> <Step> ### Add the plugin to your auth config Add the `emailOTP` plugin to your auth config and implement the `sendVerificationOTP()` method. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { emailOTP } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ // ... other config options plugins: [ emailOTP({ // [!code highlight] async sendVerificationOTP({ email, otp, type }) { // [!code highlight] if (type === "sign-in") { // [!code highlight] // Send the OTP for sign in // [!code highlight] } else if (type === "email-verification") { // [!code highlight] // Send the OTP for email verification // [!code highlight] } else { // [!code highlight] // Send the OTP for password reset // [!code highlight] } // [!code highlight] }, // [!code highlight] }) // [!code highlight] ] }) ``` </Step> <Step> ### Add the client plugin ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { emailOTPClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [ emailOTPClient() ] }) ``` </Step> </Steps> ## Usage ### Send an OTP Use the `sendVerificationOtp()` method to send an OTP to the user's email address. <APIMethod path="/email-otp/send-verification-otp" method="POST"> ```ts type sendVerificationOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" /** * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. */ type: "email-verification" | "sign-in" | "forget-password" = "sign-in" } ``` </APIMethod> ### Check an OTP (optional) Use the `checkVerificationOtp()` method to check if an OTP is valid. <APIMethod path="/email-otp/check-verification-otp" method="POST"> ```ts type checkVerificationOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" /** * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. */ type: "email-verification" | "sign-in" | "forget-password" = "sign-in" /** * OTP sent to the email. */ otp: string = "123456" } ``` </APIMethod> ### Sign In with OTP To sign in with OTP, use the `sendVerificationOtp()` method to send a "sign-in" OTP to the user's email address. <APIMethod path="/email-otp/send-verification-otp" method="POST"> ```ts type sendVerificationOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" /** * Type of the OTP. */ type: "sign-in" = "sign-in" } ``` </APIMethod> Once the user provides the OTP, you can sign in the user using the `signIn.emailOtp()` method. <APIMethod path="/sign-in/email-otp" method="POST"> ```ts type signInEmailOTP = { /** * Email address to sign in. */ email: string = "[email protected]" /** * OTP sent to the email. */ otp: string = "123456" } ``` </APIMethod> <Callout> If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the [options](#options). </Callout> ### Verify Email with OTP To verify the user's email address with OTP, use the `sendVerificationOtp()` method to send an "email-verification" OTP to the user's email address. <APIMethod path="/email-otp/send-verification-otp" method="POST"> ```ts type sendVerificationOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" /** * Type of the OTP. */ type: "email-verification" = "email-verification" } ``` </APIMethod> Once the user provides the OTP, use the `verifyEmail()` method to complete email verification. <APIMethod path="/email-otp/verify-email" method="POST"> ```ts type verifyEmailOTP = { /** * Email address to verify. */ email: string = "[email protected]" /** * OTP to verify. */ otp: string = "123456" } ``` </APIMethod> ### Reset Password with OTP To reset the user's password with OTP, use the `forgetPassword.emailOTP()` method to send a "forget-password" OTP to the user's email address. <APIMethod path="/forget-password/email-otp" method="POST"> ```ts type forgetPasswordEmailOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" } ``` </APIMethod> Once the user provides the OTP, use the `checkVerificationOtp()` method to check if it's valid (optional). <APIMethod path="/email-otp/check-verification-otp" method="POST"> ```ts type checkVerificationOTP = { /** * Email address to send the OTP. */ email: string = "[email protected]" /** * Type of the OTP. */ type: "forget-password" = "forget-password" /** * OTP sent to the email. */ otp: string = "123456" } ``` </APIMethod> Then, use the `resetPassword()` method to reset the user's password. <APIMethod path="/email-otp/reset-password" method="POST"> ```ts type resetPasswordEmailOTP = { /** * Email address to reset the password. */ email: string = "[email protected]" /** * OTP sent to the email. */ otp: string = "123456" /** * New password. */ password: string = "new-secure-password" } ``` </APIMethod> ### Override Default Email Verification To override the default email verification, pass `overrideDefaultEmailVerification: true` in the options. This will make the system use an email OTP instead of the default verification link whenever email verification is triggered. In other words, the user will verify their email using an OTP rather than clicking a link. ```ts title="auth.ts" import { betterAuth } from "better-auth"; export const auth = betterAuth({ plugins: [ emailOTP({ overrideDefaultEmailVerification: true, // [!code highlight] async sendVerificationOTP({ email, otp, type }) { // Implement the sendVerificationOTP method to send the OTP to the user's email address }, }), ], }); ``` ## Options - `sendVerificationOTP`: A function that sends the OTP to the user's email address. The function receives an object with the following properties: - `email`: The user's email address. - `otp`: The OTP to send. - `type`: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password". - `otpLength`: The length of the OTP. Defaults to `6`. - `expiresIn`: The expiry time of the OTP in seconds. Defaults to `300` seconds. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ emailOTP({ otpLength: 8, expiresIn: 600 }) ] }) ``` - `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to `false`. - `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to `false`. - `generateOTP`: A function that generates the OTP. Defaults to a random 6-digit number. - `allowedAttempts`: The maximum number of attempts allowed for verifying an OTP. Defaults to `3`. After exceeding this limit, the OTP becomes invalid and the user needs to request a new one. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ emailOTP({ allowedAttempts: 5, // Allow 5 attempts before invalidating the OTP expiresIn: 300 }) ] }) ``` When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`. - `storeOTP`: The method to store the OTP in your database, wether `encrypted`, `hashed` or `plain` text. Default is `plain` text. <Callout> Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database. </Callout> Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database. **Custom encryptor** ```ts title="auth.ts" emailOTP({ storeOTP: { encrypt: async (otp) => { return myCustomEncryptor(otp); }, decrypt: async (otp) => { return myCustomDecryptor(otp); }, } }) ``` **Custom hasher** ```ts title="auth.ts" emailOTP({ storeOTP: { hash: async (otp) => { return myCustomHasher(otp); }, } }) ``` ``` -------------------------------------------------------------------------------- /docs/components/builder/beam.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import React from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; export const BackgroundBeams = React.memo( ({ className }: { className?: string }) => { const paths = [ "M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875", "M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867", "M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859", "M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851", "M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843", "M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835", "M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827", "M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819", "M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811", "M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803", "M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795", "M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787", "M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779", "M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771", "M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763", "M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755", "M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747", "M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739", "M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731", "M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723", "M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715", "M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707", "M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699", "M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691", "M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683", "M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675", "M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667", "M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659", "M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651", "M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643", "M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635", "M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627", "M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619", "M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611", "M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603", "M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595", "M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587", "M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579", "M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571", "M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563", "M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555", "M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547", "M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539", "M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531", "M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523", "M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515", "M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507", "M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499", "M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491", "M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483", ]; return ( <div className={cn( "absolute h-full w-full inset-0 [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center", className, )} > <svg className=" z-0 h-full w-full pointer-events-none absolute " width="100%" height="100%" viewBox="0 0 696 316" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419" stroke="url(#paint0_radial_242_278)" strokeOpacity="0.05" strokeWidth="0.5" ></path> {paths.map((path, index) => ( <motion.path key={`path-` + index} d={path} stroke={`url(#linearGradient-${index})`} strokeOpacity="0.4" strokeWidth="0.5" ></motion.path> ))} <defs> {paths.map((path, index) => ( <motion.linearGradient id={`linearGradient-${index}`} key={`gradient-${index}`} initial={{ x1: "0%", x2: "0%", y1: "0%", y2: "0%", }} animate={{ x1: ["0%", "100%"], x2: ["0%", "95%"], y1: ["0%", "100%"], y2: ["0%", `${93 + Math.random() * 8}%`], }} transition={{ duration: Math.random() * 10 + 10, ease: "easeInOut", repeat: Infinity, delay: Math.random() * 10, }} > <stop stopColor="#18CCFC" stopOpacity="0"></stop> <stop stopColor="#18CCFC"></stop> <stop offset="32.5%" stopColor="#6344F5"></stop> <stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop> </motion.linearGradient> ))} <radialGradient id="paint0_radial_242_278" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)" > <stop offset="0.0666667" stopColor="var(--neutral-300)"></stop> <stop offset="0.243243" stopColor="var(--neutral-300)"></stop> <stop offset="0.43594" stopColor="white" stopOpacity="0"></stop> </radialGradient> </defs> </svg> </div> ); }, ); BackgroundBeams.displayName = "BackgroundBeams"; ``` -------------------------------------------------------------------------------- /docs/components/ui/background-beams.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import React from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; export const BackgroundBeams = React.memo( ({ className }: { className?: string }) => { const paths = [ "M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875", "M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867", "M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859", "M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851", "M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843", "M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835", "M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827", "M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819", "M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811", "M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803", "M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795", "M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787", "M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779", "M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771", "M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763", "M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755", "M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747", "M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739", "M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731", "M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723", "M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715", "M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707", "M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699", "M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691", "M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683", "M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675", "M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667", "M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659", "M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651", "M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643", "M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635", "M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627", "M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619", "M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611", "M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603", "M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595", "M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587", "M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579", "M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571", "M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563", "M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555", "M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547", "M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539", "M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531", "M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523", "M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515", "M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507", "M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499", "M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491", "M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483", ]; return ( <div className={cn( "absolute h-full w-full inset-0 [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center", className, )} > <svg className=" z-0 h-full w-full pointer-events-none absolute " width="100%" height="100%" viewBox="0 0 696 316" fill="none" xmlns="http://www.w3.org/2000/svg" > <path d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419" stroke="url(#paint0_radial_242_278)" strokeOpacity="0.05" strokeWidth="0.5" ></path> {paths.map((path, index) => ( <motion.path key={`path-` + index} d={path} stroke={`url(#linearGradient-${index})`} strokeOpacity="0.4" strokeWidth="0.5" ></motion.path> ))} <defs> {paths.map((path, index) => ( <motion.linearGradient id={`linearGradient-${index}`} key={`gradient-${index}`} initial={{ x1: "0%", x2: "0%", y1: "0%", y2: "0%", }} animate={{ x1: ["0%", "100%"], x2: ["0%", "95%"], y1: ["0%", "100%"], y2: ["0%", `${93 + Math.random() * 8}%`], }} transition={{ duration: Math.random() * 10 + 10, ease: "easeInOut", repeat: Infinity, delay: Math.random() * 10, }} > <stop stopColor="#18CCFC" stopOpacity="0"></stop> <stop stopColor="#18CCFC"></stop> <stop offset="32.5%" stopColor="#6344F5"></stop> <stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop> </motion.linearGradient> ))} <radialGradient id="paint0_radial_242_278" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)" > <stop offset="0.0666667" stopColor="var(--neutral-300)"></stop> <stop offset="0.243243" stopColor="var(--neutral-300)"></stop> <stop offset="0.43594" stopColor="white" stopOpacity="0"></stop> </radialGradient> </defs> </svg> </div> ); }, ); BackgroundBeams.displayName = "BackgroundBeams"; ``` -------------------------------------------------------------------------------- /docs/content/docs/concepts/session-management.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Session Management description: Better Auth session management. --- Better Auth manages session using a traditional cookie-based session management. The session is stored in a cookie and is sent to the server on every request. The server then verifies the session and returns the user data if the session is valid. ## Session table The session table stores the session data. The session table has the following fields: - `id`: The session token. Which is also used as the session cookie. - `userId`: The user ID of the user. - `expiresAt`: The expiration date of the session. - `ipAddress`: The IP address of the user. - `userAgent`: The user agent of the user. It stores the user agent header from the request. ## Session Expiration The session expires after 7 days by default. But whenever the session is used and the `updateAge` is reached, the session expiration is updated to the current time plus the `expiresIn` value. You can change both the `expiresIn` and `updateAge` values by passing the `session` object to the `auth` configuration. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ //... other config options session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24 // 1 day (every 1 day the session expiration is updated) } }) ``` ### Disable Session Refresh You can disable session refresh so that the session is not updated regardless of the `updateAge` option. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ //... other config options session: { disableSessionRefresh: true } }) ``` ## Session Freshness Some endpoints in Better Auth require the session to be **fresh**. A session is considered fresh if its `createdAt` is within the `freshAge` limit. By default, the `freshAge` is set to **1 day** (60 * 60 * 24). You can customize the `freshAge` value by passing a `session` object in the `auth` configuration: ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ //... other config options session: { freshAge: 60 * 5 // 5 minutes (the session is fresh if created within the last 5 minutes) } }) ``` To **disable the freshness check**, set `freshAge` to `0`: ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ //... other config options session: { freshAge: 0 // Disable freshness check } }) ``` ## Session Management Better Auth provides a set of functions to manage sessions. ### Get Session The `getSession` function retrieves the current active session. ```ts client="client.ts" import { authClient } from "@/lib/client" const { data: session } = await authClient.getSession() ``` To learn how to customize the session response check the [Customizing Session Response](#customizing-session-response) section. ### Use Session The `useSession` action provides a reactive way to access the current session. ```ts client="client.ts" import { authClient } from "@/lib/client" const { data: session } = authClient.useSession() ``` ### List Sessions The `listSessions` function returns a list of sessions that are active for the user. ```ts title="auth-client.ts" import { authClient } from "@/lib/client" const sessions = await authClient.listSessions() ``` ### Revoke Session When a user signs out of a device, the session is automatically ended. However, you can also end a session manually from any device the user is signed into. To end a session, use the `revokeSession` function. Just pass the session token as a parameter. ```ts title="auth-client.ts" await authClient.revokeSession({ token: "session-token" }) ``` ### Revoke Other Sessions To revoke all other sessions except the current session, you can use the `revokeOtherSessions` function. ```ts title="auth-client.ts" await authClient.revokeOtherSessions() ``` ### Revoke All Sessions To revoke all sessions, you can use the `revokeSessions` function. ```ts title="auth-client.ts" await authClient.revokeSessions() ``` ### Revoking Sessions on Password Change You can revoke all sessions when the user changes their password by passing `revokeOtherSessions` as true on `changePassword` function. ```ts title="auth.ts" await authClient.changePassword({ newPassword: newPassword, currentPassword: currentPassword, revokeOtherSessions: true, }) ``` ## Session Caching ### Cookie Cache Calling your database every time `useSession` or `getSession` invoked isn’t ideal, especially if sessions don’t change frequently. Cookie caching handles this by storing session data in a short-lived, signed cookie—similar to how JWT access tokens are used with refresh tokens. When cookie caching is enabled, the server can check session validity from the cookie itself instead of hitting the database each time. The cookie is signed to prevent tampering, and a short `maxAge` ensures that the session data gets refreshed regularly. If a session is revoked or expires, the cookie will be invalidated automatically. To turn on cookie caching, just set `session.cookieCache` in your auth config: ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ session: { cookieCache: { enabled: true, maxAge: 5 * 60 // Cache duration in seconds } } }); ``` If you want to disable returning from the cookie cache when fetching the session, you can pass `disableCookieCache:true` this will force the server to fetch the session from the database and also refresh the cookie cache. ```ts title="auth-client.ts" const session = await authClient.getSession({ query: { disableCookieCache: true }}) ``` or on the server ```ts title="server.ts" await auth.api.getSession({ query: { disableCookieCache: true, }, headers: req.headers, // pass the headers }); ``` ## Customizing Session Response When you call `getSession` or `useSession`, the session data is returned as a `user` and `session` object. You can customize this response using the `customSession` plugin. ```ts title="auth.ts" import { customSession } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ customSession(async ({ user, session }) => { const roles = findUserRoles(session.session.userId); return { roles, user: { ...user, newField: "newField", }, session }; }), ], }); ``` This will add `roles` and `user.newField` to the session response. **Infer on the Client** ```ts title="auth-client.ts" import { customSessionClient } from "better-auth/client/plugins"; import type { auth } from "@/lib/auth"; // Import the auth instance as a type const authClient = createAuthClient({ plugins: [customSessionClient<typeof auth>()], }); const { data } = authClient.useSession(); const { data: sessionData } = await authClient.getSession(); // data.roles // data.user.newField ``` ### Caveats on Customizing Session Response 1. The passed `session` object to the callback does not infer fields added by plugins. However, as a workaround, you can pull up your auth options and pass it to the plugin to infer the fields. ```ts import { betterAuth, BetterAuthOptions } from "better-auth"; const options = { //...config options plugins: [ //...plugins ] } satisfies BetterAuthOptions; export const auth = betterAuth({ ...options, plugins: [ ...(options.plugins ?? []), customSession(async ({ user, session }, ctx) => { // now both user and session will infer the fields added by plugins and your custom fields return { user, session } }, options), // pass options here // [!code highlight] ] }) ``` 2. When your server and client code are in separate projects or repositories, and you cannot import the `auth` instance as a type reference, type inference for custom session fields will not work on the client side. 3. Session caching, including secondary storage or cookie cache, does not include custom fields. Each time the session is fetched, your custom session function will be called. **Mutating the list-device-sessions endpoint** The `/multi-session/list-device-sessions` endpoint from the [multi-session](/docs/plugins/multi-session) plugin is used to list the devices that the user is signed into. You can mutate the response of this endpoint by passing the `shouldMutateListDeviceSessionsEndpoint` option to the `customSession` plugin. By default, we do not mutate the response of this endpoint. ```ts title="auth.ts" import { customSession } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ customSession(async ({ user, session }, ctx) => { return { user, session } }, {}, { shouldMutateListDeviceSessionsEndpoint: true }), // [!code highlight] ], }); ``` ``` -------------------------------------------------------------------------------- /packages/cli/test/info.test.ts: -------------------------------------------------------------------------------- ```typescript import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import { exec } from "node:child_process"; import { promisify } from "node:util"; const execAsync = promisify(exec); let tmpDir = "."; describe("info command", () => { beforeEach(async () => { const tmp = path.join( process.cwd(), "node_modules", ".cache", "info_test-", ); await fs.mkdir(path.join(tmp, "node_modules", ".cache"), { recursive: true, }); tmpDir = await fs.mkdtemp(tmp); // Mock console methods to avoid noise in test output vi.spyOn(console, "log").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true }); vi.restoreAllMocks(); }); it("should display system information without auth config", async () => { // Create a minimal package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Check system information expect(output.system).toHaveProperty("platform"); expect(output.system).toHaveProperty("arch"); expect(output.system).toHaveProperty("cpuCount"); expect(output.system).toHaveProperty("totalMemory"); // Check node information expect(output.node).toHaveProperty("version"); expect(output.node).toHaveProperty("env"); // Check package manager expect(output.packageManager).toHaveProperty("name"); expect(output.packageManager).toHaveProperty("version"); // Better Auth config should have an error since no auth file exists expect(output.betterAuth).toHaveProperty("version"); expect(output.betterAuth.config).toBeNull(); }); it("should load and sanitize auth configuration", async () => { // Create package.json with dependencies await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", next: "^14.0.0", react: "^18.0.0", }, }), ); // Create auth.ts with sensitive data - using in-memory database to avoid adapter errors await fs.writeFile( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ secret: "super-secret-key-123", baseURL: "https://example.com", emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: "github-client-id", clientSecret: "github-client-secret" }, google: { clientId: "google-client-id", clientSecret: "google-client-secret" } } })`, ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Check that sensitive data is redacted expect(output.betterAuth.config).toBeDefined(); expect(output.betterAuth.config.secret).toBe("[REDACTED]"); // Check social providers are sanitized expect(output.betterAuth.config.socialProviders).toBeDefined(); expect(output.betterAuth.config.socialProviders.github.clientId).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.github.clientSecret).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.google.clientId).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.google.clientSecret).toBe( "[REDACTED]", ); // Check non-sensitive data is preserved expect(output.betterAuth.config.emailAndPassword).toEqual({ enabled: true, }); expect(output.betterAuth.config.baseURL).toBe("https://example.com"); }); it("should detect installed frameworks", async () => { // Create package.json with various frameworks await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", next: "^14.0.0", react: "^18.0.0", }, devDependencies: { "@sveltejs/kit": "^2.0.0", svelte: "^4.0.0", }, }), ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Check frameworks are detected expect(output.frameworks).toContainEqual({ name: "next", version: "^14.0.0", }); expect(output.frameworks).toContainEqual({ name: "react", version: "^18.0.0", }); expect(output.frameworks).toContainEqual({ name: "@sveltejs/kit", version: "^2.0.0", }); expect(output.frameworks).toContainEqual({ name: "svelte", version: "^4.0.0", }); }); it("should detect database clients", async () => { // Create package.json with database clients await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", "@prisma/client": "^5.0.0", kysely: "^0.26.0", }, devDependencies: { "drizzle-orm": "^0.29.0", "better-sqlite3": "^9.0.0", }, }), ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Check database clients are detected expect(output.databases).toContainEqual({ name: "@prisma/client", version: "^5.0.0", }); expect(output.databases).toContainEqual({ name: "kysely", version: "^0.26.0", }); expect(output.databases).toContainEqual({ name: "drizzle", version: "^0.29.0", }); expect(output.databases).toContainEqual({ name: "better-sqlite3", version: "^9.0.0", }); }); it("should support custom config path", async () => { // Create package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); // Create custom directory for auth config const customPath = path.join(tmpDir, "config"); await fs.mkdir(customPath, { recursive: true }); // Create auth config in custom location await fs.writeFile( path.join(customPath, "auth.config.ts"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ secret: "my-secret", appName: "Custom Config App", emailAndPassword: { enabled: true, } })`, ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync( `node ${cliPath} info --config config/auth.config.ts --json`, { cwd: tmpDir }, ); const output = JSON.parse(stdout); // Check that custom config was loaded expect(output.betterAuth.config).toBeDefined(); expect(output.betterAuth.config.appName).toBe("Custom Config App"); expect(output.betterAuth.config.secret).toBe("[REDACTED]"); expect(output.betterAuth.config.emailAndPassword).toEqual({ enabled: true, }); }); it("should sanitize plugin configurations", async () => { // Create package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); // Create auth.ts with plugins await fs.writeFile( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; import { twoFactor, organization } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ twoFactor({ otpOptions: { secret: "otp-secret-key" } }), organization({ apiKey: "org-api-key", webhookSecret: "webhook-secret" }) ] })`, ); const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Check that plugin configs are sanitized expect(output.betterAuth.config.plugins).toBeDefined(); expect(Array.isArray(output.betterAuth.config.plugins)).toBe(true); // Plugin sensitive data should be redacted const plugins = output.betterAuth.config.plugins; plugins.forEach((plugin: any) => { if (plugin.config) { // Check that sensitive keys are redacted const configStr = JSON.stringify(plugin.config); expect(configStr).toContain("[REDACTED]"); } }); }); it("should handle missing package.json gracefully", async () => { // Don't create package.json const cliPath = path.join(process.cwd(), "dist", "index.js"); const { stdout } = await execAsync(`node ${cliPath} info --json`, { cwd: tmpDir, }); const output = JSON.parse(stdout); // Should still return system info expect(output.system).toBeDefined(); expect(output.node).toBeDefined(); expect(output.packageManager).toBeDefined(); // Frameworks and databases should be null expect(output.frameworks).toBeNull(); expect(output.databases).toBeNull(); }); }, 20000); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-up.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { createAuthEndpoint } from "@better-auth/core/api"; import { createEmailVerificationToken } from "./email-verification"; import { setSessionCookie } from "../../cookies"; import { APIError } from "better-call"; import type { AdditionalUserFieldsInput, User } from "../../types"; import type { BetterAuthOptions } from "@better-auth/core"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; import { isDevelopment } from "@better-auth/core/env"; import { runWithTransaction } from "@better-auth/core/context"; import { parseUserInput } from "../../db"; export const signUpEmail = <O extends BetterAuthOptions>() => createAuthEndpoint( "/sign-up/email", { method: "POST", body: z.record(z.string(), z.any()), metadata: { $Infer: { body: {} as { name: string; email: string; password: string; image?: string; callbackURL?: string; rememberMe?: boolean; } & AdditionalUserFieldsInput<O>, }, openapi: { description: "Sign up a user using email and password", requestBody: { content: { "application/json": { schema: { type: "object", properties: { name: { type: "string", description: "The name of the user", }, email: { type: "string", description: "The email of the user", }, password: { type: "string", description: "The password of the user", }, image: { type: "string", description: "The profile image URL of the user", }, callbackURL: { type: "string", description: "The URL to use for email verification callback", }, rememberMe: { type: "boolean", description: "If this is false, the session will not be remembered. Default is `true`.", }, }, required: ["name", "email", "password"], }, }, }, }, responses: { "200": { description: "Successfully created user", content: { "application/json": { schema: { type: "object", properties: { token: { type: "string", nullable: true, description: "Authentication token for the session", }, user: { type: "object", properties: { id: { type: "string", description: "The unique identifier of the user", }, email: { type: "string", format: "email", description: "The email address of the user", }, name: { type: "string", description: "The name of the user", }, image: { type: "string", format: "uri", nullable: true, description: "The profile image URL of the user", }, emailVerified: { type: "boolean", description: "Whether the email has been verified", }, createdAt: { type: "string", format: "date-time", description: "When the user was created", }, updatedAt: { type: "string", format: "date-time", description: "When the user was last updated", }, }, required: [ "id", "email", "name", "emailVerified", "createdAt", "updatedAt", ], }, }, required: ["user"], // token is optional }, }, }, }, "422": { description: "Unprocessable Entity. User already exists or failed to create user.", content: { "application/json": { schema: { type: "object", properties: { message: { type: "string", }, }, }, }, }, }, }, }, }, }, async (ctx) => { return runWithTransaction(ctx.context.adapter, async () => { if ( !ctx.context.options.emailAndPassword?.enabled || ctx.context.options.emailAndPassword?.disableSignUp ) { throw new APIError("BAD_REQUEST", { message: "Email and password sign up is not enabled", }); } const body = ctx.body as any as User & { password: string; callbackURL?: string; rememberMe?: boolean; } & { [key: string]: any; }; const { name, email, password, image, callbackURL, rememberMe, ...rest } = body; const isValidEmail = z.email().safeParse(email); if (!isValidEmail.success) { throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL, }); } const minPasswordLength = ctx.context.password.config.minPasswordLength; if (password.length < minPasswordLength) { ctx.context.logger.error("Password is too short"); throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT, }); } const maxPasswordLength = ctx.context.password.config.maxPasswordLength; if (password.length > maxPasswordLength) { ctx.context.logger.error("Password is too long"); throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.PASSWORD_TOO_LONG, }); } const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); if (dbUser?.user) { ctx.context.logger.info( `Sign-up attempt for existing email: ${email}`, ); throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL, }); } /** * Hash the password * * This is done prior to creating the user * to ensure that any plugin that * may break the hashing should break * before the user is created. */ const hash = await ctx.context.password.hash(password); let createdUser: User; try { const data = parseUserInput(ctx.context.options, rest, "create"); createdUser = await ctx.context.internalAdapter.createUser({ email: email.toLowerCase(), name, image, ...data, emailVerified: false, }); if (!createdUser) { throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, }); } } catch (e) { if (isDevelopment()) { ctx.context.logger.error("Failed to create user", e); } if (e instanceof APIError) { throw e; } ctx.context.logger?.error("Failed to create user", e); throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, details: e, }); } if (!createdUser) { throw new APIError("UNPROCESSABLE_ENTITY", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_USER, }); } await ctx.context.internalAdapter.linkAccount({ userId: createdUser.id, providerId: "credential", accountId: createdUser.id, password: hash, }); if ( ctx.context.options.emailVerification?.sendOnSignUp || ctx.context.options.emailAndPassword.requireEmailVerification ) { const token = await createEmailVerificationToken( ctx.context.secret, createdUser.email, undefined, ctx.context.options.emailVerification?.expiresIn, ); const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; const args: Parameters< Required< Required<BetterAuthOptions>["emailVerification"] >["sendVerificationEmail"] > = ctx.request ? [ { user: createdUser, url, token, }, ctx.request, ] : [ { user: createdUser, url, token, }, ]; await ctx.context.options.emailVerification?.sendVerificationEmail?.( ...args, ); } if ( ctx.context.options.emailAndPassword.autoSignIn === false || ctx.context.options.emailAndPassword.requireEmailVerification ) { return ctx.json({ token: null, user: { id: createdUser.id, email: createdUser.email, name: createdUser.name, image: createdUser.image, emailVerified: createdUser.emailVerified, createdAt: createdUser.createdAt, updatedAt: createdUser.updatedAt, }, }); } const session = await ctx.context.internalAdapter.createSession( createdUser.id, rememberMe === false, ); if (!session) { throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION, }); } await setSessionCookie( ctx, { session, user: createdUser, }, rememberMe === false, ); return ctx.json({ token: session.token, user: { id: createdUser.id, email: createdUser.email, name: createdUser.name, image: createdUser.image, emailVerified: createdUser.emailVerified, createdAt: createdUser.createdAt, updatedAt: createdUser.updatedAt, }, }); }); }, ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/multi-session/index.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { APIError, sessionMiddleware } from "../../api"; import { createAuthEndpoint, createAuthMiddleware, } from "@better-auth/core/api"; import { deleteSessionCookie, parseCookies, parseSetCookieHeader, setSessionCookie, } from "../../cookies"; import type { BetterAuthPlugin } from "@better-auth/core"; import { defineErrorCodes } from "@better-auth/core/utils"; interface MultiSessionConfig { /** * The maximum number of sessions a user can have * at a time * @default 5 */ maximumSessions?: number; } const ERROR_CODES = defineErrorCodes({ INVALID_SESSION_TOKEN: "Invalid session token", }); export const multiSession = (options?: MultiSessionConfig) => { const opts = { maximumSessions: 5, ...options, }; const isMultiSessionCookie = (key: string) => key.includes("_multi-"); return { id: "multi-session", endpoints: { /** * ### Endpoint * * GET `/multi-session/list-device-sessions` * * ### API Methods * * **server:** * `auth.api.listDeviceSessions` * * **client:** * `authClient.multiSession.listDeviceSessions` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-list-device-sessions) */ listDeviceSessions: createAuthEndpoint( "/multi-session/list-device-sessions", { method: "GET", requireHeaders: true, }, async (ctx) => { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return ctx.json([]); const cookies = Object.fromEntries(parseCookies(cookieHeader)); const sessionTokens = ( await Promise.all( Object.entries(cookies) .filter(([key]) => isMultiSessionCookie(key)) .map( async ([key]) => await ctx.getSignedCookie(key, ctx.context.secret), ), ) ).filter((v) => v !== null); if (!sessionTokens.length) return ctx.json([]); const sessions = await ctx.context.internalAdapter.findSessions(sessionTokens); const validSessions = sessions.filter( (session) => session && session.session.expiresAt > new Date(), ); const uniqueUserSessions = validSessions.reduce( (acc, session) => { if (!acc.find((s) => s.user.id === session.user.id)) { acc.push(session); } return acc; }, [] as typeof validSessions, ); return ctx.json(uniqueUserSessions); }, ), /** * ### Endpoint * * POST `/multi-session/set-active` * * ### API Methods * * **server:** * `auth.api.setActiveSession` * * **client:** * `authClient.multiSession.setActive` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-set-active) */ setActiveSession: createAuthEndpoint( "/multi-session/set-active", { method: "POST", body: z.object({ sessionToken: z.string().meta({ description: "The session token to set as active", }), }), requireHeaders: true, use: [sessionMiddleware], metadata: { openapi: { description: "Set the active session", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { session: { $ref: "#/components/schemas/Session", }, }, }, }, }, }, }, }, }, }, async (ctx) => { const sessionToken = ctx.body.sessionToken; const multiSessionCookieName = `${ ctx.context.authCookies.sessionToken.name }_multi-${sessionToken.toLowerCase()}`; const sessionCookie = await ctx.getSignedCookie( multiSessionCookieName, ctx.context.secret, ); if (!sessionCookie) { throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN, }); } const session = await ctx.context.internalAdapter.findSession(sessionToken); if (!session || session.session.expiresAt < new Date()) { ctx.setCookie(multiSessionCookieName, "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0, }); throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN, }); } await setSessionCookie(ctx, session); return ctx.json(session); }, ), /** * ### Endpoint * * POST `/multi-session/revoke` * * ### API Methods * * **server:** * `auth.api.revokeDeviceSession` * * **client:** * `authClient.multiSession.revoke` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/multi-session#api-method-multi-session-revoke) */ revokeDeviceSession: createAuthEndpoint( "/multi-session/revoke", { method: "POST", body: z.object({ sessionToken: z.string().meta({ description: "The session token to revoke", }), }), requireHeaders: true, use: [sessionMiddleware], metadata: { openapi: { description: "Revoke a device session", responses: { 200: { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", }, }, }, }, }, }, }, }, }, }, async (ctx) => { const sessionToken = ctx.body.sessionToken; const multiSessionCookieName = `${ ctx.context.authCookies.sessionToken.name }_multi-${sessionToken.toLowerCase()}`; const sessionCookie = await ctx.getSignedCookie( multiSessionCookieName, ctx.context.secret, ); if (!sessionCookie) { throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.INVALID_SESSION_TOKEN, }); } await ctx.context.internalAdapter.deleteSession(sessionToken); ctx.setCookie(multiSessionCookieName, "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0, }); const isActive = ctx.context.session?.session.token === sessionToken; if (!isActive) return ctx.json({ status: true }); const cookieHeader = ctx.headers?.get("cookie"); if (cookieHeader) { const cookies = Object.fromEntries(parseCookies(cookieHeader)); const sessionTokens = ( await Promise.all( Object.entries(cookies) .filter(([key]) => isMultiSessionCookie(key)) .map( async ([key]) => await ctx.getSignedCookie(key, ctx.context.secret), ), ) ).filter((v): v is string => v !== undefined); const internalAdapter = ctx.context.internalAdapter; if (sessionTokens.length > 0) { const sessions = await internalAdapter.findSessions(sessionTokens); const validSessions = sessions.filter( (session) => session && session.session.expiresAt > new Date(), ); if (validSessions.length > 0) { const nextSession = validSessions[0]!; await setSessionCookie(ctx, nextSession); } else { deleteSessionCookie(ctx); } } else { deleteSessionCookie(ctx); } } else { deleteSessionCookie(ctx); } return ctx.json({ status: true, }); }, ), }, hooks: { after: [ { matcher: () => true, handler: createAuthMiddleware(async (ctx) => { const cookieString = ctx.context.responseHeaders?.get("set-cookie"); if (!cookieString) return; const setCookies = parseSetCookieHeader(cookieString); const sessionCookieConfig = ctx.context.authCookies.sessionToken; const sessionToken = ctx.context.newSession?.session.token; if (!sessionToken) return; const cookies = parseCookies(ctx.headers?.get("cookie") || ""); const cookieName = `${ sessionCookieConfig.name }_multi-${sessionToken.toLowerCase()}`; if (setCookies.get(cookieName) || cookies.get(cookieName)) return; const currentMultiSessions = Object.keys(Object.fromEntries(cookies)).filter( isMultiSessionCookie, ).length + (cookieString.includes("session_token") ? 1 : 0); if (currentMultiSessions >= opts.maximumSessions) { return; } await ctx.setSignedCookie( cookieName, sessionToken, ctx.context.secret, sessionCookieConfig.options, ); }), }, { matcher: (context) => context.path === "/sign-out", handler: createAuthMiddleware(async (ctx) => { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return; const cookies = Object.fromEntries(parseCookies(cookieHeader)); const ids = Object.keys(cookies) .map((key) => { if (isMultiSessionCookie(key)) { ctx.setCookie( key.toLowerCase().replace("__secure-", "__Secure-"), "", { ...ctx.context.authCookies.sessionToken.options, maxAge: 0, }, ); const token = cookies[key]!.split(".")[0]!; return token; } return null; }) .filter((v): v is string => v !== null); await ctx.context.internalAdapter.deleteSessions(ids); }), }, ], }, $ERROR_CODES: ERROR_CODES, } satisfies BetterAuthPlugin; }; ```