This is page 39 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── 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-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 │ ├── 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/app/sitemap.xml: -------------------------------------------------------------------------------- ``` 1 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> 2 | <url> 3 | <loc>https://www.better-auth.com/</loc> 4 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 5 | <priority>1.00</priority> 6 | </url> 7 | <url> 8 | <loc>https://www.better-auth.com/docs/introduction</loc> 9 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 10 | <priority>0.80</priority> 11 | </url> 12 | <url> 13 | <loc>https://www.better-auth.com/changelogs</loc> 14 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 15 | <priority>0.80</priority> 16 | </url> 17 | <url> 18 | <loc>https://www.better-auth.com/blog</loc> 19 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 20 | <priority>0.80</priority> 21 | </url> 22 | <url> 23 | <loc>https://www.better-auth.com/community</loc> 24 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 25 | <priority>0.80</priority> 26 | </url> 27 | <url> 28 | <loc>https://www.better-auth.com/docs/examples/next-js</loc> 29 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 30 | <priority>0.80</priority> 31 | </url> 32 | <url> 33 | <loc>https://www.better-auth.com/docs/comparison</loc> 34 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 35 | <priority>0.64</priority> 36 | </url> 37 | <url> 38 | <loc>https://www.better-auth.com/docs/installation</loc> 39 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 40 | <priority>0.64</priority> 41 | </url> 42 | <url> 43 | <loc>https://www.better-auth.com/docs/basic-usage</loc> 44 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 45 | <priority>0.64</priority> 46 | </url> 47 | <url> 48 | <loc>https://www.better-auth.com/blog/1-3</loc> 49 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 50 | <priority>0.64</priority> 51 | </url> 52 | <url> 53 | <loc>https://www.better-auth.com/blog/seed-round</loc> 54 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 55 | <priority>0.64</priority> 56 | </url> 57 | <url> 58 | <loc>https://www.better-auth.com/docs/adapters/other-relational-databases</loc> 59 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 60 | <priority>0.51</priority> 61 | </url> 62 | <url> 63 | <loc>https://www.better-auth.com/docs/concepts/cli</loc> 64 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 65 | <priority>0.51</priority> 66 | </url> 67 | <url> 68 | <loc>https://www.better-auth.com/docs/concepts/database</loc> 69 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 70 | <priority>0.51</priority> 71 | </url> 72 | <url> 73 | <loc>https://www.better-auth.com/docs/plugins/passkey</loc> 74 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 75 | <priority>0.51</priority> 76 | </url> 77 | <url> 78 | <loc>https://www.better-auth.com/docs/plugins/username</loc> 79 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 80 | <priority>0.51</priority> 81 | </url> 82 | <url> 83 | <loc>https://www.better-auth.com/docs/plugins/magic-link</loc> 84 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 85 | <priority>0.51</priority> 86 | </url> 87 | <url> 88 | <loc>https://www.better-auth.com/docs/plugins/email-otp</loc> 89 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 90 | <priority>0.51</priority> 91 | </url> 92 | <url> 93 | <loc>https://www.better-auth.com/docs/integrations/next</loc> 94 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 95 | <priority>0.51</priority> 96 | </url> 97 | <url> 98 | <loc>https://www.better-auth.com/docs/concepts/session-management</loc> 99 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 100 | <priority>0.51</priority> 101 | </url> 102 | <url> 103 | <loc>https://www.better-auth.com/docs/plugins/2fa</loc> 104 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 105 | <priority>0.51</priority> 106 | </url> 107 | <url> 108 | <loc>https://www.better-auth.com/docs/concepts/api</loc> 109 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 110 | <priority>0.51</priority> 111 | </url> 112 | <url> 113 | <loc>https://www.better-auth.com/docs/plugins/sso</loc> 114 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 115 | <priority>0.51</priority> 116 | </url> 117 | <url> 118 | <loc>https://www.better-auth.com/docs/plugins/oidc-provider</loc> 119 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 120 | <priority>0.51</priority> 121 | </url> 122 | <url> 123 | <loc>https://www.better-auth.com/docs/plugins/mcp</loc> 124 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 125 | <priority>0.51</priority> 126 | </url> 127 | <url> 128 | <loc>https://www.better-auth.com/docs/plugins/stripe</loc> 129 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 130 | <priority>0.51</priority> 131 | </url> 132 | <url> 133 | <loc>https://www.better-auth.com/docs/plugins/siwe</loc> 134 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 135 | <priority>0.51</priority> 136 | </url> 137 | <url> 138 | <loc>https://www.better-auth.com/docs/adapters/mysql</loc> 139 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 140 | <priority>0.41</priority> 141 | </url> 142 | <url> 143 | <loc>https://www.better-auth.com/docs/adapters/sqlite</loc> 144 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 145 | <priority>0.41</priority> 146 | </url> 147 | <url> 148 | <loc>https://www.better-auth.com/docs/adapters/postgresql</loc> 149 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 150 | <priority>0.41</priority> 151 | </url> 152 | <url> 153 | <loc>https://www.better-auth.com/docs/adapters/mssql</loc> 154 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 155 | <priority>0.41</priority> 156 | </url> 157 | <url> 158 | <loc>https://www.better-auth.com/docs/adapters/drizzle</loc> 159 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 160 | <priority>0.41</priority> 161 | </url> 162 | <url> 163 | <loc>https://www.better-auth.com/docs/concepts/client</loc> 164 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 165 | <priority>0.41</priority> 166 | </url> 167 | <url> 168 | <loc>https://www.better-auth.com/docs/concepts/typescript</loc> 169 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 170 | <priority>0.41</priority> 171 | </url> 172 | <url> 173 | <loc>https://www.better-auth.com/docs/concepts/hooks</loc> 174 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 175 | <priority>0.41</priority> 176 | </url> 177 | <url> 178 | <loc>https://www.better-auth.com/docs/concepts/cookies</loc> 179 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 180 | <priority>0.41</priority> 181 | </url> 182 | <url> 183 | <loc>https://www.better-auth.com/docs/concepts/email</loc> 184 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 185 | <priority>0.41</priority> 186 | </url> 187 | <url> 188 | <loc>https://www.better-auth.com/docs/plugins/generic-oauth</loc> 189 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 190 | <priority>0.41</priority> 191 | </url> 192 | <url> 193 | <loc>https://www.better-auth.com/docs/plugins/anonymous</loc> 194 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 195 | <priority>0.41</priority> 196 | </url> 197 | <url> 198 | <loc>https://www.better-auth.com/docs/plugins/phone-number</loc> 199 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 200 | <priority>0.41</priority> 201 | </url> 202 | <url> 203 | <loc>https://www.better-auth.com/docs/integrations/remix</loc> 204 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 205 | <priority>0.41</priority> 206 | </url> 207 | <url> 208 | <loc>https://www.better-auth.com/docs/integrations/nuxt</loc> 209 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 210 | <priority>0.41</priority> 211 | </url> 212 | <url> 213 | <loc>https://www.better-auth.com/docs/concepts/rate-limit</loc> 214 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 215 | <priority>0.41</priority> 216 | </url> 217 | <url> 218 | <loc>https://www.better-auth.com/docs/integrations/expo</loc> 219 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 220 | <priority>0.41</priority> 221 | </url> 222 | <url> 223 | <loc>https://www.better-auth.com/docs/plugins/bearer</loc> 224 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 225 | <priority>0.41</priority> 226 | </url> 227 | <url> 228 | <loc>https://www.better-auth.com/docs/plugins/organization</loc> 229 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 230 | <priority>0.41</priority> 231 | </url> 232 | <url> 233 | <loc>https://www.better-auth.com/docs/plugins/api-key</loc> 234 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 235 | <priority>0.41</priority> 236 | </url> 237 | <url> 238 | <loc>https://www.better-auth.com/docs/plugins/jwt</loc> 239 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 240 | <priority>0.41</priority> 241 | </url> 242 | <url> 243 | <loc>https://www.better-auth.com/docs/plugins/polar</loc> 244 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 245 | <priority>0.41</priority> 246 | </url> 247 | <url> 248 | <loc>https://www.better-auth.com/docs/plugins/one-tap</loc> 249 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 250 | <priority>0.41</priority> 251 | </url> 252 | <url> 253 | <loc>https://www.better-auth.com/docs/plugins/admin</loc> 254 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 255 | <priority>0.41</priority> 256 | </url> 257 | <url> 258 | <loc>https://www.better-auth.com/docs/guides/optimizing-for-performance</loc> 259 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 260 | <priority>0.33</priority> 261 | </url> 262 | <url> 263 | <loc>https://www.better-auth.com/docs/authentication/other-social-providers</loc> 264 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 265 | <priority>0.33</priority> 266 | </url> 267 | <url> 268 | <loc>https://www.better-auth.com/docs/adapters/prisma</loc> 269 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 270 | <priority>0.33</priority> 271 | </url> 272 | <url> 273 | <loc>https://www.better-auth.com/docs/concepts/users-accounts</loc> 274 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 275 | <priority>0.33</priority> 276 | </url> 277 | <url> 278 | <loc>https://www.better-auth.com/docs/concepts/plugins</loc> 279 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 280 | <priority>0.33</priority> 281 | </url> 282 | <url> 283 | <loc>https://www.better-auth.com/docs/authentication/email-password</loc> 284 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 285 | <priority>0.33</priority> 286 | </url> 287 | <url> 288 | <loc>https://www.better-auth.com/docs/integrations/astro</loc> 289 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 290 | <priority>0.33</priority> 291 | </url> 292 | <url> 293 | <loc>https://www.better-auth.com/docs/integrations/svelte-kit</loc> 294 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 295 | <priority>0.33</priority> 296 | </url> 297 | <url> 298 | <loc>https://www.better-auth.com/docs/concepts/oauth</loc> 299 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 300 | <priority>0.33</priority> 301 | </url> 302 | <url> 303 | <loc>https://www.better-auth.com/docs/integrations/nestjs</loc> 304 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 305 | <priority>0.33</priority> 306 | </url> 307 | <url> 308 | <loc>https://www.better-auth.com/docs/plugins/captcha</loc> 309 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 310 | <priority>0.33</priority> 311 | </url> 312 | <url> 313 | <loc>https://www.better-auth.com/docs/plugins/open-api</loc> 314 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 315 | <priority>0.33</priority> 316 | </url> 317 | <url> 318 | <loc>https://www.better-auth.com/docs/plugins/autumn</loc> 319 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 320 | <priority>0.33</priority> 321 | </url> 322 | <url> 323 | <loc>https://www.better-auth.com/docs/guides/browser-extension-guide</loc> 324 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 325 | <priority>0.26</priority> 326 | </url> 327 | <url> 328 | <loc>https://www.better-auth.com/docs/reference/options</loc> 329 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 330 | <priority>0.26</priority> 331 | </url> 332 | <url> 333 | <loc>https://www.better-auth.com/docs/authentication/zoom</loc> 334 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 335 | <priority>0.26</priority> 336 | </url> 337 | <url> 338 | <loc>https://www.better-auth.com/docs/adapters/mongo</loc> 339 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 340 | <priority>0.26</priority> 341 | </url> 342 | <url> 343 | <loc>https://www.better-auth.com/docs/authentication/apple</loc> 344 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 345 | <priority>0.26</priority> 346 | </url> 347 | <url> 348 | <loc>https://www.better-auth.com/docs/adapters/community-adapters</loc> 349 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 350 | <priority>0.26</priority> 351 | </url> 352 | <url> 353 | <loc>https://www.better-auth.com/docs/integrations/solid-start</loc> 354 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 355 | <priority>0.26</priority> 356 | </url> 357 | <url> 358 | <loc>https://www.better-auth.com/docs/integrations/nitro</loc> 359 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 360 | <priority>0.26</priority> 361 | </url> 362 | <url> 363 | <loc>https://www.better-auth.com/docs/plugins/have-i-been-pwned</loc> 364 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 365 | <priority>0.26</priority> 366 | </url> 367 | <url> 368 | <loc>https://www.better-auth.com/docs/plugins/one-time-token</loc> 369 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 370 | <priority>0.26</priority> 371 | </url> 372 | <url> 373 | <loc>https://www.better-auth.com/docs/plugins/dodopayments</loc> 374 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 375 | <priority>0.26</priority> 376 | </url> 377 | <url> 378 | <loc>https://www.better-auth.com/docs/integrations/hono</loc> 379 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 380 | <priority>0.21</priority> 381 | </url> 382 | <url> 383 | <loc>https://www.better-auth.com/docs/guides/create-a-db-adapter</loc> 384 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 385 | <priority>0.21</priority> 386 | </url> 387 | <url> 388 | <loc>https://www.better-auth.com/docs/reference/contributing</loc> 389 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 390 | <priority>0.21</priority> 391 | </url> 392 | <url> 393 | <loc>https://www.better-auth.com/docs/authentication/vk</loc> 394 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 395 | <priority>0.21</priority> 396 | </url> 397 | <url> 398 | <loc>https://www.better-auth.com/docs/authentication/discord</loc> 399 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 400 | <priority>0.21</priority> 401 | </url> 402 | <url> 403 | <loc>https://www.better-auth.com/docs/integrations/tanstack</loc> 404 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 405 | <priority>0.21</priority> 406 | </url> 407 | <url> 408 | <loc>https://www.better-auth.com/docs/integrations/elysia</loc> 409 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 410 | <priority>0.21</priority> 411 | </url> 412 | <url> 413 | <loc>https://www.better-auth.com/docs/plugins/multi-session</loc> 414 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 415 | <priority>0.21</priority> 416 | </url> 417 | <url> 418 | <loc>https://www.better-auth.com/docs/plugins/oauth-proxy</loc> 419 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 420 | <priority>0.21</priority> 421 | </url> 422 | <url> 423 | <loc>https://www.better-auth.com/docs/plugins/dub</loc> 424 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 425 | <priority>0.21</priority> 426 | </url> 427 | <url> 428 | <loc>https://www.better-auth.com/docs/integrations/fastify</loc> 429 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 430 | <priority>0.17</priority> 431 | </url> 432 | <url> 433 | <loc>https://www.better-auth.com/docs/guides/your-first-plugin</loc> 434 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 435 | <priority>0.17</priority> 436 | </url> 437 | <url> 438 | <loc>https://www.better-auth.com/docs/reference/resources</loc> 439 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 440 | <priority>0.17</priority> 441 | </url> 442 | <url> 443 | <loc>https://www.better-auth.com/docs/authentication/spotify</loc> 444 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 445 | <priority>0.17</priority> 446 | </url> 447 | <url> 448 | <loc>https://www.better-auth.com/docs/authentication/facebook</loc> 449 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 450 | <priority>0.17</priority> 451 | </url> 452 | <url> 453 | <loc>https://www.better-auth.com/docs/integrations/express</loc> 454 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 455 | <priority>0.17</priority> 456 | </url> 457 | <url> 458 | <loc>https://www.better-auth.com/docs/plugins/community-plugins</loc> 459 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 460 | <priority>0.17</priority> 461 | </url> 462 | <url> 463 | <loc>https://www.better-auth.com/docs/guides/clerk-migration-guide</loc> 464 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 465 | <priority>0.13</priority> 466 | </url> 467 | <url> 468 | <loc>https://www.better-auth.com/docs/reference/security</loc> 469 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 470 | <priority>0.13</priority> 471 | </url> 472 | <url> 473 | <loc>https://www.better-auth.com/docs/authentication/roblox</loc> 474 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 475 | <priority>0.13</priority> 476 | </url> 477 | <url> 478 | <loc>https://www.better-auth.com/docs/authentication/github</loc> 479 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 480 | <priority>0.13</priority> 481 | </url> 482 | <url> 483 | <loc>https://www.better-auth.com/docs/guides/next-auth-migration-guide</loc> 484 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 485 | <priority>0.13</priority> 486 | </url> 487 | <url> 488 | <loc>https://www.better-auth.com/docs/guides/supabase-migration-guide</loc> 489 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 490 | <priority>0.11</priority> 491 | </url> 492 | <url> 493 | <loc>https://www.better-auth.com/docs/reference/faq</loc> 494 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 495 | <priority>0.11</priority> 496 | </url> 497 | <url> 498 | <loc>https://www.better-auth.com/docs/authentication/reddit</loc> 499 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 500 | <priority>0.11</priority> 501 | </url> 502 | <url> 503 | <loc>https://www.better-auth.com/docs/authentication/google</loc> 504 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 505 | <priority>0.11</priority> 506 | </url> 507 | <url> 508 | <loc>https://www.better-auth.com/docs/authentication/gitlab</loc> 509 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 510 | <priority>0.09</priority> 511 | </url> 512 | <url> 513 | <loc>https://www.better-auth.com/docs/authentication/huggingface</loc> 514 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 515 | <priority>0.09</priority> 516 | </url> 517 | <url> 518 | <loc>https://www.better-auth.com/docs/authentication/linkedin</loc> 519 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 520 | <priority>0.07</priority> 521 | </url> 522 | <url> 523 | <loc>https://www.better-auth.com/docs/authentication/kick</loc> 524 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 525 | <priority>0.07</priority> 526 | </url> 527 | <url> 528 | <loc>https://www.better-auth.com/docs/authentication/linear</loc> 529 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 530 | <priority>0.05</priority> 531 | </url> 532 | <url> 533 | <loc>https://www.better-auth.com/docs/authentication/microsoft</loc> 534 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 535 | <priority>0.05</priority> 536 | </url> 537 | <url> 538 | <loc>https://www.better-auth.com/docs/authentication/dropbox</loc> 539 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 540 | <priority>0.04</priority> 541 | </url> 542 | <url> 543 | <loc>https://www.better-auth.com/docs/authentication/slack</loc> 544 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 545 | <priority>0.04</priority> 546 | </url> 547 | <url> 548 | <loc>https://www.better-auth.com/docs/authentication/twitter</loc> 549 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 550 | <priority>0.04</priority> 551 | </url> 552 | <url> 553 | <loc>https://www.better-auth.com/docs/authentication/notion</loc> 554 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 555 | <priority>0.04</priority> 556 | </url> 557 | <url> 558 | <loc>https://www.better-auth.com/docs/authentication/twitch</loc> 559 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 560 | <priority>0.03</priority> 561 | </url> 562 | <url> 563 | <loc>https://www.better-auth.com/docs/authentication/tiktok</loc> 564 | <lastmod>2025-07-31T12:33:38+00:00</lastmod> 565 | <priority>0.03</priority> 566 | </url> 567 | </urlset> ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/options.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Options 3 | description: Better Auth configuration options reference. 4 | --- 5 | 6 | List of all the available options for configuring Better Auth. See [Better Auth Options](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/types/options.ts#L13). 7 | 8 | ## `appName` 9 | 10 | The name of the application. 11 | 12 | ```ts 13 | import { betterAuth } from "better-auth"; 14 | export const auth = betterAuth({ 15 | appName: "My App", 16 | }) 17 | ``` 18 | 19 | ## `baseURL` 20 | 21 | Base URL for Better Auth. This is typically the root URL where your application server is hosted. Note: If you include a path in the baseURL, it will take precedence over the default path. 22 | 23 | ```ts 24 | import { betterAuth } from "better-auth"; 25 | export const auth = betterAuth({ 26 | baseURL: "https://example.com", 27 | }) 28 | ``` 29 | 30 | If not explicitly set, the system will check for the environment variable `process.env.BETTER_AUTH_URL` 31 | 32 | ## `basePath` 33 | 34 | Base path for Better Auth. This is typically the path where the Better Auth routes are mounted. It will be overridden if there is a path component within `baseURL`. 35 | 36 | ```ts 37 | import { betterAuth } from "better-auth"; 38 | export const auth = betterAuth({ 39 | basePath: "/api/auth", 40 | }) 41 | ``` 42 | 43 | Default: `/api/auth` 44 | 45 | ## `trustedOrigins` 46 | 47 | List of trusted origins. You can provide a static array of origins, a function that returns origins dynamically, or use wildcard patterns to match multiple domains. 48 | 49 | ### Static Origins 50 | 51 | You can provide a static array of origins: 52 | 53 | ```ts 54 | import { betterAuth } from "better-auth"; 55 | export const auth = betterAuth({ 56 | trustedOrigins: ["http://localhost:3000", "https://example.com"], 57 | }) 58 | ``` 59 | 60 | ### Dynamic Origins 61 | 62 | You can provide a function that returns origins dynamically: 63 | 64 | ```ts 65 | export const auth = betterAuth({ 66 | trustedOrigins: async (request: Request) => { 67 | // Return an array of trusted origins based on the request 68 | return ["https://dynamic-origin.com"]; 69 | } 70 | }) 71 | ``` 72 | 73 | ### Wildcard Support 74 | 75 | You can use wildcard patterns in trusted origins: 76 | 77 | ```ts 78 | export const auth = betterAuth({ 79 | trustedOrigins: [ 80 | "*.example.com", // Trust all subdomains of example.com 81 | "https://*.example.com", // Trust only HTTPS subdomains 82 | "http://*.dev.example.com" // Trust HTTP subdomains of dev.example.com 83 | ] 84 | }) 85 | ``` 86 | 87 | ## `secret` 88 | 89 | The secret used for encryption, signing, and hashing. 90 | 91 | ```ts 92 | import { betterAuth } from "better-auth"; 93 | export const auth = betterAuth({ 94 | secret: "your-secret-key", 95 | }) 96 | ``` 97 | 98 | By default, Better Auth will look for the following environment variables: 99 | - `process.env.BETTER_AUTH_SECRET` 100 | - `process.env.AUTH_SECRET` 101 | 102 | If none of these environment variables are set, it will default to `"better-auth-secret-123456789"`. In production, if it's not set, it will throw an error. 103 | 104 | You can generate a good secret using the following command: 105 | 106 | ```bash 107 | openssl rand -base64 32 108 | ``` 109 | 110 | ## `database` 111 | 112 | Database configuration for Better Auth. 113 | 114 | ```ts 115 | import { betterAuth } from "better-auth"; 116 | export const auth = betterAuth({ 117 | database: { 118 | dialect: "postgres", 119 | type: "postgres", 120 | casing: "camel" 121 | }, 122 | }) 123 | ``` 124 | 125 | Better Auth supports various database configurations including [PostgreSQL](/docs/adapters/postgresql), [MySQL](/docs/adapters/mysql), and [SQLite](/docs/adapters/sqlite). 126 | 127 | Read more about databases [here](/docs/concepts/database). 128 | 129 | ## `secondaryStorage` 130 | 131 | Secondary storage configuration used to store session and rate limit data. 132 | 133 | ```ts 134 | import { betterAuth } from "better-auth"; 135 | 136 | export const auth = betterAuth({ 137 | // ... other options 138 | secondaryStorage: { 139 | // Your implementation here 140 | }, 141 | }) 142 | ``` 143 | 144 | Read more about secondary storage [here](/docs/concepts/database#secondary-storage). 145 | 146 | ## `emailVerification` 147 | 148 | Email verification configuration. 149 | 150 | ```ts 151 | import { betterAuth } from "better-auth"; 152 | export const auth = betterAuth({ 153 | emailVerification: { 154 | sendVerificationEmail: async ({ user, url, token }) => { 155 | // Send verification email to user 156 | }, 157 | sendOnSignUp: true, 158 | autoSignInAfterVerification: true, 159 | expiresIn: 3600 // 1 hour 160 | }, 161 | }) 162 | ``` 163 | 164 | - `sendVerificationEmail`: Function to send verification email 165 | - `sendOnSignUp`: Send verification email automatically after sign up (default: `false`) 166 | - `sendOnSignIn`: Send verification email automatically on sign in when the user's email is not verified (default: `false`) 167 | - `autoSignInAfterVerification`: Auto sign in the user after they verify their email 168 | - `expiresIn`: Number of seconds the verification token is valid for (default: `3600` seconds) 169 | 170 | ## `emailAndPassword` 171 | 172 | Email and password authentication configuration. 173 | 174 | ```ts 175 | import { betterAuth } from "better-auth"; 176 | export const auth = betterAuth({ 177 | emailAndPassword: { 178 | enabled: true, 179 | disableSignUp: false, 180 | requireEmailVerification: true, 181 | minPasswordLength: 8, 182 | maxPasswordLength: 128, 183 | autoSignIn: true, 184 | sendResetPassword: async ({ user, url, token }) => { 185 | // Send reset password email 186 | }, 187 | resetPasswordTokenExpiresIn: 3600, // 1 hour 188 | password: { 189 | hash: async (password) => { 190 | // Custom password hashing 191 | return hashedPassword; 192 | }, 193 | verify: async ({ hash, password }) => { 194 | // Custom password verification 195 | return isValid; 196 | } 197 | } 198 | }, 199 | }) 200 | ``` 201 | 202 | - `enabled`: Enable email and password authentication (default: `false`) 203 | - `disableSignUp`: Disable email and password sign up (default: `false`) 204 | - `requireEmailVerification`: Require email verification before a session can be created 205 | - `minPasswordLength`: Minimum password length (default: `8`) 206 | - `maxPasswordLength`: Maximum password length (default: `128`) 207 | - `autoSignIn`: Automatically sign in the user after sign up 208 | - `sendResetPassword`: Function to send reset password email 209 | - `resetPasswordTokenExpiresIn`: Number of seconds the reset password token is valid for (default: `3600` seconds) 210 | - `password`: Custom password hashing and verification functions 211 | 212 | ## `socialProviders` 213 | 214 | Configure social login providers. 215 | 216 | ```ts 217 | import { betterAuth } from "better-auth"; 218 | export const auth = betterAuth({ 219 | socialProviders: { 220 | google: { 221 | clientId: "your-client-id", 222 | clientSecret: "your-client-secret", 223 | redirectURI: "https://example.com/api/auth/callback/google" 224 | }, 225 | github: { 226 | clientId: "your-client-id", 227 | clientSecret: "your-client-secret", 228 | redirectURI: "https://example.com/api/auth/callback/github" 229 | } 230 | }, 231 | }) 232 | ``` 233 | 234 | ## `plugins` 235 | 236 | List of Better Auth plugins. 237 | 238 | ```ts 239 | import { betterAuth } from "better-auth"; 240 | import { emailOTP } from "better-auth/plugins"; 241 | 242 | export const auth = betterAuth({ 243 | plugins: [ 244 | emailOTP({ 245 | sendVerificationOTP: async ({ email, otp, type }) => { 246 | // Send OTP to user's email 247 | } 248 | }) 249 | ], 250 | }) 251 | ``` 252 | 253 | ## `user` 254 | 255 | User configuration options. 256 | 257 | ```ts 258 | import { betterAuth } from "better-auth"; 259 | export const auth = betterAuth({ 260 | user: { 261 | modelName: "users", 262 | fields: { 263 | email: "emailAddress", 264 | name: "fullName" 265 | }, 266 | additionalFields: { 267 | customField: { 268 | type: "string", 269 | } 270 | }, 271 | changeEmail: { 272 | enabled: true, 273 | sendChangeEmailVerification: async ({ user, newEmail, url, token }) => { 274 | // Send change email verification 275 | } 276 | }, 277 | deleteUser: { 278 | enabled: true, 279 | sendDeleteAccountVerification: async ({ user, url, token }) => { 280 | // Send delete account verification 281 | }, 282 | beforeDelete: async (user) => { 283 | // Perform actions before user deletion 284 | }, 285 | afterDelete: async (user) => { 286 | // Perform cleanup after user deletion 287 | } 288 | } 289 | }, 290 | }) 291 | ``` 292 | 293 | - `modelName`: The model name for the user (default: `"user"`) 294 | - `fields`: Map fields to different column names 295 | - `additionalFields`: Additional fields for the user table 296 | - `changeEmail`: Configuration for changing email 297 | - `deleteUser`: Configuration for user deletion 298 | 299 | ## `session` 300 | 301 | Session configuration options. 302 | 303 | ```ts 304 | import { betterAuth } from "better-auth"; 305 | export const auth = betterAuth({ 306 | session: { 307 | modelName: "sessions", 308 | fields: { 309 | userId: "user_id" 310 | }, 311 | expiresIn: 604800, // 7 days 312 | updateAge: 86400, // 1 day 313 | disableSessionRefresh: true, // Disable session refresh so that the session is not updated regardless of the `updateAge` option. (default: `false`) 314 | additionalFields: { // Additional fields for the session table 315 | customField: { 316 | type: "string", 317 | } 318 | }, 319 | storeSessionInDatabase: true, // Store session in database when secondary storage is provided (default: `false`) 320 | preserveSessionInDatabase: false, // Preserve session records in database when deleted from secondary storage (default: `false`) 321 | cookieCache: { 322 | enabled: true, // Enable caching session in cookie (default: `false`) 323 | maxAge: 300 // 5 minutes 324 | } 325 | }, 326 | }) 327 | ``` 328 | 329 | - `modelName`: The model name for the session (default: `"session"`) 330 | - `fields`: Map fields to different column names 331 | - `expiresIn`: Expiration time for the session token in seconds (default: `604800` - 7 days) 332 | - `updateAge`: How often the session should be refreshed in seconds (default: `86400` - 1 day) 333 | - `additionalFields`: Additional fields for the session table 334 | - `storeSessionInDatabase`: Store session in database when secondary storage is provided (default: `false`) 335 | - `preserveSessionInDatabase`: Preserve session records in database when deleted from secondary storage (default: `false`) 336 | - `cookieCache`: Enable caching session in cookie 337 | 338 | ## `account` 339 | 340 | Account configuration options. 341 | 342 | ```ts 343 | import { betterAuth } from "better-auth"; 344 | export const auth = betterAuth({ 345 | account: { 346 | modelName: "accounts", 347 | fields: { 348 | userId: "user_id" 349 | }, 350 | encryptOAuthTokens: true, // Encrypt OAuth tokens before storing them in the database 351 | accountLinking: { 352 | enabled: true, 353 | trustedProviders: ["google", "github", "email-password"], 354 | allowDifferentEmails: false 355 | } 356 | }, 357 | }) 358 | ``` 359 | 360 | - `modelName`: The model name for the account 361 | - `fields`: Map fields to different column names 362 | 363 | ### `encryptOAuthTokens` 364 | 365 | Encrypt OAuth tokens before storing them in the database. Default: `false`. 366 | 367 | ### `updateAccountOnSignIn` 368 | 369 | If enabled (true), the user account data (accessToken, idToken, refreshToken, etc.) 370 | will be updated on sign in with the latest data from the provider. 371 | 372 | ### `accountLinking` 373 | 374 | Configuration for account linking. 375 | 376 | - `enabled`: Enable account linking (default: `false`) 377 | - `trustedProviders`: List of trusted providers 378 | - `allowDifferentEmails`: Allow users to link accounts with different email addresses 379 | - `allowUnlinkingAll`: Allow users to unlink all accounts 380 | 381 | ## `verification` 382 | 383 | Verification configuration options. 384 | 385 | ```ts 386 | import { betterAuth } from "better-auth"; 387 | export const auth = betterAuth({ 388 | verification: { 389 | modelName: "verifications", 390 | fields: { 391 | userId: "user_id" 392 | }, 393 | disableCleanup: false 394 | }, 395 | }) 396 | ``` 397 | 398 | - `modelName`: The model name for the verification table 399 | - `fields`: Map fields to different column names 400 | - `disableCleanup`: Disable cleaning up expired values when a verification value is fetched 401 | 402 | ## `rateLimit` 403 | 404 | Rate limiting configuration. 405 | 406 | ```ts 407 | import { betterAuth } from "better-auth"; 408 | export const auth = betterAuth({ 409 | rateLimit: { 410 | enabled: true, 411 | window: 10, 412 | max: 100, 413 | customRules: { 414 | "/example/path": { 415 | window: 10, 416 | max: 100 417 | } 418 | }, 419 | storage: "memory", 420 | modelName: "rateLimit" 421 | } 422 | }) 423 | ``` 424 | 425 | - `enabled`: Enable rate limiting (defaults: `true` in production, `false` in development) 426 | - `window`: Time window to use for rate limiting. The value should be in seconds. (default: `10`) 427 | - `max`: The default maximum number of requests allowed within the window. (default: `100`) 428 | - `customRules`: Custom rate limit rules to apply to specific paths. 429 | - `storage`: Storage configuration. If you passed a secondary storage, rate limiting will be stored in the secondary storage. (options: `"memory", "database", "secondary-storage"`, default: `"memory"`) 430 | - `modelName`: The name of the table to use for rate limiting if database is used as storage. (default: `"rateLimit"`) 431 | 432 | 433 | ## `advanced` 434 | 435 | Advanced configuration options. 436 | 437 | ```ts 438 | import { betterAuth } from "better-auth"; 439 | export const auth = betterAuth({ 440 | advanced: { 441 | ipAddress: { 442 | ipAddressHeaders: ["x-client-ip", "x-forwarded-for"], 443 | disableIpTracking: false 444 | }, 445 | useSecureCookies: true, 446 | disableCSRFCheck: false, 447 | crossSubDomainCookies: { 448 | enabled: true, 449 | additionalCookies: ["custom_cookie"], 450 | domain: "example.com" 451 | }, 452 | cookies: { 453 | session_token: { 454 | name: "custom_session_token", 455 | attributes: { 456 | httpOnly: true, 457 | secure: true 458 | } 459 | } 460 | }, 461 | defaultCookieAttributes: { 462 | httpOnly: true, 463 | secure: true 464 | }, 465 | cookiePrefix: "myapp", 466 | database: { 467 | // If your DB is using auto-incrementing IDs, set this to true. 468 | useNumberId: false, 469 | // Use your own custom ID generator, or disable generating IDs as a whole. 470 | generateId: (((options: { 471 | model: LiteralUnion<Models, string>; 472 | size?: number; 473 | }) => { 474 | return "my-super-unique-id"; 475 | })) | false, 476 | defaultFindManyLimit: 100, 477 | } 478 | }, 479 | }) 480 | ``` 481 | 482 | - `ipAddress`: IP address configuration for rate limiting and session tracking 483 | - `useSecureCookies`: Use secure cookies (default: `false`) 484 | - `disableCSRFCheck`: Disable trusted origins check (⚠️ security risk) 485 | - `crossSubDomainCookies`: Configure cookies to be shared across subdomains 486 | - `cookies`: Customize cookie names and attributes 487 | - `defaultCookieAttributes`: Default attributes for all cookies 488 | - `cookiePrefix`: Prefix for cookies 489 | - `generateId`: Function to generate a unique ID for a model 490 | 491 | ## `logger` 492 | 493 | Logger configuration for Better Auth. 494 | 495 | ```ts 496 | import { betterAuth } from "better-auth"; 497 | export const auth = betterAuth({ 498 | logger: { 499 | disabled: false, 500 | disableColors: false, 501 | level: "error", 502 | log: (level, message, ...args) => { 503 | // Custom logging implementation 504 | console.log(`[${level}] ${message}`, ...args); 505 | } 506 | } 507 | }) 508 | ``` 509 | 510 | The logger configuration allows you to customize how Better Auth handles logging. It supports the following options: 511 | 512 | - `disabled`: Disable all logging when set to `true` (default: `false`) 513 | - `disableColors`: Disable colors in the default logger implementation (default: determined by the terminal's color support) 514 | - `level`: Set the minimum log level to display. Available levels are: 515 | - `"info"`: Show all logs 516 | - `"warn"`: Show warnings and errors 517 | - `"error"`: Show only errors 518 | - `"debug"`: Show all logs including debug information 519 | - `log`: Custom logging function that receives: 520 | - `level`: The log level (`"info"`, `"warn"`, `"error"`, or `"debug"`) 521 | - `message`: The log message 522 | - `...args`: Additional arguments passed to the logger 523 | 524 | Example with custom logging: 525 | 526 | ```ts 527 | import { betterAuth } from "better-auth"; 528 | export const auth = betterAuth({ 529 | logger: { 530 | level: "info", 531 | log: (level, message, ...args) => { 532 | // Send logs to a custom logging service 533 | myLoggingService.log({ 534 | level, 535 | message, 536 | metadata: args, 537 | timestamp: new Date().toISOString() 538 | }); 539 | } 540 | } 541 | }) 542 | ``` 543 | 544 | ## `databaseHooks` 545 | 546 | Database lifecycle hooks for core operations. 547 | 548 | ```ts 549 | import { betterAuth } from "better-auth"; 550 | export const auth = betterAuth({ 551 | databaseHooks: { 552 | user: { 553 | create: { 554 | before: async (user) => { 555 | // Modify user data before creation 556 | return { data: { ...user, customField: "value" } }; 557 | }, 558 | after: async (user) => { 559 | // Perform actions after user creation 560 | } 561 | }, 562 | update: { 563 | before: async (userData) => { 564 | // Modify user data before update 565 | return { data: { ...userData, updatedAt: new Date() } }; 566 | }, 567 | after: async (user) => { 568 | // Perform actions after user update 569 | } 570 | } 571 | }, 572 | session: { 573 | // Session hooks 574 | }, 575 | account: { 576 | // Account hooks 577 | }, 578 | verification: { 579 | // Verification hooks 580 | } 581 | }, 582 | }) 583 | ``` 584 | 585 | ## `onAPIError` 586 | 587 | API error handling configuration. 588 | 589 | ```ts 590 | import { betterAuth } from "better-auth"; 591 | export const auth = betterAuth({ 592 | onAPIError: { 593 | throw: true, 594 | onError: (error, ctx) => { 595 | // Custom error handling 596 | console.error("Auth error:", error); 597 | }, 598 | errorURL: "/auth/error" 599 | }, 600 | }) 601 | ``` 602 | 603 | - `throw`: Throw an error on API error (default: `false`) 604 | - `onError`: Custom error handler 605 | - `errorURL`: URL to redirect to on error (default: `/api/auth/error`) 606 | 607 | ## `hooks` 608 | 609 | Request lifecycle hooks. 610 | 611 | ```ts 612 | import { betterAuth } from "better-auth"; 613 | import { createAuthMiddleware } from "better-auth/api"; 614 | 615 | export const auth = betterAuth({ 616 | hooks: { 617 | before: createAuthMiddleware(async (ctx) => { 618 | // Execute before processing the request 619 | console.log("Request path:", ctx.path); 620 | }), 621 | after: createAuthMiddleware(async (ctx) => { 622 | // Execute after processing the request 623 | console.log("Response:", ctx.context.returned); 624 | }) 625 | }, 626 | }) 627 | ``` 628 | 629 | For more details and examples, see the [Hooks documentation](/docs/concepts/hooks). 630 | 631 | ## `disabledPaths` 632 | 633 | Disable specific auth paths. 634 | 635 | ```ts 636 | import { betterAuth } from "better-auth"; 637 | export const auth = betterAuth({ 638 | disabledPaths: ["/sign-up/email", "/sign-in/email"], 639 | }) 640 | ``` 641 | 642 | ## `telemetry` 643 | 644 | Enable or disable Better Auth's telemetry collection. (default: `false`) 645 | 646 | ```ts 647 | import { betterAuth } from "better-auth"; 648 | export const auth = betterAuth({ 649 | telemetry: { 650 | enabled: false, 651 | } 652 | }) 653 | ``` 654 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/cookies/cookies.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../test-utils/test-instance"; 3 | import { getCookieCache, getCookies, getSessionCookie } from "../cookies"; 4 | import { parseSetCookieHeader } from "./cookie-utils"; 5 | import type { BetterAuthOptions } from "@better-auth/core"; 6 | 7 | describe("cookies", async () => { 8 | const { client, testUser } = await getTestInstance(); 9 | it("should set cookies with default options", async () => { 10 | await client.signIn.email( 11 | { 12 | email: testUser.email, 13 | password: testUser.password, 14 | }, 15 | { 16 | onResponse(context) { 17 | const setCookie = context.response.headers.get("set-cookie"); 18 | expect(setCookie).toBeDefined(); 19 | expect(setCookie).toContain("Path=/"); 20 | expect(setCookie).toContain("HttpOnly"); 21 | expect(setCookie).toContain("SameSite=Lax"); 22 | expect(setCookie).toContain("better-auth"); 23 | }, 24 | }, 25 | ); 26 | }); 27 | 28 | it("should set multiple cookies", async () => { 29 | await client.signIn.social( 30 | { 31 | provider: "github", 32 | callbackURL: "https://example.com", 33 | }, 34 | { 35 | onSuccess(context) { 36 | const cookies = context.response.headers.get("Set-Cookie"); 37 | expect(cookies?.split(",").length).toBeGreaterThan(1); 38 | }, 39 | }, 40 | ); 41 | }); 42 | 43 | it("should use secure cookies", async () => { 44 | const { client, testUser } = await getTestInstance({ 45 | advanced: { useSecureCookies: true }, 46 | }); 47 | const res = await client.signIn.email( 48 | { 49 | email: testUser.email, 50 | password: testUser.password, 51 | }, 52 | { 53 | onResponse(context) { 54 | const setCookie = context.response.headers.get("set-cookie"); 55 | expect(setCookie).toContain("Secure"); 56 | }, 57 | }, 58 | ); 59 | }); 60 | 61 | it("should use secure cookies when the base url is https", async () => { 62 | const { client, testUser } = await getTestInstance({ 63 | baseURL: "https://example.com", 64 | }); 65 | 66 | await client.signIn.email( 67 | { 68 | email: testUser.email, 69 | password: testUser.password, 70 | }, 71 | { 72 | onResponse(context) { 73 | const setCookie = context.response.headers.get("set-cookie"); 74 | expect(setCookie).toContain("Secure"); 75 | }, 76 | }, 77 | ); 78 | }); 79 | }); 80 | 81 | describe("crossSubdomainCookies", () => { 82 | it("should update cookies with custom domain", async () => { 83 | const { client, testUser } = await getTestInstance({ 84 | advanced: { 85 | crossSubDomainCookies: { 86 | enabled: true, 87 | domain: "example.com", 88 | }, 89 | }, 90 | }); 91 | 92 | await client.signIn.email( 93 | { 94 | email: testUser.email, 95 | password: testUser.password, 96 | }, 97 | { 98 | onResponse(context) { 99 | const setCookie = context.response.headers.get("set-cookie"); 100 | expect(setCookie).toContain("Domain=example.com"); 101 | expect(setCookie).toContain("SameSite=Lax"); 102 | }, 103 | }, 104 | ); 105 | }); 106 | 107 | it("should use default domain from baseURL if not provided", async () => { 108 | const { testUser, client } = await getTestInstance({ 109 | baseURL: "https://example.com", 110 | advanced: { 111 | crossSubDomainCookies: { 112 | enabled: true, 113 | }, 114 | }, 115 | }); 116 | 117 | await client.signIn.email( 118 | { 119 | email: testUser.email, 120 | password: testUser.password, 121 | }, 122 | { 123 | onResponse(context) { 124 | const setCookie = context.response.headers.get("set-cookie"); 125 | expect(setCookie).toContain("Domain=example.com"); 126 | }, 127 | }, 128 | ); 129 | }); 130 | }); 131 | 132 | describe("cookie configuration", () => { 133 | it("should return correct cookie options based on configuration", async () => { 134 | const options = { 135 | baseURL: "https://example.com", 136 | database: {} as BetterAuthOptions["database"], 137 | advanced: { 138 | useSecureCookies: true, 139 | crossSubDomainCookies: { 140 | enabled: true, 141 | domain: "example.com", 142 | }, 143 | cookiePrefix: "test-prefix", 144 | }, 145 | } satisfies BetterAuthOptions; 146 | 147 | const cookies = getCookies(options); 148 | 149 | expect(cookies.sessionToken.options.secure).toBe(true); 150 | expect(cookies.sessionToken.name).toContain("test-prefix.session_token"); 151 | expect(cookies.sessionData.options.sameSite).toBe("lax"); 152 | expect(cookies.sessionData.options.domain).toBe("example.com"); 153 | }); 154 | }); 155 | 156 | describe("cookie-utils parseSetCookieHeader", () => { 157 | it("handles Expires with commas and multiple cookies", () => { 158 | const header = 159 | "a=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/, b=2; Expires=Thu, 22 Oct 2015 07:28:00 GMT; Path=/"; 160 | const map = parseSetCookieHeader(header); 161 | expect(map.get("a")?.value).toBe("1"); 162 | expect(map.get("b")?.value).toBe("2"); 163 | }); 164 | }); 165 | 166 | describe("getSessionCookie", async () => { 167 | it("should return the correct session cookie", async () => { 168 | const { client, testUser, signInWithTestUser } = await getTestInstance(); 169 | const { headers } = await signInWithTestUser(); 170 | const request = new Request("http://localhost:3000/api/auth/session", { 171 | headers, 172 | }); 173 | const cookies = getSessionCookie(request); 174 | expect(cookies).not.toBeNull(); 175 | expect(cookies).toBeDefined(); 176 | }); 177 | 178 | it("should return the correct session cookie on production", async () => { 179 | const { client, testUser, cookieSetter } = await getTestInstance({ 180 | baseURL: "https://example.com", 181 | }); 182 | const headers = new Headers(); 183 | await client.signIn.email( 184 | { 185 | email: testUser.email, 186 | password: testUser.password, 187 | }, 188 | { 189 | onSuccess: cookieSetter(headers), 190 | }, 191 | ); 192 | const request = new Request("https://example.com/api/auth/session", { 193 | headers, 194 | }); 195 | const cookies = getSessionCookie(request); 196 | expect(cookies).not.toBeNull(); 197 | expect(cookies).toBeDefined(); 198 | }); 199 | 200 | it("should allow override cookie prefix", async () => { 201 | const { client, testUser, cookieSetter } = await getTestInstance({ 202 | advanced: { 203 | useSecureCookies: true, 204 | cookiePrefix: "test-prefix", 205 | }, 206 | }); 207 | const headers = new Headers(); 208 | await client.signIn.email( 209 | { 210 | email: testUser.email, 211 | password: testUser.password, 212 | }, 213 | { onSuccess: cookieSetter(headers) }, 214 | ); 215 | const request = new Request("https://example.com/api/auth/session", { 216 | headers, 217 | }); 218 | const cookies = getSessionCookie(request, { 219 | cookiePrefix: "test-prefix", 220 | }); 221 | expect(cookies).not.toBeNull(); 222 | }); 223 | 224 | it("should allow override cookie name", async () => { 225 | const { client, testUser, cookieSetter } = await getTestInstance({ 226 | advanced: { 227 | useSecureCookies: true, 228 | cookiePrefix: "test", 229 | cookies: { 230 | session_token: { 231 | name: "test-session-token", 232 | }, 233 | }, 234 | }, 235 | }); 236 | const headers = new Headers(); 237 | await client.signIn.email( 238 | { 239 | email: testUser.email, 240 | password: testUser.password, 241 | }, 242 | { 243 | onSuccess: cookieSetter(headers), 244 | }, 245 | ); 246 | const request = new Request("https://example.com/api/auth/session", { 247 | headers, 248 | }); 249 | const cookies = getSessionCookie(request, { 250 | cookieName: "session-token", 251 | cookiePrefix: "test", 252 | }); 253 | expect(cookies).not.toBeNull(); 254 | }); 255 | 256 | it("should return cookie cache", async () => { 257 | const { client, testUser, cookieSetter } = await getTestInstance({ 258 | secret: "better-auth.secret", 259 | session: { 260 | cookieCache: { 261 | enabled: true, 262 | }, 263 | }, 264 | }); 265 | 266 | const headers = new Headers(); 267 | 268 | await client.signIn.email( 269 | { 270 | email: testUser.email, 271 | password: testUser.password, 272 | }, 273 | { 274 | onSuccess: cookieSetter(headers), 275 | }, 276 | ); 277 | 278 | const request = new Request("https://example.com/api/auth/session", { 279 | headers, 280 | }); 281 | 282 | const cache = await getCookieCache(request, { 283 | secret: "better-auth.secret", 284 | }); 285 | expect(cache).not.toBeNull(); 286 | expect(cache?.user?.email).toEqual(testUser.email); 287 | expect(cache?.session?.token).toEqual(expect.any(String)); 288 | }); 289 | 290 | it("should respect dontRememberMe when storing session in cookie cache", async () => { 291 | const { client, testUser } = await getTestInstance({ 292 | secret: "better-auth.secret", 293 | session: { 294 | cookieCache: { 295 | enabled: true, 296 | }, 297 | }, 298 | }); 299 | 300 | await client.signIn.email( 301 | { 302 | email: testUser.email, 303 | password: testUser.password, 304 | rememberMe: false, 305 | }, 306 | { 307 | onSuccess(c) { 308 | const headers = c.response.headers; 309 | const setCookieHeader = headers.get("set-cookie"); 310 | expect(setCookieHeader).toBeDefined(); 311 | 312 | const parsed = parseSetCookieHeader(setCookieHeader!); 313 | 314 | const sessionTokenCookie = parsed.get("better-auth.session_token")!; 315 | expect(sessionTokenCookie).toBeDefined(); 316 | expect(sessionTokenCookie["max-age"]).toBeUndefined(); 317 | 318 | const sessionDataCookie = parsed.get("better-auth.session_data")!; 319 | expect(sessionDataCookie).toBeDefined(); 320 | expect(sessionDataCookie["max-age"]).toBeUndefined(); 321 | }, 322 | }, 323 | ); 324 | }); 325 | 326 | it("should return null if the cookie is invalid", async () => { 327 | const { client, testUser, cookieSetter } = await getTestInstance({ 328 | session: { 329 | cookieCache: { 330 | enabled: true, 331 | }, 332 | }, 333 | }); 334 | const headers = new Headers(); 335 | await client.signIn.email({ 336 | email: testUser.email, 337 | password: testUser.password, 338 | }); 339 | const request = new Request("https://example.com/api/auth/session", { 340 | headers, 341 | }); 342 | const cache = await getCookieCache(request, { 343 | secret: "wrong-secret", 344 | }); 345 | expect(cache).toBeNull(); 346 | }); 347 | 348 | it("should throw an error if the secret is not provided", async () => { 349 | const { client, testUser, cookieSetter } = await getTestInstance({ 350 | session: { 351 | cookieCache: { 352 | enabled: true, 353 | }, 354 | }, 355 | }); 356 | const headers = new Headers(); 357 | await client.signIn.email( 358 | { 359 | email: testUser.email, 360 | password: testUser.password, 361 | }, 362 | { 363 | onSuccess: cookieSetter(headers), 364 | }, 365 | ); 366 | const request = new Request("https://example.com/api/auth/session", { 367 | headers, 368 | }); 369 | await expect(getCookieCache(request)).rejects.toThrow(); 370 | }); 371 | 372 | it("should log error and skip setting cookie when data exceeds size limit", async () => { 373 | const loggerErrors: string[] = []; 374 | const mockLogger = { 375 | log: (level: string, message: string) => { 376 | if (level === "error") { 377 | loggerErrors.push(message); 378 | } 379 | }, 380 | }; 381 | 382 | const { auth } = await getTestInstance({ 383 | secret: "better-auth.secret", 384 | user: { 385 | additionalFields: { 386 | customField1: { 387 | type: "string", 388 | defaultValue: "", 389 | }, 390 | customField2: { 391 | type: "string", 392 | defaultValue: "", 393 | }, 394 | customField3: { 395 | type: "string", 396 | defaultValue: "", 397 | }, 398 | }, 399 | }, 400 | session: { 401 | cookieCache: { 402 | enabled: true, 403 | }, 404 | }, 405 | logger: mockLogger, 406 | }); 407 | 408 | // Create a very large string that will exceed the cookie size limit when combined with session data 409 | // The limit is 4093 bytes, so we create data that will definitely exceed it 410 | const largeString = "x".repeat(2000); 411 | 412 | // Sign up with large user data using the server API 413 | const result = await auth.api.signUpEmail({ 414 | body: { 415 | name: "Test User", 416 | email: "[email protected]", 417 | password: "password123", 418 | customField1: largeString, 419 | customField2: largeString, 420 | customField3: largeString, 421 | }, 422 | }); 423 | 424 | // Check that logger recorded an error about exceeding size limit 425 | const sizeError = loggerErrors.find((msg) => 426 | msg.includes("Session data exceeds cookie size limit"), 427 | ); 428 | expect(sizeError).toBeDefined(); 429 | expect(sizeError).toContain("4093 bytes"); 430 | 431 | // The sign up should still succeed 432 | expect(result).toBeDefined(); 433 | expect(result?.user).toBeDefined(); 434 | }); 435 | }); 436 | 437 | describe("Cookie Cache Field Filtering", () => { 438 | it("should exclude user fields with returned: false from cookie cache", async () => { 439 | const { client, testUser, cookieSetter } = await getTestInstance({ 440 | secret: "better-auth.secret", 441 | user: { 442 | additionalFields: { 443 | internalNote: { 444 | type: "string", 445 | defaultValue: "", 446 | returned: false, 447 | }, 448 | }, 449 | }, 450 | session: { 451 | cookieCache: { 452 | enabled: true, 453 | }, 454 | }, 455 | }); 456 | 457 | const headers = new Headers(); 458 | 459 | await client.signIn.email( 460 | { 461 | email: testUser.email, 462 | password: testUser.password, 463 | }, 464 | { 465 | onSuccess: cookieSetter(headers), 466 | }, 467 | ); 468 | 469 | const request = new Request("https://example.com/api/auth/session", { 470 | headers, 471 | }); 472 | 473 | const cache = await getCookieCache(request, { 474 | secret: "better-auth.secret", 475 | }); 476 | 477 | expect(cache).not.toBeNull(); 478 | expect(cache?.user?.email).toEqual(testUser.email); 479 | expect(cache?.user?.internalNote).toBeUndefined(); 480 | }); 481 | 482 | it("should correctly filter multiple user fields based on returned config", async () => { 483 | const { client, testUser, cookieSetter } = await getTestInstance({ 484 | secret: "better-auth.secret", 485 | user: { 486 | additionalFields: { 487 | publicBio: { 488 | type: "string", 489 | defaultValue: "default-bio", 490 | returned: true, 491 | }, 492 | internalNotes: { 493 | type: "string", 494 | defaultValue: "internal-notes", 495 | returned: false, 496 | }, 497 | preferences: { 498 | type: "string", 499 | defaultValue: "default-prefs", 500 | returned: true, 501 | }, 502 | adminFlags: { 503 | type: "string", 504 | defaultValue: "admin-flags", 505 | returned: false, 506 | }, 507 | }, 508 | }, 509 | session: { 510 | cookieCache: { 511 | enabled: true, 512 | }, 513 | }, 514 | }); 515 | 516 | const headers = new Headers(); 517 | await client.signIn.email( 518 | { 519 | email: testUser.email, 520 | password: testUser.password, 521 | }, 522 | { 523 | onSuccess: cookieSetter(headers), 524 | }, 525 | ); 526 | 527 | const request = new Request("https://example.com/api/auth/session", { 528 | headers, 529 | }); 530 | 531 | const cache = await getCookieCache(request, { 532 | secret: "better-auth.secret", 533 | }); 534 | 535 | expect(cache).not.toBeNull(); 536 | // Fields with returned: true should be included 537 | expect(cache?.user?.publicBio).toBeDefined(); 538 | expect(cache?.user?.preferences).toBeDefined(); 539 | // Fields with returned: false should be excluded 540 | expect(cache?.user?.internalNotes).toBeUndefined(); 541 | expect(cache?.user?.adminFlags).toBeUndefined(); 542 | }); 543 | 544 | it("should reduce cookie size when large fields are excluded", async () => { 545 | const largeString = "x".repeat(2000); 546 | const { client, testUser, cookieSetter } = await getTestInstance({ 547 | secret: "better-auth.secret", 548 | user: { 549 | additionalFields: { 550 | largeBio: { 551 | type: "string", 552 | defaultValue: largeString, 553 | returned: false, 554 | }, 555 | smallField: { 556 | type: "string", 557 | defaultValue: "small-value", 558 | returned: true, 559 | }, 560 | }, 561 | }, 562 | session: { 563 | cookieCache: { 564 | enabled: true, 565 | }, 566 | }, 567 | }); 568 | 569 | const headers = new Headers(); 570 | 571 | // Sign in with testUser 572 | await client.signIn.email( 573 | { 574 | email: testUser.email, 575 | password: testUser.password, 576 | }, 577 | { 578 | onSuccess: cookieSetter(headers), 579 | }, 580 | ); 581 | 582 | const request = new Request("https://example.com/api/auth/session", { 583 | headers, 584 | }); 585 | 586 | const cache = await getCookieCache(request, { 587 | secret: "better-auth.secret", 588 | }); 589 | 590 | // Cookie cache should exist (not exceed size limit) 591 | expect(cache).not.toBeNull(); 592 | // Large field should be excluded 593 | expect(cache?.user?.largeBio).toBeUndefined(); 594 | // Small field should be included 595 | expect(cache?.user?.smallField).toBeDefined(); 596 | }); 597 | 598 | it("should maintain session field filtering (regression check)", async () => { 599 | const { client, testUser, cookieSetter } = await getTestInstance({ 600 | secret: "better-auth.secret", 601 | session: { 602 | additionalFields: { 603 | internalSessionData: { 604 | type: "string", 605 | defaultValue: "internal-data", 606 | returned: false, 607 | }, 608 | publicSessionData: { 609 | type: "string", 610 | defaultValue: "public-data", 611 | returned: true, 612 | }, 613 | }, 614 | cookieCache: { 615 | enabled: true, 616 | }, 617 | }, 618 | }); 619 | 620 | const headers = new Headers(); 621 | 622 | // Sign in with testUser 623 | await client.signIn.email( 624 | { 625 | email: testUser.email, 626 | password: testUser.password, 627 | }, 628 | { 629 | onSuccess: cookieSetter(headers), 630 | }, 631 | ); 632 | 633 | const request = new Request("https://example.com/api/auth/session", { 634 | headers, 635 | }); 636 | 637 | const cache = await getCookieCache(request, { 638 | secret: "better-auth.secret", 639 | }); 640 | 641 | expect(cache).not.toBeNull(); 642 | // Verify session field filtering still works correctly 643 | // Session should have token 644 | expect(cache?.session?.token).toEqual(expect.any(String)); 645 | // Session field with returned: false should be excluded 646 | expect(cache?.session?.internalSessionData).toBeUndefined(); 647 | }); 648 | 649 | it("should include unknown user fields for backward compatibility", async () => { 650 | const { client, testUser, cookieSetter } = await getTestInstance({ 651 | secret: "better-auth.secret", 652 | user: { 653 | additionalFields: { 654 | knownField: { 655 | type: "string", 656 | defaultValue: "known-value", 657 | returned: false, 658 | }, 659 | }, 660 | }, 661 | session: { 662 | cookieCache: { 663 | enabled: true, 664 | }, 665 | }, 666 | }); 667 | 668 | const headers = new Headers(); 669 | 670 | await client.signIn.email( 671 | { 672 | email: testUser.email, 673 | password: testUser.password, 674 | }, 675 | { 676 | onSuccess: cookieSetter(headers), 677 | }, 678 | ); 679 | 680 | const request = new Request("https://example.com/api/auth/session", { 681 | headers, 682 | }); 683 | 684 | const cache = await getCookieCache(request, { 685 | secret: "better-auth.secret", 686 | }); 687 | 688 | expect(cache).not.toBeNull(); 689 | // Known field with returned: false should be excluded 690 | expect(cache?.user?.knownField).toBeUndefined(); 691 | // Standard fields like email, name should be included (backward compatibility) 692 | expect(cache?.user?.email).toEqual(testUser.email); 693 | expect(cache?.user?.name).toBeDefined(); 694 | }); 695 | }); 696 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/internal-adapter.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeAll, expect, it, describe, vi, afterEach } from "vitest"; 2 | import type { 3 | BetterAuthOptions, 4 | BetterAuthPlugin, 5 | Session, 6 | User, 7 | } from "../types"; 8 | import Database from "better-sqlite3"; 9 | import { init } from "../init"; 10 | import { betterAuth } from "../auth"; 11 | import { getMigrations } from "./get-migration"; 12 | import { Kysely, SqliteDialect } from "kysely"; 13 | import { getTestInstance } from "../test-utils/test-instance"; 14 | import { safeJSONParse } from "../utils/json"; 15 | import { runWithEndpointContext } from "@better-auth/core/context"; 16 | import type { GenericEndpointContext } from "@better-auth/core"; 17 | 18 | describe("adapter test", async () => { 19 | const sqliteDialect = new SqliteDialect({ 20 | database: new Database(":memory:"), 21 | }); 22 | const map = new Map(); 23 | const expirationMap = new Map(); 24 | let id = 1; 25 | const hookUserCreateBefore = vi.fn(); 26 | const hookUserCreateAfter = vi.fn(); 27 | const pluginHookUserCreateBefore = vi.fn(); 28 | const pluginHookUserCreateAfter = vi.fn(); 29 | const opts = { 30 | database: { 31 | dialect: sqliteDialect, 32 | type: "sqlite", 33 | }, 34 | user: { 35 | fields: { 36 | email: "email_address", 37 | emailVerified: "email_verified", 38 | }, 39 | }, 40 | secondaryStorage: { 41 | set(key, value, ttl) { 42 | map.set(key, value); 43 | expirationMap.set(key, ttl); 44 | }, 45 | get(key) { 46 | return map.get(key); 47 | }, 48 | delete(key) { 49 | map.delete(key); 50 | expirationMap.delete(key); 51 | }, 52 | }, 53 | advanced: { 54 | database: { 55 | generateId() { 56 | return (id++).toString(); 57 | }, 58 | }, 59 | }, 60 | databaseHooks: { 61 | user: { 62 | create: { 63 | async before(user, context) { 64 | hookUserCreateBefore(user, context); 65 | return { data: user }; 66 | }, 67 | async after(user, context) { 68 | hookUserCreateAfter(user, context); 69 | return; 70 | }, 71 | }, 72 | }, 73 | }, 74 | plugins: [ 75 | { 76 | id: "test-plugin", 77 | init(ctx) { 78 | return { 79 | options: { 80 | databaseHooks: { 81 | user: { 82 | create: { 83 | async before(user, context) { 84 | pluginHookUserCreateBefore(user, context); 85 | return { data: user }; 86 | }, 87 | async after(user, context) { 88 | pluginHookUserCreateAfter(user, context); 89 | }, 90 | }, 91 | }, 92 | session: { 93 | create: { 94 | before: async (session) => { 95 | return { 96 | data: { 97 | ...session, 98 | activeOrganizationId: "1", 99 | }, 100 | }; 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }; 107 | }, 108 | } satisfies BetterAuthPlugin, 109 | ], 110 | } satisfies BetterAuthOptions; 111 | beforeAll(async () => { 112 | (await getMigrations(opts)).runMigrations(); 113 | }); 114 | afterEach(async () => { 115 | vi.clearAllMocks(); 116 | map.clear(); 117 | }); 118 | const authContext = await init(opts); 119 | const ctx = { 120 | context: authContext, 121 | } as GenericEndpointContext; 122 | const internalAdapter = authContext.internalAdapter; 123 | 124 | it("should create oauth user with custom generate id", async () => { 125 | const user = await runWithEndpointContext(ctx, () => 126 | internalAdapter.createOAuthUser( 127 | { 128 | email: "[email protected]", 129 | name: "name", 130 | emailVerified: false, 131 | }, 132 | { 133 | providerId: "provider", 134 | accountId: "account", 135 | accessTokenExpiresAt: new Date(), 136 | refreshTokenExpiresAt: new Date(), 137 | createdAt: new Date(), 138 | updatedAt: new Date(), 139 | }, 140 | ), 141 | ); 142 | expect(user).toMatchObject({ 143 | user: { 144 | id: "1", 145 | name: "name", 146 | email: "[email protected]", 147 | emailVerified: false, 148 | image: null, 149 | createdAt: expect.any(Date), 150 | updatedAt: expect.any(Date), 151 | }, 152 | account: { 153 | id: "2", 154 | userId: expect.any(String), 155 | providerId: "provider", 156 | accountId: "account", 157 | accessToken: null, 158 | refreshToken: null, 159 | refreshTokenExpiresAt: expect.any(Date), 160 | accessTokenExpiresAt: expect.any(Date), 161 | }, 162 | }); 163 | expect(user?.user.id).toBe(user?.account.userId); 164 | expect(pluginHookUserCreateAfter).toHaveBeenCalledOnce(); 165 | expect(pluginHookUserCreateBefore).toHaveBeenCalledOnce(); 166 | expect(hookUserCreateAfter).toHaveBeenCalledOnce(); 167 | expect(hookUserCreateBefore).toHaveBeenCalledOnce(); 168 | }); 169 | it("should find session with custom userId", async () => { 170 | const { client, signInWithTestUser } = await getTestInstance({ 171 | session: { 172 | fields: { 173 | userId: "user_id", 174 | }, 175 | }, 176 | }); 177 | const { headers } = await signInWithTestUser(); 178 | const session = await client.getSession({ 179 | fetchOptions: { 180 | headers, 181 | }, 182 | }); 183 | expect(session.data?.session).toBeDefined(); 184 | }); 185 | 186 | it("should delete expired verification values on find", async () => { 187 | await runWithEndpointContext(ctx, () => 188 | internalAdapter.createVerificationValue({ 189 | identifier: `test-id-1`, 190 | value: "test-id-1", 191 | expiresAt: new Date(Date.now() - 1000), 192 | }), 193 | ); 194 | const value = await internalAdapter.findVerificationValue("test-id-1"); 195 | expect(value).toMatchObject({ 196 | identifier: "test-id-1", 197 | }); 198 | const value2 = await internalAdapter.findVerificationValue("test-id-1"); 199 | expect(value2).toBe(undefined); 200 | await runWithEndpointContext(ctx, () => 201 | internalAdapter.createVerificationValue({ 202 | identifier: `test-id-1`, 203 | value: "test-id-1", 204 | expiresAt: new Date(Date.now() + 1000), 205 | }), 206 | ); 207 | const value3 = await internalAdapter.findVerificationValue("test-id-1"); 208 | expect(value3).toMatchObject({ 209 | identifier: "test-id-1", 210 | }); 211 | const value4 = await internalAdapter.findVerificationValue("test-id-1"); 212 | expect(value4).toMatchObject({ 213 | identifier: "test-id-1", 214 | }); 215 | }); 216 | 217 | it("runs the after hook after adding user to db", async () => { 218 | const sampleUser = { 219 | name: "sample", 220 | email: "[email protected]", 221 | password: "sampliiiiiing", 222 | }; 223 | const hookUserCreateAfter = vi.fn(); 224 | 225 | const dialect = new SqliteDialect({ 226 | database: new Database(":memory:"), 227 | }); 228 | 229 | const db = new Kysely<any>({ 230 | dialect, 231 | }); 232 | 233 | const opts: BetterAuthOptions = { 234 | database: { 235 | dialect, 236 | type: "sqlite", 237 | }, 238 | databaseHooks: { 239 | user: { 240 | create: { 241 | async after(user, context) { 242 | hookUserCreateAfter(user, context); 243 | 244 | const userFromDb: any = await db 245 | .selectFrom("user") 246 | .selectAll() 247 | .where("id", "=", user.id) 248 | .executeTakeFirst(); 249 | 250 | expect(user.id).toBe(userFromDb.id); 251 | expect(user.name).toBe(userFromDb.name); 252 | expect(user.email).toBe(userFromDb.email); 253 | expect(user.image).toBe(userFromDb.image); 254 | expect(user.emailVerified).toBe( 255 | Boolean(userFromDb.emailVerified), 256 | ); 257 | expect(user.createdAt).toStrictEqual( 258 | new Date(userFromDb.createdAt), 259 | ); 260 | expect(user.updatedAt).toStrictEqual( 261 | new Date(userFromDb.updatedAt), 262 | ); 263 | }, 264 | }, 265 | }, 266 | }, 267 | emailAndPassword: { enabled: true }, 268 | } satisfies BetterAuthOptions; 269 | 270 | const migrations = await getMigrations(opts); 271 | await migrations.runMigrations(); 272 | 273 | const auth = betterAuth(opts); 274 | 275 | await auth.api.signUpEmail({ 276 | body: { 277 | name: sampleUser.name, 278 | email: sampleUser.email, 279 | password: sampleUser.password, 280 | }, 281 | }); 282 | 283 | expect(hookUserCreateAfter).toHaveBeenCalledOnce(); 284 | }); 285 | 286 | it("should calculate TTL correctly with Math.floor for secondary storage", async () => { 287 | const mockStorage = new Map<string, { value: string; ttl?: number }>(); 288 | const capturedTTLs: number[] = []; 289 | 290 | const testOpts = { 291 | database: { 292 | dialect: new SqliteDialect({ 293 | database: new Database(":memory:"), 294 | }), 295 | type: "sqlite", 296 | }, 297 | secondaryStorage: { 298 | set(key: string, value: string, ttl?: number) { 299 | if (ttl !== undefined) { 300 | capturedTTLs.push(ttl); 301 | mockStorage.set(key, { value, ttl }); 302 | } else { 303 | mockStorage.set(key, { value }); 304 | } 305 | }, 306 | get(key: string) { 307 | const item = mockStorage.get(key); 308 | return item?.value || null; 309 | }, 310 | delete(key: string) { 311 | mockStorage.delete(key); 312 | }, 313 | }, 314 | } satisfies BetterAuthOptions; 315 | 316 | // Run migrations for the new database 317 | (await getMigrations(testOpts)).runMigrations(); 318 | 319 | // Test the actual refreshUserSessions functionality from internal adapter 320 | const testCtx = await init(testOpts); 321 | const ctx = { 322 | context: testCtx, 323 | } as GenericEndpointContext; 324 | 325 | const testUser = { 326 | id: "test-user-id", 327 | name: "Test User", 328 | email: "[email protected]", 329 | emailVerified: true, 330 | image: null, 331 | createdAt: new Date(), 332 | updatedAt: new Date(), 333 | }; 334 | 335 | // Create a user in the database first 336 | await runWithEndpointContext(ctx, () => 337 | ctx.context.internalAdapter.createUser(testUser), 338 | ); 339 | 340 | // Test case 1: Session with fractional seconds in TTL 341 | const expiresAt = new Date(Date.now() + 3599500); // 59 minutes and 59.5 seconds from now 342 | const expectedTTL = Math.floor(3599500 / 1000); // Should be 3599 seconds (rounded down) 343 | 344 | const session = { 345 | id: "test-session-id", 346 | userId: testUser.id, 347 | token: "test-token", 348 | expiresAt, 349 | ipAddress: "127.0.0.1", 350 | userAgent: "test-agent", 351 | }; 352 | 353 | // Set up active sessions and session data for refresh test 354 | const activeSessions = [ 355 | { token: session.token, expiresAt: expiresAt.getTime() }, 356 | ]; 357 | 358 | await testCtx.options.secondaryStorage?.set( 359 | `active-sessions-${testUser.id}`, 360 | JSON.stringify(activeSessions), 361 | ); 362 | 363 | await testCtx.options.secondaryStorage?.set( 364 | session.token, 365 | JSON.stringify({ session, user: testUser }), 366 | ); 367 | 368 | // Trigger refreshUserSessions by updating the user 369 | await runWithEndpointContext(ctx, () => 370 | ctx.context.internalAdapter.updateUser(testUser.id, { 371 | name: "Updated Name", 372 | }), 373 | ); 374 | 375 | // The TTL should be properly rounded down 376 | const lastTTL = capturedTTLs[capturedTTLs.length - 1]; 377 | expect(lastTTL).toBeLessThanOrEqual(expectedTTL); 378 | expect(lastTTL).toBeGreaterThanOrEqual(expectedTTL - 1); // Allow for 1 second of test execution time 379 | 380 | // Test case 2: Very small TTL (less than 1 second should round to 0) 381 | capturedTTLs.length = 0; // Clear array 382 | const almostExpiredSession = { 383 | ...session, 384 | token: "almost-expired-token", 385 | expiresAt: new Date(Date.now() + 500), // 0.5 seconds from now 386 | }; 387 | 388 | await testCtx.options.secondaryStorage?.set( 389 | `active-sessions-${testUser.id}`, 390 | JSON.stringify([ 391 | { 392 | token: almostExpiredSession.token, 393 | expiresAt: almostExpiredSession.expiresAt.getTime(), 394 | }, 395 | ]), 396 | ); 397 | 398 | await testCtx.options.secondaryStorage?.set( 399 | almostExpiredSession.token, 400 | JSON.stringify({ session: almostExpiredSession, user: testUser }), 401 | ); 402 | 403 | await runWithEndpointContext(ctx, () => 404 | ctx.context.internalAdapter.updateUser(testUser.id, { 405 | name: "Updated Again", 406 | }), 407 | ); 408 | 409 | // Should be rounded down to 0 410 | expect(capturedTTLs.at(-1)).toBe(0); 411 | 412 | // Test case 3: Large TTL with fractional component 413 | capturedTTLs.length = 0; // Clear array 414 | const longSession = { 415 | ...session, 416 | token: "long-token", 417 | expiresAt: new Date(Date.now() + 7199999), // ~2 hours from now (7199.999 seconds) 418 | }; 419 | 420 | await testCtx.options.secondaryStorage?.set( 421 | `active-sessions-${testUser.id}`, 422 | JSON.stringify([ 423 | { 424 | token: longSession.token, 425 | expiresAt: longSession.expiresAt.getTime(), 426 | }, 427 | ]), 428 | ); 429 | 430 | await testCtx.options.secondaryStorage?.set( 431 | longSession.token, 432 | JSON.stringify({ session: longSession, user: testUser }), 433 | ); 434 | 435 | await runWithEndpointContext(ctx, () => 436 | ctx.context.internalAdapter.updateUser(testUser.id, { 437 | name: "Final Update", 438 | }), 439 | ); 440 | 441 | // Should be rounded down to 7199 442 | const finalTTL = capturedTTLs.at(-1); 443 | expect(finalTTL).toBeLessThanOrEqual(7199); 444 | expect(finalTTL).toBeGreaterThanOrEqual(7198); // Allow for test execution time 445 | }); 446 | 447 | it("should create on secondary storage", async () => { 448 | // Create session 449 | const now = Date.now(); 450 | const expiresAt = new Date(now + 60 * 60 * 24 * 7 * 1000); 451 | const user = await runWithEndpointContext(ctx, () => 452 | internalAdapter.createUser({ 453 | name: "test-user", 454 | email: "[email protected]", 455 | }), 456 | ); 457 | const session = await runWithEndpointContext(ctx, () => 458 | internalAdapter.createSession(user.id), 459 | ); 460 | const storedSessions: { token: string; expiresAt: number }[] = JSON.parse( 461 | map.get(`active-sessions-${user.id}`), 462 | ); 463 | const token = session.token; 464 | // Check stored sessions 465 | expect(storedSessions.length).toBe(1); 466 | expect(storedSessions.at(0)?.token).toBe(session.token); 467 | // Check expiration time set is the last expiration set 468 | const lastExpiration = storedSessions.reduce((prev, curr) => 469 | prev.expiresAt >= curr.expiresAt ? prev : curr, 470 | ); 471 | const actualExp = expirationMap.get(`active-sessions-${user.id}`); 472 | const expectedExp = Math.floor( 473 | (lastExpiration.expiresAt - Date.now()) / 1000, 474 | ); 475 | // max 1s clock drift between check and set 476 | expect(actualExp - expectedExp).toBeLessThanOrEqual(1); 477 | expect(actualExp - expectedExp).toBeGreaterThanOrEqual(0); 478 | 479 | const storedSession = safeJSONParse<{ 480 | session: Session; 481 | user: User; 482 | }>(map.get(token)); 483 | expect(storedSession?.user).toMatchObject(user); 484 | expect(storedSession?.session).toMatchObject({ 485 | ...session, 486 | activeOrganizationId: "1", 487 | }); 488 | const actualTokenExp = expirationMap.get(token); 489 | const expectedTokenExp = Math.floor( 490 | (expiresAt.getTime() - Date.now()) / 1000, 491 | ); 492 | // max 1s clock drift between check and set 493 | expect(actualTokenExp - expectedTokenExp).toBeLessThanOrEqual(1); 494 | expect(actualTokenExp - expectedTokenExp).toBeGreaterThanOrEqual(0); 495 | }); 496 | 497 | it("should delete on secondary storage", async () => { 498 | // Create multiple sessions in past and future 499 | const now = Date.now(); 500 | const userId = "test-user"; 501 | // 10 consecutive days (5 in past, 1 now, 4 in future) 502 | for (let i = -5; i < 5; i++) { 503 | const expiresIn = i * 60 * 60 * 24 * 1000; 504 | const expiresAt = new Date(now + expiresIn); 505 | await runWithEndpointContext(ctx, () => 506 | internalAdapter.createSession( 507 | userId, 508 | undefined, 509 | { 510 | expiresAt, 511 | }, 512 | true, 513 | ), 514 | ); 515 | if (i > 0) { 516 | const actualExp = expirationMap.get(`active-sessions-${userId}`); 517 | const expectedExp = Math.floor( 518 | (expiresAt.getTime() - Date.now()) / 1000, 519 | ); 520 | expect(actualExp - expectedExp).toBeLessThanOrEqual(1); // max 1s clock drift between check and set 521 | expect(actualExp - expectedExp).toBeGreaterThanOrEqual(0); // max 1s clock drift between check and set 522 | } else { 523 | expect(expirationMap.get(`active-sessions-${userId}`)).toBeUndefined(); 524 | } 525 | } 526 | const storedSessions: { token: string; expiresAt: number }[] = JSON.parse( 527 | map.get(`active-sessions-${userId}`), 528 | ); 529 | expect(storedSessions.length).toBe(4); 530 | const token = storedSessions.at(-1)?.token; 531 | const tokenStored = map.get(token); 532 | expect(tokenStored).toBeDefined(); 533 | 534 | // Delete session should clean expiresAt and token 535 | await runWithEndpointContext(ctx, () => 536 | internalAdapter.deleteSession(token!), 537 | ); 538 | const afterDeleted: { token: string; expiresAt: number }[] = JSON.parse( 539 | map.get(`active-sessions-${userId}`), 540 | ); 541 | expect(afterDeleted.length).toBe(3); 542 | const removedToken = map.get(token); 543 | expect(removedToken).toBeUndefined(); 544 | // Check expiration time set is the last expiration set 545 | const lastExpiration = afterDeleted.reduce((prev, curr) => 546 | prev.expiresAt >= curr.expiresAt ? prev : curr, 547 | ); 548 | const actualExp = expirationMap.get(`active-sessions-${userId}`); 549 | const expectedExp = Math.floor( 550 | (lastExpiration.expiresAt - Date.now()) / 1000, 551 | ); 552 | expect(actualExp - expectedExp).toBeLessThanOrEqual(1); // max 1s clock drift between check and set 553 | expect(actualExp - expectedExp).toBeGreaterThanOrEqual(0); // max 1s clock drift between check and set 554 | }); 555 | 556 | it("should delete a single account", async () => { 557 | const user = await runWithEndpointContext(ctx, () => 558 | internalAdapter.createUser({ 559 | name: "Account Delete User", 560 | email: "[email protected]", 561 | }), 562 | ); 563 | 564 | const account = await runWithEndpointContext(ctx, () => 565 | internalAdapter.createAccount({ 566 | userId: user.id, 567 | providerId: "test-provider", 568 | accountId: "test-account-id-1", 569 | }), 570 | ); 571 | 572 | let foundAccount = await internalAdapter.findAccount(account.accountId); 573 | expect(foundAccount).toBeDefined(); 574 | 575 | await runWithEndpointContext(ctx, () => 576 | internalAdapter.deleteAccount(account.id), 577 | ); 578 | 579 | foundAccount = await internalAdapter.findAccount(account.accountId); 580 | expect(foundAccount).toBeNull(); 581 | }); 582 | 583 | it("should delete multiple accounts for a user", async () => { 584 | const user = await runWithEndpointContext(ctx, () => 585 | internalAdapter.createUser({ 586 | name: "Accounts Delete User", 587 | email: "[email protected]", 588 | }), 589 | ); 590 | 591 | await runWithEndpointContext(ctx, () => 592 | internalAdapter.createAccount({ 593 | userId: user.id, 594 | providerId: "test-provider-1", 595 | accountId: "test-account-id-2", 596 | }), 597 | ); 598 | 599 | await runWithEndpointContext(ctx, () => 600 | internalAdapter.createAccount({ 601 | userId: user.id, 602 | providerId: "test-provider-2", 603 | accountId: "test-account-id-3", 604 | }), 605 | ); 606 | 607 | let accounts = await internalAdapter.findAccounts(user.id); 608 | expect(accounts.length).toBe(2); 609 | 610 | await runWithEndpointContext(ctx, () => 611 | internalAdapter.deleteAccounts(user.id), 612 | ); 613 | 614 | accounts = await internalAdapter.findAccounts(user.id); 615 | expect(accounts.length).toBe(0); 616 | }); 617 | }); 618 | ```