This is page 4 of 49. Use http://codebase.md/better-auth/better-auth?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 │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── sso │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sso.test.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── middleware │ │ │ │ └── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/client.test.ts: -------------------------------------------------------------------------------- ```typescript import { betterAuth } from "../../auth"; import { createAuthClient } from "../../client"; import { inferOrgAdditionalFields, organizationClient } from "./client"; import { organization } from "./organization"; import { describe, it } from "vitest"; describe("organization", () => { const auth = betterAuth({ plugins: [ organization({ schema: { organization: { additionalFields: { newField: { type: "string", }, }, }, }, }), ], }); it("should infer additional fields", async () => { const client = createAuthClient({ plugins: [ organizationClient({ schema: inferOrgAdditionalFields<typeof auth>(), }), ], fetchOptions: { customFetchImpl: async () => new Response(), }, }); client.organization.create({ name: "Test", slug: "test", newField: "123", //this should be allowed //@ts-expect-error - this field is not available unavalibleField: "123", //this should be not allowed }); }); it("should infer filed when schema is provided", () => { const client = createAuthClient({ plugins: [ organizationClient({ schema: inferOrgAdditionalFields({ organization: { additionalFields: { newField: { type: "string", }, }, }, }), }), ], fetchOptions: { customFetchImpl: async () => new Response(), }, }); client.organization.create({ name: "Test", slug: "test", newField: "123", //this should be allowed //@ts-expect-error - this field is not available unavalibleField: "123", //this should be not allowed }); }); }); ``` -------------------------------------------------------------------------------- /e2e/smoke/test/vite.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it } from "node:test"; import { fileURLToPath } from "node:url"; import { join } from "node:path"; import { spawn } from "node:child_process"; import { readFile } from "node:fs/promises"; import * as assert from "node:assert"; const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); describe("(vite) client build", () => { it("builds client without better-call imports", async () => { const viteDir = join(fixturesDir, "vite"); // Run vite build const buildProcess = spawn("npx", ["vite", "build"], { cwd: viteDir, stdio: "pipe", }); // Wait for build to complete await new Promise<void>((resolve, reject) => { buildProcess.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Vite build failed with code ${code}`)); } }); buildProcess.on("error", (error) => { reject(error); }); // Log build output for debugging buildProcess.stdout.on("data", (data) => { console.log(data.toString()); }); buildProcess.stderr.on("data", (data) => { console.error(data.toString()); }); }); const clientFile = join(viteDir, "dist", "client.js"); const clientContent = await readFile(clientFile, "utf-8"); assert.ok( !clientContent.includes("createEndpoint"), "Built output should not contain 'better-call' imports", ); assert.ok( !clientContent.includes("async_hooks"), "Built output should not contain 'async_hooks' imports", ); assert.ok( !clientContent.includes("AsyncLocalStorage"), "Built output should not contain 'AsyncLocalStorage' imports", ); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.pg.test.ts: -------------------------------------------------------------------------------- ```typescript import { testAdapter } from "../../test-adapter"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { prismaAdapter } from "../prisma-adapter"; import { generateAuthConfigFile } from "./generate-auth-config"; import { generatePrismaSchema } from "./generate-prisma-schema"; import { pushPrismaSchema } from "./push-prisma-schema"; import type { BetterAuthOptions } from "@better-auth/core"; import { destroyPrismaClient, getPrismaClient, incrementMigrationCount, } from "./get-prisma-client"; import { Pool } from "pg"; const dialect = "postgresql"; const { execute } = await testAdapter({ adapter: async () => { const db = await getPrismaClient(dialect); return prismaAdapter(db, { provider: dialect, debugLogs: { isRunningAdapterTests: true }, }); }, runMigrations: async (options: BetterAuthOptions) => { const db = await getPrismaClient(dialect); const pgDB = new Pool({ connectionString: "postgres://user:password@localhost:5434/better_auth", }); await pgDB.query(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`); await pgDB.end(); const migrationCount = incrementMigrationCount(); await generateAuthConfigFile(options); await generatePrismaSchema(options, db, migrationCount, dialect); await pushPrismaSchema(dialect); destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); }, tests: [ normalTestSuite(), transactionsTestSuite(), authFlowTestSuite(), numberIdTestSuite(), performanceTestSuite({ dialect }), ], onFinish: async () => {}, prefixTests: "pg", }); execute(); ``` -------------------------------------------------------------------------------- /packages/stripe/src/schema.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; import type { StripeOptions } from "./types"; import { mergeSchema } from "better-auth/db"; export const subscriptions = { subscription: { fields: { plan: { type: "string", required: true, }, referenceId: { type: "string", required: true, }, stripeCustomerId: { type: "string", required: false, }, stripeSubscriptionId: { type: "string", required: false, }, status: { type: "string", defaultValue: "incomplete", }, periodStart: { type: "date", required: false, }, periodEnd: { type: "date", required: false, }, trialStart: { type: "date", required: false, }, trialEnd: { type: "date", required: false, }, cancelAtPeriodEnd: { type: "boolean", required: false, defaultValue: false, }, seats: { type: "number", required: false, }, }, }, } satisfies BetterAuthPluginDBSchema; export const user = { user: { fields: { stripeCustomerId: { type: "string", required: false, }, }, }, } satisfies BetterAuthPluginDBSchema; export const getSchema = (options: StripeOptions) => { let baseSchema = {}; if (options.subscription?.enabled) { baseSchema = { ...subscriptions, ...user, }; } else { baseSchema = { ...user, }; } if ( options.schema && !options.subscription?.enabled && "subscription" in options.schema ) { const { subscription, ...restSchema } = options.schema; return mergeSchema(baseSchema, restSchema); } return mergeSchema(baseSchema, options.schema); }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.sqlite.test.ts: -------------------------------------------------------------------------------- ```typescript import { testAdapter } from "../../test-adapter"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { prismaAdapter } from "../prisma-adapter"; import { generateAuthConfigFile } from "./generate-auth-config"; import { generatePrismaSchema } from "./generate-prisma-schema"; import { pushPrismaSchema } from "./push-prisma-schema"; import type { BetterAuthOptions } from "@better-auth/core"; import { join } from "path"; import fs from "node:fs/promises"; import { destroyPrismaClient, getPrismaClient, incrementMigrationCount, } from "./get-prisma-client"; const dialect = "sqlite"; const { execute } = await testAdapter({ adapter: async () => { const db = await getPrismaClient(dialect); return prismaAdapter(db, { provider: dialect, debugLogs: { isRunningAdapterTests: true }, }); }, runMigrations: async (options: BetterAuthOptions) => { const dbPath = join(import.meta.dirname, "dev.db"); try { await fs.unlink(dbPath); } catch { console.log("db path not found"); } const db = await getPrismaClient(dialect); const migrationCount = incrementMigrationCount(); await generateAuthConfigFile(options); await generatePrismaSchema(options, db, migrationCount, dialect); await pushPrismaSchema(dialect); await db.$disconnect(); destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); }, tests: [ normalTestSuite({}), transactionsTestSuite(), authFlowTestSuite(), numberIdTestSuite({}), performanceTestSuite({ dialect }), ], onFinish: async () => {}, prefixTests: dialect, }); execute(); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/generate-prisma-schema.ts: -------------------------------------------------------------------------------- ```typescript import type { PrismaClient } from "@prisma/client"; import type { BetterAuthOptions } from "@better-auth/core"; import type { DBAdapter } from "@better-auth/core/db/adapter"; import { prismaAdapter } from "../prisma-adapter"; import { join } from "path"; import fs from "fs/promises"; export async function generatePrismaSchema( betterAuthOptions: BetterAuthOptions, db: PrismaClient, iteration: number, dialect: "sqlite" | "postgresql" | "mysql", ) { const i = async (x: string) => await import(x); const { generateSchema } = (await i( "./../../../../../cli/src/generators/index", )) as { generateSchema: (opts: { adapter: DBAdapter<BetterAuthOptions>; file?: string; options: BetterAuthOptions; }) => Promise<{ code: string | undefined; fileName: string; overwrite: boolean | undefined; }>; }; const prismaDB = prismaAdapter(db, { provider: dialect }); let { fileName, code } = await generateSchema({ file: join(import.meta.dirname, `schema-${dialect}.prisma`), adapter: prismaDB({}), options: { ...betterAuthOptions, database: prismaDB }, }); if (dialect === "postgresql") { code = code?.replace( `env("DATABASE_URL")`, '"postgres://user:password@localhost:5434/better_auth"', ); } else if (dialect === "mysql") { code = code?.replace( `env("DATABASE_URL")`, '"mysql://user:password@localhost:3308/better_auth"', ); } code = code ?.split("\n") .map((line, index) => { if (index === 2) { return ( line + `\n output = "./.tmp/prisma-client-${dialect}-${iteration}"` ); } return line; }) .join("\n"); await fs.writeFile(fileName, code || "", "utf-8"); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.mysql.test.ts: -------------------------------------------------------------------------------- ```typescript import { Kysely, MysqlDialect } from "kysely"; import { testAdapter } from "../../test-adapter"; import { kyselyAdapter } from "../kysely-adapter"; import { createPool } from "mysql2/promise"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { getMigrations } from "../../../db"; import { assert } from "vitest"; const mysqlDB = createPool({ uri: "mysql://user:password@localhost:3307/better_auth", timezone: "Z", }); let kyselyDB = new Kysely({ dialect: new MysqlDialect(mysqlDB), }); const { execute } = await testAdapter({ adapter: () => kyselyAdapter(kyselyDB, { type: "mysql", debugLogs: { isRunningAdapterTests: true }, }), async runMigrations(betterAuthOptions) { await mysqlDB.query("DROP DATABASE IF EXISTS better_auth"); await mysqlDB.query("CREATE DATABASE better_auth"); await mysqlDB.query("USE better_auth"); const opts = Object.assign(betterAuthOptions, { database: mysqlDB }); const { runMigrations } = await getMigrations(opts); await runMigrations(); // ensure migrations were run successfully const [tables_result] = (await mysqlDB.query("SHOW TABLES")) as unknown as [ { Tables_in_better_auth: string }[], ]; const tables = tables_result.map((table) => table.Tables_in_better_auth); assert(tables.length > 0, "No tables found"); }, prefixTests: "mysql", tests: [ normalTestSuite(), transactionsTestSuite({ disableTests: { ALL: true } }), authFlowTestSuite(), numberIdTestSuite(), performanceTestSuite({ dialect: "mysql" }), ], async onFinish() { await mysqlDB.end(); }, }); execute(); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "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", { variants: { variant: { default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; } const Button = ({ className, variant, size, asChild = false, ...props }: ButtonProps) => { const Comp = asChild ? Slot : "button"; return ( <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} /> ); }; Button.displayName = "Button"; export { Button, buttonVariants }; ``` -------------------------------------------------------------------------------- /docs/lib/blog.ts: -------------------------------------------------------------------------------- ```typescript import { readFile, readdir } from "fs/promises"; import matter from "gray-matter"; import { join } from "path"; import { cache } from "react"; export interface BlogPost { _id: string; slug: string; title: string; description?: string; date: string; content: string; image?: string; author?: { name: string; avatar?: string; twitter?: string; }; tags?: string[]; } const BLOGS_PATH = join(process.cwd(), "docs/content/blogs"); export const getBlogPost = cache( async (slug: string): Promise<BlogPost | null> => { try { const filePath = join(BLOGS_PATH, `${slug}.mdx`); const source = await readFile(filePath, "utf-8"); const { data, content } = matter(source); return { _id: slug, slug, content, title: data.title, description: data.description, date: data.date, image: data.image, author: data.author, tags: data.tags, }; } catch (error) { return null; } }, ); export const getAllBlogPosts = cache(async (): Promise<BlogPost[]> => { try { const files = await readdir(BLOGS_PATH); const mdxFiles = files.filter((file) => file.endsWith(".mdx")); const posts = await Promise.all( mdxFiles.map(async (file) => { const slug = file.replace(/\.mdx$/, ""); const post = await getBlogPost(slug); return post; }), ); return posts .filter((post): post is BlogPost => post !== null) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); } catch (error) { return []; } }); export function formatBlogDate(date: Date) { let d = new Date(date); return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- ```yaml name: Feature request description: Suggest an idea for this project body: - type: checkboxes attributes: label: Is this suited for github? description: Feel free to join the discord community [here](https://discord.gg/better-auth), we can usually respond faster to any questions. options: - label: Yes, this is suited for github - type: markdown attributes: value: | This template is used for suggesting a feature with better-auth. Bug reports should be opened in [here](https://github.com/better-auth/better-auth/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml). 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. - type: textarea attributes: label: Is your feature request related to a problem? Please describe. description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. validations: required: true - type: textarea attributes: label: Additional context description: Add any other context or screenshots about the feature request here. ``` -------------------------------------------------------------------------------- /docs/components/docs/docs.tsx: -------------------------------------------------------------------------------- ```typescript import type { PageTree } from "fumadocs-core/server"; import { type ReactNode, type HTMLAttributes } from "react"; import { cn } from "../../lib/utils"; import { type BaseLayoutProps } from "./shared"; import { TreeContextProvider } from "fumadocs-ui/provider"; import { NavProvider } from "./layout/nav"; import { type PageStyles, StylesProvider } from "fumadocs-ui/provider"; import ArticleLayout from "../side-bar"; export interface DocsLayoutProps extends BaseLayoutProps { tree: PageTree.Root; containerProps?: HTMLAttributes<HTMLDivElement>; } export function DocsLayout({ children, ...props }: DocsLayoutProps): ReactNode { const variables = cn( "[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] lg:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]", ); const pageStyles: PageStyles = { tocNav: cn("xl:hidden"), toc: cn("max-xl:hidden"), }; return ( <TreeContextProvider tree={props.tree}> <NavProvider> <main id="nd-docs-layout" {...props.containerProps} className={cn( "flex flex-1 flex-row pe-(--fd-layout-offset)", variables, props.containerProps?.className, )} style={ { "--fd-layout-offset": "max(calc(50vw - var(--fd-layout-width) / 2), 0px)", ...props.containerProps?.style, } as object } > <div className={cn( "[--fd-tocnav-height:36px] md:mr-[268px] lg:mr-[286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px] ", )} > <ArticleLayout /> </div> <StylesProvider {...pageStyles}>{children}</StylesProvider> </main> </NavProvider> </TreeContextProvider> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/helper.ts: -------------------------------------------------------------------------------- ```typescript export type Primitive = | string | number | symbol | bigint | boolean | null | undefined; export type LiteralString = "" | (string & Record<never, never>); export type LiteralNumber = 0 | (number & Record<never, never>); export type Awaitable<T> = Promise<T> | T; export type OmitId<T extends { id: unknown }> = Omit<T, "id">; export type Prettify<T> = Omit<T, never>; export type PreserveJSDoc<T> = { [K in keyof T]: T[K]; } & {}; export type PrettifyDeep<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : T[K] extends object ? T[K] extends Array<any> ? T[K] : T[K] extends Date ? T[K] : PrettifyDeep<T[K]> : T[K]; } & {}; export type LiteralUnion<LiteralType, BaseType extends Primitive> = | LiteralType | (BaseType & Record<never, never>); export type UnionToIntersection<U> = ( U extends any ? (k: U) => void : never ) extends (k: infer I) => void ? I : never; export type RequiredKeysOf<BaseType extends object> = Exclude< { [Key in keyof BaseType]: BaseType extends Record<Key, BaseType[Key]> ? Key : never; }[keyof BaseType], undefined >; export type HasRequiredKeys<BaseType extends object> = RequiredKeysOf<BaseType> extends never ? false : true; export type WithoutEmpty<T> = T extends T ? ({} extends T ? never : T) : never; export type StripEmptyObjects<T> = T extends { [K in keyof T]: never } ? never : T extends object ? { [K in keyof T as T[K] extends never ? never : K]: T[K] } : T; export type DeepPartial<T> = T extends Function ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never; ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/icons/google.tsx: -------------------------------------------------------------------------------- ```typescript import Svg, { Path, SvgProps } from "react-native-svg"; export function GoogleIcon(props: SvgProps) { return ( <Svg width="1em" height="1em" viewBox="0 0 128 128"> <Path fill="#fff" 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" ></Path> <Path fill="#e33629" 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" ></Path> <Path fill="#f8bd00" 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" ></Path> <Path fill="#587dbd" 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" ></Path> <Path fill="#319f43" 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" ></Path> </Svg> ); } ``` -------------------------------------------------------------------------------- /demo/expo-example/src/global.css: -------------------------------------------------------------------------------- ```css @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 224 71.4% 4.1%; --card: 0 0% 100%; --card-foreground: 224 71.4% 4.1%; --popover: 0 0% 100%; --popover-foreground: 224 71.4% 4.1%; --primary: 220.9 39.3% 11%; --primary-foreground: 210 20% 98%; --secondary: 220 14.3% 95.9%; --secondary-foreground: 220.9 39.3% 11%; --muted: 220 14.3% 95.9%; --muted-foreground: 220 8.9% 46.1%; --accent: 220 14.3% 95.9%; --accent-foreground: 220.9 39.3% 11%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 20% 98%; --border: 220 13% 91%; --input: 220 13% 91%; --ring: 224 71.4% 4.1%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; } .dark { --background: 224 71.4% 4.1%; --foreground: 210 20% 98%; --card: 224 71.4% 4.1%; --card-foreground: 210 20% 98%; --popover: 224 71.4% 4.1%; --popover-foreground: 210 20% 98%; --primary: 210 20% 98%; --primary-foreground: 220.9 39.3% 11%; --secondary: 215 27.9% 16.9%; --secondary-foreground: 210 20% 98%; --muted: 215 27.9% 16.9%; --muted-foreground: 217.9 10.6% 64.9%; --accent: 215 27.9% 16.9%; --accent-foreground: 210 20% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 20% 98%; --border: 215 27.9% 16.9%; --input: 215 27.9% 16.9%; --ring: 216 12.2% 83.9%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/resizable.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { DragHandleDots2Icon } from "@radix-ui/react-icons"; import * as ResizablePrimitive from "react-resizable-panels"; import { cn } from "@/lib/utils"; const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( <ResizablePrimitive.PanelGroup className={cn( "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className, )} {...props} /> ); const ResizablePanel = ResizablePrimitive.Panel; const ResizableHandle = ({ withHandle, className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { withHandle?: boolean; }) => ( <ResizablePrimitive.PanelResizeHandle className={cn( "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", className, )} {...props} > {withHandle && ( <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border"> <DragHandleDots2Icon className="h-2.5 w-2.5" /> </div> )} </ResizablePrimitive.PanelResizeHandle> ); export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/test/prisma.mysql.test.ts: -------------------------------------------------------------------------------- ```typescript import { testAdapter } from "../../test-adapter"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { prismaAdapter } from "../prisma-adapter"; import { generateAuthConfigFile } from "./generate-auth-config"; import { generatePrismaSchema } from "./generate-prisma-schema"; import { pushPrismaSchema } from "./push-prisma-schema"; import type { BetterAuthOptions } from "@better-auth/core"; import { destroyPrismaClient, getPrismaClient, incrementMigrationCount, } from "./get-prisma-client"; import { createPool } from "mysql2/promise"; const dialect = "mysql"; const { execute } = await testAdapter({ adapter: async () => { const db = await getPrismaClient(dialect); return prismaAdapter(db, { provider: dialect, debugLogs: { isRunningAdapterTests: true }, }); }, runMigrations: async (options: BetterAuthOptions) => { const mysqlDB = createPool({ uri: "mysql://user:password@localhost:3308/better_auth", timezone: "Z", }); await mysqlDB.query("DROP DATABASE IF EXISTS better_auth"); await mysqlDB.query("CREATE DATABASE better_auth"); await mysqlDB.end(); const db = await getPrismaClient(dialect); const migrationCount = incrementMigrationCount(); await generateAuthConfigFile(options); await generatePrismaSchema(options, db, migrationCount, dialect); await pushPrismaSchema(dialect); destroyPrismaClient({ migrationCount: migrationCount - 1, dialect }); }, tests: [ normalTestSuite(), transactionsTestSuite(), authFlowTestSuite(), numberIdTestSuite(), performanceTestSuite({ dialect }), ], onFinish: async () => {}, prefixTests: dialect, }); execute(); ``` -------------------------------------------------------------------------------- /docs/components/ui/tabs.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; import { cn } from "@/lib/utils"; function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { return ( <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} /> ); } function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) { return ( <TabsPrimitive.List data-slot="tabs-list" className={cn( "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-1", className, )} {...props} /> ); } function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { return ( <TabsPrimitive.Trigger data-slot="tabs-trigger" className={cn( "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", className, )} {...props} /> ); } function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { return ( <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} /> ); } export { Tabs, TabsList, TabsTrigger, TabsContent }; ``` -------------------------------------------------------------------------------- /docs/content/blogs/seed-round.mdx: -------------------------------------------------------------------------------- ```markdown --- title: "Announcing our $5M seed round" description: "We raised $5M seed led by Peak XV Partners" date: 2025-06-24 author: name: "Bereket Engida" avatar: "/blogs/bereket.png" twitter: "iambereket" image: "/blogs/seed-round.png" tags: ["seed round", "authentication", "funding"] --- ## Announcing our $5M seed round 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. This funding fuels the next phase of **Better Auth**. 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. 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. On top of the framework, we’re also building the infrastructure to cover the gaps we couldn't cover in the framework: * A unified dashboard to manage users and user analytics * Enterprise-grade security: bot, abuse, and fraud protection * Authentication Email and SMS service * Fast, globally distributed session storage * and more. [Join the waitlist](https://better-auth.build) to get early access to the infrastructure. And if you're excited about making auth accessible - we're hiring! Reach out to [[email protected]](mailto:[email protected]). ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/react/react-store.ts: -------------------------------------------------------------------------------- ```typescript import { listenKeys } from "nanostores"; import { useCallback, useRef, useSyncExternalStore } from "react"; import type { Store, StoreValue } from "nanostores"; import type { DependencyList } from "react"; type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown } ? K : never; export interface UseStoreOptions<SomeStore> { /** * @default * ```ts * [store, options.keys] * ``` */ deps?: DependencyList; /** * Will re-render components only on specific key changes. */ keys?: StoreKeys<SomeStore>[]; } /** * Subscribe to store changes and get store's value. * * Can be used with store builder too. * * ```js * import { useStore } from 'nanostores/react' * * import { router } from '../store/router' * * export const Layout = () => { * let page = useStore(router) * if (page.route === 'home') { * return <HomePage /> * } else { * return <Error404 /> * } * } * ``` * * @param store Store instance. * @returns Store value. */ export function useStore<SomeStore extends Store>( store: SomeStore, options: UseStoreOptions<SomeStore> = {}, ): StoreValue<SomeStore> { let snapshotRef = useRef<StoreValue<SomeStore>>(store.get()); const { keys, deps = [store, keys] } = options; let subscribe = useCallback((onChange: () => void) => { const emitChange = (value: StoreValue<SomeStore>) => { if (snapshotRef.current === value) return; snapshotRef.current = value; onChange(); }; emitChange(store.value); if (keys?.length) { return listenKeys(store as any, keys, emitChange); } return store.listen(emitChange); }, deps); let get = () => snapshotRef.current as StoreValue<SomeStore>; return useSyncExternalStore(subscribe, get, get); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/google-recaptcha.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { middlewareResponse } from "../../../utils/middleware-response"; import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes"; import { encodeToURLParams } from "../utils"; type Params = { siteVerifyURL: string; secretKey: string; captchaResponse: string; minScore?: number; remoteIP?: string; }; type SiteVerifyResponse = { success: boolean; challenge_ts: string; hostname: string; "error-codes": | Array< | "missing-input-secret" | "invalid-input-secret" | "missing-input-response" | "invalid-input-response" | "bad-request" | "timeout-or-duplicate" > | undefined; }; type SiteVerifyV3Response = SiteVerifyResponse & { score: number; }; const isV3 = ( response: SiteVerifyResponse | SiteVerifyV3Response, ): response is SiteVerifyV3Response => { return "score" in response && typeof response.score === "number"; }; export const googleRecaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, minScore = 0.5, remoteIP, }: Params) => { const response = await betterFetch<SiteVerifyResponse | SiteVerifyV3Response>( siteVerifyURL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: encodeToURLParams({ secret: secretKey, response: captchaResponse, ...(remoteIP && { remoteip: remoteIP }), }), }, ); if (!response.data || response.error) { throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE); } if ( !response.data.success || (isV3(response.data) && response.data.score < minScore) ) { return middlewareResponse({ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED, status: 403, }); } return undefined; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/lynx/lynx-store.ts: -------------------------------------------------------------------------------- ```typescript import { listenKeys } from "nanostores"; import { useCallback, useRef, useSyncExternalStore } from "@lynx-js/react"; import type { Store, StoreValue } from "nanostores"; import type { DependencyList } from "@lynx-js/react"; type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown } ? K : never; export interface UseStoreOptions<SomeStore> { /** * @default * ```ts * [store, options.keys] * ``` */ deps?: DependencyList; /** * Will re-render components only on specific key changes. */ keys?: StoreKeys<SomeStore>[]; } /** * Subscribe to store changes and get store's value. * * Can be used with store builder too. * * ```js * import { useStore } from 'nanostores/react' * * import { router } from '../store/router' * * export const Layout = () => { * let page = useStore(router) * if (page.route === 'home') { * return <HomePage /> * } else { * return <Error404 /> * } * } * ``` * * @param store Store instance. * @returns Store value. */ export function useStore<SomeStore extends Store>( store: SomeStore, options: UseStoreOptions<SomeStore> = {}, ): StoreValue<SomeStore> { let snapshotRef = useRef<StoreValue<SomeStore>>(store.get()); const { keys, deps = [store, keys] } = options; let subscribe = useCallback((onChange: () => void) => { const emitChange = (value: StoreValue<SomeStore>) => { if (snapshotRef.current === value) return; snapshotRef.current = value; onChange(); }; emitChange(store.value); if (keys?.length) { return listenKeys(store as any, keys, emitChange); } return store.listen(emitChange); }, deps); let get = () => snapshotRef.current as StoreValue<SomeStore>; return useSyncExternalStore(subscribe, get, get); } ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/vk.mdx: -------------------------------------------------------------------------------- ```markdown --- title: VK description: VK ID Provider --- <Steps> <Step> ### Get your VK ID credentials 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). 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { vk: { // [!code highlight] clientId: process.env.VK_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.VK_CLIENT_SECRET as string, // [!code highlight] }, }, }); ``` </Step> <Step> ### Sign In with VK 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: - `provider`: The provider to use. It should be set to `vk`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; const authClient = createAuthClient(); const signIn = async () => { const data = await authClient.signIn.social({ provider: "vk", }); }; ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/h-captcha.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { middlewareResponse } from "../../../utils/middleware-response"; import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes"; import { encodeToURLParams } from "../utils"; type Params = { siteVerifyURL: string; secretKey: string; captchaResponse: string; siteKey?: string; remoteIP?: string; }; type SiteVerifyResponse = { success: boolean; challenge_ts: number; hostname: string; credit: true | false | undefined; "error-codes": | Array< | "missing-input-secret" | "invalid-input-secret" | "missing-input-response" | "invalid-input-response" | "expired-input-response" | "already-seen-response" | "bad-request" | "missing-remoteip" | "invalid-remoteip" | "not-using-dummy-passcode" | "sitekey-secret-mismatch" > | undefined; score: number | undefined; // ENTERPRISE feature: a score denoting malicious activity. score_reason: Array<unknown> | undefined; // ENTERPRISE feature: reason(s) for score. }; export const hCaptcha = async ({ siteVerifyURL, captchaResponse, secretKey, siteKey, remoteIP, }: Params) => { const response = await betterFetch<SiteVerifyResponse>(siteVerifyURL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: encodeToURLParams({ secret: secretKey, response: captchaResponse, ...(siteKey && { sitekey: siteKey }), ...(remoteIP && { remoteip: remoteIP }), }), }); if (!response.data || response.error) { throw new Error(INTERNAL_ERROR_CODES.SERVICE_UNAVAILABLE); } if (!response.data.success) { return middlewareResponse({ message: EXTERNAL_ERROR_CODES.VERIFICATION_FAILED, status: 403, }); } return undefined; }; ``` -------------------------------------------------------------------------------- /docs/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { cn } from "@/lib/utils"; function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> ); } function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> ); } function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; } function TooltipContent({ className, sideOffset = 0, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "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", className, )} {...props} > {children} <TooltipPrimitive.Arrow className="-z-10 relative bg-primary fill-primary size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> ); } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/bearer/bearer.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; describe("bearer", async () => { const { client, auth, testUser } = await getTestInstance( {}, { disableTestUser: true, }, ); let token: string; it("should get session", async () => { await client.signUp.email( { email: testUser.email, password: testUser.password, name: testUser.name, }, { onSuccess: (ctx) => { token = ctx.response.headers.get("set-auth-token") || ""; }, }, ); const session = await client.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${token}`, }, }, }); expect(session.data?.session).toBeDefined(); }); it("should list session", async () => { const sessions = await client.listSessions({ fetchOptions: { headers: { Authorization: `Bearer ${token}`, }, }, }); expect(sessions.data).toHaveLength(1); }); it("should work on server actions", async () => { const session = await auth.api.getSession({ headers: new Headers({ authorization: `Bearer ${token}`, }), }); expect(session?.session).toBeDefined(); }); it("should work with ", async () => { const session = await client.getSession({ fetchOptions: { headers: { authorization: `Bearer ${token.split(".")[0]}`, }, }, }); expect(session.data?.session).toBeDefined(); }); it("should work if valid cookie is provided even if authorization header isn't valid", async () => { const session = await client.getSession({ fetchOptions: { headers: { Authorization: `Bearer invalid.token`, cookie: `better-auth.session_token=${token}`, }, }, }); expect(session.data?.session).toBeDefined(); }); }); ``` -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/cli", "version": "1.4.0-beta.10", "type": "module", "description": "The CLI for Better Auth", "module": "dist/index.js", "repository": { "type": "git", "url": "https://github.com/better-auth/better-auth", "directory": "packages/cli" }, "homepage": "https://www.better-auth.com/docs/concepts/cli", "main": "./dist/index.js", "scripts": { "build": "tsdown", "start": "node ./dist/index.js", "dev": "tsx ./src/index.ts", "test": "vitest", "typecheck": "tsc --project tsconfig.json" }, "publishConfig": { "access": "public", "executableFiles": [ "./dist/index.js" ] }, "license": "MIT", "keywords": [ "auth", "cli", "typescript", "better-auth" ], "exports": "./dist/index.js", "bin": "./dist/index.js", "devDependencies": { "@types/semver": "^7.7.1", "tsx": "^4.20.5", "typescript": "catalog:", "tsdown": "catalog:" }, "dependencies": { "@babel/core": "^7.28.4", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@better-auth/utils": "0.3.0", "@clack/prompts": "^0.11.0", "@mrleebo/prisma-ast": "^0.13.0", "@prisma/client": "^5.22.0", "@types/better-sqlite3": "^7.6.13", "@types/prompts": "^2.4.9", "better-auth": "workspace:*", "better-sqlite3": "^12.2.0", "c12": "^3.2.0", "chalk": "^5.6.2", "commander": "^12.1.0", "dotenv": "^17.2.2", "drizzle-orm": "^0.33.0", "get-tsconfig": "^4.10.1", "jiti": "^2.6.0", "open": "^10.2.0", "prettier": "^3.6.2", "prisma": "^5.22.0", "prompts": "^2.4.2", "semver": "^7.7.2", "tinyexec": "^0.3.2", "yocto-spinner": "^0.2.3", "zod": "^4.1.5" }, "files": [ "dist" ] } ``` -------------------------------------------------------------------------------- /packages/core/src/types/plugin-client.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPlugin } from "./plugin"; import type { BetterFetch, BetterFetchOption, BetterFetchPlugin, } from "@better-fetch/fetch"; import type { LiteralString } from "./helper"; import type { BetterAuthOptions } from "./init-options"; import type { WritableAtom, Atom } from "nanostores"; export interface ClientStore { notify: (signal: string) => void; listen: (signal: string, listener: () => void) => void; atoms: Record<string, WritableAtom<any>>; } export type ClientAtomListener = { matcher: (path: string) => boolean; signal: "$sessionSignal" | Omit<string, "$sessionSignal">; }; export interface BetterAuthClientOptions { fetchOptions?: BetterFetchOption; plugins?: BetterAuthClientPlugin[]; baseURL?: string; basePath?: string; disableDefaultFetchPlugins?: boolean; $InferAuth?: BetterAuthOptions; } export interface BetterAuthClientPlugin { id: LiteralString; /** * only used for type inference. don't pass the * actual plugin */ $InferServerPlugin?: BetterAuthPlugin; /** * Custom actions */ getActions?: ( $fetch: BetterFetch, $store: ClientStore, /** * better-auth client options */ options: BetterAuthClientOptions | undefined, ) => Record<string, any>; /** * State atoms that'll be resolved by each framework * auth store. */ getAtoms?: ($fetch: BetterFetch) => Record<string, Atom<any>>; /** * specify path methods for server plugin inferred * endpoints to force a specific method. */ pathMethods?: Record<string, "POST" | "GET">; /** * Better fetch plugins */ fetchPlugins?: BetterFetchPlugin[]; /** * a list of recaller based on a matcher function. * The signal name needs to match a signal in this * plugin or any plugin the user might have added. */ atomListeners?: ClientAtomListener[]; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { cn } from "@/lib/utils"; function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> ); } function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> ); } function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; } function TooltipContent({ className, sideOffset = 0, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "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", className, )} {...props} > {children} <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]" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> ); } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; ``` -------------------------------------------------------------------------------- /docs/components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; import { type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; import { toggleVariants } from "@/components/ui/toggle"; const ToggleGroupContext = React.createContext< VariantProps<typeof toggleVariants> >({ size: "default", variant: "default", }); function ToggleGroup({ className, variant, size, children, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>) { return ( <ToggleGroupPrimitive.Root data-slot="toggle-group" data-variant={variant} data-size={size} className={cn( "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs", className, )} {...props} > <ToggleGroupContext.Provider value={{ variant, size }}> {children} </ToggleGroupContext.Provider> </ToggleGroupPrimitive.Root> ); } function ToggleGroupItem({ className, children, variant, size, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>) { const context = React.useContext(ToggleGroupContext); return ( <ToggleGroupPrimitive.Item data-slot="toggle-group-item" data-variant={context.variant || variant} data-size={context.size || size} className={cn( toggleVariants({ variant: context.variant || variant, size: context.size || size, }), "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", className, )} {...props} > {children} </ToggleGroupPrimitive.Item> ); } export { ToggleGroup, ToggleGroupItem }; ``` -------------------------------------------------------------------------------- /docs/components/ui/callout.tsx: -------------------------------------------------------------------------------- ```typescript import { CircleCheck, CircleX, Info, TriangleAlert } from "lucide-react"; import { forwardRef, type HTMLAttributes, type ReactNode } from "react"; import { cn } from "@/lib/utils"; import { cva } from "class-variance-authority"; type CalloutProps = Omit< HTMLAttributes<HTMLDivElement>, "title" | "type" | "icon" > & { title?: ReactNode; /** * @defaultValue info */ type?: "info" | "warn" | "error" | "success" | "warning"; /** * Force an icon */ icon?: ReactNode; }; const calloutVariants = cva( "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", { variants: { type: { info: "border-s-blue-500/50", warn: "border-s-orange-500/50", error: "border-s-red-500/50", success: "border-s-green-500/50", }, }, }, ); export const Callout = forwardRef<HTMLDivElement, CalloutProps>( ({ className, children, title, type = "info", icon, ...props }, ref) => { if (type === "warning") type = "warn"; return ( <div ref={ref} className={cn( calloutVariants({ type: type, }), className, )} {...props} > {icon ?? { info: <Info className="size-5 fill-blue-500 text-fd-card" />, warn: ( <TriangleAlert className="size-5 fill-orange-500 text-fd-card" /> ), error: <CircleX className="size-5 fill-red-500 text-fd-card" />, success: ( <CircleCheck className="size-5 fill-green-500 text-fd-card" /> ), }[type]} <div className="min-w-0 flex flex-col gap-2 flex-1"> {title ? <p className="font-medium !my-0">{title}</p> : null} <div className="text-fd-muted-foreground prose-no-margin empty:hidden"> {children} </div> </div> </div> ); }, ); Callout.displayName = "Callout"; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/last-login-method/client.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthClientPlugin } from "@better-auth/core"; /** * Configuration for the client-side last login method plugin */ export interface LastLoginMethodClientConfig { /** * Name of the cookie to read the last login method from * @default "better-auth.last_used_login_method" */ cookieName?: string; } function getCookieValue(name: string): string | null { if (typeof document === "undefined") { return null; } const cookie = document.cookie .split("; ") .find((row) => row.startsWith(`${name}=`)); return cookie ? cookie.split("=")[1]! : null; } /** * Client-side plugin to retrieve the last used login method */ export const lastLoginMethodClient = ( config: LastLoginMethodClientConfig = {}, ) => { const cookieName = config.cookieName || "better-auth.last_used_login_method"; return { id: "last-login-method-client", getActions() { return { /** * Get the last used login method from cookies * @returns The last used login method or null if not found */ getLastUsedLoginMethod: (): string | null => { return getCookieValue(cookieName); }, /** * Clear the last used login method cookie * This sets the cookie with an expiration date in the past */ clearLastUsedLoginMethod: (): void => { if (typeof document !== "undefined") { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } }, /** * Check if a specific login method was the last used * @param method The method to check * @returns True if the method was the last used, false otherwise */ isLastUsedLoginMethod: (method: string): boolean => { const lastMethod = getCookieValue(cookieName); return lastMethod === method; }, }; }, } satisfies BetterAuthClientPlugin; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/naver.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Naver description: Naver provider setup and usage. --- <Steps> <Step> ### Get your Naver Credentials 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/). 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { naver: { // [!code highlight] clientId: process.env.NAVER_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.NAVER_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] } }) ``` </Step> <Step> ### Sign In with Naver 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: - `provider`: The provider to use. It should be set to `naver`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "naver" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/kakao.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Kakao description: Kakao provider setup and usage. --- <Steps> <Step> ### Get your Kakao Credentials 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). 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { kakao: { // [!code highlight] clientId: process.env.KAKAO_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.KAKAO_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] } }) ``` </Step> <Step> ### Sign In with Kakao 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: - `provider`: The provider to use. It should be set to `kakao`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "kakao" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/components/resource-section.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { cn } from "@/lib/utils"; import { useState } from "react"; import { ResourceCard } from "./resource-card"; interface Resource { title: string; description: string; href: string; tags: string[]; } interface ResourceProps { resources: Resource[]; className?: string; } export function Resource({ className, resources }: ResourceProps) { const [activeTag, setActiveTag] = useState<string | null>(null); const tags = Array.from( new Set(resources.flatMap((resource) => resource.tags)), ); const filterResources = (activeTag: string | null): Resource[] => { if (!activeTag) return resources; return resources.filter((resource) => resource.tags.includes(activeTag)); }; return ( <div> <div className={cn("space-y-4", className)}> <div className="flex flex-wrap gap-2"> <button onClick={() => setActiveTag(null)} className={cn( "inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors", activeTag === null ? "bg-primary text-primary-foreground" : "bg-secondary/10 text-secondary-foreground hover:bg-secondary/20", )} > All </button> {tags.map((tag) => ( <button key={tag} onClick={() => setActiveTag(tag)} className={cn( "inline-flex items-center rounded-md px-3 py-1 text-sm font-medium transition-colors", activeTag === tag ? "bg-primary text-primary-foreground" : "bg-secondary/10 text-secondary-foreground hover:bg-secondary/20", )} > {tag} </button> ))} </div> </div> <div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-2"> {filterResources(activeTag).map((resource) => ( <ResourceCard key={resource.href} {...resource} /> ))} </div> </div> ); } ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/nav.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import Link, { type LinkProps } from "fumadocs-core/link"; import { createContext, type ReactNode, useContext, useEffect, useMemo, useState, } from "react"; import { cn } from "../../../lib/utils"; import { useI18n } from "fumadocs-ui/provider"; export interface NavProviderProps { /** * Use transparent background * * @defaultValue none */ transparentMode?: "always" | "top" | "none"; } export interface TitleProps { title?: ReactNode; /** * Redirect url of title * @defaultValue '/' */ url?: string; } interface NavContextType { isTransparent: boolean; } const NavContext = createContext<NavContextType>({ isTransparent: false, }); export function NavProvider({ transparentMode = "none", children, }: NavProviderProps & { children: ReactNode }) { const [transparent, setTransparent] = useState(transparentMode !== "none"); useEffect(() => { if (transparentMode !== "top") return; const listener = () => { if (document.documentElement.hasAttribute("data-anchor-scrolling")) { return; } setTransparent(window.scrollY < 10); }; listener(); window.addEventListener("scroll", listener, { passive: true }); return () => { window.removeEventListener("scroll", listener); }; }, [transparentMode]); return ( <NavContext.Provider value={useMemo(() => ({ isTransparent: transparent }), [transparent])} > {children} </NavContext.Provider> ); } export function useNav(): NavContextType { return useContext(NavContext); } export function Title({ title, url, ...props }: TitleProps & Omit<LinkProps, "title">) { const { locale } = useI18n(); return ( <Link href={url ?? (locale ? `/${locale}` : "/")} {...props} className={cn( "inline-flex items-center gap-2.5 font-semibold", props.className, )} > {title} </Link> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/get-tables.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getAuthTables } from "./get-tables"; describe("getAuthTables", () => { it("should use correct field name for refreshTokenExpiresAt", () => { const tables = getAuthTables({ account: { fields: { refreshTokenExpiresAt: "custom_refresh_token_expires_at", }, }, }); const accountTable = tables.account; const refreshTokenExpiresAtField = accountTable!.fields.refreshTokenExpiresAt!; expect(refreshTokenExpiresAtField.fieldName).toBe( "custom_refresh_token_expires_at", ); }); it("should not use accessTokenExpiresAt field name for refreshTokenExpiresAt", () => { const tables = getAuthTables({ account: { fields: { accessTokenExpiresAt: "custom_access_token_expires_at", refreshTokenExpiresAt: "custom_refresh_token_expires_at", }, }, }); const accountTable = tables.account; const refreshTokenExpiresAtField = accountTable!.fields.refreshTokenExpiresAt!; const accessTokenExpiresAtField = accountTable!.fields.accessTokenExpiresAt!; expect(refreshTokenExpiresAtField.fieldName).toBe( "custom_refresh_token_expires_at", ); expect(accessTokenExpiresAtField.fieldName).toBe( "custom_access_token_expires_at", ); expect(refreshTokenExpiresAtField.fieldName).not.toBe( accessTokenExpiresAtField.fieldName, ); }); it("should use default field names when no custom names provided", () => { const tables = getAuthTables({}); const accountTable = tables.account; const refreshTokenExpiresAtField = accountTable!.fields.refreshTokenExpiresAt!; const accessTokenExpiresAtField = accountTable!.fields.accessTokenExpiresAt!; expect(refreshTokenExpiresAtField.fieldName).toBe("refreshTokenExpiresAt"); expect(accessTokenExpiresAtField.fieldName).toBe("accessTokenExpiresAt"); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/kick.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Kick description: Kick provider setup and usage. --- <Steps> <Step> ### Get your Kick Credentials 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). 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { kick: { // [!code highlight] clientId: process.env.KICK_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.KICK_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] } }) ``` </Step> <Step> ### Sign In with Kick 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: - `provider`: The provider to use. It should be set to `kick`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "kick" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/components/ui/slider.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SliderPrimitive from "@radix-ui/react-slider"; import { cn } from "@/lib/utils"; function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: React.ComponentProps<typeof SliderPrimitive.Root>) { const _values = React.useMemo( () => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max], [value, defaultValue, min, max], ); return ( <SliderPrimitive.Root data-slot="slider" defaultValue={defaultValue} value={value} min={min} max={max} className={cn( "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", className, )} {...props} > <SliderPrimitive.Track data-slot="slider-track" className={cn( "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", )} > <SliderPrimitive.Range data-slot="slider-range" className={cn( "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full", )} /> </SliderPrimitive.Track> {Array.from({ length: _values.length }, (_, index) => ( <SliderPrimitive.Thumb data-slot="slider-thumb" key={index} 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" /> ))} </SliderPrimitive.Root> ); } export { Slider }; ``` -------------------------------------------------------------------------------- /docs/components/docs/ui/scroll-area.tsx: -------------------------------------------------------------------------------- ```typescript import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; import * as React from "react"; import { cn } from "../../../lib/utils"; const ScrollArea = React.forwardRef< React.ComponentRef<typeof ScrollAreaPrimitive.Root>, React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> >(({ className, children, ...props }, ref) => ( <ScrollAreaPrimitive.Root ref={ref} className={cn("overflow-hidden", className)} {...props} > {children} <ScrollAreaPrimitive.Corner /> <ScrollBar orientation="vertical" /> </ScrollAreaPrimitive.Root> )); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollViewport = React.forwardRef< React.ComponentRef<typeof ScrollAreaPrimitive.Viewport>, React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> >(({ className, children, ...props }, ref) => ( <ScrollAreaPrimitive.Viewport ref={ref} className={cn("size-full rounded-[inherit]", className)} {...props} > {children} </ScrollAreaPrimitive.Viewport> )); ScrollViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; const ScrollBar = React.forwardRef< React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>, React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar> >(({ className, orientation = "vertical", ...props }, ref) => ( <ScrollAreaPrimitive.Scrollbar ref={ref} orientation={orientation} className={cn( "flex select-none data-[state=hidden]:animate-fd-fade-out", orientation === "vertical" && "h-full w-1.5", orientation === "horizontal" && "h-1.5 flex-col", className, )} {...props} > <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-fd-border" /> </ScrollAreaPrimitive.Scrollbar> )); ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName; export { ScrollArea, ScrollBar, ScrollViewport }; ``` -------------------------------------------------------------------------------- /e2e/smoke/test/ssr.ts: -------------------------------------------------------------------------------- ```typescript import { betterAuth } from "better-auth"; import { describe, test } from "node:test"; import { DatabaseSync } from "node:sqlite"; import { apiKey } from "better-auth/plugins"; import { createAuthClient } from "better-auth/client"; import { apiKeyClient } from "better-auth/client/plugins"; import { getMigrations } from "better-auth/db"; import assert from "node:assert/strict"; describe("server side client", () => { test("can use api key on server side", async () => { const database = new DatabaseSync(":memory:"); const auth = betterAuth({ baseURL: "http://localhost:3000", database, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, }, }, emailAndPassword: { enabled: true, }, plugins: [ apiKey({ rateLimit: { enabled: false, }, }), ], }); const { runMigrations } = await getMigrations(auth.options); await runMigrations(); const authClient: ReturnType<typeof createAuthClient> = createAuthClient({ baseURL: "http://localhost:3000", plugins: [apiKeyClient()], fetchOptions: { customFetchImpl: async (url, init) => { return auth.handler(new Request(url, init)); }, }, }); const { user } = await auth.api.signUpEmail({ body: { name: "Alex", email: "[email protected]", password: "hello123", }, }); const { key, id, userId } = await auth.api.createApiKey({ body: { name: "my-api-key", userId: user.id, }, }); const ret = database.prepare(`SELECT * FROM apiKey;`).all(); assert.equal(ret.length, 1); const first = ret.at(-1)!; assert.equal(first.id, id); assert.equal(first.userId, userId); await authClient.getSession({ fetchOptions: { headers: { "x-api-key": key, }, }, }); }); }); ``` -------------------------------------------------------------------------------- /docs/components/builder/tabs.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useState } from "react"; import { cn } from "@/lib/utils"; type Tab = { title: string; value: string; content?: string | React.ReactNode | any; }; export const AuthTabs = ({ tabs: propTabs }: { tabs: Tab[] }) => { const [active, setActive] = useState<Tab>(propTabs[0]); const [tabs, setTabs] = useState<Tab[]>(propTabs); const isActive = (tab: Tab) => { return tab.value === tabs[0].value; }; const moveSelectedTabToTop = (idx: number) => { const newTabs = [...propTabs]; const selectedTab = newTabs.splice(idx, 1); newTabs.unshift(selectedTab[0]); setTabs(newTabs); setActive(newTabs[0]); }; return ( <> <div className={cn( "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", )} > {propTabs.map((tab, idx) => ( <button key={tab.title} onClick={() => { moveSelectedTabToTop(idx); }} className={cn( "relative px-4 py-2 rounded-full opacity-80 hover:opacity-100", )} > {active.value === tab.value && ( <div className={cn( "absolute inset-0 bg-gray-200 dark:bg-zinc-900/90 opacity-100", )} /> )} <span className={cn( "relative block text-black dark:text-white", active.value === tab.value ? "text-opacity-100 font-medium" : "opacity-40 ", )} > {tab.title} </span> </button> ))} </div> <div className="relative w-full h-full"> {tabs.map((tab, idx) => ( <div key={tab.value} style={{ scale: 1 - idx * 0.1, zIndex: -idx, opacity: idx < 3 ? 1 - idx * 0.1 : 0, }} className={cn("h-full", isActive(tab) ? "" : "hidden")} > {tab.content} </div> ))} </div> </> ); }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/twitch.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Twitch description: Twitch provider setup and usage. --- <Steps> <Step> ### Get your Twitch Credentials 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). 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { twitch: { // [!code highlight] clientId: process.env.TWITCH_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.TWITCH_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] } }) ``` </Step> <Step> ### Sign In with Twitch 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: - `provider`: The provider to use. It should be set to `twitch`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "twitch" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/dashboard.tsx: -------------------------------------------------------------------------------- ```typescript import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader } from "@/components/ui/card"; import { Text } from "@/components/ui/text"; import { authClient } from "@/lib/auth-client"; import { View } from "react-native"; import Ionicons from "@expo/vector-icons/AntDesign"; import { router } from "expo-router"; import { useEffect } from "react"; import { useStore } from "@nanostores/react"; export default function Dashboard() { const { data: session, isPending } = useStore(authClient.useSession); useEffect(() => { if (!session && !isPending) { router.push("/"); } }, [session, isPending]); return ( <Card className="w-10/12"> <CardHeader> <View className="flex-row items-center gap-2"> <Avatar alt="user-image"> <AvatarImage source={{ uri: session?.user?.image || "", }} /> <AvatarFallback> <Text>{session?.user?.name[0]}</Text> </AvatarFallback> </Avatar> <View> <Text className="font-bold">{session?.user?.name}</Text> <Text className="text-sm">{session?.user?.email}</Text> </View> </View> </CardHeader> <CardFooter className="justify-between"> <Button variant="default" size="sm" className="flex-row items-center gap-2 " > <Ionicons name="edit" size={16} color="white" /> <Text>Edit User</Text> </Button> <Button variant="secondary" className="flex-row items-center gap-2" size="sm" onPress={async () => { await authClient.signOut({ fetchOptions: { onSuccess: () => { router.push("/"); }, }, }); }} > <Ionicons name="logout" size={14} color="black" /> <Text>Sign Out</Text> </Button> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/tabs.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; import { cn } from "@/lib/utils"; const Tabs = TabsPrimitive.Root; const TabsList = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & { ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.List>>; }) => ( <TabsPrimitive.List ref={ref} className={cn( "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className, )} {...props} /> ); TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & { ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.Trigger>>; }) => ( <TabsPrimitive.Trigger ref={ref} className={cn( "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", className, )} {...props} /> ); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & { ref: React.RefObject<React.ElementRef<typeof TabsPrimitive.Content>>; }) => ( <TabsPrimitive.Content ref={ref} className={cn( "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className, )} {...props} /> ); TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsList, TabsTrigger, TabsContent }; ``` -------------------------------------------------------------------------------- /docs/components/ui/background-boxes.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import React from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; export const BoxesCore = ({ className, ...rest }: { className?: string }) => { const rows = new Array(150).fill(1); const cols = new Array(100).fill(1); let colors = [ "--sky-300", "--pink-300", "--green-300", "--yellow-300", "--red-300", "--purple-300", "--blue-300", "--indigo-300", "--violet-300", ]; const getRandomColor = () => { return colors[Math.floor(Math.random() * colors.length)]; }; return ( <div style={{ transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`, }} className={cn( "absolute left-1/4 p-4 -top-1/4 flex -translate-x-1/2 -translate-y-1/2 w-full h-full z-0 ", className, )} {...rest} > {rows.map((_, i) => ( <motion.div key={`row` + i} className="w-16 h-8 border-l border-slate-700 relative" > {cols.map((_, j) => ( <motion.div whileHover={{ backgroundColor: `var(${getRandomColor()})`, transition: { duration: 0 }, }} animate={{ transition: { duration: 2 }, }} key={`col` + j} className="w-16 h-8 border-r border-t border-slate-700 relative" > {j % 2 === 0 && i % 2 === 0 ? ( <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="absolute h-6 w-10 -top-[14px] -left-[22px] text-slate-700 stroke-[1px] pointer-events-none" > <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" /> </svg> ) : null} </motion.div> ))} </motion.div> ))} </div> ); }; export const Boxes = React.memo(BoxesCore); ``` -------------------------------------------------------------------------------- /packages/expo/package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/expo", "version": "1.4.0-beta.10", "type": "module", "description": "Better Auth integration for Expo and React Native applications.", "main": "dist/index.js", "module": "dist/index.js", "repository": { "type": "git", "url": "https://github.com/better-auth/better-auth", "directory": "packages/expo" }, "homepage": "https://www.better-auth.com/docs/integrations/expo", "scripts": { "test": "vitest", "build": "tsdown", "dev": "tsdown --watch", "typecheck": "tsc --project tsconfig.json" }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js", "require": "./dist/client.cjs" } }, "typesVersions": { "*": { "*": [ "./dist/index.d.ts" ], "client": [ "./dist/client.d.ts" ] } }, "keywords": [ "auth", "expo", "react-native", "typescript", "better-auth" ], "publishConfig": { "access": "public" }, "license": "MIT", "devDependencies": { "@better-fetch/fetch": "catalog:", "@types/better-sqlite3": "^7.6.13", "better-auth": "workspace:*", "better-sqlite3": "^12.2.0", "expo-constants": "~17.1.7", "expo-crypto": "^13.0.2", "expo-linking": "~7.1.7", "expo-secure-store": "~14.2.3", "expo-web-browser": "~14.2.0", "react-native": "~0.80.2", "tsdown": "catalog:" }, "peerDependencies": { "better-auth": "workspace:*", "expo-constants": ">=17.0.0", "expo-crypto": ">=13.0.0", "expo-linking": ">=7.0.0", "expo-secure-store": ">=14.0.0", "expo-web-browser": ">=14.0.0" }, "dependencies": { "@better-fetch/fetch": "catalog:", "zod": "^4.1.5" }, "files": [ "dist" ] } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/url.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { createAuthClient } from "./vanilla"; import { testClientPlugin } from "./test-plugin"; describe("url", () => { it("should not require base url", async () => { const client = createAuthClient({ plugins: [testClientPlugin()], baseURL: "", fetchOptions: { customFetchImpl: async (url, init) => { return new Response(JSON.stringify({ hello: "world" })); }, }, }); const response = await client.test(); expect(response.data).toEqual({ hello: "world" }); }); it("should use base url and append `/api/auth` by default", async () => { const client = createAuthClient({ plugins: [testClientPlugin()], baseURL: "http://localhost:3000", fetchOptions: { customFetchImpl: async (url, init) => { return new Response(JSON.stringify({ url })); }, }, }); const response = await client.test(); expect(response.data).toEqual({ url: "http://localhost:3000/api/auth/test", }); }); it("should use base url and use the provider path if provided", async () => { const client = createAuthClient({ plugins: [testClientPlugin()], baseURL: "http://localhost:3000/auth", fetchOptions: { customFetchImpl: async (url, init) => { return new Response(JSON.stringify({ url })); }, }, }); const response = await client.test(); expect(response.data).toEqual({ url: "http://localhost:3000/auth/test", }); }); it("should use be able to detect `/` in the base url", async () => { const client = createAuthClient({ plugins: [testClientPlugin()], baseURL: "http://localhost:3000", basePath: "/", fetchOptions: { customFetchImpl: async (url, init) => { return new Response(JSON.stringify({ url })); }, }, }); const response = await client.test(); expect(response.data).toEqual({ url: "http://localhost:3000/test", }); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { haveIBeenPwned } from "./index"; describe("have-i-been-pwned", async () => { const { client, auth } = await getTestInstance( { plugins: [haveIBeenPwned()], }, { disableTestUser: true, }, ); const ctx = await auth.$context; it("should prevent account creation with compromised password", async () => { const uniqueEmail = `test-${Date.now()}@example.com`; const compromisedPassword = "123456789"; const result = await client.signUp.email({ email: uniqueEmail, password: compromisedPassword, name: "Test User", }); const user = await ctx.internalAdapter.findUserByEmail(uniqueEmail); expect(user).toBeNull(); expect(result.error).not.toBeNull(); expect(result.error?.status).toBe(400); expect(result.error?.code).toBe("PASSWORD_COMPROMISED"); }); it("should allow account creation with strong, uncompromised password", async () => { const uniqueEmail = `test-${Date.now()}@example.com`; const strongPassword = `Str0ng!P@ssw0rd-${Date.now()}`; const result = await client.signUp.email({ email: uniqueEmail, password: strongPassword, name: "Test User", }); expect(result.data?.user).toBeDefined(); }); it("should prevent password update to compromised password", async () => { const uniqueEmail = `test-${Date.now()}@example.com`; const initialPassword = `Str0ng!P@ssw0rd-${Date.now()}`; const res = await client.signUp.email({ email: uniqueEmail, password: initialPassword, name: "Test User", }); const result = await client.changePassword( { currentPassword: initialPassword, newPassword: "123456789", }, { headers: { authorization: `Bearer ${res.data?.token}`, }, }, ); expect(result.error).toBeDefined(); expect(result.error?.status).toBe(400); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/shim.ts: -------------------------------------------------------------------------------- ```typescript import type { AuthContext } from "@better-auth/core"; export const shimContext = <T extends Record<string, any>>( originalObject: T, newContext: Record<string, any>, ) => { const shimmedObj: Record<string, any> = {}; for (const [key, value] of Object.entries(originalObject)) { shimmedObj[key] = (ctx: Record<string, any>) => { return value({ ...ctx, context: { ...newContext, ...ctx.context, }, }); }; shimmedObj[key].path = value.path; shimmedObj[key].method = value.method; shimmedObj[key].options = value.options; shimmedObj[key].headers = value.headers; } return shimmedObj as T; }; export const shimEndpoint = (ctx: AuthContext, value: any) => { return async (context: any) => { for (const plugin of ctx.options.plugins || []) { if (plugin.hooks?.before) { for (const hook of plugin.hooks.before) { const match = hook.matcher({ ...context, ...value, }); if (match) { const hookRes = await hook.handler(context); if ( hookRes && typeof hookRes === "object" && "context" in hookRes ) { context = { ...context, ...(hookRes.context as any), ...value, }; } } } } } const endpointRes = value({ ...context, context: { ...ctx, ...context.context, }, }); let response = endpointRes; for (const plugin of ctx.options.plugins || []) { if (plugin.hooks?.after) { for (const hook of plugin.hooks.after) { const match = hook.matcher(context); if (match) { const obj = Object.assign(context, { returned: endpointRes, }); const hookRes = await hook.handler(obj); if ( hookRes && typeof hookRes === "object" && "response" in hookRes ) { response = hookRes.response as any; } } } } } return response; }; }; ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/theme-toggle.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { cva } from "class-variance-authority"; import { Moon, Sun, Airplay } from "lucide-react"; import { useTheme } from "next-themes"; import { type HTMLAttributes, useLayoutEffect, useState } from "react"; import { cn } from "../../../lib/utils"; const itemVariants = cva( "size-6.5 rounded-full p-1.5 text-fd-muted-foreground", { variants: { active: { true: "bg-fd-accent text-fd-accent-foreground", false: "text-fd-muted-foreground", }, }, }, ); const full = [ ["light", Sun] as const, ["dark", Moon] as const, ["system", Airplay] as const, ]; export function ThemeToggle({ className, mode = "light-dark", ...props }: HTMLAttributes<HTMLElement> & { mode?: "light-dark" | "light-dark-system"; }) { const { setTheme, theme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); useLayoutEffect(() => { setMounted(true); }, []); const container = cn( "inline-flex items-center rounded-full border p-1", className, ); if (mode === "light-dark") { const value = mounted ? resolvedTheme : null; return ( <button className={container} aria-label={`Toggle Theme`} onClick={() => setTheme(value === "light" ? "dark" : "light")} data-theme-toggle="" {...props} > {full.map(([key, Icon]) => { if (key === "system") return; return ( <Icon key={key} fill="currentColor" className={cn(itemVariants({ active: value === key }))} /> ); })} </button> ); } const value = mounted ? theme : null; return ( <div className={container} data-theme-toggle="" {...props}> {full.map(([key, Icon]) => ( <button key={key} aria-label={key} className={cn(itemVariants({ active: value === key }))} onClick={() => setTheme(key)} > <Icon className="size-full" fill="currentColor" /> </button> ))} </div> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/spotify.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Spotify description: Spotify provider setup and usage. --- <Steps> <Step> ### Get your Spotify Credentials To use Spotify sign in, you need a client ID and client secret. You can get them from the [Spotify Developer Portal](https://developer.spotify.com/dashboard/applications). Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/spotify` 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { spotify: { // [!code highlight] clientId: process.env.SPOTIFY_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> <Step> ### Sign In with Spotify To sign in with Spotify, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `spotify`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "spotify" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); function Button({ className, variant, size, asChild = false, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> ); } export { Button, buttonVariants }; ``` -------------------------------------------------------------------------------- /docs/components/search-dialog.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from "fumadocs-ui/components/dialog/search"; import { useDocsSearch } from "fumadocs-core/search/client"; import { OramaClient } from "@oramacloud/client"; import { useI18n } from "fumadocs-ui/contexts/i18n"; import { AIChatModal, aiChatModalAtom } from "./ai-chat-modal"; import { useAtom } from "jotai"; const client = new OramaClient({ endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!, api_key: process.env.NEXT_PUBLIC_ORAMA_PUBLIC_API_KEY!, }); export function CustomSearchDialog(props: SharedProps) { const { locale } = useI18n(); const [isAIModalOpen, setIsAIModalOpen] = useAtom(aiChatModalAtom); const { search, setSearch, query } = useDocsSearch({ type: "orama-cloud", client, locale, }); const handleAskAIClick = () => { props.onOpenChange?.(false); setIsAIModalOpen(true); }; const handleAIModalClose = () => { setIsAIModalOpen(false); }; return ( <> <SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props} > <SearchDialogOverlay /> <SearchDialogContent className="mt-12 md:mt-0"> <SearchDialogHeader> <SearchDialogIcon /> <SearchDialogInput /> <SearchDialogClose className="hidden md:block" /> </SearchDialogHeader> <SearchDialogList items={query.data !== "empty" ? query.data : null} /> <SearchDialogFooter> <a href="https://orama.com" rel="noreferrer noopener" className="ms-auto text-xs text-fd-muted-foreground" > Search powered by Orama </a> </SearchDialogFooter> </SearchDialogContent> </SearchDialog> <AIChatModal isOpen={isAIModalOpen} onClose={handleAIModalClose} /> </> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/types.ts: -------------------------------------------------------------------------------- ```typescript import { type Session, type User } from "../../types"; import { type InferOptionSchema } from "../../types"; import { type AccessControl, type Role } from "../access"; import { type AdminSchema } from "./schema"; export interface UserWithRole extends User { role?: string; banned?: boolean | null; banReason?: string | null; banExpires?: Date | null; } export interface SessionWithImpersonatedBy extends Session { impersonatedBy?: string; } export interface AdminOptions { /** * The default role for a user * * @default "user" */ defaultRole?: string; /** * Roles that are considered admin roles. * * Any user role that isn't in this list, even if they have the permission, * will not be considered an admin. * * @default ["admin"] */ adminRoles?: string | string[]; /** * A default ban reason * * By default, no reason is provided */ defaultBanReason?: string; /** * Number of seconds until the ban expires * * By default, the ban never expires */ defaultBanExpiresIn?: number; /** * Duration of the impersonation session in seconds * * By default, the impersonation session lasts 1 hour */ impersonationSessionDuration?: number; /** * Custom schema for the admin plugin */ schema?: InferOptionSchema<AdminSchema>; /** * Configure the roles and permissions for the admin * plugin. */ ac?: AccessControl; /** * Custom permissions for roles. */ roles?: { [key in string]?: Role; }; /** * List of user ids that should have admin access * * If this is set, the `adminRole` option is ignored */ adminUserIds?: string[]; /** * Message to show when a user is banned * * By default, the message is "You have been banned from this application" */ bannedUserMessage?: string; } export type InferAdminRolesFromOption<O extends AdminOptions | undefined> = O extends { roles: Record<string, unknown> } ? keyof O["roles"] : "user" | "admin"; ``` -------------------------------------------------------------------------------- /docs/components/ui/resizable.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { GripVerticalIcon } from "lucide-react"; import * as ResizablePrimitive from "react-resizable-panels"; import { cn } from "@/lib/utils"; function ResizablePanelGroup({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) { return ( <ResizablePrimitive.PanelGroup data-slot="resizable-panel-group" className={cn( "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className, )} {...props} /> ); } function ResizablePanel({ ...props }: React.ComponentProps<typeof ResizablePrimitive.Panel>) { return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />; } function ResizableHandle({ withHandle, className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { withHandle?: boolean; }) { return ( <ResizablePrimitive.PanelResizeHandle data-slot="resizable-handle" className={cn( "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden 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", className, )} {...props} > {withHandle && ( <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border"> <GripVerticalIcon className="size-2.5" /> </div> )} </ResizablePrimitive.PanelResizeHandle> ); } export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; ``` -------------------------------------------------------------------------------- /docs/components/ui/accordion.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) { return <AccordionPrimitive.Root data-slot="accordion" {...props} />; } function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) { return ( <AccordionPrimitive.Item data-slot="accordion-item" className={cn("border-b last:border-b-0", className)} {...props} /> ); } function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) { return ( <AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Trigger data-slot="accordion-trigger" className={cn( "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", className, )} {...props} > {children} <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> </AccordionPrimitive.Trigger> </AccordionPrimitive.Header> ); } function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) { return ( <AccordionPrimitive.Content data-slot="accordion-content" className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" {...props} > <div className={cn("pt-0 pb-4", className)}>{children}</div> </AccordionPrimitive.Content> ); } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/models.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthOptions } from "@better-auth/core"; import type { Auth } from "../auth"; import type { InferFieldsFromOptions, InferFieldsFromPlugins } from "../db"; import type { StripEmptyObjects, UnionToIntersection } from "./helper"; import type { BetterAuthPlugin } from "@better-auth/core"; import type { User, Session } from "@better-auth/core/db"; export type AdditionalUserFieldsInput<Options extends BetterAuthOptions> = InferFieldsFromPlugins<Options, "user", "input"> & InferFieldsFromOptions<Options, "user", "input">; export type AdditionalUserFieldsOutput<Options extends BetterAuthOptions> = InferFieldsFromPlugins<Options, "user"> & InferFieldsFromOptions<Options, "user">; export type AdditionalSessionFieldsInput<Options extends BetterAuthOptions> = InferFieldsFromPlugins<Options, "session", "input"> & InferFieldsFromOptions<Options, "session", "input">; export type AdditionalSessionFieldsOutput<Options extends BetterAuthOptions> = InferFieldsFromPlugins<Options, "session"> & InferFieldsFromOptions<Options, "session">; export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection< StripEmptyObjects< User & (O extends BetterAuthOptions ? AdditionalUserFieldsOutput<O> : O extends Auth ? AdditionalUserFieldsOutput<O["options"]> : {}) > >; export type InferSession<O extends BetterAuthOptions | Auth> = UnionToIntersection< StripEmptyObjects< Session & (O extends BetterAuthOptions ? AdditionalSessionFieldsOutput<O> : O extends Auth ? AdditionalSessionFieldsOutput<O["options"]> : {}) > >; export type InferPluginTypes<O extends BetterAuthOptions> = O["plugins"] extends Array<infer P> ? UnionToIntersection< P extends BetterAuthPlugin ? P["$Infer"] extends Record<string, any> ? P["$Infer"] : {} : {} > : {}; export type { User, Account, Session, Verification, RateLimit, } from "@better-auth/core/db"; ``` -------------------------------------------------------------------------------- /demo/expo-example/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript /** @type {import('tailwindcss').Config} */ module.exports = { // NOTE: Update this to include the paths to all of your component files. content: ["./src/**/*.{js,jsx,ts,tsx}"], presets: [require("nativewind/preset")], theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, boxShadow: { input: `0px 2px 3px -1px rgba(0,0,0,0.1), 0px 1px 0px 0px rgba(25,28,33,0.02), 0px 0px 0px 1px rgba(25,28,33,0.08)`, }, }, }, plugins: [], }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/client.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthClientPlugin } from "@better-auth/core"; import { type AccessControl, type Role } from "../access"; import { adminAc, defaultStatements, userAc } from "./access"; import type { admin } from "./admin"; import { hasPermission } from "./has-permission"; interface AdminClientOptions { ac?: AccessControl; roles?: { [key in string]: Role; }; } export const adminClient = <O extends AdminClientOptions>(options?: O) => { type DefaultStatements = typeof defaultStatements; type Statements = O["ac"] extends AccessControl<infer S> ? S : DefaultStatements; type PermissionType = { [key in keyof Statements]?: Array< Statements[key] extends readonly unknown[] ? Statements[key][number] : never >; }; type PermissionExclusive = | { /** * @deprecated Use `permissions` instead */ permission: PermissionType; permissions?: never; } | { permissions: PermissionType; permission?: never; }; const roles = { admin: adminAc, user: userAc, ...options?.roles, }; return { id: "admin-client", $InferServerPlugin: {} as ReturnType< typeof admin<{ ac: O["ac"] extends AccessControl ? O["ac"] : AccessControl<DefaultStatements>; roles: O["roles"] extends Record<string, Role> ? O["roles"] : { admin: Role; user: Role; }; }> >, getActions: () => ({ admin: { checkRolePermission: < R extends O extends { roles: any } ? keyof O["roles"] : "admin" | "user", >( data: PermissionExclusive & { role: R; }, ) => { const isAuthorized = hasPermission({ role: data.role as string, options: { ac: options?.ac, roles: roles, }, permissions: (data.permissions ?? data.permission) as any, }); return isAuthorized; }, }, }), pathMethods: { "/admin/list-users": "GET", "/admin/stop-impersonating": "POST", }, } satisfies BetterAuthClientPlugin; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/huggingface.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Hugging Face description: Hugging Face provider setup and usage. --- <Steps> <Step> ### Get your Hugging Face credentials To use Hugging Face sign in, you need a client ID and client secret. [Hugging Face OAuth documentation](https://huggingface.co/docs/hub/oauth). Make sure the created oauth app on Hugging Face has the "email" scope. Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/huggingface` 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. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { huggingface: { // [!code highlight] clientId: process.env.HUGGINGFACE_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> <Step> ### Sign In with Hugging Face To sign in with Hugging Face, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `huggingface`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "huggingface" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/toc-thumb.tsx: -------------------------------------------------------------------------------- ```typescript import { type HTMLAttributes, type RefObject, useEffect, useRef } from "react"; import * as Primitive from "fumadocs-core/toc"; import { useOnChange } from "fumadocs-core/utils/use-on-change"; import { useEffectEvent } from "fumadocs-core/utils/use-effect-event"; export type TOCThumb = [top: number, height: number]; function calc(container: HTMLElement, active: string[]): TOCThumb { if (active.length === 0 || container.clientHeight === 0) { return [0, 0]; } let upper = Number.MAX_VALUE, lower = 0; for (const item of active) { const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`); if (!element) continue; const styles = getComputedStyle(element); upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop)); lower = Math.max( lower, element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom), ); } return [upper, lower - upper]; } function update(element: HTMLElement, info: TOCThumb): void { element.style.setProperty("--fd-top", `${info[0]}px`); element.style.setProperty("--fd-height", `${info[1]}px`); } export function TocThumb({ containerRef, ...props }: HTMLAttributes<HTMLDivElement> & { containerRef: RefObject<HTMLElement | null>; }) { const active = Primitive.useActiveAnchors(); const thumbRef = useRef<HTMLDivElement>(null); const onResize = useEffectEvent(() => { if (!containerRef.current || !thumbRef.current) return; update(thumbRef.current, calc(containerRef.current, active)); }); useEffect(() => { if (!containerRef.current) return; const container = containerRef.current; onResize(); const observer = new ResizeObserver(onResize); observer.observe(container); return () => { observer.disconnect(); }; }, [containerRef, onResize]); useOnChange(active, () => { if (!containerRef.current || !thumbRef.current) return; update(thumbRef.current, calc(containerRef.current, active)); }); return <div ref={thumbRef} role="none" {...props} />; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/accordion.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { cn } from "@/lib/utils"; const Accordion = AccordionPrimitive.Root; const AccordionItem = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & { ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Item>>; }) => ( <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} /> ); AccordionItem.displayName = "AccordionItem"; const AccordionTrigger = ({ ref, className, children, ...props }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Trigger>>; }) => ( <AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Trigger ref={ref} className={cn( "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", className, )} {...props} > {children} <ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" /> </AccordionPrimitive.Trigger> </AccordionPrimitive.Header> ); AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; const AccordionContent = ({ ref, className, children, ...props }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> & { ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Content>>; }) => ( <AccordionPrimitive.Content ref={ref} className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" {...props} > <div className={cn("pb-4 pt-0", className)}>{children}</div> </AccordionPrimitive.Content> ); AccordionContent.displayName = AccordionPrimitive.Content.displayName; export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; ``` -------------------------------------------------------------------------------- /demo/expo-example/package.json: -------------------------------------------------------------------------------- ```json { "name": "expo-example", "main": "index.ts", "private": true, "version": "1.0.0", "scripts": { "clean": "git clean -xdf .cache .expo .turbo android ios node_modules", "start": "expo start", "dev": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint", "android": "expo run:android" }, "dependencies": { "@better-auth/expo": "workspace:*", "@expo/metro-runtime": "^6.1.2", "@expo/vector-icons": "^15.0.2", "@nanostores/react": "^1.0.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.1.17", "@rn-primitives/avatar": "^1.1.0", "@rn-primitives/separator": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@rn-primitives/types": "^1.1.0", "@types/better-sqlite3": "^7.6.12", "babel-plugin-transform-import-meta": "^2.2.1", "better-auth": "workspace:*", "better-sqlite3": "^11.6.0", "expo": "~54.0.10", "expo-constants": "~18.0.9", "expo-crypto": "^15.0.7", "expo-font": "~14.0.8", "expo-linking": "~8.0.8", "expo-router": "~6.0.8", "expo-secure-store": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", "expo-system-ui": "~6.0.7", "expo-web-browser": "~15.0.7", "nanostores": "^0.11.3", "nativewind": "^4.1.23", "pg": "^8.13.1", "react": "^19.2.0", "react-dom": "^19.2.0", "react-native": "~0.81.4", "react-native-css-interop": "^0.2.1", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.2", "react-native-safe-area-context": "5.6.1", "react-native-screens": "4.16.0", "react-native-svg": "^15.12.1", "react-native-web": "~0.21.1", "react-native-worklets": "^0.5.1", "tailwindcss": "^3.4.16" }, "devDependencies": { "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", "@types/babel__core": "^7.20.5", "@types/react": "^19.2.2" } } ```