This is page 3 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 │ ├── middleware.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ └── user-additional-fields.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── sso │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sso.test.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── middleware │ │ │ │ └── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/siwe/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * SIWE Plugin Type Definitions 3 | */ 4 | 5 | export interface WalletAddress { 6 | id: string; 7 | userId: string; 8 | address: string; 9 | chainId: number; 10 | isPrimary: boolean; 11 | createdAt: Date; 12 | } 13 | 14 | interface CacaoHeader { 15 | t: "caip122"; 16 | } 17 | 18 | // Signed Cacao (CAIP-74) 19 | interface CacaoPayload { 20 | domain: string; 21 | aud: string; 22 | nonce: string; 23 | iss: string; 24 | version?: string; 25 | iat?: string; 26 | nbf?: string; 27 | exp?: string; 28 | statement?: string; 29 | requestId?: string; 30 | resources?: string[]; 31 | type?: string; 32 | } 33 | 34 | interface Cacao { 35 | h: CacaoHeader; 36 | p: CacaoPayload; 37 | s: { 38 | t: "eip191" | "eip1271"; 39 | s: string; 40 | m?: string; 41 | }; 42 | } 43 | 44 | export interface SIWEVerifyMessageArgs { 45 | message: string; 46 | signature: string; 47 | address: string; 48 | chainId: number; 49 | cacao?: Cacao; 50 | } 51 | 52 | export interface ENSLookupArgs { 53 | walletAddress: string; 54 | } 55 | 56 | export interface ENSLookupResult { 57 | name: string; 58 | avatar: string; 59 | } 60 | ``` -------------------------------------------------------------------------------- /docs/components/landing/gradient-bg.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import React from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | export function GradientBG({ 6 | children, 7 | className, 8 | ...props 9 | }: React.PropsWithChildren< 10 | { 11 | className?: string; 12 | } & React.HTMLAttributes<HTMLElement> 13 | >) { 14 | return ( 15 | <div 16 | className={cn( 17 | "relative flex content-center transition duration-500 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-full", 18 | )} 19 | {...props} 20 | > 21 | <div className={cn("w-auto z-10 px-4 py-2 rounded-none", className)}> 22 | {children} 23 | </div> 24 | <div 25 | className={cn( 26 | "flex-none inset-0 overflow-hidden absolute z-0 rounded-none bg-gradient-to-tl dark:from-amber-100/30 dark:via-zinc-900 dark:to-black blur-md opacity-50", 27 | )} 28 | /> 29 | <div className="bg-zinc-100 dark:bg-zinc-950 absolute z-1 flex-none inset-[2px] " /> 30 | </div> 31 | ); 32 | } 33 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Providers } from "./constants"; 2 | 3 | export type Provider = (typeof Providers)[keyof typeof Providers]; 4 | 5 | export interface BaseCaptchaOptions { 6 | secretKey: string; 7 | endpoints?: string[]; 8 | siteVerifyURLOverride?: string; 9 | } 10 | 11 | export interface GoogleRecaptchaOptions extends BaseCaptchaOptions { 12 | provider: typeof Providers.GOOGLE_RECAPTCHA; 13 | minScore?: number; 14 | } 15 | 16 | export interface CloudflareTurnstileOptions extends BaseCaptchaOptions { 17 | provider: typeof Providers.CLOUDFLARE_TURNSTILE; 18 | } 19 | 20 | export interface HCaptchaOptions extends BaseCaptchaOptions { 21 | provider: typeof Providers.HCAPTCHA; 22 | siteKey?: string; 23 | } 24 | 25 | export interface CaptchaFoxOptions extends BaseCaptchaOptions { 26 | provider: typeof Providers.CAPTCHAFOX; 27 | siteKey?: string; 28 | } 29 | 30 | export type CaptchaOptions = 31 | | GoogleRecaptchaOptions 32 | | CloudflareTurnstileOptions 33 | | HCaptchaOptions 34 | | CaptchaFoxOptions; 35 | ``` -------------------------------------------------------------------------------- /docs/app/v1/_components/v1-text.tsx: -------------------------------------------------------------------------------- ```typescript 1 | export const ShipText = () => { 2 | const voxels = [ 3 | // V 4 | [0, 0], 5 | [0, 1], 6 | [0, 2], 7 | [1, 3], 8 | [2, 2], 9 | [2, 1], 10 | [2, 0], 11 | // 1 12 | [4, 0], 13 | [4, 1], 14 | [4, 2], 15 | [4, 3], 16 | // . 17 | [6, 3], 18 | // 0 19 | [8, 0], 20 | [8, 1], 21 | [8, 2], 22 | [8, 3], 23 | [9, 0], 24 | [9, 3], 25 | [10, 0], 26 | [10, 1], 27 | [10, 2], 28 | [10, 3], 29 | ]; 30 | 31 | return ( 32 | <div className="flex justify-center items-center mb-0 h-[80%]"> 33 | <div className="grid grid-cols-11 gap-2"> 34 | {Array.from({ length: 44 }).map((_, index) => { 35 | const row = Math.floor(index / 11); 36 | const col = index % 11; 37 | const isActive = voxels.some(([x, y]) => x === col && y === row); 38 | return ( 39 | <div 40 | key={index} 41 | className={`w-8 h-8 ${ 42 | isActive 43 | ? "bg-gradient-to-tr from-stone-100 via-white/90 to-zinc-900" 44 | : "bg-transparent" 45 | }`} 46 | /> 47 | ); 48 | })} 49 | </div> 50 | </div> 51 | ); 52 | }; 53 | ``` -------------------------------------------------------------------------------- /packages/telemetry/src/detectors/detect-runtime.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getEnvVar, isTest } from "@better-auth/core/env"; 2 | import { isCI } from "./detect-system-info"; 3 | 4 | export function detectRuntime() { 5 | // @ts-expect-error: TS doesn't know about Deno global 6 | if (typeof Deno !== "undefined") { 7 | // @ts-expect-error: TS doesn't know about Deno global 8 | const denoVersion = Deno?.version?.deno ?? null; 9 | return { name: "deno", version: denoVersion }; 10 | } 11 | 12 | if (typeof Bun !== "undefined") { 13 | const bunVersion = Bun?.version ?? null; 14 | return { name: "bun", version: bunVersion }; 15 | } 16 | 17 | if (typeof process !== "undefined" && process?.versions?.node) { 18 | return { name: "node", version: process.versions.node ?? null }; 19 | } 20 | return { name: "edge", version: null }; 21 | } 22 | 23 | export function detectEnvironment() { 24 | return getEnvVar("NODE_ENV") === "production" 25 | ? "production" 26 | : isCI() 27 | ? "ci" 28 | : isTest() 29 | ? "test" 30 | : "development"; 31 | } 32 | ``` -------------------------------------------------------------------------------- /docs/components/endpoint.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { useState } from "react"; 4 | 5 | function Method({ method }: { method: "POST" | "GET" | "DELETE" | "PUT" }) { 6 | return ( 7 | <div className="flex items-center justify-center h-6 px-2 text-sm font-semibold uppercase border rounded-lg select-none w-fit font-display bg-background"> 8 | {method} 9 | </div> 10 | ); 11 | } 12 | 13 | export function Endpoint({ 14 | path, 15 | method, 16 | isServerOnly, 17 | className, 18 | }: { 19 | path: string; 20 | method: "POST" | "GET" | "DELETE" | "PUT"; 21 | isServerOnly?: boolean; 22 | className?: string; 23 | }) { 24 | const [copying, setCopying] = useState(false); 25 | return ( 26 | <div 27 | className={cn( 28 | "relative flex items-center w-full gap-2 p-2 border-t border-x border-border bg-fd-secondary/50 group", 29 | className, 30 | )} 31 | > 32 | <Method method={method} /> 33 | <span className="font-mono text-sm text-muted-foreground">{path}</span> 34 | </div> 35 | ); 36 | } 37 | ``` -------------------------------------------------------------------------------- /docs/components/display-techstack.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { cn } from "@/lib/utils"; 2 | import { techStackIcons } from "./techstack-icons"; 3 | import { 4 | Tooltip, 5 | TooltipContent, 6 | TooltipProvider, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | 10 | export const TechStackDisplay = ({ 11 | skills, 12 | className, 13 | }: { 14 | skills: string[]; 15 | className?: string; 16 | }) => { 17 | return ( 18 | <div 19 | className={cn( 20 | "flex gap-7 flex-wrap mt-3 justify-center items-center max-w-4xl", 21 | className, 22 | )} 23 | > 24 | {skills.map((icon) => { 25 | return ( 26 | <TooltipProvider delayDuration={50} key={icon}> 27 | <Tooltip> 28 | <TooltipTrigger asChild> 29 | <span className="transform duration-300 hover:rotate-12 transition-transform"> 30 | {techStackIcons[icon].icon} 31 | </span> 32 | </TooltipTrigger> 33 | <TooltipContent>{techStackIcons[icon].name}</TooltipContent> 34 | </Tooltip> 35 | </TooltipProvider> 36 | ); 37 | })} 38 | </div> 39 | ); 40 | }; 41 | ``` -------------------------------------------------------------------------------- /docs/components/ui/use-copy-button.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { type MouseEventHandler, useEffect, useRef, useState } from "react"; 3 | import { useEffectEvent } from "fumadocs-core/utils/use-effect-event"; 4 | 5 | export function useCopyButton( 6 | onCopy: () => void | Promise<void>, 7 | ): [checked: boolean, onClick: MouseEventHandler] { 8 | const [checked, setChecked] = useState(false); 9 | const timeoutRef = useRef<number | null>(null); 10 | 11 | const onClick: MouseEventHandler = useEffectEvent(() => { 12 | if (timeoutRef.current) window.clearTimeout(timeoutRef.current); 13 | const res = Promise.resolve(onCopy()); 14 | 15 | void res.then(() => { 16 | setChecked(true); 17 | timeoutRef.current = window.setTimeout(() => { 18 | setChecked(false); 19 | }, 1500); 20 | }); 21 | }); 22 | 23 | // Avoid updates after being unmounted 24 | useEffect(() => { 25 | return () => { 26 | if (timeoutRef.current) window.clearTimeout(timeoutRef.current); 27 | }; 28 | }, []); 29 | 30 | return [checked, onClick]; 31 | } 32 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/username/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | 3 | export const getSchema = (normalizer: { 4 | username: (username: string) => string; 5 | displayUsername: (displayUsername: string) => string; 6 | }) => { 7 | return { 8 | user: { 9 | fields: { 10 | username: { 11 | type: "string", 12 | required: false, 13 | sortable: true, 14 | unique: true, 15 | returned: true, 16 | transform: { 17 | input(value) { 18 | return typeof value !== "string" 19 | ? value 20 | : normalizer.username(value as string); 21 | }, 22 | }, 23 | }, 24 | displayUsername: { 25 | type: "string", 26 | required: false, 27 | transform: { 28 | input(value) { 29 | return typeof value !== "string" 30 | ? value 31 | : normalizer.displayUsername(value as string); 32 | }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | } satisfies BetterAuthPluginDBSchema; 38 | }; 39 | 40 | export type UsernameSchema = ReturnType<typeof getSchema>; 41 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/solid/solid-store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Store, StoreValue } from "nanostores"; 2 | import { createStore, reconcile } from "solid-js/store"; 3 | import type { Accessor } from "solid-js"; 4 | import { onCleanup } from "solid-js"; 5 | 6 | /** 7 | * Subscribes to store changes and gets store’s value. 8 | * 9 | * @param store Store instance. 10 | * @returns Store value. 11 | */ 12 | export function useStore< 13 | SomeStore extends Store, 14 | Value extends StoreValue<SomeStore>, 15 | >(store: SomeStore): Accessor<Value> { 16 | // Activate the store explicitly: 17 | // https://github.com/nanostores/solid/issues/19 18 | const unbindActivation = store.listen(() => {}); 19 | 20 | const [state, setState] = createStore({ 21 | value: store.get(), 22 | }); 23 | 24 | const unsubscribe = store.subscribe((newValue) => { 25 | setState("value", reconcile(newValue)); 26 | }); 27 | 28 | onCleanup(() => unsubscribe()); 29 | 30 | // Remove temporary listener now that there is already a proper subscriber. 31 | unbindActivation(); 32 | 33 | return () => state.value; 34 | } 35 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/vue/vue-store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Store, StoreValue } from "nanostores"; 2 | import { 3 | getCurrentInstance, 4 | getCurrentScope, 5 | onScopeDispose, 6 | readonly, 7 | shallowRef, 8 | type DeepReadonly, 9 | type ShallowRef, 10 | type UnwrapNestedRefs, 11 | } from "vue"; 12 | 13 | export function registerStore(store: Store) { 14 | let instance = getCurrentInstance(); 15 | if (instance && instance.proxy) { 16 | let vm = instance.proxy as any; 17 | let cache = "_nanostores" in vm ? vm._nanostores : (vm._nanostores = []); 18 | cache.push(store); 19 | } 20 | } 21 | 22 | export function useStore< 23 | SomeStore extends Store, 24 | Value extends StoreValue<SomeStore>, 25 | >(store: SomeStore): DeepReadonly<UnwrapNestedRefs<ShallowRef<Value>>> { 26 | let state = shallowRef(); 27 | 28 | let unsubscribe = store.subscribe((value) => { 29 | state.value = value; 30 | }); 31 | 32 | getCurrentScope() && onScopeDispose(unsubscribe); 33 | 34 | if (process.env.NODE_ENV !== "production") { 35 | registerStore(store); 36 | return readonly(state); 37 | } 38 | return state; 39 | } 40 | ``` -------------------------------------------------------------------------------- /demo/expo-example/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "skipLibCheck": true, 5 | "target": "ES2022", 6 | "lib": ["ES2022"], 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "incremental": true, 12 | "disableSourceOfProjectReferenceRedirect": true, 13 | "tsBuildInfoFile": "${configDir}/.cache/tsbuildinfo.json", 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "checkJs": false, 17 | "types": ["nativewind"], 18 | "module": "es2022", 19 | "moduleResolution": "Bundler", 20 | "noEmit": true, 21 | "jsx": "react-native", 22 | "moduleSuffixes": [".ios", ".android", ".native", ""], 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": [ 28 | "src", 29 | ".expo/types/**/*.ts", 30 | "expo-env.d.ts", 31 | "nativewind-env.d.ts" 32 | ], 33 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"], 34 | "extends": "expo/tsconfig.base" 35 | } 36 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/input.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | <input 8 | type={type} 9 | data-slot="input" 10 | className={cn( 11 | "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 12 | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 13 | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 14 | className, 15 | )} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | export { Input }; 22 | ``` -------------------------------------------------------------------------------- /docs/components/ui/input.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | <input 8 | type={type} 9 | data-slot="input" 10 | className={cn( 11 | "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", 12 | "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 13 | "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 14 | className, 15 | )} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | export { Input }; 22 | ``` -------------------------------------------------------------------------------- /packages/core/src/db/schema/account.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { coreSchema } from "./shared"; 3 | 4 | export const accountSchema = coreSchema.extend({ 5 | providerId: z.string(), 6 | accountId: z.string(), 7 | userId: z.coerce.string(), 8 | accessToken: z.string().nullish(), 9 | refreshToken: z.string().nullish(), 10 | idToken: z.string().nullish(), 11 | /** 12 | * Access token expires at 13 | */ 14 | accessTokenExpiresAt: z.date().nullish(), 15 | /** 16 | * Refresh token expires at 17 | */ 18 | refreshTokenExpiresAt: z.date().nullish(), 19 | /** 20 | * The scopes that the user has authorized 21 | */ 22 | scope: z.string().nullish(), 23 | /** 24 | * Password is only stored in the credential provider 25 | */ 26 | password: z.string().nullish(), 27 | }); 28 | 29 | /** 30 | * Account schema type used by better-auth, note that it's possible that account could have additional fields 31 | * 32 | * todo: we should use generics to extend this type with additional fields from plugins and options in the future 33 | */ 34 | export type Account = z.infer<typeof accountSchema>; 35 | ``` -------------------------------------------------------------------------------- /packages/stripe/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # @better-auth/stripe 2 | 3 | ## 1.3.4 4 | 5 | ### Patch Changes 6 | 7 | - ac6baba: chore: fix typo on `freeTrial` 8 | - c2fb1aa: Fix duplicate trials when switching plans 9 | - 2bd2fa9: Added support for listing organization members with pagination, sorting, and filtering, and improved client inference for additional organization fields. Also fixed date handling in rate limits and tokens, improved Notion OAuth user extraction, and ensured session is always set in context. 10 | 11 | Organization 12 | 13 | - Added listMembers API with pagination, sorting, and filtering. 14 | - Added membersLimit param to getFullOrganization. 15 | - Improved client inference for additional fields in organization schemas. 16 | - Bug Fixes 17 | - Fixed date handling by casting DB values to Date objects before using date methods. 18 | - Fixed Notion OAuth to extract user info correctly. 19 | - Ensured session is set in context when reading from cookie cach 20 | 21 | - Updated dependencies [2bd2fa9] 22 | - [email protected] 23 | ``` -------------------------------------------------------------------------------- /packages/telemetry/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@better-auth/telemetry", 3 | "version": "1.4.0-beta.10", 4 | "description": "Telemetry package for Better Auth", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "exports": { 9 | ".": { 10 | "import": { 11 | "types": "./dist/index.d.ts", 12 | "default": "./dist/index.js" 13 | }, 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "default": "./dist/index.cjs" 17 | } 18 | } 19 | }, 20 | "typesVersions": { 21 | "*": { 22 | "index": [ 23 | "dist/index.d.ts" 24 | ] 25 | } 26 | }, 27 | "scripts": { 28 | "build": "tsdown", 29 | "dev": "tsdown --watch", 30 | "typecheck": "tsc --project tsconfig.json" 31 | }, 32 | "devDependencies": { 33 | "@better-auth/core": "workspace:*", 34 | "tsdown": "^0.15.6", 35 | "type-fest": "^4.31.0" 36 | }, 37 | "dependencies": { 38 | "@better-auth/utils": "0.3.0", 39 | "@better-fetch/fetch": "catalog:" 40 | }, 41 | "peerDependencies": { 42 | "@better-auth/core": "workspace:*" 43 | } 44 | } 45 | ``` -------------------------------------------------------------------------------- /docs/components/landing/section.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type React from "react"; 2 | import SectionSvg from "./section-svg"; 3 | 4 | const Section = ({ 5 | className, 6 | id, 7 | crosses, 8 | crossesOffset, 9 | customPaddings, 10 | children, 11 | }: { 12 | className: string; 13 | id: string; 14 | crosses?: boolean; 15 | crossesOffset: string; 16 | customPaddings: boolean; 17 | children: React.ReactNode; 18 | }) => { 19 | return ( 20 | <div 21 | id={id} 22 | className={` 23 | relative 24 | ${customPaddings || `py-10 lg:py-16 ${crosses ? "" : ""}`} 25 | ${className || " "}`} 26 | > 27 | {children} 28 | 29 | <div className="hidden absolute top-0 left-5 w-[0.0625rem] h-[calc(100%_+_30px)] dark:bg-[#26242C] bg-stone-200 pointer-events-none lg:block lg:left-16 xl:left-16" /> 30 | <div className="hidden absolute top-0 right-5 w-[0.0625rem] h-[calc(100%_+_30px)] dark:bg-[#26242C] bg-stone-200 pointer-events-none lg:block lg:right-14 xl:right-14" /> 31 | 32 | {crosses && ( 33 | <> 34 | <SectionSvg crossesOffset={crossesOffset} /> 35 | </> 36 | )} 37 | </div> 38 | ); 39 | }; 40 | 41 | export default Section; 42 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/mongo.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: MongoDB Adapter 3 | description: Integrate Better Auth with MongoDB. 4 | --- 5 | 6 | MongoDB is a popular NoSQL database that is widely used for building scalable and flexible applications. It provides a flexible schema that allows for easy data modeling and querying. 7 | Read more here: [MongoDB](https://www.mongodb.com/). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have MongoDB installed and configured. 12 | Then, you can use the mongodb adapter. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { MongoClient } from "mongodb"; 17 | import { mongodbAdapter } from "better-auth/adapters/mongodb"; 18 | 19 | const client = new MongoClient("mongodb://localhost:27017/database"); 20 | const db = client.db(); 21 | 22 | export const auth = betterAuth({ 23 | database: mongodbAdapter(db, { 24 | // Optional: if you don't provide a client, database transactions won't be enabled. 25 | client 26 | }), 27 | }); 28 | ``` 29 | 30 | ## Schema generation & migration 31 | 32 | For MongoDB, we don't need to generate or migrate the schema. ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/input.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { TextInput, type TextInputProps } from "react-native"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef< 6 | React.ElementRef<typeof TextInput>, 7 | TextInputProps 8 | >(({ className, placeholderClassName, ...props }, ref) => { 9 | return ( 10 | <TextInput 11 | ref={ref} 12 | className={cn( 13 | "web:flex h-10 native:h-12 web:w-full rounded-md border border-input bg-background px-3 web:py-2 text-base lg:text-sm native:text-lg native:leading-[1.25] text-foreground placeholder:text-muted-foreground web:ring-offset-background file:border-0 file:bg-transparent file:font-medium web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2", 14 | props.editable === false && "opacity-50 web:cursor-not-allowed", 15 | className, 16 | )} 17 | placeholderClassName={cn("text-muted-foreground", placeholderClassName)} 18 | {...props} 19 | /> 20 | ); 21 | }); 22 | 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | ``` -------------------------------------------------------------------------------- /docs/components/techstack-icons.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { Icons } from "./icons"; 2 | type TechStackIconType = { 3 | [key: string]: { 4 | name: string; 5 | icon: any; 6 | }; 7 | }; 8 | export const techStackIcons: TechStackIconType = { 9 | nextJs: { 10 | name: "Next.js", 11 | icon: <Icons.nextJS className="w-10 h-10" />, 12 | }, 13 | nuxt: { 14 | name: "Nuxt", 15 | icon: <Icons.nuxt className="w-10 h-10" />, 16 | }, 17 | svelteKit: { 18 | name: "SvelteKit", 19 | icon: <Icons.svelteKit className="w-10 h-10" />, 20 | }, 21 | solidStart: { 22 | name: "SolidStart", 23 | icon: <Icons.solidStart className="w-10 h-10" />, 24 | }, 25 | react: { 26 | name: "React", 27 | icon: <Icons.react className="w-10 h-10" />, 28 | }, 29 | hono: { 30 | name: "Hono", 31 | icon: <Icons.hono className="w-10 h-10" />, 32 | }, 33 | astro: { 34 | name: "Astro", 35 | icon: <Icons.astro className="w-10 h-10" />, 36 | }, 37 | tanstack: { 38 | name: "TanStack Start", 39 | icon: <Icons.tanstack className="w-10 h-10" />, 40 | }, 41 | expo: { 42 | name: "Expo", 43 | icon: <Icons.expo className="w-10 h-10" />, 44 | }, 45 | nitro: { 46 | name: "Nitro", 47 | icon: <Icons.nitro className="w-10 h-10" />, 48 | }, 49 | }; 50 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/has-permission.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultRoles } from "./access"; 2 | import type { AdminOptions } from "./types"; 3 | 4 | type PermissionExclusive = 5 | | { 6 | /** 7 | * @deprecated Use `permissions` instead 8 | */ 9 | permission: { [key: string]: string[] }; 10 | permissions?: never; 11 | } 12 | | { 13 | permissions: { [key: string]: string[] }; 14 | permission?: never; 15 | }; 16 | 17 | export const hasPermission = ( 18 | input: { 19 | userId?: string; 20 | role?: string; 21 | options?: AdminOptions; 22 | } & PermissionExclusive, 23 | ) => { 24 | if (input.userId && input.options?.adminUserIds?.includes(input.userId)) { 25 | return true; 26 | } 27 | if (!input.permissions && !input.permission) { 28 | return false; 29 | } 30 | const roles = (input.role || input.options?.defaultRole || "user").split(","); 31 | const acRoles = input.options?.roles || defaultRoles; 32 | for (const role of roles) { 33 | const _role = acRoles[role as keyof typeof acRoles]; 34 | const result = _role?.authorize(input.permission ?? input.permissions); 35 | if (result?.success) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | }; 41 | ``` -------------------------------------------------------------------------------- /docs/components/ui/switch.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof SwitchPrimitive.Root>) { 12 | return ( 13 | <SwitchPrimitive.Root 14 | data-slot="switch" 15 | className={cn( 16 | "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 17 | className, 18 | )} 19 | {...props} 20 | > 21 | <SwitchPrimitive.Thumb 22 | data-slot="switch-thumb" 23 | className={cn( 24 | "bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-lg transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0", 25 | )} 26 | /> 27 | </SwitchPrimitive.Root> 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | ``` -------------------------------------------------------------------------------- /docs/components/ui/aside-link.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import type { ClassValue } from "clsx"; 3 | import Link from "next/link"; 4 | import { useSelectedLayoutSegment } from "next/navigation"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | type Props = { 9 | href: string; 10 | children: React.ReactNode; 11 | startWith: string; 12 | title?: string | null; 13 | className?: ClassValue; 14 | activeClassName?: ClassValue; 15 | } & React.AnchorHTMLAttributes<HTMLAnchorElement>; 16 | 17 | export const AsideLink = ({ 18 | href, 19 | children, 20 | startWith, 21 | title, 22 | className, 23 | activeClassName, 24 | ...props 25 | }: Props) => { 26 | const segment = useSelectedLayoutSegment(); 27 | const path = href; 28 | const isActive = path.replace("/docs/", "") === segment; 29 | 30 | return ( 31 | <Link 32 | href={href} 33 | className={cn( 34 | isActive 35 | ? cn("text-foreground bg-primary/10", activeClassName) 36 | : "text-muted-foreground hover:text-foreground hover:bg-primary/10", 37 | "w-full transition-colors flex items-center gap-x-2.5 hover:bg-primary/10 px-5 py-1", 38 | className, 39 | )} 40 | {...props} 41 | > 42 | {children} 43 | </Link> 44 | ); 45 | }; 46 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/mongodb-adapter/adapter.mongo-db.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { MongoClient, ObjectId } from "mongodb"; 2 | import { testAdapter } from "../test-adapter"; 3 | import { mongodbAdapter } from "./mongodb-adapter"; 4 | import { 5 | normalTestSuite, 6 | performanceTestSuite, 7 | authFlowTestSuite, 8 | transactionsTestSuite, 9 | } from "../tests"; 10 | 11 | const dbClient = async (connectionString: string, dbName: string) => { 12 | const client = new MongoClient(connectionString); 13 | await client.connect(); 14 | const db = client.db(dbName); 15 | return { db, client }; 16 | }; 17 | 18 | const { db, client } = await dbClient( 19 | "mongodb://127.0.0.1:27017", 20 | "better-auth", 21 | ); 22 | 23 | const { execute } = await testAdapter({ 24 | adapter: (options) => { 25 | return mongodbAdapter(db, { transaction: false }); 26 | }, 27 | runMigrations: async (betterAuthOptions) => {}, 28 | tests: [ 29 | normalTestSuite(), 30 | authFlowTestSuite(), 31 | transactionsTestSuite(), 32 | // numberIdTestSuite(), // Mongo doesn't support number ids 33 | performanceTestSuite(), 34 | ], 35 | customIdGenerator: () => new ObjectId().toString(), 36 | defaultRetryCount: 20, 37 | }); 38 | 39 | execute(); 40 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/device/denied/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Card } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { X } from "lucide-react"; 6 | import Link from "next/link"; 7 | 8 | export default function DeviceDeniedPage() { 9 | return ( 10 | <div className="flex min-h-screen items-center justify-center p-4"> 11 | <Card className="w-full max-w-md p-6"> 12 | <div className="space-y-4 text-center"> 13 | <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100"> 14 | <X className="h-6 w-6 text-red-600" /> 15 | </div> 16 | 17 | <div> 18 | <h1 className="text-2xl font-bold">Device Denied</h1> 19 | <p className="text-muted-foreground mt-2"> 20 | The device authorization request has been denied. 21 | </p> 22 | </div> 23 | 24 | <p className="text-sm text-muted-foreground"> 25 | The device will not be able to access your account. 26 | </p> 27 | 28 | <Button asChild className="w-full"> 29 | <Link href="/">Return to Home</Link> 30 | </Button> 31 | </div> 32 | </Card> 33 | </div> 34 | ); 35 | } 36 | ``` -------------------------------------------------------------------------------- /docs/components/generate-secret.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { createRandomStringGenerator } from "@better-auth/utils/random"; 3 | import { useState } from "react"; 4 | import { Button } from "./ui/button"; 5 | export const GenerateSecret = () => { 6 | const [generated, setGenerated] = useState(false); 7 | const generateRandomString = createRandomStringGenerator("a-z", "0-9", "A-Z"); 8 | return ( 9 | <div className="my-2"> 10 | <Button 11 | variant="outline" 12 | size="sm" 13 | disabled={generated} 14 | onClick={() => { 15 | const elements = document.querySelectorAll("pre code span.line span"); 16 | for (let i = 0; i < elements.length; i++) { 17 | if (elements[i].textContent === "BETTER_AUTH_SECRET=") { 18 | elements[i].textContent = 19 | `BETTER_AUTH_SECRET=${generateRandomString(32)}`; 20 | setGenerated(true); 21 | setTimeout(() => { 22 | elements[i].textContent = "BETTER_AUTH_SECRET="; 23 | setGenerated(false); 24 | }, 5000); 25 | } 26 | } 27 | }} 28 | > 29 | {generated ? "Generated" : "Generate Secret"} 30 | </Button> 31 | </div> 32 | ); 33 | }; 34 | ``` -------------------------------------------------------------------------------- /e2e/integration/solid-vinxi/e2e/test.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "@playwright/test"; 2 | import { runClient, setup } from "./utils"; 3 | 4 | const { ref, start, clean } = setup(); 5 | test.describe("solid-vinxi", async () => { 6 | test.beforeEach(async () => start()); 7 | test.afterEach(async () => clean()); 8 | 9 | test("signIn with existing email and password should work", async ({ 10 | page, 11 | }) => { 12 | await page.goto(`http://localhost:${ref.clientPort}`); 13 | await page.locator("text=Ready").waitFor(); 14 | 15 | await expect( 16 | runClient(page, ({ client }) => typeof client !== "undefined"), 17 | ).resolves.toBe(true); 18 | await expect( 19 | runClient(page, async ({ client }) => client.getSession()), 20 | ).resolves.toEqual({ data: null, error: null }); 21 | await runClient(page, ({ client }) => 22 | client.signIn.email({ 23 | email: "[email protected]", 24 | password: "password123", 25 | }), 26 | ); 27 | 28 | // Check that the session is now set 29 | const cookies = await page.context().cookies(); 30 | expect( 31 | cookies.find((c) => c.name === "better-auth.session_token"), 32 | ).toBeDefined(); 33 | }); 34 | }); 35 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/get-prisma-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { PrismaClient } from "@prisma/client"; 2 | type PC = InstanceType<typeof PrismaClient>; 3 | 4 | let migrationCount = 0; 5 | const clientMap = new Map<string, PC>(); 6 | export const getPrismaClient = async ( 7 | dialect: "sqlite" | "postgresql" | "mysql", 8 | ) => { 9 | if (clientMap.has(`${dialect}-${migrationCount}`)) { 10 | return clientMap.get(`${dialect}-${migrationCount}`) as PC; 11 | } 12 | const { PrismaClient } = await import( 13 | migrationCount === 0 14 | ? "@prisma/client" 15 | : `./.tmp/prisma-client-${dialect}-${migrationCount}` 16 | ); 17 | const db = new PrismaClient(); 18 | clientMap.set(`${dialect}-${migrationCount}`, db); 19 | return db as PC; 20 | }; 21 | 22 | export const incrementMigrationCount = () => { 23 | migrationCount++; 24 | return migrationCount; 25 | }; 26 | 27 | export const destroyPrismaClient = ({ 28 | migrationCount, 29 | dialect, 30 | }: { 31 | migrationCount: number; 32 | dialect: "sqlite" | "postgresql" | "mysql"; 33 | }) => { 34 | const db = clientMap.get(`${dialect}-${migrationCount}`); 35 | if (db) { 36 | db.$disconnect(); 37 | } 38 | clientMap.delete(`${dialect}-${migrationCount}`); 39 | }; 40 | ``` -------------------------------------------------------------------------------- /docs/lib/chat/inkeep-qa-schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | const InkeepRecordTypes = z.enum([ 4 | "documentation", 5 | "site", 6 | "discourse_post", 7 | "github_issue", 8 | "github_discussion", 9 | "stackoverflow_question", 10 | "discord_forum_post", 11 | "discord_message", 12 | "custom_question_answer", 13 | ]); 14 | 15 | const LinkType = z.union([InkeepRecordTypes, z.string()]); 16 | 17 | const LinkSchema = z.object({ 18 | label: z.string().nullish(), 19 | url: z.string(), 20 | title: z.string().nullish(), 21 | type: LinkType.nullish(), 22 | breadcrumbs: z.array(z.string()).nullish(), 23 | }); 24 | 25 | const LinksSchema = z.array(LinkSchema).nullish(); 26 | 27 | export const ProvideLinksToolSchema = z.object({ 28 | links: LinksSchema, 29 | }); 30 | 31 | const KnownAnswerConfidence = z.enum([ 32 | "very_confident", 33 | "somewhat_confident", 34 | "not_confident", 35 | "no_sources", 36 | "other", 37 | ]); 38 | 39 | const AnswerConfidence = z.union([KnownAnswerConfidence, z.string()]); // evolvable 40 | 41 | const AIAnnotationsToolSchema = z.object({ 42 | answerConfidence: AnswerConfidence, 43 | }); 44 | 45 | export const ProvideAIAnnotationsToolSchema = z.object({ 46 | aiAnnotations: AIAnnotationsToolSchema, 47 | }); 48 | ``` -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalDependencies": [ 4 | "package.json", 5 | "tsconfig.json", 6 | "pnpm-lock.yaml", 7 | "pnpm-workspace.yaml" 8 | ], 9 | "tasks": { 10 | "dev": { 11 | "cache": false, 12 | "persistent": true 13 | }, 14 | "build": { 15 | "dependsOn": ["^build"], 16 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 17 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 18 | }, 19 | "clean": {}, 20 | "format": { 21 | "dependsOn": ["//#format"] 22 | }, 23 | "//#format": {}, 24 | "lint": {}, 25 | "knip": { 26 | "cache": true 27 | }, 28 | "test": { 29 | "dependsOn": ["build"], 30 | "outputs": [] 31 | }, 32 | "e2e:smoke": { 33 | "dependsOn": ["^build"], 34 | "outputs": [] 35 | }, 36 | "e2e:integration": { 37 | "dependsOn": ["^build"], 38 | "outputs": [] 39 | }, 40 | "typecheck": { 41 | "outputs": [".tsbuildinfo", "dist/**"], 42 | "cache": true 43 | }, 44 | "deploy": { 45 | "cache": false 46 | }, 47 | "migrate": { 48 | "cache": false 49 | }, 50 | "generate": { 51 | "cache": false 52 | } 53 | } 54 | } 55 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/device/success/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Card } from "@/components/ui/card"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Check } from "lucide-react"; 6 | import Link from "next/link"; 7 | 8 | export default function DeviceSuccessPage() { 9 | return ( 10 | <div className="flex min-h-screen items-center justify-center p-4"> 11 | <Card className="w-full max-w-md p-6"> 12 | <div className="space-y-4 text-center"> 13 | <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100"> 14 | <Check className="h-6 w-6 text-green-600" /> 15 | </div> 16 | 17 | <div> 18 | <h1 className="text-2xl font-bold">Device Approved</h1> 19 | <p className="text-muted-foreground mt-2"> 20 | The device has been successfully authorized to access your 21 | account. 22 | </p> 23 | </div> 24 | 25 | <p className="text-sm text-muted-foreground"> 26 | You can now return to your device to continue. 27 | </p> 28 | 29 | <Button asChild className="w-full"> 30 | <Link href="/">Return to Home</Link> 31 | </Button> 32 | </div> 33 | </Card> 34 | </div> 35 | ); 36 | } 37 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/two-factor/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthClientPlugin } from "@better-auth/core"; 2 | import type { twoFactor as twoFa } from "../../plugins/two-factor"; 3 | 4 | export const twoFactorClient = (options?: { 5 | /** 6 | * a redirect function to call if a user needs to verify 7 | * their two factor 8 | */ 9 | onTwoFactorRedirect?: () => void | Promise<void>; 10 | }) => { 11 | return { 12 | id: "two-factor", 13 | $InferServerPlugin: {} as ReturnType<typeof twoFa>, 14 | atomListeners: [ 15 | { 16 | matcher: (path) => path.startsWith("/two-factor/"), 17 | signal: "$sessionSignal", 18 | }, 19 | ], 20 | pathMethods: { 21 | "/two-factor/disable": "POST", 22 | "/two-factor/enable": "POST", 23 | "/two-factor/send-otp": "POST", 24 | "/two-factor/generate-backup-codes": "POST", 25 | }, 26 | fetchPlugins: [ 27 | { 28 | id: "two-factor", 29 | name: "two-factor", 30 | hooks: { 31 | async onSuccess(context) { 32 | if (context.data?.twoFactorRedirect) { 33 | if (options?.onTwoFactorRedirect) { 34 | await options.onTwoFactorRedirect(); 35 | } 36 | } 37 | }, 38 | }, 39 | }, 40 | ], 41 | } satisfies BetterAuthClientPlugin; 42 | }; 43 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/avatar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof AvatarPrimitive.Root>) { 12 | return ( 13 | <AvatarPrimitive.Root 14 | data-slot="avatar" 15 | className={cn( 16 | "relative flex size-8 shrink-0 overflow-hidden rounded-full", 17 | className, 18 | )} 19 | {...props} 20 | /> 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps<typeof AvatarPrimitive.Image>) { 28 | return ( 29 | <AvatarPrimitive.Image 30 | data-slot="avatar-image" 31 | className={cn("aspect-square size-full", className)} 32 | {...props} 33 | /> 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { 41 | return ( 42 | <AvatarPrimitive.Fallback 43 | data-slot="avatar-fallback" 44 | className={cn( 45 | "bg-muted flex size-full items-center justify-center rounded-full", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | ``` -------------------------------------------------------------------------------- /docs/components/nav-link.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import Link from "next/link"; 4 | import { useSelectedLayoutSegment } from "next/navigation"; 5 | 6 | type Props = { 7 | href: string; 8 | children: React.ReactNode; 9 | className?: string; 10 | external?: boolean; 11 | }; 12 | 13 | export const NavLink = ({ href, children, className, external }: Props) => { 14 | const segment = useSelectedLayoutSegment(); 15 | const isActive = 16 | segment === href.slice(1) || (segment === null && href === "/"); 17 | 18 | return ( 19 | <li className={cn("relative group", className)}> 20 | <Link 21 | href={href} 22 | className={cn( 23 | "w-full h-full block py-4 px-5 transition-colors", 24 | "group-hover:text-foreground", 25 | isActive ? "text-foreground" : "text-muted-foreground", 26 | )} 27 | target={external ? "_blank" : "_parent"} 28 | > 29 | {children} 30 | </Link> 31 | <div 32 | className={cn( 33 | "absolute bottom-0 h-0.5 bg-muted-foreground opacity-0 transition-all duration-500", 34 | "group-hover:opacity-100 group-hover:w-full", 35 | isActive ? "opacity-100 w-full" : "w-0", 36 | )} 37 | /> 38 | </li> 39 | ); 40 | }; 41 | ``` -------------------------------------------------------------------------------- /docs/components/ui/avatar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof AvatarPrimitive.Root>) { 12 | return ( 13 | <AvatarPrimitive.Root 14 | data-slot="avatar" 15 | className={cn( 16 | "relative flex size-8 shrink-0 overflow-hidden rounded-full", 17 | className, 18 | )} 19 | {...props} 20 | /> 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps<typeof AvatarPrimitive.Image>) { 28 | return ( 29 | <AvatarPrimitive.Image 30 | data-slot="avatar-image" 31 | className={cn("aspect-square size-full", className)} 32 | {...props} 33 | /> 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { 41 | return ( 42 | <AvatarPrimitive.Fallback 43 | data-slot="avatar-fallback" 44 | className={cn( 45 | "bg-muted flex size-full items-center justify-center rounded-full", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/tier-labels.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const tierVariants = cva( 6 | "inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ring-1 ring-inset transition-all duration-300 ease-in-out", 7 | { 8 | variants: { 9 | variant: { 10 | free: "bg-gray-500 text-white ring-gray-400 hover:bg-gray-600", 11 | plus: "bg-lime-700/40 text-white ring-lime-200/40 hover:bg-lime-600", 12 | pro: "bg-purple-800/80 ring-purple-400 hover:bg-purple-700", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "free", 17 | }, 18 | }, 19 | ); 20 | 21 | export interface SubscriptionTierLabelProps 22 | extends React.HTMLAttributes<HTMLSpanElement>, 23 | VariantProps<typeof tierVariants> { 24 | tier?: "free" | "plus" | "pro"; 25 | } 26 | 27 | export const SubscriptionTierLabel: React.FC<SubscriptionTierLabelProps> = ({ 28 | tier = "free", 29 | className, 30 | ...props 31 | }) => { 32 | return ( 33 | <span className={cn(tierVariants({ variant: tier }), className)} {...props}> 34 | {tier.charAt(0).toUpperCase() + tier.slice(1)} 35 | </span> 36 | ); 37 | }; 38 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/slider.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Slider = ({ 9 | ref, 10 | className, 11 | ...props 12 | }: React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & { 13 | ref: React.RefObject<React.ElementRef<typeof SliderPrimitive.Root>>; 14 | }) => ( 15 | <SliderPrimitive.Root 16 | ref={ref} 17 | className={cn( 18 | "relative flex w-full touch-none select-none items-center", 19 | className, 20 | )} 21 | {...props} 22 | > 23 | <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"> 24 | <SliderPrimitive.Range className="absolute h-full bg-primary" /> 25 | </SliderPrimitive.Track> 26 | <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" /> 27 | </SliderPrimitive.Root> 28 | ); 29 | Slider.displayName = SliderPrimitive.Root.displayName; 30 | 31 | export { Slider }; 32 | ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/test.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { test, expect } from "@playwright/test"; 2 | import { runClient, setup } from "./utils"; 3 | 4 | const { ref, start, clean } = setup(); 5 | test.describe("vanilla-node", async () => { 6 | test.beforeEach(async () => start()); 7 | test.afterEach(async () => clean()); 8 | 9 | test("signIn with existing email and password should work", async ({ 10 | page, 11 | }) => { 12 | await page.goto( 13 | `http://localhost:${ref.clientPort}/?port=${ref.serverPort}`, 14 | ); 15 | await page.locator("text=Ready").waitFor(); 16 | 17 | await expect( 18 | runClient(page, ({ client }) => typeof client !== "undefined"), 19 | ).resolves.toBe(true); 20 | await expect( 21 | runClient(page, async ({ client }) => client.getSession()), 22 | ).resolves.toEqual({ data: null, error: null }); 23 | await runClient(page, ({ client }) => 24 | client.signIn.email({ 25 | email: "[email protected]", 26 | password: "password123", 27 | }), 28 | ); 29 | 30 | // Check that the session is now set 31 | const cookies = await page.context().cookies(); 32 | expect( 33 | cookies.find((c) => c.name === "better-auth.session_token"), 34 | ).toBeDefined(); 35 | }); 36 | }); 37 | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { base64Url } from "@better-auth/utils/base64"; 2 | import type { OAuth2Tokens } from "./oauth-provider"; 3 | 4 | export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens { 5 | const getDate = (seconds: number) => { 6 | const now = new Date(); 7 | return new Date(now.getTime() + seconds * 1000); 8 | }; 9 | 10 | return { 11 | tokenType: data.token_type, 12 | accessToken: data.access_token, 13 | refreshToken: data.refresh_token, 14 | accessTokenExpiresAt: data.expires_in 15 | ? getDate(data.expires_in) 16 | : undefined, 17 | refreshTokenExpiresAt: data.refresh_token_expires_in 18 | ? getDate(data.refresh_token_expires_in) 19 | : undefined, 20 | scopes: data?.scope 21 | ? typeof data.scope === "string" 22 | ? data.scope.split(" ") 23 | : data.scope 24 | : [], 25 | idToken: data.id_token, 26 | }; 27 | } 28 | 29 | export async function generateCodeChallenge(codeVerifier: string) { 30 | const encoder = new TextEncoder(); 31 | const data = encoder.encode(codeVerifier); 32 | const hash = await crypto.subtle.digest("SHA-256", data); 33 | return base64Url.encode(new Uint8Array(hash), { 34 | padding: false, 35 | }); 36 | } 37 | ``` -------------------------------------------------------------------------------- /docs/scripts/endpoint-to-doc/output.mdx: -------------------------------------------------------------------------------- ```markdown 1 | {/* -------------------------------------------------------- */} 2 | {/* APIMethod component */} 3 | {/* -------------------------------------------------------- */} 4 | 5 | <APIMethod 6 | path="/subscription/restore" 7 | method="POST" 8 | requireSession 9 | > 10 | ```ts 11 | type restoreSubscription = { 12 | /** 13 | * Reference id of the subscription to restore. 14 | */ 15 | referenceId?: string = '123' 16 | /** 17 | * The id of the subscription to restore. 18 | */ 19 | subscriptionId: string = 'sub_123' 20 | } 21 | ``` 22 | </APIMethod> 23 | 24 | {/* -------------------------------------------------------- */} 25 | {/* JSDOC For the endpoint */} 26 | {/* -------------------------------------------------------- */} 27 | 28 | /** 29 | * ### Endpoint 30 | * 31 | * POST `/subscription/restore` 32 | * 33 | * ### API Methods 34 | * 35 | * **server:** 36 | * `auth.api.restoreSubscription` 37 | * 38 | * **client:** 39 | * `authClient.subscription.restore` 40 | * 41 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/subscription#api-method-subscription-restore) 42 | */ ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./organization"; 2 | export * from "./two-factor"; 3 | export * from "./username"; 4 | export * from "./bearer"; 5 | export * from "../types/plugins"; 6 | export * from "../utils/hide-metadata"; 7 | export * from "./magic-link"; 8 | export * from "./phone-number"; 9 | export * from "./anonymous"; 10 | export * from "./admin"; 11 | export * from "./generic-oauth"; 12 | export * from "./jwt"; 13 | export * from "./multi-session"; 14 | export * from "./email-otp"; 15 | export * from "./one-tap"; 16 | export * from "./oauth-proxy"; 17 | export * from "./custom-session"; 18 | export * from "./open-api"; 19 | export * from "./oidc-provider"; 20 | export * from "./captcha"; 21 | export * from "./api-key"; 22 | export * from "./haveibeenpwned"; 23 | export * from "./one-time-token"; 24 | export * from "./mcp"; 25 | export * from "./siwe"; 26 | export * from "./device-authorization"; 27 | export * from "./last-login-method"; 28 | /** 29 | * @deprecated Please import from `better-auth/api` directly. 30 | */ 31 | export { 32 | createAuthEndpoint, 33 | createAuthMiddleware, 34 | optionsMiddleware, 35 | type AuthEndpoint, 36 | type AuthMiddleware, 37 | } from "@better-auth/core/middleware"; 38 | ``` -------------------------------------------------------------------------------- /demo/expo-example/app.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ConfigContext, ExpoConfig } from "expo/config"; 2 | 3 | export default ({ config }: ConfigContext): ExpoConfig => ({ 4 | ...config, 5 | name: "Better Auth", 6 | slug: "better-auth", 7 | scheme: "better-auth", 8 | version: "0.1.0", 9 | orientation: "portrait", 10 | icon: "./assets/icon.png", 11 | userInterfaceStyle: "automatic", 12 | splash: { 13 | image: "./assets/icon.png", 14 | resizeMode: "contain", 15 | backgroundColor: "#1F104A", 16 | }, 17 | web: { 18 | bundler: "metro", 19 | output: "server", 20 | }, 21 | updates: { 22 | fallbackToCacheTimeout: 0, 23 | }, 24 | assetBundlePatterns: ["**/*"], 25 | ios: { 26 | bundleIdentifier: "your.bundle.identifier", 27 | supportsTablet: true, 28 | }, 29 | android: { 30 | package: "your.bundle.identifier", 31 | adaptiveIcon: { 32 | foregroundImage: "./assets/icon.png", 33 | backgroundColor: "#1F104A", 34 | }, 35 | }, 36 | // extra: { 37 | // eas: { 38 | // projectId: "your-eas-project-id", 39 | // }, 40 | // }, 41 | experiments: { 42 | tsconfigPaths: true, 43 | typedRoutes: true, 44 | }, 45 | plugins: [ 46 | [ 47 | "expo-router", 48 | { 49 | origin: "http://localhost:8081", 50 | }, 51 | ], 52 | "expo-secure-store", 53 | "expo-font", 54 | ], 55 | }); 56 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/migrations.sql: -------------------------------------------------------------------------------- ```sql 1 | create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); 2 | 3 | create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade); 4 | 5 | create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id") on delete cascade, "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); 6 | 7 | create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date not null, "updatedAt" date not null); ``` -------------------------------------------------------------------------------- /docs/components/docs/ui/collapsible.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 3 | import { forwardRef, useEffect, useState } from "react"; 4 | import { cn } from "../../../lib/utils"; 5 | 6 | const Collapsible = CollapsiblePrimitive.Root; 7 | 8 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 9 | 10 | const CollapsibleContent = forwardRef< 11 | HTMLDivElement, 12 | React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent> 13 | >(({ children, ...props }, ref) => { 14 | const [mounted, setMounted] = useState(false); 15 | 16 | useEffect(() => { 17 | setMounted(true); 18 | }, []); 19 | 20 | return ( 21 | <CollapsiblePrimitive.CollapsibleContent 22 | ref={ref} 23 | {...props} 24 | className={cn( 25 | "overflow-hidden", 26 | mounted && 27 | "data-[state=closed]:animate-fd-collapsible-up data-[state=open]:animate-fd-collapsible-down", 28 | props.className, 29 | )} 30 | > 31 | {children} 32 | </CollapsiblePrimitive.CollapsibleContent> 33 | ); 34 | }); 35 | 36 | CollapsibleContent.displayName = 37 | CollapsiblePrimitive.CollapsibleContent.displayName; 38 | 39 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 40 | ``` -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | async rewrites() { 8 | return [ 9 | { 10 | source: "/docs/:path*.mdx", 11 | destination: "/llms.txt/:path*", 12 | }, 13 | ]; 14 | }, 15 | redirects: async () => { 16 | return [ 17 | { 18 | source: "/docs", 19 | destination: "/docs/introduction", 20 | permanent: true, 21 | }, 22 | { 23 | source: "/docs/examples", 24 | destination: "/docs/examples/next-js", 25 | permanent: true, 26 | }, 27 | ]; 28 | }, 29 | serverExternalPackages: [ 30 | "ts-morph", 31 | "typescript", 32 | "oxc-transform", 33 | "@shikijs/twoslash", 34 | ], 35 | images: { 36 | remotePatterns: [ 37 | { 38 | hostname: "images.unsplash.com", 39 | }, 40 | { 41 | hostname: "assets.aceternity.com", 42 | }, 43 | { 44 | hostname: "pbs.twimg.com", 45 | }, 46 | { 47 | hostname: "github.com", 48 | }, 49 | { 50 | hostname: "hebbkx1anhila5yf.public.blob.vercel-storage.com", 51 | }, 52 | ], 53 | }, 54 | reactStrictMode: true, 55 | typescript: { 56 | ignoreBuildErrors: true, 57 | }, 58 | experimental: { 59 | turbopackFileSystemCacheForDev: true, 60 | }, 61 | }; 62 | 63 | export default withMDX(config); 64 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | //#region Re-exports necessaries from core module 2 | export * from "@better-auth/core/env"; 3 | export * from "@better-auth/core"; 4 | export * from "@better-auth/core/oauth2"; 5 | export * from "@better-auth/core/error"; 6 | export * from "@better-auth/core/utils"; 7 | //#endregion 8 | export { getCurrentAdapter } from "@better-auth/core/context"; 9 | export * from "./auth"; 10 | export * from "./types"; 11 | export * from "./utils"; 12 | export type * from "better-call"; 13 | export type * from "zod/v4"; 14 | // @ts-expect-error we need to export core to make sure type annotations works with v4/core 15 | export type * from "zod/v4/core"; 16 | //@ts-expect-error: we need to export helper types even when they conflict with better-call types to avoid "The inferred type of 'auth' cannot be named without a reference to..." 17 | export type * from "./types/helper"; 18 | // export this as we are referencing OAuth2Tokens in the `refresh-token` api as return type 19 | 20 | // telemetry exports for CLI and consumers 21 | export { 22 | createTelemetry, 23 | getTelemetryAuthConfig, 24 | type TelemetryEvent, 25 | } from "@better-auth/telemetry"; 26 | export { APIError } from "./api"; 27 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/fixtures/cloudflare/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Hono } from "hono"; 2 | 3 | import { betterAuth } from "better-auth"; 4 | import { createDrizzle } from "./db"; 5 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 6 | 7 | interface CloudflareBindings { 8 | DB: D1Database; 9 | } 10 | 11 | const createAuth = (env: CloudflareBindings) => 12 | betterAuth({ 13 | baseURL: "http://localhost:4000", 14 | database: drizzleAdapter(createDrizzle(env.DB), { provider: "sqlite" }), 15 | emailAndPassword: { 16 | enabled: true, 17 | }, 18 | logger: { 19 | level: "debug", 20 | }, 21 | }); 22 | 23 | type Auth = ReturnType<typeof createAuth>; 24 | 25 | const app = new Hono<{ 26 | Bindings: CloudflareBindings; 27 | Variables: { 28 | auth: Auth; 29 | }; 30 | }>(); 31 | 32 | app.use("*", async (c, next) => { 33 | const auth = createAuth(c.env); 34 | c.set("auth", auth); 35 | await next(); 36 | }); 37 | 38 | app.on(["POST", "GET"], "/api/auth/*", (c) => c.var.auth.handler(c.req.raw)); 39 | 40 | app.get("/", async (c) => { 41 | const session = await c.var.auth.api.getSession({ 42 | headers: c.req.raw.headers, 43 | }); 44 | if (session) return c.text("Hello " + session.user.name); 45 | return c.text("Not logged in"); 46 | }); 47 | 48 | export default app satisfies ExportedHandler<CloudflareBindings>; 49 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/badge.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes<HTMLDivElement>, 28 | VariantProps<typeof badgeVariants> {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 | <div className={cn(badgeVariants({ variant }), className)} {...props} /> 33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | ``` -------------------------------------------------------------------------------- /docs/source.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | defineDocs, 3 | defineConfig, 4 | defineCollections, 5 | } from "fumadocs-mdx/config"; 6 | import { z } from "zod"; 7 | import { remarkAutoTypeTable, createGenerator } from "fumadocs-typescript"; 8 | import { remarkNpm } from "fumadocs-core/mdx-plugins"; 9 | 10 | export const docs = defineDocs({ 11 | dir: "./content/docs", 12 | }); 13 | 14 | export const changelogCollection = defineCollections({ 15 | type: "doc", 16 | dir: "./content/changelogs", 17 | schema: z.object({ 18 | title: z.string(), 19 | description: z.string(), 20 | date: z.date(), 21 | }), 22 | }); 23 | 24 | export const blogCollection = defineCollections({ 25 | type: "doc", 26 | dir: "./content/blogs", 27 | schema: z.object({ 28 | title: z.string(), 29 | description: z.string(), 30 | date: z.date(), 31 | author: z.object({ 32 | name: z.string(), 33 | avatar: z.string(), 34 | twitter: z.string().optional(), 35 | }), 36 | image: z.string(), 37 | tags: z.array(z.string()), 38 | }), 39 | }); 40 | 41 | const generator = createGenerator(); 42 | 43 | export default defineConfig({ 44 | mdxOptions: { 45 | remarkPlugins: [ 46 | [ 47 | remarkNpm, 48 | { 49 | persist: { 50 | id: "persist-install", 51 | }, 52 | }, 53 | ], 54 | [remarkAutoTypeTable, { generator }], 55 | ], 56 | }, 57 | }); 58 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/merger.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { clone } from "./clone"; 2 | 3 | const mergeObjects = (target: any, source: any): any => { 4 | for (const key in source) { 5 | if (!source.hasOwnProperty(key)) continue; 6 | 7 | if (key === "constructor" || key === "prototype" || key === "__proto__") 8 | continue; 9 | 10 | const value = source[key]; 11 | 12 | if (isPrimitive(value)) { 13 | if (value !== undefined || !(key in target)) { 14 | target[key] = value; 15 | } 16 | } else if (!target[key] || isArray(value)) { 17 | target[key] = clone(value); 18 | } else { 19 | target[key] = mergeObjects(target[key], value); 20 | } 21 | } 22 | 23 | return target; 24 | }; 25 | 26 | const isArray = (value: unknown): value is unknown[] => { 27 | return Array.isArray(value); 28 | }; 29 | 30 | const isPrimitive = ( 31 | value: unknown, 32 | ): value is bigint | symbol | string | number | boolean | null | undefined => { 33 | if (value === null) return true; 34 | 35 | const type = typeof value; 36 | 37 | return type !== "object" && type !== "function"; 38 | }; 39 | 40 | export const merge = (objects: object[]): object => { 41 | const target = clone(objects[0]!); 42 | 43 | for (let i = 1, l = objects.length; i < l; i++) { 44 | mergeObjects(target, objects[i]!); 45 | } 46 | 47 | return target; 48 | }; 49 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/access/statement.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createAccessControl } from "../../access"; 2 | 3 | export const defaultStatements = { 4 | organization: ["update", "delete"], 5 | member: ["create", "update", "delete"], 6 | invitation: ["create", "cancel"], 7 | team: ["create", "update", "delete"], 8 | ac: ["create", "read", "update", "delete"], 9 | } as const; 10 | 11 | export const defaultAc = createAccessControl(defaultStatements); 12 | 13 | export const adminAc = defaultAc.newRole({ 14 | organization: ["update"], 15 | invitation: ["create", "cancel"], 16 | member: ["create", "update", "delete"], 17 | team: ["create", "update", "delete"], 18 | ac: ["create", "read", "update", "delete"], 19 | }); 20 | 21 | export const ownerAc = defaultAc.newRole({ 22 | organization: ["update", "delete"], 23 | member: ["create", "update", "delete"], 24 | invitation: ["create", "cancel"], 25 | team: ["create", "update", "delete"], 26 | ac: ["create", "read", "update", "delete"], 27 | }); 28 | 29 | export const memberAc = defaultAc.newRole({ 30 | organization: [], 31 | member: [], 32 | invitation: [], 33 | team: [], 34 | ac: ["read"], // Allow members to see all roles for their org. 35 | }); 36 | 37 | export const defaultRoles = { 38 | admin: adminAc, 39 | owner: ownerAc, 40 | member: memberAc, 41 | }; 42 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/tests/number-id.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect } from "vitest"; 2 | import { createTestSuite } from "../create-test-suite"; 3 | import type { User } from "better-auth/types"; 4 | import { getNormalTestSuiteTests } from "./normal"; 5 | 6 | export const numberIdTestSuite = createTestSuite( 7 | "number-id", 8 | { 9 | defaultBetterAuthOptions: { 10 | advanced: { 11 | database: { 12 | useNumberId: true, 13 | }, 14 | }, 15 | }, 16 | alwaysMigrate: true, 17 | prefixTests: "number-id", 18 | }, 19 | (helpers) => { 20 | const { "create - should use generateId if provided": _, ...normalTests } = 21 | getNormalTestSuiteTests({ ...helpers }); 22 | 23 | return { 24 | "init - tests": async () => { 25 | const opts = helpers.getBetterAuthOptions(); 26 | expect(opts.advanced?.database?.useNumberId).toBe(true); 27 | }, 28 | "create - should return a number id": async () => { 29 | const user = await helpers.generate("user"); 30 | const res = await helpers.adapter.create<User>({ 31 | model: "user", 32 | data: user, 33 | forceAllowId: true, 34 | }); 35 | expect(res).toHaveProperty("id"); 36 | expect(typeof res.id).toBe("string"); 37 | expect(parseInt(res.id)).toBeGreaterThan(0); 38 | }, 39 | ...normalTests, 40 | }; 41 | }, 42 | ); 43 | ``` -------------------------------------------------------------------------------- /docs/components/docs/ui/popover.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 3 | import * as React from "react"; 4 | import { cn } from "../../../lib/utils"; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ComponentRef<typeof PopoverPrimitive.Content>, 12 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | <PopoverPrimitive.Portal> 15 | <PopoverPrimitive.Content 16 | ref={ref} 17 | align={align} 18 | sideOffset={sideOffset} 19 | side="bottom" 20 | className={cn( 21 | "z-50 min-w-[220px] max-w-[98vw] rounded-lg border bg-fd-popover p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[state=closed]:animate-fd-popover-out data-[state=open]:animate-fd-popover-in", 22 | className, 23 | )} 24 | {...props} 25 | /> 26 | </PopoverPrimitive.Portal> 27 | )); 28 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 29 | 30 | const PopoverClose = PopoverPrimitive.PopoverClose; 31 | 32 | export { Popover, PopoverTrigger, PopoverContent, PopoverClose }; 33 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/crypto/password.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { constantTimeEqual } from "./buffer"; 2 | import { scryptAsync } from "@noble/hashes/scrypt.js"; 3 | import { hex } from "@better-auth/utils/hex"; 4 | import { hexToBytes } from "@noble/hashes/utils.js"; 5 | import { BetterAuthError } from "@better-auth/core/error"; 6 | 7 | const config = { 8 | N: 16384, 9 | r: 16, 10 | p: 1, 11 | dkLen: 64, 12 | }; 13 | 14 | async function generateKey(password: string, salt: string) { 15 | return await scryptAsync(password.normalize("NFKC"), salt, { 16 | N: config.N, 17 | p: config.p, 18 | r: config.r, 19 | dkLen: config.dkLen, 20 | maxmem: 128 * config.N * config.r * 2, 21 | }); 22 | } 23 | 24 | export const hashPassword = async (password: string) => { 25 | const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16))); 26 | const key = await generateKey(password, salt); 27 | return `${salt}:${hex.encode(key)}`; 28 | }; 29 | 30 | export const verifyPassword = async ({ 31 | hash, 32 | password, 33 | }: { 34 | hash: string; 35 | password: string; 36 | }) => { 37 | const [salt, key] = hash.split(":"); 38 | if (!salt || !key) { 39 | throw new BetterAuthError("Invalid password hash"); 40 | } 41 | const targetKey = await generateKey(password, salt!); 42 | return constantTimeEqual(targetKey, hexToBytes(key)); 43 | }; 44 | ``` -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Publish Any Commit 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Cache turbo build setup 14 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 15 | with: 16 | path: .turbo 17 | key: ${{ runner.os }}-turbo-${{ github.sha }} 18 | restore-keys: | 19 | ${{ runner.os }}-turbo- 20 | 21 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 22 | 23 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 24 | with: 25 | node-version: 22.x 26 | registry-url: 'https://registry.npmjs.org' 27 | cache: pnpm 28 | 29 | - name: Install 30 | run: pnpm install 31 | 32 | - name: Build 33 | env: 34 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 35 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 36 | TURBO_CACHE: remote:rw 37 | run: pnpm build 38 | 39 | - run: pnpm dlx pkg-pr-new publish --pnpm ./packages/* ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/toggle.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 3 | import { cva } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const toggleVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-transparent", 13 | outline: 14 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", 15 | }, 16 | size: { 17 | default: "h-9 px-3", 18 | sm: "h-8 px-2", 19 | lg: "h-10 px-3", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | size: "default", 25 | }, 26 | }, 27 | ); 28 | 29 | const Toggle = ({ ref, className, variant, size, ...props }) => ( 30 | <TogglePrimitive.Root 31 | ref={ref} 32 | className={cn(toggleVariants({ variant, size, className }))} 33 | {...props} 34 | /> 35 | ); 36 | 37 | Toggle.displayName = TogglePrimitive.Root.displayName; 38 | 39 | export { Toggle, toggleVariants }; 40 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { 13 | return ( 14 | <CheckboxPrimitive.Root 15 | data-slot="checkbox" 16 | className={cn( 17 | "peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 18 | className, 19 | )} 20 | {...props} 21 | > 22 | <CheckboxPrimitive.Indicator 23 | data-slot="checkbox-indicator" 24 | className="flex items-center justify-center text-current transition-none" 25 | > 26 | <CheckIcon className="size-3.5" /> 27 | </CheckboxPrimitive.Indicator> 28 | </CheckboxPrimitive.Root> 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | ``` -------------------------------------------------------------------------------- /docs/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { 13 | return ( 14 | <CheckboxPrimitive.Root 15 | data-slot="checkbox" 16 | className={cn( 17 | "peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 18 | className, 19 | )} 20 | {...props} 21 | > 22 | <CheckboxPrimitive.Indicator 23 | data-slot="checkbox-indicator" 24 | className="flex items-center justify-center text-current transition-none" 25 | > 26 | <CheckIcon className="size-3.5" /> 27 | </CheckboxPrimitive.Indicator> 28 | </CheckboxPrimitive.Root> 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/device-authorization/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | import * as z from "zod"; 3 | 4 | export const schema = { 5 | deviceCode: { 6 | fields: { 7 | deviceCode: { 8 | type: "string", 9 | required: true, 10 | }, 11 | userCode: { 12 | type: "string", 13 | required: true, 14 | }, 15 | userId: { 16 | type: "string", 17 | required: false, 18 | }, 19 | expiresAt: { 20 | type: "date", 21 | required: true, 22 | }, 23 | status: { 24 | type: "string", 25 | required: true, 26 | }, 27 | lastPolledAt: { 28 | type: "date", 29 | required: false, 30 | }, 31 | pollingInterval: { 32 | type: "number", 33 | required: false, 34 | }, 35 | clientId: { 36 | type: "string", 37 | required: false, 38 | }, 39 | scope: { 40 | type: "string", 41 | required: false, 42 | }, 43 | }, 44 | }, 45 | } satisfies BetterAuthPluginDBSchema; 46 | 47 | export const deviceCode = z.object({ 48 | id: z.string(), 49 | deviceCode: z.string(), 50 | userCode: z.string(), 51 | userId: z.string().optional(), 52 | expiresAt: z.date(), 53 | status: z.string(), 54 | lastPolledAt: z.date().optional(), 55 | pollingInterval: z.number().optional(), 56 | clientId: z.string().optional(), 57 | scope: z.string().optional(), 58 | }); 59 | 60 | export type DeviceCode = z.infer<typeof deviceCode>; 61 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import "./globals.css"; 2 | import { Toaster } from "@/components/ui/sonner"; 3 | import { ThemeProvider } from "@/components/theme-provider"; 4 | import { GeistMono } from "geist/font/mono"; 5 | import { GeistSans } from "geist/font/sans"; 6 | import { Wrapper, WrapperWithQuery } from "@/components/wrapper"; 7 | import { createMetadata } from "@/lib/metadata"; 8 | 9 | export const metadata = createMetadata({ 10 | title: { 11 | template: "%s | Better Auth", 12 | default: "Better Auth", 13 | }, 14 | description: "The most comprehensive authentication library for typescript", 15 | metadataBase: new URL("https://demo.better-auth.com"), 16 | }); 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | <html lang="en" suppressHydrationWarning> 25 | <head> 26 | <link rel="icon" href="/favicon/favicon.ico" sizes="any" /> 27 | </head> 28 | <body className={`${GeistSans.variable} ${GeistMono.variable} font-sans`}> 29 | <ThemeProvider attribute="class" defaultTheme="dark"> 30 | <Wrapper> 31 | <WrapperWithQuery>{children}</WrapperWithQuery> 32 | </Wrapper> 33 | <Toaster richColors closeButton /> 34 | </ThemeProvider> 35 | </body> 36 | </html> 37 | ); 38 | } 39 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/crypto/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createHash } from "@better-auth/utils/hash"; 2 | import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; 3 | import { 4 | bytesToHex, 5 | hexToBytes, 6 | utf8ToBytes, 7 | managedNonce, 8 | } from "@noble/ciphers/utils.js"; 9 | 10 | export type SymmetricEncryptOptions = { 11 | key: string; 12 | data: string; 13 | }; 14 | 15 | export const symmetricEncrypt = async ({ 16 | key, 17 | data, 18 | }: SymmetricEncryptOptions) => { 19 | const keyAsBytes = await createHash("SHA-256").digest(key); 20 | const dataAsBytes = utf8ToBytes(data); 21 | const chacha = managedNonce(xchacha20poly1305)(new Uint8Array(keyAsBytes)); 22 | return bytesToHex(chacha.encrypt(dataAsBytes)); 23 | }; 24 | 25 | export type SymmetricDecryptOptions = { 26 | key: string; 27 | data: string; 28 | }; 29 | 30 | export const symmetricDecrypt = async ({ 31 | key, 32 | data, 33 | }: SymmetricDecryptOptions) => { 34 | const keyAsBytes = await createHash("SHA-256").digest(key); 35 | const dataAsBytes = hexToBytes(data); 36 | const chacha = managedNonce(xchacha20poly1305)(new Uint8Array(keyAsBytes)); 37 | return new TextDecoder().decode(chacha.decrypt(dataAsBytes)); 38 | }; 39 | 40 | export * from "./buffer"; 41 | export * from "./hash"; 42 | export * from "./jwt"; 43 | export * from "./password"; 44 | export * from "./random"; 45 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/get-request-ip.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthOptions } from "@better-auth/core"; 2 | import { isDevelopment, isTest } from "@better-auth/core/env"; 3 | import { z } from "zod"; 4 | 5 | export function getIp( 6 | req: Request | Headers, 7 | options: BetterAuthOptions, 8 | ): string | null { 9 | if (options.advanced?.ipAddress?.disableIpTracking) { 10 | return null; 11 | } 12 | 13 | if (isTest()) { 14 | return "127.0.0.1"; // Use a fixed IP for test environments 15 | } 16 | if (isDevelopment) { 17 | return "127.0.0.1"; // Use a fixed IP for development environments 18 | } 19 | 20 | const headers = "headers" in req ? req.headers : req; 21 | 22 | const defaultHeaders = ["x-forwarded-for"]; 23 | 24 | const ipHeaders = 25 | options.advanced?.ipAddress?.ipAddressHeaders || defaultHeaders; 26 | 27 | for (const key of ipHeaders) { 28 | const value = "get" in headers ? headers.get(key) : headers[key]; 29 | if (typeof value === "string") { 30 | const ip = value.split(",")[0]!.trim(); 31 | if (isValidIP(ip)) { 32 | return ip; 33 | } 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | function isValidIP(ip: string): boolean { 40 | const ipv4 = z.ipv4().safeParse(ip); 41 | 42 | if (ipv4.success) { 43 | return true; 44 | } 45 | 46 | const ipv6 = z.ipv6().safeParse(ip); 47 | if (ipv6.success) { 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/switch.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Switch = ({ 9 | ref, 10 | className, 11 | ...props 12 | }: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & { 13 | ref: React.RefObject<React.ElementRef<typeof SwitchPrimitives.Root>>; 14 | }) => ( 15 | <SwitchPrimitives.Root 16 | className={cn( 17 | "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", 18 | className, 19 | )} 20 | {...props} 21 | ref={ref} 22 | > 23 | <SwitchPrimitives.Thumb 24 | className={cn( 25 | "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0", 26 | )} 27 | /> 28 | </SwitchPrimitives.Root> 29 | ); 30 | Switch.displayName = SwitchPrimitives.Root.displayName; 31 | 32 | export { Switch }; 33 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/accept-invitation/[id]/invitation-error.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | Card, 3 | CardHeader, 4 | CardTitle, 5 | CardDescription, 6 | CardContent, 7 | CardFooter, 8 | } from "@/components/ui/card"; 9 | import { Button } from "@/components/ui/button"; 10 | import { AlertCircle } from "lucide-react"; 11 | import Link from "next/link"; 12 | 13 | export function InvitationError() { 14 | return ( 15 | <Card className="w-full max-w-md mx-auto"> 16 | <CardHeader> 17 | <div className="flex items-center space-x-2"> 18 | <AlertCircle className="w-6 h-6 text-destructive" /> 19 | <CardTitle className="text-xl text-destructive"> 20 | Invitation Error 21 | </CardTitle> 22 | </div> 23 | <CardDescription> 24 | There was an issue with your invitation. 25 | </CardDescription> 26 | </CardHeader> 27 | <CardContent> 28 | <p className="mb-4 text-sm text-muted-foreground"> 29 | The invitation you're trying to access is either invalid or you don't 30 | have the correct permissions. Please check your email for a valid 31 | invitation or contact the person who sent it. 32 | </p> 33 | </CardContent> 34 | <CardFooter> 35 | <Link href="/" className="w-full"> 36 | <Button variant="outline" className="w-full"> 37 | Go back to home 38 | </Button> 39 | </Link> 40 | </CardFooter> 41 | </Card> 42 | ); 43 | } 44 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/oauth/authorize/concet-buttons.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { CardFooter } from "@/components/ui/card"; 5 | import { client } from "@/lib/auth-client"; 6 | import { Loader2 } from "lucide-react"; 7 | import { useState } from "react"; 8 | import { toast } from "sonner"; 9 | 10 | export function ConsentBtns() { 11 | const [loading, setLoading] = useState(false); 12 | return ( 13 | <CardFooter className="flex items-center gap-2"> 14 | <Button 15 | onClick={async () => { 16 | setLoading(true); 17 | const res = await client.oauth2.consent({ 18 | accept: true, 19 | }); 20 | setLoading(false); 21 | if (res.data?.redirectURI) { 22 | window.location.href = res.data.redirectURI; 23 | return; 24 | } 25 | toast.error("Failed to authorize"); 26 | }} 27 | > 28 | {loading ? <Loader2 size={15} className="animate-spin" /> : "Authorize"} 29 | </Button> 30 | <Button 31 | variant="outline" 32 | onClick={async () => { 33 | const res = await client.oauth2.consent({ 34 | accept: false, 35 | }); 36 | if (res.data?.redirectURI) { 37 | window.location.href = res.data.redirectURI; 38 | return; 39 | } 40 | toast.error("Failed to cancel"); 41 | }} 42 | > 43 | Cancel 44 | </Button> 45 | </CardFooter> 46 | ); 47 | } 48 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/plugins/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "../../plugins/organization/client"; 2 | export * from "../../plugins/username/client"; 3 | export * from "../../plugins/passkey/client"; 4 | export * from "../../plugins/two-factor/client"; 5 | export * from "../../plugins/magic-link/client"; 6 | export * from "../../plugins/phone-number/client"; 7 | export * from "../../plugins/anonymous/client"; 8 | export * from "../../plugins/additional-fields/client"; 9 | export * from "../../plugins/admin/client"; 10 | export * from "../../plugins/generic-oauth/client"; 11 | export * from "../../plugins/jwt/client"; 12 | export * from "../../plugins/multi-session/client"; 13 | export * from "../../plugins/email-otp/client"; 14 | export * from "../../plugins/one-tap/client"; 15 | export * from "../../plugins/custom-session/client"; 16 | export * from "./infer-plugin"; 17 | export * from "../../plugins/sso/client"; 18 | export * from "../../plugins/oidc-provider/client"; 19 | export * from "../../plugins/api-key/client"; 20 | export * from "../../plugins/one-time-token/client"; 21 | export * from "../../plugins/siwe/client"; 22 | export * from "../../plugins/device-authorization/client"; 23 | export type * from "@simplewebauthn/server"; 24 | export * from "../../plugins/last-login-method/client"; 25 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-out.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createAuthEndpoint } from "@better-auth/core/middleware"; 2 | import { deleteSessionCookie } from "../../cookies"; 3 | import { APIError } from "better-call"; 4 | import { BASE_ERROR_CODES } from "@better-auth/core/error"; 5 | 6 | export const signOut = createAuthEndpoint( 7 | "/sign-out", 8 | { 9 | method: "POST", 10 | requireHeaders: true, 11 | metadata: { 12 | openapi: { 13 | description: "Sign out the current user", 14 | responses: { 15 | "200": { 16 | description: "Success", 17 | content: { 18 | "application/json": { 19 | schema: { 20 | type: "object", 21 | properties: { 22 | success: { 23 | type: "boolean", 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | async (ctx) => { 35 | const sessionCookieToken = await ctx.getSignedCookie( 36 | ctx.context.authCookies.sessionToken.name, 37 | ctx.context.secret, 38 | ); 39 | if (!sessionCookieToken) { 40 | deleteSessionCookie(ctx); 41 | throw new APIError("BAD_REQUEST", { 42 | message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION, 43 | }); 44 | } 45 | await ctx.context.internalAdapter.deleteSession(sessionCookieToken); 46 | deleteSessionCookie(ctx); 47 | return ctx.json({ 48 | success: true, 49 | }); 50 | }, 51 | ); 52 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/get-schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getAuthTables } from "."; 2 | import type { BetterAuthOptions } from "@better-auth/core"; 3 | import type { DBFieldAttribute } from "@better-auth/core/db"; 4 | 5 | export function getSchema(config: BetterAuthOptions) { 6 | const tables = getAuthTables(config); 7 | let schema: Record< 8 | string, 9 | { 10 | fields: Record<string, DBFieldAttribute>; 11 | order: number; 12 | } 13 | > = {}; 14 | for (const key in tables) { 15 | const table = tables[key]!; 16 | const fields = table.fields; 17 | let actualFields: Record<string, DBFieldAttribute> = {}; 18 | Object.entries(fields).forEach(([key, field]) => { 19 | actualFields[field.fieldName || key] = field; 20 | if (field.references) { 21 | const refTable = tables[field.references.model]; 22 | if (refTable) { 23 | actualFields[field.fieldName || key]!.references = { 24 | ...field.references, 25 | model: refTable.modelName, 26 | field: field.references.field, 27 | }; 28 | } 29 | } 30 | }); 31 | if (schema[table.modelName]) { 32 | schema[table.modelName]!.fields = { 33 | ...schema[table.modelName]!.fields, 34 | ...actualFields, 35 | }; 36 | continue; 37 | } 38 | schema[table.modelName] = { 39 | fields: actualFields, 40 | order: table.order || Infinity, 41 | }; 42 | } 43 | return schema; 44 | } 45 | ``` -------------------------------------------------------------------------------- /docs/components/docs/docs.client.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Menu, X } from "lucide-react"; 4 | import { type ButtonHTMLAttributes, type HTMLAttributes } from "react"; 5 | import { cn } from "../../lib/utils"; 6 | import { buttonVariants } from "./ui/button"; 7 | import { useSidebar } from "fumadocs-ui/provider"; 8 | import { useNav } from "./layout/nav"; 9 | import { SidebarTrigger } from "fumadocs-core/sidebar"; 10 | 11 | export function Navbar(props: HTMLAttributes<HTMLElement>) { 12 | const { open } = useSidebar(); 13 | const { isTransparent } = useNav(); 14 | 15 | return ( 16 | <header 17 | id="nd-subnav" 18 | {...props} 19 | className={cn( 20 | "sticky top-(--fd-banner-height) z-30 flex h-14 flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors", 21 | (!isTransparent || open) && "bg-fd-background/80", 22 | props.className, 23 | )} 24 | > 25 | {props.children} 26 | </header> 27 | ); 28 | } 29 | 30 | export function NavbarSidebarTrigger( 31 | props: ButtonHTMLAttributes<HTMLButtonElement>, 32 | ) { 33 | const { open } = useSidebar(); 34 | 35 | return ( 36 | <SidebarTrigger 37 | {...props} 38 | className={cn( 39 | buttonVariants({ 40 | color: "ghost", 41 | size: "icon", 42 | }), 43 | props.className, 44 | )} 45 | > 46 | {open ? <X /> : <Menu />} 47 | </SidebarTrigger> 48 | ); 49 | } 50 | ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/nuxt.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Nuxt Example 3 | description: Better Auth Nuxt example. 4 | --- 5 | 6 | This is an example of how to use Better Auth with Nuxt. 7 | 8 | **Implements the following features:** 9 | Email & Password . Social Sign-in with Google 10 | 11 | <ForkButton url="better-auth/better-auth/tree/main/examples/nuxt-example" /> 12 | 13 | 14 | <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/nuxt-example?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" 15 | style={{ 16 | width: "100%", 17 | height: "500px", 18 | border: 0, 19 | borderRadius: "4px", 20 | overflow: "hidden" 21 | }} 22 | title="Better Auth Nuxt Example" 23 | allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" 24 | sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" 25 | > 26 | </iframe> 27 | 28 | 29 | ## How to run 30 | 31 | 1. Clone the code sandbox (or the repo) and open it in your code editor 32 | 2. Move .env.example to .env and provide necessary variables 33 | 3. Run the following commands 34 | ```bash 35 | pnpm install 36 | pnpm dev 37 | ``` 38 | 4. Open the browser and navigate to `http://localhost:3000` 39 | ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/postgres-js.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterAuth } from "better-auth"; 2 | import { nextCookies } from "better-auth/next-js"; 3 | import { PostgresJSDialect } from "kysely-postgres-js"; 4 | import postgres from "postgres"; 5 | import { getMigrations } from "better-auth/db"; 6 | import { expect, test } from "@playwright/test"; 7 | 8 | test.describe("postgres-js", async () => { 9 | test("run migration", async () => { 10 | const sql = postgres( 11 | process.env.DATABASE_URL || 12 | "postgres://user:password@localhost:5432/better_auth", 13 | ); 14 | const dialect = new PostgresJSDialect({ 15 | postgres: sql, 16 | }); 17 | const auth = betterAuth({ 18 | database: { 19 | dialect, 20 | type: "postgres", 21 | transaction: false, 22 | }, 23 | emailAndPassword: { 24 | enabled: true, 25 | }, 26 | plugins: [nextCookies()], 27 | baseURL: "http://localhost:3000", 28 | }); 29 | 30 | const { runMigrations } = await getMigrations(auth.options); 31 | await runMigrations(); 32 | const allTables = await sql` 33 | SELECT table_name 34 | FROM information_schema.tables 35 | WHERE table_schema='public' 36 | AND table_type='BASE TABLE'; 37 | `; 38 | const tableNames = allTables.map((row) => row.table_name); 39 | expect(tableNames).toEqual(["user", "session", "account", "verification"]); 40 | }); 41 | }); 42 | ``` -------------------------------------------------------------------------------- /packages/core/src/async_hooks/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * AsyncLocalStorage will be import directly in 1.5.x 3 | */ 4 | import type { AsyncLocalStorage } from "node:async_hooks"; 5 | 6 | // We only export the type here to avoid issues in environments where AsyncLocalStorage is not available. 7 | export type { AsyncLocalStorage }; 8 | 9 | const AsyncLocalStoragePromise: Promise<typeof AsyncLocalStorage> = import( 10 | /* @vite-ignore */ 11 | /* webpackIgnore: true */ 12 | "node:async_hooks" 13 | ) 14 | .then((mod) => mod.AsyncLocalStorage) 15 | .catch((err) => { 16 | if ("AsyncLocalStorage" in globalThis) { 17 | return (globalThis as any).AsyncLocalStorage; 18 | } 19 | console.warn( 20 | "[better-auth] Warning: AsyncLocalStorage is not available in this environment. Some features may not work as expected.", 21 | ); 22 | console.warn( 23 | "[better-auth] Please read more about this warning at https://better-auth.com/docs/installation#mount-handler", 24 | ); 25 | console.warn( 26 | "[better-auth] If you are using Cloudflare Workers, please see: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag", 27 | ); 28 | throw err; 29 | }); 30 | 31 | export async function getAsyncLocalStorage(): Promise< 32 | typeof AsyncLocalStorage 33 | > { 34 | return AsyncLocalStoragePromise; 35 | } 36 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createAdapterFactory, 3 | type AdapterFactory, 4 | type AdapterFactoryOptions, 5 | type AdapterTestDebugLogs, 6 | type AdapterFactoryConfig, 7 | type CustomAdapter, 8 | type AdapterFactoryCustomizeAdapterCreator, 9 | } from "./adapter-factory"; 10 | 11 | export * from "@better-auth/core/db/adapter"; 12 | 13 | export type { 14 | AdapterFactoryOptions, 15 | AdapterFactory, 16 | AdapterTestDebugLogs, 17 | AdapterFactoryConfig, 18 | CustomAdapter, 19 | AdapterFactoryCustomizeAdapterCreator, 20 | }; 21 | 22 | export { createAdapterFactory }; 23 | 24 | /** 25 | * @deprecated Use `createAdapterFactory` instead. This export will be removed in the next major version. 26 | */ 27 | export const createAdapter = createAdapterFactory; 28 | 29 | /** 30 | * @deprecated Use `AdapterFactoryOptions` instead. This export will be removed in the next major version. 31 | */ 32 | export type CreateAdapterOptions = AdapterFactoryOptions; 33 | 34 | /** 35 | * @deprecated Use `AdapterFactoryConfig` instead. This export will be removed in the next major version. 36 | */ 37 | export type AdapterConfig = AdapterFactoryConfig; 38 | 39 | /** 40 | * @deprecated Use `AdapterFactoryCustomizeAdapterCreator` instead. This export will be removed in the next major version. 41 | */ 42 | export type CreateCustomAdapter = AdapterFactoryCustomizeAdapterCreator; 43 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import SignIn from "@/components/sign-in"; 4 | import { SignUp } from "@/components/sign-up"; 5 | import { Tabs } from "@/components/ui/tabs2"; 6 | import { client } from "@/lib/auth-client"; 7 | import { useRouter, useSearchParams } from "next/navigation"; 8 | import { useEffect } from "react"; 9 | import { toast } from "sonner"; 10 | import { getCallbackURL } from "@/lib/shared"; 11 | 12 | export default function Page() { 13 | const router = useRouter(); 14 | const params = useSearchParams(); 15 | useEffect(() => { 16 | client.oneTap({ 17 | fetchOptions: { 18 | onError: ({ error }) => { 19 | toast.error(error.message || "An error occurred"); 20 | }, 21 | onSuccess: () => { 22 | toast.success("Successfully signed in"); 23 | router.push(getCallbackURL(params)); 24 | }, 25 | }, 26 | }); 27 | }, []); 28 | 29 | return ( 30 | <div className="w-full"> 31 | <div className="flex items-center flex-col justify-center w-full md:py-10"> 32 | <div className="md:w-[400px]"> 33 | <Tabs 34 | tabs={[ 35 | { 36 | title: "Sign In", 37 | value: "sign-in", 38 | content: <SignIn />, 39 | }, 40 | { 41 | title: "Sign Up", 42 | value: "sign-up", 43 | content: <SignUp />, 44 | }, 45 | ]} 46 | /> 47 | </div> 48 | </div> 49 | </div> 50 | ); 51 | } 52 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { middlewareResponse } from "../../../utils/middleware-response"; 3 | import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes"; 4 | 5 | type Params = { 6 | siteVerifyURL: string; 7 | secretKey: string; 8 | captchaResponse: string; 9 | remoteIP?: string; 10 | }; 11 | 12 | type SiteVerifyResponse = { 13 | success: boolean; 14 | "error-codes"?: string[]; 15 | challenge_ts?: string; 16 | hostname?: string; 17 | action?: string; 18 | cdata?: string; 19 | metadata?: { 20 | interactive: boolean; 21 | }; 22 | messages?: string[]; 23 | }; 24 | 25 | export const cloudflareTurnstile = async ({ 26 | siteVerifyURL, 27 | captchaResponse, 28 | secretKey, 29 | remoteIP, 30 | }: Params) => { 31 | const response = await betterFetch<SiteVerifyResponse>(siteVerifyURL, { 32 | method: "POST", 33 | headers: { "Content-Type": "application/json" }, 34 | body: JSON.stringify({ 35 | secret: secretKey, 36 | response: captchaResponse, 37 | ...(remoteIP && { remoteip: remoteIP }), 38 | }), 39 | }); 40 | 41 | if (!response.data || response.error) { 42 | throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE); 43 | } 44 | 45 | if (!response.data.success) { 46 | return middlewareResponse({ 47 | message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED, 48 | status: 403, 49 | }); 50 | } 51 | 52 | return undefined; 53 | }; 54 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/cloudflare.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it } from "node:test"; 2 | import { spawn } from "node:child_process"; 3 | import { fileURLToPath } from "node:url"; 4 | import { join } from "node:path"; 5 | import assert from "node:assert/strict"; 6 | 7 | const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); 8 | 9 | describe("(cloudflare) simple server", () => { 10 | it("check repo", async (t) => { 11 | const cp = spawn("npm", ["run", "check"], { 12 | cwd: join(fixturesDir, "cloudflare"), 13 | stdio: "pipe", 14 | }); 15 | 16 | t.after(() => { 17 | cp.kill("SIGINT"); 18 | }); 19 | 20 | const unexpectedStrings = new Set(["node:sqlite"]); 21 | 22 | cp.stdout.on("data", (data) => { 23 | console.log(data.toString()); 24 | for (const str of unexpectedStrings) { 25 | assert( 26 | !data.toString().includes(str), 27 | `Output should not contain "${str}"`, 28 | ); 29 | } 30 | }); 31 | 32 | cp.stderr.on("data", (data) => { 33 | console.error(data.toString()); 34 | for (const str of unexpectedStrings) { 35 | assert( 36 | !data.toString().includes(str), 37 | `Error output should not contain "${str}"`, 38 | ); 39 | } 40 | }); 41 | 42 | await new Promise<void>((resolve) => { 43 | cp.stdout.on("data", (data) => { 44 | if (data.toString().includes("exiting now.")) { 45 | resolve(); 46 | } 47 | }); 48 | }); 49 | }); 50 | }); 51 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const HoverCard = HoverCardPrimitive.Root; 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 11 | 12 | const HoverCardContent = ({ 13 | ref, 14 | className, 15 | align = "center", 16 | sideOffset = 4, 17 | ...props 18 | }: React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & { 19 | ref: React.RefObject<React.ElementRef<typeof HoverCardPrimitive.Content>>; 20 | }) => ( 21 | <HoverCardPrimitive.Content 22 | ref={ref} 23 | align={align} 24 | sideOffset={sideOffset} 25 | className={cn( 26 | "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 27 | className, 28 | )} 29 | {...props} 30 | /> 31 | ); 32 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 33 | 34 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 35 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as AvatarPrimitive from "@rn-primitives/avatar"; 2 | import * as React from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Avatar = React.forwardRef< 6 | AvatarPrimitive.RootRef, 7 | AvatarPrimitive.RootProps 8 | >(({ className, ...props }, ref) => ( 9 | <AvatarPrimitive.Root 10 | ref={ref} 11 | className={cn( 12 | "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", 13 | className, 14 | )} 15 | {...props} 16 | /> 17 | )); 18 | Avatar.displayName = AvatarPrimitive.Root.displayName; 19 | 20 | const AvatarImage = React.forwardRef< 21 | AvatarPrimitive.ImageRef, 22 | AvatarPrimitive.ImageProps 23 | >(({ className, ...props }, ref) => ( 24 | <AvatarPrimitive.Image 25 | ref={ref} 26 | className={cn("aspect-square h-full w-full", className)} 27 | {...props} 28 | /> 29 | )); 30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 31 | 32 | const AvatarFallback = React.forwardRef< 33 | AvatarPrimitive.FallbackRef, 34 | AvatarPrimitive.FallbackProps 35 | >(({ className, ...props }, ref) => ( 36 | <AvatarPrimitive.Fallback 37 | ref={ref} 38 | className={cn( 39 | "flex h-full w-full items-center justify-center rounded-full bg-muted", 40 | className, 41 | )} 42 | {...props} 43 | /> 44 | )); 45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 46 | 47 | export { Avatar, AvatarFallback, AvatarImage }; 48 | ``` -------------------------------------------------------------------------------- /demo/nextjs/lib/auth-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createAuthClient } from "better-auth/react"; 2 | import { 3 | organizationClient, 4 | passkeyClient, 5 | twoFactorClient, 6 | adminClient, 7 | multiSessionClient, 8 | oneTapClient, 9 | oidcClient, 10 | genericOAuthClient, 11 | deviceAuthorizationClient, 12 | lastLoginMethodClient, 13 | } from "better-auth/client/plugins"; 14 | import { toast } from "sonner"; 15 | import { stripeClient } from "@better-auth/stripe/client"; 16 | 17 | export const client = createAuthClient({ 18 | plugins: [ 19 | organizationClient(), 20 | twoFactorClient({ 21 | onTwoFactorRedirect() { 22 | window.location.href = "/two-factor"; 23 | }, 24 | }), 25 | passkeyClient(), 26 | adminClient(), 27 | multiSessionClient(), 28 | oneTapClient({ 29 | clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, 30 | promptOptions: { 31 | maxAttempts: 1, 32 | }, 33 | }), 34 | oidcClient(), 35 | genericOAuthClient(), 36 | stripeClient({ 37 | subscription: true, 38 | }), 39 | deviceAuthorizationClient(), 40 | lastLoginMethodClient(), 41 | ], 42 | fetchOptions: { 43 | onError(e) { 44 | if (e.error.status === 429) { 45 | toast.error("Too many requests. Please try again later."); 46 | } 47 | }, 48 | }, 49 | }); 50 | 51 | export const { 52 | signUp, 53 | signIn, 54 | signOut, 55 | useSession, 56 | organization, 57 | useListOrganizations, 58 | useActiveOrganization, 59 | useActiveMember, 60 | useActiveMemberRole, 61 | } = client; 62 | ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/domain.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { chromium, expect, test } from "@playwright/test"; 2 | import { runClient, setup } from "./utils"; 3 | 4 | const { ref, start, clean } = setup(); 5 | test.describe("cross domain", async () => { 6 | test.beforeEach(async () => start()); 7 | test.afterEach(async () => clean()); 8 | 9 | test("should work across domains", async () => { 10 | const browser = await chromium.launch({ 11 | args: [`--host-resolver-rules=MAP * localhost`], 12 | }); 13 | 14 | const page = await browser.newPage(); 15 | 16 | await page.goto( 17 | `http://test.com:${ref.clientPort}/?port=${ref.serverPort}`, 18 | ); 19 | await page.locator("text=Ready").waitFor(); 20 | 21 | await expect( 22 | runClient(page, ({ client }) => typeof client !== "undefined"), 23 | ).resolves.toBe(true); 24 | await expect( 25 | runClient(page, async ({ client }) => client.getSession()), 26 | ).resolves.toEqual({ data: null, error: null }); 27 | await runClient(page, ({ client }) => 28 | client.signIn.email({ 29 | email: "[email protected]", 30 | password: "password123", 31 | }), 32 | ); 33 | 34 | // Check that the session is not set because of we didn't set the cookie domain correctly 35 | const cookies = await page.context().cookies(); 36 | expect( 37 | cookies.find((c) => c.name === "better-auth.session_token"), 38 | ).not.toBeDefined(); 39 | }); 40 | }); 41 | ```