This is page 5 of 68. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── sso │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sso.test.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/core/src/env/logger.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from "vitest"; 2 | import { shouldPublishLog, type LogLevel } from "./logger"; 3 | 4 | describe("shouldPublishLog", () => { 5 | const testCases: { 6 | currentLogLevel: LogLevel; 7 | logLevel: LogLevel; 8 | expected: boolean; 9 | }[] = [ 10 | { currentLogLevel: "info", logLevel: "info", expected: true }, 11 | { currentLogLevel: "info", logLevel: "warn", expected: false }, 12 | { currentLogLevel: "info", logLevel: "error", expected: false }, 13 | { currentLogLevel: "info", logLevel: "debug", expected: false }, 14 | { currentLogLevel: "warn", logLevel: "info", expected: true }, 15 | { currentLogLevel: "warn", logLevel: "warn", expected: true }, 16 | { currentLogLevel: "warn", logLevel: "error", expected: false }, 17 | { currentLogLevel: "warn", logLevel: "debug", expected: false }, 18 | { currentLogLevel: "error", logLevel: "info", expected: true }, 19 | { currentLogLevel: "error", logLevel: "warn", expected: true }, 20 | { currentLogLevel: "error", logLevel: "error", expected: true }, 21 | { currentLogLevel: "error", logLevel: "debug", expected: false }, 22 | { currentLogLevel: "debug", logLevel: "info", expected: true }, 23 | { currentLogLevel: "debug", logLevel: "warn", expected: true }, 24 | { currentLogLevel: "debug", logLevel: "error", expected: true }, 25 | { currentLogLevel: "debug", logLevel: "debug", expected: true }, 26 | ]; 27 | 28 | testCases.forEach(({ currentLogLevel, logLevel, expected }) => { 29 | it(`should return "${expected}" when currentLogLevel is "${currentLogLevel}" and logLevel is "${logLevel}"`, () => { 30 | expect(shouldPublishLog(currentLogLevel, logLevel)).toBe(expected); 31 | }); 32 | }); 33 | }); 34 | ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/astro.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Astro Example 3 | description: Better Auth Astro example. 4 | --- 5 | 6 | This is an example of how to use Better Auth with Astro. It uses Solid for building the components. 7 | 8 | 9 | **Implements the following features:** 10 | Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management 11 | 12 | <ForkButton url="better-auth/examples/tree/main/astro-example" /> 13 | 14 | <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/astro-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 Astro+Solid 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. Provide .env file with the following variables 33 | ```txt 34 | GOOGLE_CLIENT_ID= 35 | GOOGLE_CLIENT_SECRET= 36 | BETTER_AUTH_SECRET= 37 | ``` 38 | //if you don't have these, you can get them from the google developer console. If you don't want to use google sign-in, you can remove the google config from the `auth.ts` file. 39 | 40 | 3. Run the following commands 41 | ```bash 42 | pnpm install 43 | pnpm run dev 44 | ``` 45 | 4. Open the browser and navigate to `http://localhost:3000` 46 | ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/next-js.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Next.js Example 3 | description: Better Auth Next.js example. 4 | --- 5 | 6 | This is an example of how to use Better Auth with Next. 7 | 8 | **Implements the following features:** 9 | Email & Password . Social Sign-in . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management . Organization, Members and Roles 10 | 11 | See [Demo](https://demo.better-auth.com) 12 | 13 | <ForkButton url="better-auth/better-auth/tree/main/demo/nextjs" /> 14 | 15 | <iframe src="https://stackblitz.com/github/better-auth/better-auth/tree/main/demo/nextjs?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" 16 | style={{ 17 | width: "100%", 18 | height: "500px", 19 | border: 0, 20 | borderRadius: "4px", 21 | overflow: "hidden" 22 | }} 23 | title="Better Auth Next.js Example" 24 | allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" 25 | sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" 26 | > 27 | </iframe> 28 | 29 | 30 | ## How to run 31 | 32 | 1. Clone the code sandbox (or the repo) and open it in your code editor 33 | 2. Move .env.example to .env and provide necessary variables 34 | 3. Run the following commands 35 | ```bash 36 | pnpm install 37 | pnpm dev 38 | ``` 39 | 4. Open the browser and navigate to `http://localhost:3000` 40 | 41 | ### SSO Login Example 42 | 43 | For this example, we utilize DummyIDP. Initiate the login from the [DummyIDP login](https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/login), click "Proceed", and from here it will direct you to user's dashboard. 44 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/password-input.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { EyeIcon, EyeOffIcon } from "lucide-react"; 4 | import * as React from "react"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Input } from "@/components/ui/input"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | const PasswordInput = ({ 11 | ref, 12 | className, 13 | ...props 14 | }: any & { 15 | ref: React.RefObject<HTMLInputElement>; 16 | }) => { 17 | const [showPassword, setShowPassword] = React.useState(false); 18 | const disabled = 19 | props.value === "" || props.value === undefined || props.disabled; 20 | 21 | return ( 22 | <div className="relative"> 23 | <Input 24 | {...props} 25 | type={showPassword ? "text" : "password"} 26 | name="password_fake" 27 | className={cn("hide-password-toggle pr-10", className)} 28 | ref={ref} 29 | /> 30 | <Button 31 | type="button" 32 | variant="ghost" 33 | size="sm" 34 | className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent" 35 | onClick={() => setShowPassword((prev) => !prev)} 36 | disabled={disabled} 37 | > 38 | {showPassword && !disabled ? ( 39 | <EyeIcon className="h-4 w-4" aria-hidden="true" /> 40 | ) : ( 41 | <EyeOffIcon className="h-4 w-4" aria-hidden="true" /> 42 | )} 43 | <span className="sr-only"> 44 | {showPassword ? "Hide password" : "Show password"} 45 | </span> 46 | </Button> 47 | 48 | {/* hides browsers password toggles */} 49 | <style>{` 50 | .hide-password-toggle::-ms-reveal, 51 | .hide-password-toggle::-ms-clear { 52 | visibility: hidden; 53 | pointer-events: none; 54 | display: none; 55 | } 56 | `}</style> 57 | </div> 58 | ); 59 | }; 60 | PasswordInput.displayName = "PasswordInput"; 61 | 62 | export { PasswordInput }; 63 | ``` -------------------------------------------------------------------------------- /e2e/integration/solid-vinxi/e2e/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Page } from "@playwright/test"; 2 | import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; 3 | import { fileURLToPath } from "node:url"; 4 | import { terminate } from "@better-auth/test-utils/playwright"; 5 | 6 | const root = fileURLToPath(new URL("../", import.meta.url)); 7 | 8 | export async function runClient<R>( 9 | page: Page, 10 | fn: ({ client }: { client: Window["client"] }) => R, 11 | ): Promise<R> { 12 | const client = await page.evaluateHandle<Window["client"]>("window.client"); 13 | return page.evaluate(fn, { client }); 14 | } 15 | 16 | export function setup() { 17 | let clientChild: ChildProcessWithoutNullStreams; 18 | const ref: { 19 | clientPort: number; 20 | } = { 21 | clientPort: -1, 22 | }; 23 | return { 24 | ref, 25 | start: async () => { 26 | clientChild = spawn("pnpm", ["run", "dev"], { 27 | cwd: root, 28 | stdio: "pipe", 29 | env: { 30 | ...process.env, 31 | NO_COLOR: "1", 32 | }, 33 | }); 34 | clientChild.stderr.on("data", (data) => { 35 | const message = data.toString(); 36 | console.error(message); 37 | }); 38 | clientChild.stdout.on("data", (data) => { 39 | const message = data.toString(); 40 | console.log(message); 41 | }); 42 | 43 | await Promise.all([ 44 | new Promise<void>((resolve) => { 45 | clientChild.stdout.on("data", (data) => { 46 | const message = data.toString(); 47 | // find: http://localhost:XXXX/ for vinxi dev server 48 | if (message.includes("http://localhost:")) { 49 | const match = message.match(/http:\/\/localhost:(\d+)/); 50 | if (match) { 51 | ref.clientPort = Number(match[1]); 52 | resolve(); 53 | } 54 | } 55 | }); 56 | }), 57 | ]); 58 | }, 59 | clean: async () => { 60 | await terminate(clientChild.pid!); 61 | }, 62 | }; 63 | } 64 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = ({ 9 | ref, 10 | className, 11 | children, 12 | ...props 13 | }: React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & { 14 | ref: React.RefObject<React.ElementRef<typeof ScrollAreaPrimitive.Root>>; 15 | }) => ( 16 | <ScrollAreaPrimitive.Root 17 | ref={ref} 18 | className={cn("relative overflow-hidden", className)} 19 | {...props} 20 | > 21 | <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> 22 | {children} 23 | </ScrollAreaPrimitive.Viewport> 24 | <ScrollBar /> 25 | <ScrollAreaPrimitive.Corner /> 26 | </ScrollAreaPrimitive.Root> 27 | ); 28 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 29 | 30 | const ScrollBar = ({ 31 | ref, 32 | className, 33 | orientation = "vertical", 34 | ...props 35 | }: React.ComponentPropsWithoutRef< 36 | typeof ScrollAreaPrimitive.ScrollAreaScrollbar 37 | > & { 38 | ref: React.RefObject< 39 | React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> 40 | >; 41 | }) => ( 42 | <ScrollAreaPrimitive.ScrollAreaScrollbar 43 | ref={ref} 44 | orientation={orientation} 45 | className={cn( 46 | "flex touch-none select-none transition-colors", 47 | orientation === "vertical" && 48 | "h-full w-2.5 border-l border-l-transparent p-px", 49 | orientation === "horizontal" && 50 | "h-2.5 flex-col border-t border-t-transparent p-px", 51 | className, 52 | )} 53 | {...props} 54 | > 55 | <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> 56 | </ScrollAreaPrimitive.ScrollAreaScrollbar> 57 | ); 58 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 59 | 60 | export { ScrollArea, ScrollBar }; 61 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/client.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterAuth } from "../../auth"; 2 | import { createAuthClient } from "../../client"; 3 | import { inferOrgAdditionalFields, organizationClient } from "./client"; 4 | import { organization } from "./organization"; 5 | import { describe, it } from "vitest"; 6 | 7 | describe("organization", () => { 8 | const auth = betterAuth({ 9 | plugins: [ 10 | organization({ 11 | schema: { 12 | organization: { 13 | additionalFields: { 14 | newField: { 15 | type: "string", 16 | }, 17 | }, 18 | }, 19 | }, 20 | }), 21 | ], 22 | }); 23 | 24 | it("should infer additional fields", async () => { 25 | const client = createAuthClient({ 26 | plugins: [ 27 | organizationClient({ 28 | schema: inferOrgAdditionalFields<typeof auth>(), 29 | }), 30 | ], 31 | fetchOptions: { 32 | customFetchImpl: async () => new Response(), 33 | }, 34 | }); 35 | client.organization.create({ 36 | name: "Test", 37 | slug: "test", 38 | newField: "123", //this should be allowed 39 | //@ts-expect-error - this field is not available 40 | unavalibleField: "123", //this should be not allowed 41 | }); 42 | }); 43 | 44 | it("should infer filed when schema is provided", () => { 45 | const client = createAuthClient({ 46 | plugins: [ 47 | organizationClient({ 48 | schema: inferOrgAdditionalFields({ 49 | organization: { 50 | additionalFields: { 51 | newField: { 52 | type: "string", 53 | }, 54 | }, 55 | }, 56 | }), 57 | }), 58 | ], 59 | fetchOptions: { 60 | customFetchImpl: async () => new Response(), 61 | }, 62 | }); 63 | 64 | client.organization.create({ 65 | name: "Test", 66 | slug: "test", 67 | newField: "123", //this should be allowed 68 | //@ts-expect-error - this field is not available 69 | unavalibleField: "123", //this should be not allowed 70 | }); 71 | }); 72 | }); 73 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/vite.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it } from "node:test"; 2 | import { fileURLToPath } from "node:url"; 3 | import { join } from "node:path"; 4 | import { spawn } from "node:child_process"; 5 | import { readFile } from "node:fs/promises"; 6 | import * as assert from "node:assert"; 7 | 8 | const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); 9 | 10 | describe("(vite) client build", () => { 11 | it("builds client without better-call imports", async () => { 12 | const viteDir = join(fixturesDir, "vite"); 13 | 14 | // Run vite build 15 | const buildProcess = spawn("npx", ["vite", "build"], { 16 | cwd: viteDir, 17 | stdio: "pipe", 18 | }); 19 | 20 | // Wait for build to complete 21 | await new Promise<void>((resolve, reject) => { 22 | buildProcess.on("close", (code) => { 23 | if (code === 0) { 24 | resolve(); 25 | } else { 26 | reject(new Error(`Vite build failed with code ${code}`)); 27 | } 28 | }); 29 | 30 | buildProcess.on("error", (error) => { 31 | reject(error); 32 | }); 33 | 34 | // Log build output for debugging 35 | buildProcess.stdout.on("data", (data) => { 36 | console.log(data.toString()); 37 | }); 38 | 39 | buildProcess.stderr.on("data", (data) => { 40 | console.error(data.toString()); 41 | }); 42 | }); 43 | 44 | const clientFile = join(viteDir, "dist", "client.js"); 45 | const clientContent = await readFile(clientFile, "utf-8"); 46 | 47 | assert.ok( 48 | !clientContent.includes("createEndpoint"), 49 | "Built output should not contain 'better-call' imports", 50 | ); 51 | 52 | assert.ok( 53 | !clientContent.includes("async_hooks"), 54 | "Built output should not contain 'async_hooks' imports", 55 | ); 56 | 57 | assert.ok( 58 | !clientContent.includes("AsyncLocalStorage"), 59 | "Built output should not contain 'AsyncLocalStorage' imports", 60 | ); 61 | }); 62 | }); 63 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.pg.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { testAdapter } from "../../test-adapter"; 2 | import { 3 | authFlowTestSuite, 4 | normalTestSuite, 5 | numberIdTestSuite, 6 | performanceTestSuite, 7 | transactionsTestSuite, 8 | } from "../../tests"; 9 | import { prismaAdapter } from "../prisma-adapter"; 10 | import { generateAuthConfigFile } from "./generate-auth-config"; 11 | import { generatePrismaSchema } from "./generate-prisma-schema"; 12 | import { pushPrismaSchema } from "./push-prisma-schema"; 13 | import type { BetterAuthOptions } from "@better-auth/core"; 14 | import { 15 | destroyPrismaClient, 16 | getPrismaClient, 17 | incrementMigrationCount, 18 | } from "./get-prisma-client"; 19 | import { Pool } from "pg"; 20 | 21 | const dialect = "postgresql"; 22 | const { execute } = await testAdapter({ 23 | adapter: async () => { 24 | const db = await getPrismaClient(dialect); 25 | return prismaAdapter(db, { 26 | provider: dialect, 27 | debugLogs: { isRunningAdapterTests: true }, 28 | }); 29 | }, 30 | runMigrations: async (options: BetterAuthOptions) => { 31 | const db = await getPrismaClient(dialect); 32 | const pgDB = new Pool({ 33 | connectionString: "postgres://user:password@localhost:5434/better_auth", 34 | }); 35 | await pgDB.query(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`); 36 | await pgDB.end(); 37 | const migrationCount = incrementMigrationCount(); 38 | await generateAuthConfigFile(options); 39 | await generatePrismaSchema(options, db, migrationCount, dialect); 40 | await pushPrismaSchema(dialect); 41 | destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); 42 | }, 43 | tests: [ 44 | normalTestSuite(), 45 | transactionsTestSuite(), 46 | authFlowTestSuite(), 47 | numberIdTestSuite(), 48 | performanceTestSuite({ dialect }), 49 | ], 50 | onFinish: async () => {}, 51 | prefixTests: "pg", 52 | }); 53 | 54 | execute(); 55 | ``` -------------------------------------------------------------------------------- /packages/stripe/src/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | import type { StripeOptions } from "./types"; 3 | import { mergeSchema } from "better-auth/db"; 4 | 5 | export const subscriptions = { 6 | subscription: { 7 | fields: { 8 | plan: { 9 | type: "string", 10 | required: true, 11 | }, 12 | referenceId: { 13 | type: "string", 14 | required: true, 15 | }, 16 | stripeCustomerId: { 17 | type: "string", 18 | required: false, 19 | }, 20 | stripeSubscriptionId: { 21 | type: "string", 22 | required: false, 23 | }, 24 | status: { 25 | type: "string", 26 | defaultValue: "incomplete", 27 | }, 28 | periodStart: { 29 | type: "date", 30 | required: false, 31 | }, 32 | periodEnd: { 33 | type: "date", 34 | required: false, 35 | }, 36 | trialStart: { 37 | type: "date", 38 | required: false, 39 | }, 40 | trialEnd: { 41 | type: "date", 42 | required: false, 43 | }, 44 | cancelAtPeriodEnd: { 45 | type: "boolean", 46 | required: false, 47 | defaultValue: false, 48 | }, 49 | seats: { 50 | type: "number", 51 | required: false, 52 | }, 53 | }, 54 | }, 55 | } satisfies BetterAuthPluginDBSchema; 56 | 57 | export const user = { 58 | user: { 59 | fields: { 60 | stripeCustomerId: { 61 | type: "string", 62 | required: false, 63 | }, 64 | }, 65 | }, 66 | } satisfies BetterAuthPluginDBSchema; 67 | 68 | export const getSchema = (options: StripeOptions) => { 69 | let baseSchema = {}; 70 | 71 | if (options.subscription?.enabled) { 72 | baseSchema = { 73 | ...subscriptions, 74 | ...user, 75 | }; 76 | } else { 77 | baseSchema = { 78 | ...user, 79 | }; 80 | } 81 | 82 | if ( 83 | options.schema && 84 | !options.subscription?.enabled && 85 | "subscription" in options.schema 86 | ) { 87 | const { subscription, ...restSchema } = options.schema; 88 | return mergeSchema(baseSchema, restSchema); 89 | } 90 | 91 | return mergeSchema(baseSchema, options.schema); 92 | }; 93 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.sqlite.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { testAdapter } from "../../test-adapter"; 2 | import { 3 | authFlowTestSuite, 4 | normalTestSuite, 5 | numberIdTestSuite, 6 | performanceTestSuite, 7 | transactionsTestSuite, 8 | } from "../../tests"; 9 | import { prismaAdapter } from "../prisma-adapter"; 10 | import { generateAuthConfigFile } from "./generate-auth-config"; 11 | import { generatePrismaSchema } from "./generate-prisma-schema"; 12 | import { pushPrismaSchema } from "./push-prisma-schema"; 13 | import type { BetterAuthOptions } from "@better-auth/core"; 14 | import { join } from "path"; 15 | import fs from "node:fs/promises"; 16 | import { 17 | destroyPrismaClient, 18 | getPrismaClient, 19 | incrementMigrationCount, 20 | } from "./get-prisma-client"; 21 | 22 | const dialect = "sqlite"; 23 | const { execute } = await testAdapter({ 24 | adapter: async () => { 25 | const db = await getPrismaClient(dialect); 26 | return prismaAdapter(db, { 27 | provider: dialect, 28 | debugLogs: { isRunningAdapterTests: true }, 29 | }); 30 | }, 31 | runMigrations: async (options: BetterAuthOptions) => { 32 | const dbPath = join(import.meta.dirname, "dev.db"); 33 | try { 34 | await fs.unlink(dbPath); 35 | } catch { 36 | console.log("db path not found"); 37 | } 38 | const db = await getPrismaClient(dialect); 39 | const migrationCount = incrementMigrationCount(); 40 | await generateAuthConfigFile(options); 41 | await generatePrismaSchema(options, db, migrationCount, dialect); 42 | await pushPrismaSchema(dialect); 43 | await db.$disconnect(); 44 | destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); 45 | }, 46 | tests: [ 47 | normalTestSuite({}), 48 | transactionsTestSuite(), 49 | authFlowTestSuite(), 50 | numberIdTestSuite({}), 51 | performanceTestSuite({ dialect }), 52 | ], 53 | onFinish: async () => {}, 54 | prefixTests: dialect, 55 | }); 56 | 57 | execute(); 58 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/generate-prisma-schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { PrismaClient } from "@prisma/client"; 2 | import type { BetterAuthOptions } from "@better-auth/core"; 3 | import type { DBAdapter } from "@better-auth/core/db/adapter"; 4 | import { prismaAdapter } from "../prisma-adapter"; 5 | import { join } from "path"; 6 | import fs from "fs/promises"; 7 | 8 | export async function generatePrismaSchema( 9 | betterAuthOptions: BetterAuthOptions, 10 | db: PrismaClient, 11 | iteration: number, 12 | dialect: "sqlite" | "postgresql" | "mysql", 13 | ) { 14 | const i = async (x: string) => await import(x); 15 | const { generateSchema } = (await i( 16 | "./../../../../../cli/src/generators/index", 17 | )) as { 18 | generateSchema: (opts: { 19 | adapter: DBAdapter<BetterAuthOptions>; 20 | file?: string; 21 | options: BetterAuthOptions; 22 | }) => Promise<{ 23 | code: string | undefined; 24 | fileName: string; 25 | overwrite: boolean | undefined; 26 | }>; 27 | }; 28 | 29 | const prismaDB = prismaAdapter(db, { provider: dialect }); 30 | let { fileName, code } = await generateSchema({ 31 | file: join(import.meta.dirname, `schema-${dialect}.prisma`), 32 | adapter: prismaDB({}), 33 | options: { ...betterAuthOptions, database: prismaDB }, 34 | }); 35 | if (dialect === "postgresql") { 36 | code = code?.replace( 37 | `env("DATABASE_URL")`, 38 | '"postgres://user:password@localhost:5434/better_auth"', 39 | ); 40 | } else if (dialect === "mysql") { 41 | code = code?.replace( 42 | `env("DATABASE_URL")`, 43 | '"mysql://user:password@localhost:3308/better_auth"', 44 | ); 45 | } 46 | code = code 47 | ?.split("\n") 48 | .map((line, index) => { 49 | if (index === 2) { 50 | return ( 51 | line + `\n output = "./.tmp/prisma-client-${dialect}-${iteration}"` 52 | ); 53 | } 54 | return line; 55 | }) 56 | .join("\n"); 57 | await fs.writeFile(fileName, code || "", "utf-8"); 58 | } 59 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.mysql.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Kysely, MysqlDialect } from "kysely"; 2 | import { testAdapter } from "../../test-adapter"; 3 | import { kyselyAdapter } from "../kysely-adapter"; 4 | import { createPool } from "mysql2/promise"; 5 | import { 6 | authFlowTestSuite, 7 | normalTestSuite, 8 | numberIdTestSuite, 9 | performanceTestSuite, 10 | transactionsTestSuite, 11 | } from "../../tests"; 12 | import { getMigrations } from "../../../db"; 13 | import { assert } from "vitest"; 14 | 15 | const mysqlDB = createPool({ 16 | uri: "mysql://user:password@localhost:3307/better_auth", 17 | timezone: "Z", 18 | }); 19 | 20 | let kyselyDB = new Kysely({ 21 | dialect: new MysqlDialect(mysqlDB), 22 | }); 23 | 24 | const { execute } = await testAdapter({ 25 | adapter: () => 26 | kyselyAdapter(kyselyDB, { 27 | type: "mysql", 28 | debugLogs: { isRunningAdapterTests: true }, 29 | }), 30 | async runMigrations(betterAuthOptions) { 31 | await mysqlDB.query("DROP DATABASE IF EXISTS better_auth"); 32 | await mysqlDB.query("CREATE DATABASE better_auth"); 33 | await mysqlDB.query("USE better_auth"); 34 | const opts = Object.assign(betterAuthOptions, { database: mysqlDB }); 35 | const { runMigrations } = await getMigrations(opts); 36 | await runMigrations(); 37 | 38 | // ensure migrations were run successfully 39 | const [tables_result] = (await mysqlDB.query("SHOW TABLES")) as unknown as [ 40 | { Tables_in_better_auth: string }[], 41 | ]; 42 | const tables = tables_result.map((table) => table.Tables_in_better_auth); 43 | assert(tables.length > 0, "No tables found"); 44 | }, 45 | prefixTests: "mysql", 46 | tests: [ 47 | normalTestSuite(), 48 | transactionsTestSuite({ disableTests: { ALL: true } }), 49 | authFlowTestSuite(), 50 | numberIdTestSuite(), 51 | performanceTestSuite({ dialect: "mysql" }), 52 | ], 53 | async onFinish() { 54 | await mysqlDB.end(); 55 | }, 56 | }); 57 | execute(); 58 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes<HTMLButtonElement>, 39 | VariantProps<typeof buttonVariants> { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = ({ 44 | className, 45 | variant, 46 | size, 47 | asChild = false, 48 | ...props 49 | }: ButtonProps) => { 50 | const Comp = asChild ? Slot : "button"; 51 | return ( 52 | <Comp 53 | className={cn(buttonVariants({ variant, size, className }))} 54 | {...props} 55 | /> 56 | ); 57 | }; 58 | Button.displayName = "Button"; 59 | 60 | export { Button, buttonVariants }; 61 | ``` -------------------------------------------------------------------------------- /docs/lib/blog.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { readFile, readdir } from "fs/promises"; 2 | import matter from "gray-matter"; 3 | import { join } from "path"; 4 | import { cache } from "react"; 5 | 6 | export interface BlogPost { 7 | _id: string; 8 | slug: string; 9 | title: string; 10 | description?: string; 11 | date: string; 12 | content: string; 13 | image?: string; 14 | author?: { 15 | name: string; 16 | avatar?: string; 17 | twitter?: string; 18 | }; 19 | tags?: string[]; 20 | } 21 | 22 | const BLOGS_PATH = join(process.cwd(), "docs/content/blogs"); 23 | 24 | export const getBlogPost = cache( 25 | async (slug: string): Promise<BlogPost | null> => { 26 | try { 27 | const filePath = join(BLOGS_PATH, `${slug}.mdx`); 28 | const source = await readFile(filePath, "utf-8"); 29 | const { data, content } = matter(source); 30 | 31 | return { 32 | _id: slug, 33 | slug, 34 | content, 35 | title: data.title, 36 | description: data.description, 37 | date: data.date, 38 | image: data.image, 39 | author: data.author, 40 | tags: data.tags, 41 | }; 42 | } catch (error) { 43 | return null; 44 | } 45 | }, 46 | ); 47 | 48 | export const getAllBlogPosts = cache(async (): Promise<BlogPost[]> => { 49 | try { 50 | const files = await readdir(BLOGS_PATH); 51 | const mdxFiles = files.filter((file) => file.endsWith(".mdx")); 52 | 53 | const posts = await Promise.all( 54 | mdxFiles.map(async (file) => { 55 | const slug = file.replace(/\.mdx$/, ""); 56 | const post = await getBlogPost(slug); 57 | return post; 58 | }), 59 | ); 60 | 61 | return posts 62 | .filter((post): post is BlogPost => post !== null) 63 | .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); 64 | } catch (error) { 65 | return []; 66 | } 67 | }); 68 | 69 | export function formatBlogDate(date: Date) { 70 | let d = new Date(date); 71 | return d.toLocaleDateString("en-US", { 72 | month: "short", 73 | day: "numeric", 74 | year: "numeric", 75 | }); 76 | } 77 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: Is this suited for github? 7 | description: Feel free to join the discord community [here](https://discord.gg/better-auth), we can usually respond faster to any questions. 8 | options: 9 | - label: Yes, this is suited for github 10 | - type: markdown 11 | attributes: 12 | value: | 13 | This template is used for suggesting a feature with better-auth. 14 | 15 | Bug reports should be opened in [here](https://github.com/better-auth/better-auth/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml). 16 | 17 | Before opening a feature request, please do a [search](https://github.com/better-auth/better-auth/issues) of existing issues and :+1: upvote the existing request instead. This will result in a quicker resolution. 18 | - type: textarea 19 | attributes: 20 | label: Is your feature request related to a problem? Please describe. 21 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 22 | - type: textarea 23 | attributes: 24 | label: Describe the solution you'd like 25 | description: A clear and concise description of what you want to happen. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you've considered 31 | description: A clear and concise description of any alternative solutions or features you've considered. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional context 37 | description: Add any other context or screenshots about the feature request here. 38 | ``` -------------------------------------------------------------------------------- /docs/components/docs/docs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { PageTree } from "fumadocs-core/server"; 2 | import { type ReactNode, type HTMLAttributes } from "react"; 3 | import { cn } from "../../lib/utils"; 4 | import { type BaseLayoutProps } from "./shared"; 5 | import { TreeContextProvider } from "fumadocs-ui/provider"; 6 | import { NavProvider } from "./layout/nav"; 7 | import { type PageStyles, StylesProvider } from "fumadocs-ui/provider"; 8 | import ArticleLayout from "../side-bar"; 9 | 10 | export interface DocsLayoutProps extends BaseLayoutProps { 11 | tree: PageTree.Root; 12 | 13 | containerProps?: HTMLAttributes<HTMLDivElement>; 14 | } 15 | 16 | export function DocsLayout({ children, ...props }: DocsLayoutProps): ReactNode { 17 | const variables = cn( 18 | "[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] lg:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]", 19 | ); 20 | 21 | const pageStyles: PageStyles = { 22 | tocNav: cn("xl:hidden"), 23 | toc: cn("max-xl:hidden"), 24 | }; 25 | 26 | return ( 27 | <TreeContextProvider tree={props.tree}> 28 | <NavProvider> 29 | <main 30 | id="nd-docs-layout" 31 | {...props.containerProps} 32 | className={cn( 33 | "flex flex-1 flex-row pe-(--fd-layout-offset)", 34 | variables, 35 | props.containerProps?.className, 36 | )} 37 | style={ 38 | { 39 | "--fd-layout-offset": 40 | "max(calc(50vw - var(--fd-layout-width) / 2), 0px)", 41 | ...props.containerProps?.style, 42 | } as object 43 | } 44 | > 45 | <div 46 | className={cn( 47 | "[--fd-tocnav-height:36px] md:mr-[268px] lg:mr-[286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px] ", 48 | )} 49 | > 50 | <ArticleLayout /> 51 | </div> 52 | <StylesProvider {...pageStyles}>{children}</StylesProvider> 53 | </main> 54 | </NavProvider> 55 | </TreeContextProvider> 56 | ); 57 | } 58 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/helper.ts: -------------------------------------------------------------------------------- ```typescript 1 | export type Primitive = 2 | | string 3 | | number 4 | | symbol 5 | | bigint 6 | | boolean 7 | | null 8 | | undefined; 9 | export type LiteralString = "" | (string & Record<never, never>); 10 | export type LiteralNumber = 0 | (number & Record<never, never>); 11 | 12 | export type Awaitable<T> = Promise<T> | T; 13 | export type OmitId<T extends { id: unknown }> = Omit<T, "id">; 14 | 15 | export type Prettify<T> = Omit<T, never>; 16 | export type PreserveJSDoc<T> = { 17 | [K in keyof T]: T[K]; 18 | } & {}; 19 | export type PrettifyDeep<T> = { 20 | [K in keyof T]: T[K] extends (...args: any[]) => any 21 | ? T[K] 22 | : T[K] extends object 23 | ? T[K] extends Array<any> 24 | ? T[K] 25 | : T[K] extends Date 26 | ? T[K] 27 | : PrettifyDeep<T[K]> 28 | : T[K]; 29 | } & {}; 30 | export type LiteralUnion<LiteralType, BaseType extends Primitive> = 31 | | LiteralType 32 | | (BaseType & Record<never, never>); 33 | 34 | export type UnionToIntersection<U> = ( 35 | U extends any 36 | ? (k: U) => void 37 | : never 38 | ) extends (k: infer I) => void 39 | ? I 40 | : never; 41 | 42 | export type RequiredKeysOf<BaseType extends object> = Exclude< 43 | { 44 | [Key in keyof BaseType]: BaseType extends Record<Key, BaseType[Key]> 45 | ? Key 46 | : never; 47 | }[keyof BaseType], 48 | undefined 49 | >; 50 | 51 | export type HasRequiredKeys<BaseType extends object> = 52 | RequiredKeysOf<BaseType> extends never ? false : true; 53 | export type WithoutEmpty<T> = T extends T ? ({} extends T ? never : T) : never; 54 | 55 | export type StripEmptyObjects<T> = T extends { [K in keyof T]: never } 56 | ? never 57 | : T extends object 58 | ? { [K in keyof T as T[K] extends never ? never : K]: T[K] } 59 | : T; 60 | export type DeepPartial<T> = T extends Function 61 | ? T 62 | : T extends object 63 | ? { [K in keyof T]?: DeepPartial<T[K]> } 64 | : T; 65 | export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never; 66 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/icons/google.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Svg, { Path, SvgProps } from "react-native-svg"; 2 | 3 | export function GoogleIcon(props: SvgProps) { 4 | return ( 5 | <Svg width="1em" height="1em" viewBox="0 0 128 128"> 6 | <Path 7 | fill="#fff" 8 | d="M44.59 4.21a63.28 63.28 0 0 0 4.33 120.9a67.6 67.6 0 0 0 32.36.35a57.13 57.13 0 0 0 25.9-13.46a57.44 57.44 0 0 0 16-26.26a74.3 74.3 0 0 0 1.61-33.58H65.27v24.69h34.47a29.72 29.72 0 0 1-12.66 19.52a36.2 36.2 0 0 1-13.93 5.5a41.3 41.3 0 0 1-15.1 0A37.2 37.2 0 0 1 44 95.74a39.3 39.3 0 0 1-14.5-19.42a38.3 38.3 0 0 1 0-24.63a39.25 39.25 0 0 1 9.18-14.91A37.17 37.17 0 0 1 76.13 27a34.3 34.3 0 0 1 13.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.2 61.2 0 0 0 87.2 4.59a64 64 0 0 0-42.61-.38" 9 | ></Path> 10 | <Path 11 | fill="#e33629" 12 | d="M44.59 4.21a64 64 0 0 1 42.61.37a61.2 61.2 0 0 1 20.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.3 34.3 0 0 0-13.64-8a37.17 37.17 0 0 0-37.46 9.74a39.25 39.25 0 0 0-9.18 14.91L8.76 35.6A63.53 63.53 0 0 1 44.59 4.21" 13 | ></Path> 14 | <Path 15 | fill="#f8bd00" 16 | d="M3.26 51.5a63 63 0 0 1 5.5-15.9l20.73 16.09a38.3 38.3 0 0 0 0 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 0 1-5.5-40.9" 17 | ></Path> 18 | <Path 19 | fill="#587dbd" 20 | d="M65.27 52.15h59.52a74.3 74.3 0 0 1-1.61 33.58a57.44 57.44 0 0 1-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0 0 12.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68" 21 | ></Path> 22 | <Path 23 | fill="#319f43" 24 | d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0 0 44 95.74a37.2 37.2 0 0 0 14.08 6.08a41.3 41.3 0 0 0 15.1 0a36.2 36.2 0 0 0 13.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 0 1-25.9 13.47a67.6 67.6 0 0 1-32.36-.35a63 63 0 0 1-23-11.59A63.7 63.7 0 0 1 8.75 92.4" 25 | ></Path> 26 | </Svg> 27 | ); 28 | } 29 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/global.css: -------------------------------------------------------------------------------- ```css 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 224 71.4% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 224 71.4% 4.1%; 13 | --primary: 220.9 39.3% 11%; 14 | --primary-foreground: 210 20% 98%; 15 | --secondary: 220 14.3% 95.9%; 16 | --secondary-foreground: 220.9 39.3% 11%; 17 | --muted: 220 14.3% 95.9%; 18 | --muted-foreground: 220 8.9% 46.1%; 19 | --accent: 220 14.3% 95.9%; 20 | --accent-foreground: 220.9 39.3% 11%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 20% 98%; 23 | --border: 220 13% 91%; 24 | --input: 220 13% 91%; 25 | --ring: 224 71.4% 4.1%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 224 71.4% 4.1%; 36 | --foreground: 210 20% 98%; 37 | --card: 224 71.4% 4.1%; 38 | --card-foreground: 210 20% 98%; 39 | --popover: 224 71.4% 4.1%; 40 | --popover-foreground: 210 20% 98%; 41 | --primary: 210 20% 98%; 42 | --primary-foreground: 220.9 39.3% 11%; 43 | --secondary: 215 27.9% 16.9%; 44 | --secondary-foreground: 210 20% 98%; 45 | --muted: 215 27.9% 16.9%; 46 | --muted-foreground: 217.9 10.6% 64.9%; 47 | --accent: 215 27.9% 16.9%; 48 | --accent-foreground: 210 20% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 20% 98%; 51 | --border: 215 27.9% 16.9%; 52 | --input: 215 27.9% 16.9%; 53 | --ring: 216 12.2% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/resizable.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons"; 4 | import * as ResizablePrimitive from "react-resizable-panels"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( 12 | <ResizablePrimitive.PanelGroup 13 | className={cn( 14 | "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", 15 | className, 16 | )} 17 | {...props} 18 | /> 19 | ); 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel; 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { 28 | withHandle?: boolean; 29 | }) => ( 30 | <ResizablePrimitive.PanelResizeHandle 31 | className={cn( 32 | "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", 33 | className, 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 | <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border"> 39 | <DragHandleDots2Icon className="h-2.5 w-2.5" /> 40 | </div> 41 | )} 42 | </ResizablePrimitive.PanelResizeHandle> 43 | ); 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 46 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.mysql.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { testAdapter } from "../../test-adapter"; 2 | import { 3 | authFlowTestSuite, 4 | normalTestSuite, 5 | numberIdTestSuite, 6 | performanceTestSuite, 7 | transactionsTestSuite, 8 | } from "../../tests"; 9 | import { prismaAdapter } from "../prisma-adapter"; 10 | import { generateAuthConfigFile } from "./generate-auth-config"; 11 | import { generatePrismaSchema } from "./generate-prisma-schema"; 12 | import { pushPrismaSchema } from "./push-prisma-schema"; 13 | import type { BetterAuthOptions } from "@better-auth/core"; 14 | import { 15 | destroyPrismaClient, 16 | getPrismaClient, 17 | incrementMigrationCount, 18 | } from "./get-prisma-client"; 19 | import { createPool } from "mysql2/promise"; 20 | 21 | const dialect = "mysql"; 22 | const { execute } = await testAdapter({ 23 | adapter: async () => { 24 | const db = await getPrismaClient(dialect); 25 | return prismaAdapter(db, { 26 | provider: dialect, 27 | debugLogs: { isRunningAdapterTests: true }, 28 | }); 29 | }, 30 | runMigrations: async (options: BetterAuthOptions) => { 31 | const mysqlDB = createPool({ 32 | uri: "mysql://user:password@localhost:3308/better_auth", 33 | timezone: "Z", 34 | }); 35 | await mysqlDB.query("DROP DATABASE IF EXISTS better_auth"); 36 | await mysqlDB.query("CREATE DATABASE better_auth"); 37 | await mysqlDB.end(); 38 | const db = await getPrismaClient(dialect); 39 | const migrationCount = incrementMigrationCount(); 40 | await generateAuthConfigFile(options); 41 | await generatePrismaSchema(options, db, migrationCount, dialect); 42 | await pushPrismaSchema(dialect); 43 | destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); 44 | }, 45 | tests: [ 46 | normalTestSuite(), 47 | transactionsTestSuite(), 48 | authFlowTestSuite(), 49 | numberIdTestSuite(), 50 | performanceTestSuite({ dialect }), 51 | ], 52 | onFinish: async () => {}, 53 | prefixTests: dialect, 54 | }); 55 | 56 | execute(); 57 | ``` -------------------------------------------------------------------------------- /docs/components/ui/tabs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps<typeof TabsPrimitive.Root>) { 12 | return ( 13 | <TabsPrimitive.Root 14 | data-slot="tabs" 15 | className={cn("flex flex-col gap-2", className)} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | function TabsList({ 22 | className, 23 | ...props 24 | }: React.ComponentProps<typeof TabsPrimitive.List>) { 25 | return ( 26 | <TabsPrimitive.List 27 | data-slot="tabs-list" 28 | className={cn( 29 | "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-1", 30 | className, 31 | )} 32 | {...props} 33 | /> 34 | ); 35 | } 36 | 37 | function TabsTrigger({ 38 | className, 39 | ...props 40 | }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { 41 | return ( 42 | <TabsPrimitive.Trigger 43 | data-slot="tabs-trigger" 44 | className={cn( 45 | "data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | ); 51 | } 52 | 53 | function TabsContent({ 54 | className, 55 | ...props 56 | }: React.ComponentProps<typeof TabsPrimitive.Content>) { 57 | return ( 58 | <TabsPrimitive.Content 59 | data-slot="tabs-content" 60 | className={cn("flex-1 outline-none", className)} 61 | {...props} 62 | /> 63 | ); 64 | } 65 | 66 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 67 | ``` -------------------------------------------------------------------------------- /docs/content/blogs/seed-round.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Announcing our $5M seed round" 3 | description: "We raised $5M seed led by Peak XV Partners" 4 | date: 2025-06-24 5 | author: 6 | name: "Bereket Engida" 7 | avatar: "/blogs/bereket.png" 8 | twitter: "iambereket" 9 | image: "/blogs/seed-round.png" 10 | tags: ["seed round", "authentication", "funding"] 11 | --- 12 | 13 | ## Announcing our $5M seed round 14 | 15 | We’re excited to share that Better Auth has raised a $5 million seed round led by Peak XV Partners (formerly Sequoia Capital India & SEA), with participation from Y Combinator, Chapter One, P1 Ventures, and a group of incredible investors and angels. 16 | 17 | This funding fuels the next phase of **Better Auth**. 18 | 19 | From the start we are obsessed with making it possible for developers to **own their auth**. To **democratize high quality authentication** and make rolling your own auth not just doable, but the obvious choice. 20 | 21 | It started with building the framework. Since then, we’ve seen incredible growth and support from the community. Thank you everyone for being part of this journey. It’s still early days, and there’s so much more to build. This funding will allow us to have more people invloved and to push the boundaries of what's possible. 22 | 23 | On top of the framework, we’re also building the infrastructure to cover the gaps we couldn't cover in the framework: 24 | 25 | * A unified dashboard to manage users and user analytics 26 | * Enterprise-grade security: bot, abuse, and fraud protection 27 | * Authentication Email and SMS service 28 | * Fast, globally distributed session storage 29 | * and more. 30 | 31 | [Join the waitlist](https://better-auth.build) to get early access to the infrastructure. 32 | 33 | And if you're excited about making auth accessible - we're hiring! 34 | 35 | Reach out to [[email protected]](mailto:[email protected]). ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/react/react-store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { listenKeys } from "nanostores"; 2 | import { useCallback, useRef, useSyncExternalStore } from "react"; 3 | import type { Store, StoreValue } from "nanostores"; 4 | import type { DependencyList } from "react"; 5 | 6 | type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown } 7 | ? K 8 | : never; 9 | 10 | export interface UseStoreOptions<SomeStore> { 11 | /** 12 | * @default 13 | * ```ts 14 | * [store, options.keys] 15 | * ``` 16 | */ 17 | deps?: DependencyList; 18 | 19 | /** 20 | * Will re-render components only on specific key changes. 21 | */ 22 | keys?: StoreKeys<SomeStore>[]; 23 | } 24 | 25 | /** 26 | * Subscribe to store changes and get store's value. 27 | * 28 | * Can be used with store builder too. 29 | * 30 | * ```js 31 | * import { useStore } from 'nanostores/react' 32 | * 33 | * import { router } from '../store/router' 34 | * 35 | * export const Layout = () => { 36 | * let page = useStore(router) 37 | * if (page.route === 'home') { 38 | * return <HomePage /> 39 | * } else { 40 | * return <Error404 /> 41 | * } 42 | * } 43 | * ``` 44 | * 45 | * @param store Store instance. 46 | * @returns Store value. 47 | */ 48 | export function useStore<SomeStore extends Store>( 49 | store: SomeStore, 50 | options: UseStoreOptions<SomeStore> = {}, 51 | ): StoreValue<SomeStore> { 52 | let snapshotRef = useRef<StoreValue<SomeStore>>(store.get()); 53 | 54 | const { keys, deps = [store, keys] } = options; 55 | 56 | let subscribe = useCallback((onChange: () => void) => { 57 | const emitChange = (value: StoreValue<SomeStore>) => { 58 | if (snapshotRef.current === value) return; 59 | snapshotRef.current = value; 60 | onChange(); 61 | }; 62 | 63 | emitChange(store.value); 64 | if (keys?.length) { 65 | return listenKeys(store as any, keys, emitChange); 66 | } 67 | return store.listen(emitChange); 68 | }, deps); 69 | 70 | let get = () => snapshotRef.current as StoreValue<SomeStore>; 71 | 72 | return useSyncExternalStore(subscribe, get, get); 73 | } 74 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.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 | import { encodeToURLParams } from "../utils"; 5 | 6 | type Params = { 7 | siteVerifyURL: string; 8 | secretKey: string; 9 | captchaResponse: string; 10 | minScore?: number; 11 | remoteIP?: string; 12 | }; 13 | 14 | type SiteVerifyResponse = { 15 | success: boolean; 16 | challenge_ts: string; 17 | hostname: string; 18 | "error-codes": 19 | | Array< 20 | | "missing-input-secret" 21 | | "invalid-input-secret" 22 | | "missing-input-response" 23 | | "invalid-input-response" 24 | | "bad-request" 25 | | "timeout-or-duplicate" 26 | > 27 | | undefined; 28 | }; 29 | 30 | type SiteVerifyV3Response = SiteVerifyResponse & { 31 | score: number; 32 | }; 33 | 34 | const isV3 = ( 35 | response: SiteVerifyResponse | SiteVerifyV3Response, 36 | ): response is SiteVerifyV3Response => { 37 | return "score" in response && typeof response.score === "number"; 38 | }; 39 | 40 | export const googleRecaptcha = async ({ 41 | siteVerifyURL, 42 | captchaResponse, 43 | secretKey, 44 | minScore = 0.5, 45 | remoteIP, 46 | }: Params) => { 47 | const response = await betterFetch<SiteVerifyResponse | SiteVerifyV3Response>( 48 | siteVerifyURL, 49 | { 50 | method: "POST", 51 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 52 | body: encodeToURLParams({ 53 | secret: secretKey, 54 | response: captchaResponse, 55 | ...(remoteIP && { remoteip: remoteIP }), 56 | }), 57 | }, 58 | ); 59 | 60 | if (!response.data || response.error) { 61 | throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE); 62 | } 63 | 64 | if ( 65 | !response.data.success || 66 | (isV3(response.data) && response.data.score < minScore) 67 | ) { 68 | return middlewareResponse({ 69 | message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED, 70 | status: 403, 71 | }); 72 | } 73 | 74 | return undefined; 75 | }; 76 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/lynx/lynx-store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { listenKeys } from "nanostores"; 2 | import { useCallback, useRef, useSyncExternalStore } from "@lynx-js/react"; 3 | import type { Store, StoreValue } from "nanostores"; 4 | import type { DependencyList } from "@lynx-js/react"; 5 | 6 | type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown } 7 | ? K 8 | : never; 9 | 10 | export interface UseStoreOptions<SomeStore> { 11 | /** 12 | * @default 13 | * ```ts 14 | * [store, options.keys] 15 | * ``` 16 | */ 17 | deps?: DependencyList; 18 | 19 | /** 20 | * Will re-render components only on specific key changes. 21 | */ 22 | keys?: StoreKeys<SomeStore>[]; 23 | } 24 | 25 | /** 26 | * Subscribe to store changes and get store's value. 27 | * 28 | * Can be used with store builder too. 29 | * 30 | * ```js 31 | * import { useStore } from 'nanostores/react' 32 | * 33 | * import { router } from '../store/router' 34 | * 35 | * export const Layout = () => { 36 | * let page = useStore(router) 37 | * if (page.route === 'home') { 38 | * return <HomePage /> 39 | * } else { 40 | * return <Error404 /> 41 | * } 42 | * } 43 | * ``` 44 | * 45 | * @param store Store instance. 46 | * @returns Store value. 47 | */ 48 | export function useStore<SomeStore extends Store>( 49 | store: SomeStore, 50 | options: UseStoreOptions<SomeStore> = {}, 51 | ): StoreValue<SomeStore> { 52 | let snapshotRef = useRef<StoreValue<SomeStore>>(store.get()); 53 | 54 | const { keys, deps = [store, keys] } = options; 55 | 56 | let subscribe = useCallback((onChange: () => void) => { 57 | const emitChange = (value: StoreValue<SomeStore>) => { 58 | if (snapshotRef.current === value) return; 59 | snapshotRef.current = value; 60 | onChange(); 61 | }; 62 | 63 | emitChange(store.value); 64 | if (keys?.length) { 65 | return listenKeys(store as any, keys, emitChange); 66 | } 67 | return store.listen(emitChange); 68 | }, deps); 69 | 70 | let get = () => snapshotRef.current as StoreValue<SomeStore>; 71 | 72 | return useSyncExternalStore(subscribe, get, get); 73 | } 74 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/vk.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: VK 3 | description: VK ID Provider 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your VK ID credentials 9 | To use VK ID sign in, you need a client ID and client secret. You can get them from the [VK ID Developer Portal](https://id.vk.com/about/business/go/docs). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/vk` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 17 | ```ts title="auth.ts" 18 | import { betterAuth } from "better-auth"; 19 | 20 | export const auth = betterAuth({ 21 | socialProviders: { 22 | vk: { // [!code highlight] 23 | clientId: process.env.VK_CLIENT_ID as string, // [!code highlight] 24 | clientSecret: process.env.VK_CLIENT_SECRET as string, // [!code highlight] 25 | }, 26 | }, 27 | }); 28 | ``` 29 | </Step> 30 | <Step> 31 | ### Sign In with VK 32 | To sign in with VK, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 33 | - `provider`: The provider to use. It should be set to `vk`. 34 | 35 | 36 | ```ts title="auth-client.ts" 37 | import { createAuthClient } from "better-auth/client"; 38 | const authClient = createAuthClient(); 39 | 40 | const signIn = async () => { 41 | const data = await authClient.signIn.social({ 42 | provider: "vk", 43 | }); 44 | }; 45 | ``` 46 | </Step> 47 | 48 | </Steps> 49 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.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 | import { encodeToURLParams } from "../utils"; 5 | 6 | type Params = { 7 | siteVerifyURL: string; 8 | secretKey: string; 9 | captchaResponse: string; 10 | siteKey?: string; 11 | remoteIP?: string; 12 | }; 13 | 14 | type SiteVerifyResponse = { 15 | success: boolean; 16 | challenge_ts: number; 17 | hostname: string; 18 | credit: true | false | undefined; 19 | "error-codes": 20 | | Array< 21 | | "missing-input-secret" 22 | | "invalid-input-secret" 23 | | "missing-input-response" 24 | | "invalid-input-response" 25 | | "expired-input-response" 26 | | "already-seen-response" 27 | | "bad-request" 28 | | "missing-remoteip" 29 | | "invalid-remoteip" 30 | | "not-using-dummy-passcode" 31 | | "sitekey-secret-mismatch" 32 | > 33 | | undefined; 34 | score: number | undefined; // ENTERPRISE feature: a score denoting malicious activity. 35 | score_reason: Array<unknown> | undefined; // ENTERPRISE feature: reason(s) for score. 36 | }; 37 | 38 | export const hCaptcha = async ({ 39 | siteVerifyURL, 40 | captchaResponse, 41 | secretKey, 42 | siteKey, 43 | remoteIP, 44 | }: Params) => { 45 | const response = await betterFetch<SiteVerifyResponse>(siteVerifyURL, { 46 | method: "POST", 47 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 48 | body: encodeToURLParams({ 49 | secret: secretKey, 50 | response: captchaResponse, 51 | ...(siteKey && { sitekey: siteKey }), 52 | ...(remoteIP && { remoteip: remoteIP }), 53 | }), 54 | }); 55 | 56 | if (!response.data || response.error) { 57 | throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE); 58 | } 59 | 60 | if (!response.data.success) { 61 | return middlewareResponse({ 62 | message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED, 63 | status: 403, 64 | }); 65 | } 66 | 67 | return undefined; 68 | }; 69 | ``` -------------------------------------------------------------------------------- /docs/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 | return ( 13 | <TooltipPrimitive.Provider 14 | data-slot="tooltip-provider" 15 | delayDuration={delayDuration} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 | return ( 25 | <TooltipProvider> 26 | <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 | </TooltipProvider> 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 | return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 | return ( 44 | <TooltipPrimitive.Portal> 45 | <TooltipPrimitive.Content 46 | data-slot="tooltip-content" 47 | sideOffset={sideOffset} 48 | className={cn( 49 | "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance", 50 | className, 51 | )} 52 | {...props} 53 | > 54 | {children} 55 | <TooltipPrimitive.Arrow className="-z-10 relative bg-primary fill-primary size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> 56 | </TooltipPrimitive.Content> 57 | </TooltipPrimitive.Portal> 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/bearer/bearer.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | 4 | describe("bearer", async () => { 5 | const { client, auth, testUser } = await getTestInstance( 6 | {}, 7 | { 8 | disableTestUser: true, 9 | }, 10 | ); 11 | 12 | let token: string; 13 | it("should get session", async () => { 14 | await client.signUp.email( 15 | { 16 | email: testUser.email, 17 | password: testUser.password, 18 | name: testUser.name, 19 | }, 20 | { 21 | onSuccess: (ctx) => { 22 | token = ctx.response.headers.get("set-auth-token") || ""; 23 | }, 24 | }, 25 | ); 26 | const session = await client.getSession({ 27 | fetchOptions: { 28 | headers: { 29 | Authorization: `Bearer ${token}`, 30 | }, 31 | }, 32 | }); 33 | expect(session.data?.session).toBeDefined(); 34 | }); 35 | 36 | it("should list session", async () => { 37 | const sessions = await client.listSessions({ 38 | fetchOptions: { 39 | headers: { 40 | Authorization: `Bearer ${token}`, 41 | }, 42 | }, 43 | }); 44 | expect(sessions.data).toHaveLength(1); 45 | }); 46 | 47 | it("should work on server actions", async () => { 48 | const session = await auth.api.getSession({ 49 | headers: new Headers({ 50 | authorization: `Bearer ${token}`, 51 | }), 52 | }); 53 | expect(session?.session).toBeDefined(); 54 | }); 55 | 56 | it("should work with ", async () => { 57 | const session = await client.getSession({ 58 | fetchOptions: { 59 | headers: { 60 | authorization: `Bearer ${token.split(".")[0]}`, 61 | }, 62 | }, 63 | }); 64 | expect(session.data?.session).toBeDefined(); 65 | }); 66 | 67 | it("should work if valid cookie is provided even if authorization header isn't valid", async () => { 68 | const session = await client.getSession({ 69 | fetchOptions: { 70 | headers: { 71 | Authorization: `Bearer invalid.token`, 72 | cookie: `better-auth.session_token=${token}`, 73 | }, 74 | }, 75 | }); 76 | expect(session.data?.session).toBeDefined(); 77 | }); 78 | }); 79 | ``` -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@better-auth/cli", 3 | "version": "1.4.0-beta.10", 4 | "type": "module", 5 | "description": "The CLI for Better Auth", 6 | "module": "dist/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/better-auth/better-auth", 10 | "directory": "packages/cli" 11 | }, 12 | "homepage": "https://www.better-auth.com/docs/concepts/cli", 13 | "main": "./dist/index.js", 14 | "scripts": { 15 | "build": "tsdown", 16 | "start": "node ./dist/index.js", 17 | "dev": "tsx ./src/index.ts", 18 | "test": "vitest", 19 | "typecheck": "tsc --project tsconfig.json" 20 | }, 21 | "publishConfig": { 22 | "access": "public", 23 | "executableFiles": [ 24 | "./dist/index.js" 25 | ] 26 | }, 27 | "license": "MIT", 28 | "keywords": [ 29 | "auth", 30 | "cli", 31 | "typescript", 32 | "better-auth" 33 | ], 34 | "exports": "./dist/index.js", 35 | "bin": "./dist/index.js", 36 | "devDependencies": { 37 | "@types/semver": "^7.7.1", 38 | "tsx": "^4.20.5", 39 | "typescript": "catalog:", 40 | "tsdown": "catalog:" 41 | }, 42 | "dependencies": { 43 | "@babel/core": "^7.28.4", 44 | "@babel/preset-react": "^7.27.1", 45 | "@babel/preset-typescript": "^7.27.1", 46 | "@better-auth/utils": "0.3.0", 47 | "@clack/prompts": "^0.11.0", 48 | "@mrleebo/prisma-ast": "^0.13.0", 49 | "@prisma/client": "^5.22.0", 50 | "@types/better-sqlite3": "^7.6.13", 51 | "@types/prompts": "^2.4.9", 52 | "better-auth": "workspace:*", 53 | "better-sqlite3": "^12.2.0", 54 | "c12": "^3.2.0", 55 | "chalk": "^5.6.2", 56 | "commander": "^12.1.0", 57 | "dotenv": "^17.2.2", 58 | "drizzle-orm": "^0.33.0", 59 | "get-tsconfig": "^4.10.1", 60 | "jiti": "^2.6.0", 61 | "open": "^10.2.0", 62 | "prettier": "^3.6.2", 63 | "prisma": "^5.22.0", 64 | "prompts": "^2.4.2", 65 | "semver": "^7.7.2", 66 | "tinyexec": "^0.3.2", 67 | "yocto-spinner": "^0.2.3", 68 | "zod": "^4.1.5" 69 | }, 70 | "files": [ 71 | "dist" 72 | ] 73 | } 74 | ``` -------------------------------------------------------------------------------- /packages/core/src/types/plugin-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPlugin } from "./plugin"; 2 | import type { 3 | BetterFetch, 4 | BetterFetchOption, 5 | BetterFetchPlugin, 6 | } from "@better-fetch/fetch"; 7 | import type { LiteralString } from "./helper"; 8 | import type { BetterAuthOptions } from "./init-options"; 9 | import type { WritableAtom, Atom } from "nanostores"; 10 | 11 | export interface ClientStore { 12 | notify: (signal: string) => void; 13 | listen: (signal: string, listener: () => void) => void; 14 | atoms: Record<string, WritableAtom<any>>; 15 | } 16 | 17 | export type ClientAtomListener = { 18 | matcher: (path: string) => boolean; 19 | signal: "$sessionSignal" | Omit<string, "$sessionSignal">; 20 | }; 21 | 22 | export interface BetterAuthClientOptions { 23 | fetchOptions?: BetterFetchOption; 24 | plugins?: BetterAuthClientPlugin[]; 25 | baseURL?: string; 26 | basePath?: string; 27 | disableDefaultFetchPlugins?: boolean; 28 | $InferAuth?: BetterAuthOptions; 29 | } 30 | 31 | export interface BetterAuthClientPlugin { 32 | id: LiteralString; 33 | /** 34 | * only used for type inference. don't pass the 35 | * actual plugin 36 | */ 37 | $InferServerPlugin?: BetterAuthPlugin; 38 | /** 39 | * Custom actions 40 | */ 41 | getActions?: ( 42 | $fetch: BetterFetch, 43 | $store: ClientStore, 44 | /** 45 | * better-auth client options 46 | */ 47 | options: BetterAuthClientOptions | undefined, 48 | ) => Record<string, any>; 49 | /** 50 | * State atoms that'll be resolved by each framework 51 | * auth store. 52 | */ 53 | getAtoms?: ($fetch: BetterFetch) => Record<string, Atom<any>>; 54 | /** 55 | * specify path methods for server plugin inferred 56 | * endpoints to force a specific method. 57 | */ 58 | pathMethods?: Record<string, "POST" | "GET">; 59 | /** 60 | * Better fetch plugins 61 | */ 62 | fetchPlugins?: BetterFetchPlugin[]; 63 | /** 64 | * a list of recaller based on a matcher function. 65 | * The signal name needs to match a signal in this 66 | * plugin or any plugin the user might have added. 67 | */ 68 | atomListeners?: ClientAtomListener[]; 69 | } 70 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 | return ( 13 | <TooltipPrimitive.Provider 14 | data-slot="tooltip-provider" 15 | delayDuration={delayDuration} 16 | {...props} 17 | /> 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 | return ( 25 | <TooltipProvider> 26 | <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 | </TooltipProvider> 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 | return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 | return ( 44 | <TooltipPrimitive.Portal> 45 | <TooltipPrimitive.Content 46 | data-slot="tooltip-content" 47 | sideOffset={sideOffset} 48 | className={cn( 49 | "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance", 50 | className, 51 | )} 52 | {...props} 53 | > 54 | {children} 55 | <TooltipPrimitive.Arrow className="-z-10 relative bg-primary dark:bg-stone-900 dark:fill-stone-900 fill-primary size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" /> 56 | </TooltipPrimitive.Content> 57 | </TooltipPrimitive.Portal> 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | ``` -------------------------------------------------------------------------------- /docs/components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; 5 | import { type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { toggleVariants } from "@/components/ui/toggle"; 9 | 10 | const ToggleGroupContext = React.createContext< 11 | VariantProps<typeof toggleVariants> 12 | >({ 13 | size: "default", 14 | variant: "default", 15 | }); 16 | 17 | function ToggleGroup({ 18 | className, 19 | variant, 20 | size, 21 | children, 22 | ...props 23 | }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & 24 | VariantProps<typeof toggleVariants>) { 25 | return ( 26 | <ToggleGroupPrimitive.Root 27 | data-slot="toggle-group" 28 | data-variant={variant} 29 | data-size={size} 30 | className={cn( 31 | "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | <ToggleGroupContext.Provider value={{ variant, size }}> 37 | {children} 38 | </ToggleGroupContext.Provider> 39 | </ToggleGroupPrimitive.Root> 40 | ); 41 | } 42 | 43 | function ToggleGroupItem({ 44 | className, 45 | children, 46 | variant, 47 | size, 48 | ...props 49 | }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & 50 | VariantProps<typeof toggleVariants>) { 51 | const context = React.useContext(ToggleGroupContext); 52 | 53 | return ( 54 | <ToggleGroupPrimitive.Item 55 | data-slot="toggle-group-item" 56 | data-variant={context.variant || variant} 57 | data-size={context.size || size} 58 | className={cn( 59 | toggleVariants({ 60 | variant: context.variant || variant, 61 | size: context.size || size, 62 | }), 63 | "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l", 64 | className, 65 | )} 66 | {...props} 67 | > 68 | {children} 69 | </ToggleGroupPrimitive.Item> 70 | ); 71 | } 72 | 73 | export { ToggleGroup, ToggleGroupItem }; 74 | ``` -------------------------------------------------------------------------------- /docs/components/ui/callout.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { CircleCheck, CircleX, Info, TriangleAlert } from "lucide-react"; 2 | import { forwardRef, type HTMLAttributes, type ReactNode } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | type CalloutProps = Omit< 7 | HTMLAttributes<HTMLDivElement>, 8 | "title" | "type" | "icon" 9 | > & { 10 | title?: ReactNode; 11 | /** 12 | * @defaultValue info 13 | */ 14 | type?: "info" | "warn" | "error" | "success" | "warning"; 15 | 16 | /** 17 | * Force an icon 18 | */ 19 | icon?: ReactNode; 20 | }; 21 | 22 | const calloutVariants = cva( 23 | "my-4 flex gap-2 rounded-lg border border-s-2 bg-fd-card p-3 text-sm text-fd-card-foreground shadow-md border-dashed rounded-none", 24 | { 25 | variants: { 26 | type: { 27 | info: "border-s-blue-500/50", 28 | warn: "border-s-orange-500/50", 29 | error: "border-s-red-500/50", 30 | success: "border-s-green-500/50", 31 | }, 32 | }, 33 | }, 34 | ); 35 | 36 | export const Callout = forwardRef<HTMLDivElement, CalloutProps>( 37 | ({ className, children, title, type = "info", icon, ...props }, ref) => { 38 | if (type === "warning") type = "warn"; 39 | 40 | return ( 41 | <div 42 | ref={ref} 43 | className={cn( 44 | calloutVariants({ 45 | type: type, 46 | }), 47 | className, 48 | )} 49 | {...props} 50 | > 51 | {icon ?? 52 | { 53 | info: <Info className="size-5 fill-blue-500 text-fd-card" />, 54 | warn: ( 55 | <TriangleAlert className="size-5 fill-orange-500 text-fd-card" /> 56 | ), 57 | error: <CircleX className="size-5 fill-red-500 text-fd-card" />, 58 | success: ( 59 | <CircleCheck className="size-5 fill-green-500 text-fd-card" /> 60 | ), 61 | }[type]} 62 | <div className="min-w-0 flex flex-col gap-2 flex-1"> 63 | {title ? <p className="font-medium !my-0">{title}</p> : null} 64 | <div className="text-fd-muted-foreground prose-no-margin empty:hidden"> 65 | {children} 66 | </div> 67 | </div> 68 | </div> 69 | ); 70 | }, 71 | ); 72 | 73 | Callout.displayName = "Callout"; 74 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/last-login-method/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthClientPlugin } from "@better-auth/core"; 2 | 3 | /** 4 | * Configuration for the client-side last login method plugin 5 | */ 6 | export interface LastLoginMethodClientConfig { 7 | /** 8 | * Name of the cookie to read the last login method from 9 | * @default "better-auth.last_used_login_method" 10 | */ 11 | cookieName?: string; 12 | } 13 | 14 | function getCookieValue(name: string): string | null { 15 | if (typeof document === "undefined") { 16 | return null; 17 | } 18 | 19 | const cookie = document.cookie 20 | .split("; ") 21 | .find((row) => row.startsWith(`${name}=`)); 22 | 23 | return cookie ? cookie.split("=")[1]! : null; 24 | } 25 | 26 | /** 27 | * Client-side plugin to retrieve the last used login method 28 | */ 29 | export const lastLoginMethodClient = ( 30 | config: LastLoginMethodClientConfig = {}, 31 | ) => { 32 | const cookieName = config.cookieName || "better-auth.last_used_login_method"; 33 | 34 | return { 35 | id: "last-login-method-client", 36 | getActions() { 37 | return { 38 | /** 39 | * Get the last used login method from cookies 40 | * @returns The last used login method or null if not found 41 | */ 42 | getLastUsedLoginMethod: (): string | null => { 43 | return getCookieValue(cookieName); 44 | }, 45 | /** 46 | * Clear the last used login method cookie 47 | * This sets the cookie with an expiration date in the past 48 | */ 49 | clearLastUsedLoginMethod: (): void => { 50 | if (typeof document !== "undefined") { 51 | document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; 52 | } 53 | }, 54 | /** 55 | * Check if a specific login method was the last used 56 | * @param method The method to check 57 | * @returns True if the method was the last used, false otherwise 58 | */ 59 | isLastUsedLoginMethod: (method: string): boolean => { 60 | const lastMethod = getCookieValue(cookieName); 61 | return lastMethod === method; 62 | }, 63 | }; 64 | }, 65 | } satisfies BetterAuthClientPlugin; 66 | }; 67 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/naver.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Naver 3 | description: Naver provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Naver Credentials 9 | To use Naver sign in, you need a client ID and client secret. You can get them from the [Naver Developers](https://developers.naver.com/). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/naver` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | <Step> 14 | ### Configure the provider 15 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 16 | 17 | ```ts title="auth.ts" 18 | import { betterAuth } from "better-auth" 19 | 20 | export const auth = betterAuth({ 21 | socialProviders: { 22 | naver: { // [!code highlight] 23 | clientId: process.env.NAVER_CLIENT_ID as string, // [!code highlight] 24 | clientSecret: process.env.NAVER_CLIENT_SECRET as string, // [!code highlight] 25 | }, // [!code highlight] 26 | } 27 | }) 28 | ``` 29 | </Step> 30 | <Step> 31 | ### Sign In with Naver 32 | To sign in with Naver, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 33 | - `provider`: The provider to use. It should be set to `naver`. 34 | 35 | ```ts title="auth-client.ts" 36 | import { createAuthClient } from "better-auth/client" 37 | const authClient = createAuthClient() 38 | 39 | const signIn = async () => { 40 | const data = await authClient.signIn.social({ 41 | provider: "naver" 42 | }) 43 | } 44 | ``` 45 | </Step> 46 | 47 | </Steps> 48 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/kakao.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Kakao 3 | description: Kakao provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Kakao Credentials 9 | To use Kakao sign in, you need a client ID and client secret. You can get them from the [Kakao Developer Portal](https://developers.kakao.com). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/kakao` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | <Step> 14 | ### Configure the provider 15 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 16 | 17 | ```ts title="auth.ts" 18 | import { betterAuth } from "better-auth" 19 | 20 | export const auth = betterAuth({ 21 | socialProviders: { 22 | kakao: { // [!code highlight] 23 | clientId: process.env.KAKAO_CLIENT_ID as string, // [!code highlight] 24 | clientSecret: process.env.KAKAO_CLIENT_SECRET as string, // [!code highlight] 25 | }, // [!code highlight] 26 | } 27 | }) 28 | ``` 29 | </Step> 30 | <Step> 31 | ### Sign In with Kakao 32 | To sign in with Kakao, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 33 | - `provider`: The provider to use. It should be set to `kakao`. 34 | 35 | ```ts title="auth-client.ts" 36 | import { createAuthClient } from "better-auth/client" 37 | const authClient = createAuthClient() 38 | 39 | const signIn = async () => { 40 | const data = await authClient.signIn.social({ 41 | provider: "kakao" 42 | }) 43 | } 44 | ``` 45 | </Step> 46 | 47 | </Steps> 48 | ``` -------------------------------------------------------------------------------- /docs/components/resource-section.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { useState } from "react"; 4 | import { ResourceCard } from "./resource-card"; 5 | 6 | interface Resource { 7 | title: string; 8 | description: string; 9 | href: string; 10 | tags: string[]; 11 | } 12 | 13 | interface ResourceProps { 14 | resources: Resource[]; 15 | className?: string; 16 | } 17 | 18 | export function Resource({ className, resources }: ResourceProps) { 19 | const [activeTag, setActiveTag] = useState<string | null>(null); 20 | const tags = Array.from( 21 | new Set(resources.flatMap((resource) => resource.tags)), 22 | ); 23 | const filterResources = (activeTag: string | null): Resource[] => { 24 | if (!activeTag) return resources; 25 | return resources.filter((resource) => resource.tags.includes(activeTag)); 26 | }; 27 | return ( 28 | <div> 29 | <div className={cn("space-y-4", className)}> 30 | <div className="flex flex-wrap gap-2"> 31 | <button 32 | onClick={() => setActiveTag(null)} 33 | className={cn( 34 | "inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors", 35 | activeTag === null 36 | ? "bg-primary text-primary-foreground" 37 | : "bg-secondary/10 text-secondary-foreground hover:bg-secondary/20", 38 | )} 39 | > 40 | All 41 | </button> 42 | {tags.map((tag) => ( 43 | <button 44 | key={tag} 45 | onClick={() => setActiveTag(tag)} 46 | className={cn( 47 | "inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors", 48 | activeTag === tag 49 | ? "bg-primary text-primary-foreground" 50 | : "bg-secondary/10 text-secondary-foreground hover:bg-secondary/20", 51 | )} 52 | > 53 | {tag} 54 | </button> 55 | ))} 56 | </div> 57 | </div> 58 | <div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-2"> 59 | {filterResources(activeTag).map((resource) => ( 60 | <ResourceCard key={resource.href} {...resource} /> 61 | ))} 62 | </div> 63 | </div> 64 | ); 65 | } 66 | ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/nav.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import Link, { type LinkProps } from "fumadocs-core/link"; 3 | import { 4 | createContext, 5 | type ReactNode, 6 | useContext, 7 | useEffect, 8 | useMemo, 9 | useState, 10 | } from "react"; 11 | import { cn } from "../../../lib/utils"; 12 | import { useI18n } from "fumadocs-ui/provider"; 13 | 14 | export interface NavProviderProps { 15 | /** 16 | * Use transparent background 17 | * 18 | * @defaultValue none 19 | */ 20 | transparentMode?: "always" | "top" | "none"; 21 | } 22 | 23 | export interface TitleProps { 24 | title?: ReactNode; 25 | 26 | /** 27 | * Redirect url of title 28 | * @defaultValue '/' 29 | */ 30 | url?: string; 31 | } 32 | 33 | interface NavContextType { 34 | isTransparent: boolean; 35 | } 36 | 37 | const NavContext = createContext<NavContextType>({ 38 | isTransparent: false, 39 | }); 40 | 41 | export function NavProvider({ 42 | transparentMode = "none", 43 | children, 44 | }: NavProviderProps & { children: ReactNode }) { 45 | const [transparent, setTransparent] = useState(transparentMode !== "none"); 46 | 47 | useEffect(() => { 48 | if (transparentMode !== "top") return; 49 | 50 | const listener = () => { 51 | if (document.documentElement.hasAttribute("data-anchor-scrolling")) { 52 | return; 53 | } 54 | setTransparent(window.scrollY < 10); 55 | }; 56 | 57 | listener(); 58 | window.addEventListener("scroll", listener, { passive: true }); 59 | return () => { 60 | window.removeEventListener("scroll", listener); 61 | }; 62 | }, [transparentMode]); 63 | 64 | return ( 65 | <NavContext.Provider 66 | value={useMemo(() => ({ isTransparent: transparent }), [transparent])} 67 | > 68 | {children} 69 | </NavContext.Provider> 70 | ); 71 | } 72 | 73 | export function useNav(): NavContextType { 74 | return useContext(NavContext); 75 | } 76 | 77 | export function Title({ 78 | title, 79 | url, 80 | ...props 81 | }: TitleProps & Omit<LinkProps, "title">) { 82 | const { locale } = useI18n(); 83 | 84 | return ( 85 | <Link 86 | href={url ?? (locale ? `/${locale}` : "/")} 87 | {...props} 88 | className={cn( 89 | "inline-flex items-center gap-2.5 font-semibold", 90 | props.className, 91 | )} 92 | > 93 | {title} 94 | </Link> 95 | ); 96 | } 97 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/get-tables.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getAuthTables } from "./get-tables"; 3 | 4 | describe("getAuthTables", () => { 5 | it("should use correct field name for refreshTokenExpiresAt", () => { 6 | const tables = getAuthTables({ 7 | account: { 8 | fields: { 9 | refreshTokenExpiresAt: "custom_refresh_token_expires_at", 10 | }, 11 | }, 12 | }); 13 | 14 | const accountTable = tables.account; 15 | const refreshTokenExpiresAtField = 16 | accountTable!.fields.refreshTokenExpiresAt!; 17 | 18 | expect(refreshTokenExpiresAtField.fieldName).toBe( 19 | "custom_refresh_token_expires_at", 20 | ); 21 | }); 22 | 23 | it("should not use accessTokenExpiresAt field name for refreshTokenExpiresAt", () => { 24 | const tables = getAuthTables({ 25 | account: { 26 | fields: { 27 | accessTokenExpiresAt: "custom_access_token_expires_at", 28 | refreshTokenExpiresAt: "custom_refresh_token_expires_at", 29 | }, 30 | }, 31 | }); 32 | 33 | const accountTable = tables.account; 34 | const refreshTokenExpiresAtField = 35 | accountTable!.fields.refreshTokenExpiresAt!; 36 | const accessTokenExpiresAtField = 37 | accountTable!.fields.accessTokenExpiresAt!; 38 | 39 | expect(refreshTokenExpiresAtField.fieldName).toBe( 40 | "custom_refresh_token_expires_at", 41 | ); 42 | expect(accessTokenExpiresAtField.fieldName).toBe( 43 | "custom_access_token_expires_at", 44 | ); 45 | expect(refreshTokenExpiresAtField.fieldName).not.toBe( 46 | accessTokenExpiresAtField.fieldName, 47 | ); 48 | }); 49 | 50 | it("should use default field names when no custom names provided", () => { 51 | const tables = getAuthTables({}); 52 | 53 | const accountTable = tables.account; 54 | const refreshTokenExpiresAtField = 55 | accountTable!.fields.refreshTokenExpiresAt!; 56 | const accessTokenExpiresAtField = 57 | accountTable!.fields.accessTokenExpiresAt!; 58 | 59 | expect(refreshTokenExpiresAtField.fieldName).toBe("refreshTokenExpiresAt"); 60 | expect(accessTokenExpiresAtField.fieldName).toBe("accessTokenExpiresAt"); 61 | }); 62 | }); 63 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/kick.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Kick 3 | description: Kick provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Kick Credentials 9 | To use Kick sign in, you need a client ID and client secret. You can get them from the [Kick Developer Portal](https://kick.com/settings/developer). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/kick` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | 21 | export const auth = betterAuth({ 22 | socialProviders: { 23 | kick: { // [!code highlight] 24 | clientId: process.env.KICK_CLIENT_ID as string, // [!code highlight] 25 | clientSecret: process.env.KICK_CLIENT_SECRET as string, // [!code highlight] 26 | }, // [!code highlight] 27 | } 28 | }) 29 | ``` 30 | </Step> 31 | <Step> 32 | ### Sign In with Kick 33 | To sign in with Kick, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 34 | - `provider`: The provider to use. It should be set to `kick`. 35 | 36 | ```ts title="auth-client.ts" 37 | import { createAuthClient } from "better-auth/client" 38 | const authClient = createAuthClient() 39 | 40 | const signIn = async () => { 41 | const data = await authClient.signIn.social({ 42 | provider: "kick" 43 | }) 44 | } 45 | ``` 46 | </Step> 47 | </Steps> 48 | ``` -------------------------------------------------------------------------------- /docs/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 | function Slider({ 9 | className, 10 | defaultValue, 11 | value, 12 | min = 0, 13 | max = 100, 14 | ...props 15 | }: React.ComponentProps<typeof SliderPrimitive.Root>) { 16 | const _values = React.useMemo( 17 | () => 18 | Array.isArray(value) 19 | ? value 20 | : Array.isArray(defaultValue) 21 | ? defaultValue 22 | : [min, max], 23 | [value, defaultValue, min, max], 24 | ); 25 | 26 | return ( 27 | <SliderPrimitive.Root 28 | data-slot="slider" 29 | defaultValue={defaultValue} 30 | value={value} 31 | min={min} 32 | max={max} 33 | className={cn( 34 | "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", 35 | className, 36 | )} 37 | {...props} 38 | > 39 | <SliderPrimitive.Track 40 | data-slot="slider-track" 41 | className={cn( 42 | "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5", 43 | )} 44 | > 45 | <SliderPrimitive.Range 46 | data-slot="slider-range" 47 | className={cn( 48 | "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full", 49 | )} 50 | /> 51 | </SliderPrimitive.Track> 52 | {Array.from({ length: _values.length }, (_, index) => ( 53 | <SliderPrimitive.Thumb 54 | data-slot="slider-thumb" 55 | key={index} 56 | className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" 57 | /> 58 | ))} 59 | </SliderPrimitive.Root> 60 | ); 61 | } 62 | 63 | export { Slider }; 64 | ``` -------------------------------------------------------------------------------- /docs/components/docs/ui/scroll-area.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 2 | import * as React from "react"; 3 | import { cn } from "../../../lib/utils"; 4 | 5 | const ScrollArea = React.forwardRef< 6 | React.ComponentRef<typeof ScrollAreaPrimitive.Root>, 7 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> 8 | >(({ className, children, ...props }, ref) => ( 9 | <ScrollAreaPrimitive.Root 10 | ref={ref} 11 | className={cn("overflow-hidden", className)} 12 | {...props} 13 | > 14 | {children} 15 | <ScrollAreaPrimitive.Corner /> 16 | <ScrollBar orientation="vertical" /> 17 | </ScrollAreaPrimitive.Root> 18 | )); 19 | 20 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 21 | 22 | const ScrollViewport = React.forwardRef< 23 | React.ComponentRef<typeof ScrollAreaPrimitive.Viewport>, 24 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> 25 | >(({ className, children, ...props }, ref) => ( 26 | <ScrollAreaPrimitive.Viewport 27 | ref={ref} 28 | className={cn("size-full rounded-[inherit]", className)} 29 | {...props} 30 | > 31 | {children} 32 | </ScrollAreaPrimitive.Viewport> 33 | )); 34 | 35 | ScrollViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; 36 | 37 | const ScrollBar = React.forwardRef< 38 | React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>, 39 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar> 40 | >(({ className, orientation = "vertical", ...props }, ref) => ( 41 | <ScrollAreaPrimitive.Scrollbar 42 | ref={ref} 43 | orientation={orientation} 44 | className={cn( 45 | "flex select-none data-[state=hidden]:animate-fd-fade-out", 46 | orientation === "vertical" && "h-full w-1.5", 47 | orientation === "horizontal" && "h-1.5 flex-col", 48 | className, 49 | )} 50 | {...props} 51 | > 52 | <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-fd-border" /> 53 | </ScrollAreaPrimitive.Scrollbar> 54 | )); 55 | ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName; 56 | 57 | export { ScrollArea, ScrollBar, ScrollViewport }; 58 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/ssr.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterAuth } from "better-auth"; 2 | import { describe, test } from "node:test"; 3 | import { DatabaseSync } from "node:sqlite"; 4 | import { apiKey } from "better-auth/plugins"; 5 | import { createAuthClient } from "better-auth/client"; 6 | import { apiKeyClient } from "better-auth/client/plugins"; 7 | import { getMigrations } from "better-auth/db"; 8 | import assert from "node:assert/strict"; 9 | 10 | describe("server side client", () => { 11 | test("can use api key on server side", async () => { 12 | const database = new DatabaseSync(":memory:"); 13 | const auth = betterAuth({ 14 | baseURL: "http://localhost:3000", 15 | database, 16 | socialProviders: { 17 | github: { 18 | clientId: process.env.GITHUB_CLIENT_ID as string, 19 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 20 | }, 21 | }, 22 | emailAndPassword: { 23 | enabled: true, 24 | }, 25 | plugins: [ 26 | apiKey({ 27 | rateLimit: { 28 | enabled: false, 29 | }, 30 | }), 31 | ], 32 | }); 33 | 34 | const { runMigrations } = await getMigrations(auth.options); 35 | await runMigrations(); 36 | 37 | const authClient: ReturnType<typeof createAuthClient> = createAuthClient({ 38 | baseURL: "http://localhost:3000", 39 | plugins: [apiKeyClient()], 40 | fetchOptions: { 41 | customFetchImpl: async (url, init) => { 42 | return auth.handler(new Request(url, init)); 43 | }, 44 | }, 45 | }); 46 | 47 | const { user } = await auth.api.signUpEmail({ 48 | body: { 49 | name: "Alex", 50 | email: "[email protected]", 51 | password: "hello123", 52 | }, 53 | }); 54 | 55 | const { key, id, userId } = await auth.api.createApiKey({ 56 | body: { 57 | name: "my-api-key", 58 | userId: user.id, 59 | }, 60 | }); 61 | 62 | const ret = database.prepare(`SELECT * FROM apiKey;`).all(); 63 | assert.equal(ret.length, 1); 64 | const first = ret.at(-1)!; 65 | assert.equal(first.id, id); 66 | assert.equal(first.userId, userId); 67 | 68 | await authClient.getSession({ 69 | fetchOptions: { 70 | headers: { 71 | "x-api-key": key, 72 | }, 73 | }, 74 | }); 75 | }); 76 | }); 77 | ``` -------------------------------------------------------------------------------- /docs/components/builder/tabs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | type Tab = { 7 | title: string; 8 | value: string; 9 | content?: string | React.ReactNode | any; 10 | }; 11 | 12 | export const AuthTabs = ({ tabs: propTabs }: { tabs: Tab[] }) => { 13 | const [active, setActive] = useState<Tab>(propTabs[0]); 14 | const [tabs, setTabs] = useState<Tab[]>(propTabs); 15 | const isActive = (tab: Tab) => { 16 | return tab.value === tabs[0].value; 17 | }; 18 | const moveSelectedTabToTop = (idx: number) => { 19 | const newTabs = [...propTabs]; 20 | const selectedTab = newTabs.splice(idx, 1); 21 | newTabs.unshift(selectedTab[0]); 22 | setTabs(newTabs); 23 | setActive(newTabs[0]); 24 | }; 25 | 26 | return ( 27 | <> 28 | <div 29 | className={cn( 30 | "flex flex-row items-center justify-start mt-0 relative no-visible-scrollbar border-x w-full border-t max-w-max bg-opacity-0", 31 | )} 32 | > 33 | {propTabs.map((tab, idx) => ( 34 | <button 35 | key={tab.title} 36 | onClick={() => { 37 | moveSelectedTabToTop(idx); 38 | }} 39 | className={cn( 40 | "relative px-4 py-2 rounded-full opacity-80 hover:opacity-100", 41 | )} 42 | > 43 | {active.value === tab.value && ( 44 | <div 45 | className={cn( 46 | "absolute inset-0 bg-gray-200 dark:bg-zinc-900/90 opacity-100", 47 | )} 48 | /> 49 | )} 50 | 51 | <span 52 | className={cn( 53 | "relative block text-black dark:text-white", 54 | active.value === tab.value 55 | ? "text-opacity-100 font-medium" 56 | : "opacity-40 ", 57 | )} 58 | > 59 | {tab.title} 60 | </span> 61 | </button> 62 | ))} 63 | </div> 64 | <div className="relative w-full h-full"> 65 | {tabs.map((tab, idx) => ( 66 | <div 67 | key={tab.value} 68 | style={{ 69 | scale: 1 - idx * 0.1, 70 | zIndex: -idx, 71 | opacity: idx < 3 ? 1 - idx * 0.1 : 0, 72 | }} 73 | className={cn("h-full", isActive(tab) ? "" : "hidden")} 74 | > 75 | {tab.content} 76 | </div> 77 | ))} 78 | </div> 79 | </> 80 | ); 81 | }; 82 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/twitch.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Twitch 3 | description: Twitch provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Twitch Credentials 9 | To use Twitch sign in, you need a client ID and client secret. You can get them from the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/twitch` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | 21 | export const auth = betterAuth({ 22 | socialProviders: { 23 | twitch: { // [!code highlight] 24 | clientId: process.env.TWITCH_CLIENT_ID as string, // [!code highlight] 25 | clientSecret: process.env.TWITCH_CLIENT_SECRET as string, // [!code highlight] 26 | }, // [!code highlight] 27 | } 28 | }) 29 | ``` 30 | </Step> 31 | <Step> 32 | ### Sign In with Twitch 33 | To sign in with Twitch, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 34 | - `provider`: The provider to use. It should be set to `twitch`. 35 | 36 | ```ts title="auth-client.ts" 37 | import { createAuthClient } from "better-auth/client" 38 | const authClient = createAuthClient() 39 | 40 | const signIn = async () => { 41 | const data = await authClient.signIn.social({ 42 | provider: "twitch" 43 | }) 44 | } 45 | ``` 46 | </Step> 47 | </Steps> 48 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/dashboard.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Card, CardFooter, CardHeader } from "@/components/ui/card"; 4 | import { Text } from "@/components/ui/text"; 5 | import { authClient } from "@/lib/auth-client"; 6 | import { View } from "react-native"; 7 | import Ionicons from "@expo/vector-icons/AntDesign"; 8 | import { router } from "expo-router"; 9 | import { useEffect } from "react"; 10 | import { useStore } from "@nanostores/react"; 11 | 12 | export default function Dashboard() { 13 | const { data: session, isPending } = useStore(authClient.useSession); 14 | useEffect(() => { 15 | if (!session && !isPending) { 16 | router.push("/"); 17 | } 18 | }, [session, isPending]); 19 | return ( 20 | <Card className="w-10/12"> 21 | <CardHeader> 22 | <View className="flex-row items-center gap-2"> 23 | <Avatar alt="user-image"> 24 | <AvatarImage 25 | source={{ 26 | uri: session?.user?.image || "", 27 | }} 28 | /> 29 | <AvatarFallback> 30 | <Text>{session?.user?.name[0]}</Text> 31 | </AvatarFallback> 32 | </Avatar> 33 | <View> 34 | <Text className="font-bold">{session?.user?.name}</Text> 35 | <Text className="text-sm">{session?.user?.email}</Text> 36 | </View> 37 | </View> 38 | </CardHeader> 39 | <CardFooter className="justify-between"> 40 | <Button 41 | variant="default" 42 | size="sm" 43 | className="flex-row items-center gap-2 " 44 | > 45 | <Ionicons name="edit" size={16} color="white" /> 46 | <Text>Edit User</Text> 47 | </Button> 48 | <Button 49 | variant="secondary" 50 | className="flex-row items-center gap-2" 51 | size="sm" 52 | onPress={async () => { 53 | await authClient.signOut({ 54 | fetchOptions: { 55 | onSuccess: () => { 56 | router.push("/"); 57 | }, 58 | }, 59 | }); 60 | }} 61 | > 62 | <Ionicons name="logout" size={14} color="black" /> 63 | <Text>Sign Out</Text> 64 | </Button> 65 | </CardFooter> 66 | </Card> 67 | ); 68 | } 69 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/tabs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = ({ 11 | ref, 12 | className, 13 | ...props 14 | }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & { 15 | ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.List>>; 16 | }) => ( 17 | <TabsPrimitive.List 18 | ref={ref} 19 | className={cn( 20 | "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", 21 | className, 22 | )} 23 | {...props} 24 | /> 25 | ); 26 | TabsList.displayName = TabsPrimitive.List.displayName; 27 | 28 | const TabsTrigger = ({ 29 | ref, 30 | className, 31 | ...props 32 | }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & { 33 | ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.Trigger>>; 34 | }) => ( 35 | <TabsPrimitive.Trigger 36 | ref={ref} 37 | className={cn( 38 | "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", 39 | className, 40 | )} 41 | {...props} 42 | /> 43 | ); 44 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 45 | 46 | const TabsContent = ({ 47 | ref, 48 | className, 49 | ...props 50 | }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & { 51 | ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.Content>>; 52 | }) => ( 53 | <TabsPrimitive.Content 54 | ref={ref} 55 | className={cn( 56 | "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", 57 | className, 58 | )} 59 | {...props} 60 | /> 61 | ); 62 | TabsContent.displayName = TabsPrimitive.Content.displayName; 63 | 64 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 65 | ```