This is page 3 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 │ ├── middleware.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ └── user-additional-fields.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── sso │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sso.test.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── middleware │ │ │ │ └── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/utils/get-request-ip.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthOptions } from "@better-auth/core"; import { isDevelopment, isTest } from "@better-auth/core/env"; import { z } from "zod"; export function getIp( req: Request | Headers, options: BetterAuthOptions, ): string | null { if (options.advanced?.ipAddress?.disableIpTracking) { return null; } if (isTest()) { return "127.0.0.1"; // Use a fixed IP for test environments } if (isDevelopment) { return "127.0.0.1"; // Use a fixed IP for development environments } const headers = "headers" in req ? req.headers : req; const defaultHeaders = ["x-forwarded-for"]; const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || defaultHeaders; for (const key of ipHeaders) { const value = "get" in headers ? headers.get(key) : headers[key]; if (typeof value === "string") { const ip = value.split(",")[0]!.trim(); if (isValidIP(ip)) { return ip; } } } return null; } function isValidIP(ip: string): boolean { const ipv4 = z.ipv4().safeParse(ip); if (ipv4.success) { return true; } const ipv6 = z.ipv6().safeParse(ip); if (ipv6.success) { return true; } return false; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/switch.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SwitchPrimitives from "@radix-ui/react-switch"; import { cn } from "@/lib/utils"; const Switch = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & { ref: React.RefObject<React.ElementRef<typeof SwitchPrimitives.Root>>; }) => ( <SwitchPrimitives.Root className={cn( "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", className, )} {...props} ref={ref} > <SwitchPrimitives.Thumb className={cn( "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0", )} /> </SwitchPrimitives.Root> ); Switch.displayName = SwitchPrimitives.Root.displayName; export { Switch }; ``` -------------------------------------------------------------------------------- /demo/nextjs/app/accept-invitation/[id]/invitation-error.tsx: -------------------------------------------------------------------------------- ```typescript import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { AlertCircle } from "lucide-react"; import Link from "next/link"; export function InvitationError() { return ( <Card className="w-full max-w-md mx-auto"> <CardHeader> <div className="flex items-center space-x-2"> <AlertCircle className="w-6 h-6 text-destructive" /> <CardTitle className="text-xl text-destructive"> Invitation Error </CardTitle> </div> <CardDescription> There was an issue with your invitation. </CardDescription> </CardHeader> <CardContent> <p className="mb-4 text-sm text-muted-foreground"> The invitation you're trying to access is either invalid or you don't have the correct permissions. Please check your email for a valid invitation or contact the person who sent it. </p> </CardContent> <CardFooter> <Link href="/" className="w-full"> <Button variant="outline" className="w-full"> Go back to home </Button> </Link> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /demo/nextjs/app/oauth/authorize/concet-buttons.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Button } from "@/components/ui/button"; import { CardFooter } from "@/components/ui/card"; import { client } from "@/lib/auth-client"; import { Loader2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; export function ConsentBtns() { const [loading, setLoading] = useState(false); return ( <CardFooter className="flex items-center gap-2"> <Button onClick={async () => { setLoading(true); const res = await client.oauth2.consent({ accept: true, }); setLoading(false); if (res.data?.redirectURI) { window.location.href = res.data.redirectURI; return; } toast.error("Failed to authorize"); }} > {loading ? <Loader2 size={15} className="animate-spin" /> : "Authorize"} </Button> <Button variant="outline" onClick={async () => { const res = await client.oauth2.consent({ accept: false, }); if (res.data?.redirectURI) { window.location.href = res.data.redirectURI; return; } toast.error("Failed to cancel"); }} > Cancel </Button> </CardFooter> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/plugins/index.ts: -------------------------------------------------------------------------------- ```typescript export * from "../../plugins/organization/client"; export * from "../../plugins/username/client"; export * from "../../plugins/passkey/client"; export * from "../../plugins/two-factor/client"; export * from "../../plugins/magic-link/client"; export * from "../../plugins/phone-number/client"; export * from "../../plugins/anonymous/client"; export * from "../../plugins/additional-fields/client"; export * from "../../plugins/admin/client"; export * from "../../plugins/generic-oauth/client"; export * from "../../plugins/jwt/client"; export * from "../../plugins/multi-session/client"; export * from "../../plugins/email-otp/client"; export * from "../../plugins/one-tap/client"; export * from "../../plugins/custom-session/client"; export * from "./infer-plugin"; export * from "../../plugins/sso/client"; export * from "../../plugins/oidc-provider/client"; export * from "../../plugins/api-key/client"; export * from "../../plugins/one-time-token/client"; export * from "../../plugins/siwe/client"; export * from "../../plugins/device-authorization/client"; export type * from "@simplewebauthn/server"; export * from "../../plugins/last-login-method/client"; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-out.ts: -------------------------------------------------------------------------------- ```typescript import { createAuthEndpoint } from "@better-auth/core/middleware"; import { deleteSessionCookie } from "../../cookies"; import { APIError } from "better-call"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; export const signOut = createAuthEndpoint( "/sign-out", { method: "POST", requireHeaders: true, metadata: { openapi: { description: "Sign out the current user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean", }, }, }, }, }, }, }, }, }, }, async (ctx) => { const sessionCookieToken = await ctx.getSignedCookie( ctx.context.authCookies.sessionToken.name, ctx.context.secret, ); if (!sessionCookieToken) { deleteSessionCookie(ctx); throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION, }); } await ctx.context.internalAdapter.deleteSession(sessionCookieToken); deleteSessionCookie(ctx); return ctx.json({ success: true, }); }, ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/get-schema.ts: -------------------------------------------------------------------------------- ```typescript import { getAuthTables } from "."; import type { BetterAuthOptions } from "@better-auth/core"; import type { DBFieldAttribute } from "@better-auth/core/db"; export function getSchema(config: BetterAuthOptions) { const tables = getAuthTables(config); let schema: Record< string, { fields: Record<string, DBFieldAttribute>; order: number; } > = {}; for (const key in tables) { const table = tables[key]!; const fields = table.fields; let actualFields: Record<string, DBFieldAttribute> = {}; Object.entries(fields).forEach(([key, field]) => { actualFields[field.fieldName || key] = field; if (field.references) { const refTable = tables[field.references.model]; if (refTable) { actualFields[field.fieldName || key]!.references = { ...field.references, model: refTable.modelName, field: field.references.field, }; } } }); if (schema[table.modelName]) { schema[table.modelName]!.fields = { ...schema[table.modelName]!.fields, ...actualFields, }; continue; } schema[table.modelName] = { fields: actualFields, order: table.order || Infinity, }; } return schema; } ``` -------------------------------------------------------------------------------- /docs/components/docs/docs.client.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Menu, X } from "lucide-react"; import { type ButtonHTMLAttributes, type HTMLAttributes } from "react"; import { cn } from "../../lib/utils"; import { buttonVariants } from "./ui/button"; import { useSidebar } from "fumadocs-ui/provider"; import { useNav } from "./layout/nav"; import { SidebarTrigger } from "fumadocs-core/sidebar"; export function Navbar(props: HTMLAttributes<HTMLElement>) { const { open } = useSidebar(); const { isTransparent } = useNav(); return ( <header id="nd-subnav" {...props} className={cn( "sticky top-(--fd-banner-height) z-30 flex h-14 flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors", (!isTransparent || open) && "bg-fd-background/80", props.className, )} > {props.children} </header> ); } export function NavbarSidebarTrigger( props: ButtonHTMLAttributes<HTMLButtonElement>, ) { const { open } = useSidebar(); return ( <SidebarTrigger {...props} className={cn( buttonVariants({ color: "ghost", size: "icon", }), props.className, )} > {open ? <X /> : <Menu />} </SidebarTrigger> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/nuxt.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Nuxt Example description: Better Auth Nuxt example. --- This is an example of how to use Better Auth with Nuxt. **Implements the following features:** Email & Password . Social Sign-in with Google <ForkButton url="better-auth/better-auth/tree/main/examples/nuxt-example" /> <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/nuxt-example?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" style={{ width: "100%", height: "500px", border: 0, borderRadius: "4px", overflow: "hidden" }} title="Better Auth Nuxt Example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > </iframe> ## How to run 1. Clone the code sandbox (or the repo) and open it in your code editor 2. Move .env.example to .env and provide necessary variables 3. Run the following commands ```bash pnpm install pnpm dev ``` 4. Open the browser and navigate to `http://localhost:3000` ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/postgres-js.spec.ts: -------------------------------------------------------------------------------- ```typescript import { betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; import { PostgresJSDialect } from "kysely-postgres-js"; import postgres from "postgres"; import { getMigrations } from "better-auth/db"; import { expect, test } from "@playwright/test"; test.describe("postgres-js", async () => { test("run migration", async () => { const sql = postgres( process.env.DATABASE_URL || "postgres://user:password@localhost:5432/better_auth", ); const dialect = new PostgresJSDialect({ postgres: sql, }); const auth = betterAuth({ database: { dialect, type: "postgres", transaction: false, }, emailAndPassword: { enabled: true, }, plugins: [nextCookies()], baseURL: "http://localhost:3000", }); const { runMigrations } = await getMigrations(auth.options); await runMigrations(); const allTables = await sql` SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'; `; const tableNames = allTables.map((row) => row.table_name); expect(tableNames).toEqual(["user", "session", "account", "verification"]); }); }); ``` -------------------------------------------------------------------------------- /packages/core/src/async_hooks/index.ts: -------------------------------------------------------------------------------- ```typescript /** * AsyncLocalStorage will be import directly in 1.5.x */ import type { AsyncLocalStorage } from "node:async_hooks"; // We only export the type here to avoid issues in environments where AsyncLocalStorage is not available. export type { AsyncLocalStorage }; const AsyncLocalStoragePromise: Promise<typeof AsyncLocalStorage> = import( /* @vite-ignore */ /* webpackIgnore: true */ "node:async_hooks" ) .then((mod) => mod.AsyncLocalStorage) .catch((err) => { if ("AsyncLocalStorage" in globalThis) { return (globalThis as any).AsyncLocalStorage; } console.warn( "[better-auth] Warning: AsyncLocalStorage is not available in this environment. Some features may not work as expected.", ); console.warn( "[better-auth] Please read more about this warning at https://better-auth.com/docs/installation#mount-handler", ); console.warn( "[better-auth] If you are using Cloudflare Workers, please see: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag", ); throw err; }); export async function getAsyncLocalStorage(): Promise< typeof AsyncLocalStorage > { return AsyncLocalStoragePromise; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/index.ts: -------------------------------------------------------------------------------- ```typescript import { createAdapterFactory, type AdapterFactory, type AdapterFactoryOptions, type AdapterTestDebugLogs, type AdapterFactoryConfig, type CustomAdapter, type AdapterFactoryCustomizeAdapterCreator, } from "./adapter-factory"; export * from "@better-auth/core/db/adapter"; export type { AdapterFactoryOptions, AdapterFactory, AdapterTestDebugLogs, AdapterFactoryConfig, CustomAdapter, AdapterFactoryCustomizeAdapterCreator, }; export { createAdapterFactory }; /** * @deprecated Use `createAdapterFactory` instead. This export will be removed in the next major version. */ export const createAdapter = createAdapterFactory; /** * @deprecated Use `AdapterFactoryOptions` instead. This export will be removed in the next major version. */ export type CreateAdapterOptions = AdapterFactoryOptions; /** * @deprecated Use `AdapterFactoryConfig` instead. This export will be removed in the next major version. */ export type AdapterConfig = AdapterFactoryConfig; /** * @deprecated Use `AdapterFactoryCustomizeAdapterCreator` instead. This export will be removed in the next major version. */ export type CreateCustomAdapter = AdapterFactoryCustomizeAdapterCreator; ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import SignIn from "@/components/sign-in"; import { SignUp } from "@/components/sign-up"; import { Tabs } from "@/components/ui/tabs2"; import { client } from "@/lib/auth-client"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { toast } from "sonner"; import { getCallbackURL } from "@/lib/shared"; export default function Page() { const router = useRouter(); const params = useSearchParams(); useEffect(() => { client.oneTap({ fetchOptions: { onError: ({ error }) => { toast.error(error.message || "An error occurred"); }, onSuccess: () => { toast.success("Successfully signed in"); router.push(getCallbackURL(params)); }, }, }); }, []); return ( <div className="w-full"> <div className="flex items-center flex-col justify-center w-full md:py-10"> <div className="md:w-[400px]"> <Tabs tabs={[ { title: "Sign In", value: "sign-in", content: <SignIn />, }, { title: "Sign Up", value: "sign-up", content: <SignUp />, }, ]} /> </div> </div> </div> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/cloudflare-turnstile.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { middlewareResponse } from "../../../utils/middleware-response"; import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "../error-codes"; type Params = { siteVerifyURL: string; secretKey: string; captchaResponse: string; remoteIP?: string; }; type SiteVerifyResponse = { success: boolean; "error-codes"?: string[]; challenge_ts?: string; hostname?: string; action?: string; cdata?: string; metadata?: { interactive: boolean; }; messages?: string[]; }; export const cloudflareTurnstile = async ({ siteVerifyURL, captchaResponse, secretKey, remoteIP, }: Params) => { const response = await betterFetch<SiteVerifyResponse>(siteVerifyURL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: secretKey, response: captchaResponse, ...(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; }; ``` -------------------------------------------------------------------------------- /e2e/smoke/test/cloudflare.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it } from "node:test"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { join } from "node:path"; import assert from "node:assert/strict"; const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); describe("(cloudflare) simple server", () => { it("check repo", async (t) => { const cp = spawn("npm", ["run", "check"], { cwd: join(fixturesDir, "cloudflare"), stdio: "pipe", }); t.after(() => { cp.kill("SIGINT"); }); const unexpectedStrings = new Set(["node:sqlite"]); cp.stdout.on("data", (data) => { console.log(data.toString()); for (const str of unexpectedStrings) { assert( !data.toString().includes(str), `Output should not contain "${str}"`, ); } }); cp.stderr.on("data", (data) => { console.error(data.toString()); for (const str of unexpectedStrings) { assert( !data.toString().includes(str), `Error output should not contain "${str}"`, ); } }); await new Promise<void>((resolve) => { cp.stdout.on("data", (data) => { if (data.toString().includes("exiting now.")) { resolve(); } }); }); }); }); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; import { cn } from "@/lib/utils"; const HoverCard = HoverCardPrimitive.Root; const HoverCardTrigger = HoverCardPrimitive.Trigger; const HoverCardContent = ({ ref, className, align = "center", sideOffset = 4, ...props }: React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> & { ref: React.RefObject<React.ElementRef<typeof HoverCardPrimitive.Content>>; }) => ( <HoverCardPrimitive.Content ref={ref} align={align} sideOffset={sideOffset} className={cn( "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} /> ); HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; export { HoverCard, HoverCardTrigger, HoverCardContent }; ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- ```typescript import * as AvatarPrimitive from "@rn-primitives/avatar"; import * as React from "react"; import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< AvatarPrimitive.RootRef, AvatarPrimitive.RootProps >(({ className, ...props }, ref) => ( <AvatarPrimitive.Root ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className, )} {...props} /> )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< AvatarPrimitive.ImageRef, AvatarPrimitive.ImageProps >(({ className, ...props }, ref) => ( <AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} /> )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< AvatarPrimitive.FallbackRef, AvatarPrimitive.FallbackProps >(({ className, ...props }, ref) => ( <AvatarPrimitive.Fallback ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", className, )} {...props} /> )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarFallback, AvatarImage }; ``` -------------------------------------------------------------------------------- /demo/nextjs/lib/auth-client.ts: -------------------------------------------------------------------------------- ```typescript import { createAuthClient } from "better-auth/react"; import { organizationClient, passkeyClient, twoFactorClient, adminClient, multiSessionClient, oneTapClient, oidcClient, genericOAuthClient, deviceAuthorizationClient, lastLoginMethodClient, } from "better-auth/client/plugins"; import { toast } from "sonner"; import { stripeClient } from "@better-auth/stripe/client"; export const client = createAuthClient({ plugins: [ organizationClient(), twoFactorClient({ onTwoFactorRedirect() { window.location.href = "/two-factor"; }, }), passkeyClient(), adminClient(), multiSessionClient(), oneTapClient({ clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, promptOptions: { maxAttempts: 1, }, }), oidcClient(), genericOAuthClient(), stripeClient({ subscription: true, }), deviceAuthorizationClient(), lastLoginMethodClient(), ], fetchOptions: { onError(e) { if (e.error.status === 429) { toast.error("Too many requests. Please try again later."); } }, }, }); export const { signUp, signIn, signOut, useSession, organization, useListOrganizations, useActiveOrganization, useActiveMember, useActiveMemberRole, } = client; ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/domain.spec.ts: -------------------------------------------------------------------------------- ```typescript import { chromium, expect, test } from "@playwright/test"; import { runClient, setup } from "./utils"; const { ref, start, clean } = setup(); test.describe("cross domain", async () => { test.beforeEach(async () => start()); test.afterEach(async () => clean()); test("should work across domains", async () => { const browser = await chromium.launch({ args: [`--host-resolver-rules=MAP * localhost`], }); const page = await browser.newPage(); await page.goto( `http://test.com:${ref.clientPort}/?port=${ref.serverPort}`, ); await page.locator("text=Ready").waitFor(); await expect( runClient(page, ({ client }) => typeof client !== "undefined"), ).resolves.toBe(true); await expect( runClient(page, async ({ client }) => client.getSession()), ).resolves.toEqual({ data: null, error: null }); await runClient(page, ({ client }) => client.signIn.email({ email: "[email protected]", password: "password123", }), ); // Check that the session is not set because of we didn't set the cookie domain correctly const cookies = await page.context().cookies(); expect( cookies.find((c) => c.name === "better-auth.session_token"), ).not.toBeDefined(); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/have-i-been-pwned.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Have I Been Pwned description: A plugin to check if a password has been compromised --- The Have I Been Pwned plugin helps protect user accounts by preventing the use of passwords that have been exposed in known data breaches. It uses the [Have I Been Pwned](https://haveibeenpwned.com/) API to check if a password has been compromised. ## Installation ### Add the plugin to your **auth** config ```ts title="auth.ts" import { betterAuth } from "better-auth" import { haveIBeenPwned } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ plugins: [ haveIBeenPwned() ] }) ``` ## Usage When a user attempts to create an account or update their password with a compromised password, they'll receive the following default error: ```json { "code": "PASSWORD_COMPROMISED", "message": "Password is compromised" } ``` ## Config You can customize the error message: ```ts haveIBeenPwned({ customPasswordCompromisedMessage: "Please choose a more secure password." }) ``` ## Security Notes - Only the first 5 characters of the password hash are sent to the API - The full password is never transmitted - Provides an additional layer of account security ``` -------------------------------------------------------------------------------- /docs/components/landing/spotlight.tsx: -------------------------------------------------------------------------------- ```typescript import { cn } from "@/lib/utils"; type SpotlightProps = { className?: string; fill?: string; }; export const Spotlight = ({ className, fill }: SpotlightProps) => { return ( <svg className={cn( "animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] lg:w-[84%] opacity-0", className, )} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3787 2842" fill="none" > <g filter="url(#filter)"> <ellipse cx="1924.71" cy="273.501" rx="1924.71" ry="273.501" transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)" fill={fill || "white"} fillOpacity="0.1" ></ellipse> </g> <defs> <filter id="filter" x="0.860352" y="0.838989" width="3785.16" height="2840.26" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB" > <feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood> <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" ></feBlend> <feGaussianBlur stdDeviation="180" result="effect1_foregroundBlur_1065_8" ></feGaussianBlur> </filter> </defs> </svg> ); }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/call.ts: -------------------------------------------------------------------------------- ```typescript import type { Session, User } from "../../types"; import { createAuthMiddleware } from "@better-auth/core/middleware"; import { sessionMiddleware } from "../../api"; import type { Role } from "../access"; import type { OrganizationOptions } from "./types"; import type { defaultRoles } from "./access/statement"; import type { GenericEndpointContext } from "@better-auth/core"; export const orgMiddleware = createAuthMiddleware(async () => { return {} as { orgOptions: OrganizationOptions; roles: typeof defaultRoles & { [key: string]: Role<{}>; }; getSession: (context: GenericEndpointContext) => Promise<{ session: Session & { activeTeamId?: string; activeOrganizationId?: string; }; user: User; }>; }; }); /** * The middleware forces the endpoint to require a valid session by utilizing the `sessionMiddleware`. * It also appends additional types to the session type regarding organizations. */ export const orgSessionMiddleware = createAuthMiddleware( { use: [sessionMiddleware], }, async (ctx) => { const session = ctx.context.session as { session: Session & { activeTeamId?: string; activeOrganizationId?: string; }; user: User; }; return { session, }; }, ); ``` -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Command } from "commander"; import { init } from "./commands/init"; import { migrate } from "./commands/migrate"; import { generate } from "./commands/generate"; import { generateSecret } from "./commands/secret"; import { login } from "./commands/login"; import { info } from "./commands/info"; import { mcp } from "./commands/mcp"; import { getPackageInfo } from "./utils/get-package-info"; import "dotenv/config"; // handle exit process.on("SIGINT", () => process.exit(0)); process.on("SIGTERM", () => process.exit(0)); async function main() { const program = new Command("better-auth"); let packageInfo: Record<string, any> = {}; try { packageInfo = await getPackageInfo(); } catch (error) { // it doesn't matter if we can't read the package.json file, we'll just use an empty object } program .addCommand(init) .addCommand(migrate) .addCommand(generate) .addCommand(generateSecret) .addCommand(info) .addCommand(login) .addCommand(mcp) .version(packageInfo.version || "1.1.2") .description("Better Auth CLI") .action(() => program.help()); program.parse(); } main().catch((error) => { console.error("Error running Better Auth CLI:", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /e2e/smoke/test/deno.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it } from "node:test"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { join } from "node:path"; import assert from "node:assert"; const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); describe("(deno) simple server", () => { it("run server", async (t) => { const cp = spawn("deno", ["-A", join(fixturesDir, "deno-simple.ts")], { stdio: "pipe", }); t.after(() => { cp.kill("SIGINT"); }); cp.stdout.on("data", (data) => { console.log(data.toString()); }); cp.stderr.on("data", (data) => { console.error(data.toString()); }); const port = await new Promise<number>((resolve) => { cp.stdout.once("data", (data) => { const port = +data.toString().split(":")[2].split("/")[0]; assert.ok(port > 0); assert.ok(!isNaN(port)); assert.ok(isFinite(port)); resolve(port); }); }); const response = await fetch( `http://localhost:${port}/api/auth/sign-up/email`, { method: "POST", body: JSON.stringify({ email: "[email protected]", password: "password", name: "test-2", }), headers: { "content-type": "application/json", }, }, ); assert.ok(response.ok); }); }); ``` -------------------------------------------------------------------------------- /demo/nextjs/app/dashboard/upgrade-button.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useState } from "react"; import { motion } from "framer-motion"; import { Sparkles } from "lucide-react"; export default function UpgradeButton() { const [isHovered, setIsHovered] = useState(false); return ( <motion.button className="relative overflow-hidden px-6 py-3 rounded-md bg-linear-to-r from-gray-900 to-black text-white font-bold text-lg shadow-lg transition-all duration-300 ease-out transform hover:scale-105 hover:shadow-xl" onHoverStart={() => setIsHovered(true)} onHoverEnd={() => setIsHovered(false)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > <span className="relative z-10 flex items-center justify-center"> <Sparkles className="w-5 h-5 mr-2" /> Upgrade to Pro </span> <motion.div className="absolute inset-0 bg-linear-to-r from-gray-800 to-gray-700" initial={{ opacity: 0 }} animate={{ opacity: isHovered ? 1 : 0 }} transition={{ duration: 0.3 }} /> <motion.div className="absolute inset-0 bg-white opacity-10" initial={{ scale: 0, x: "100%", y: "100%" }} animate={{ scale: isHovered ? 2 : 0, x: "0%", y: "0%" }} transition={{ duration: 0.4, ease: "easeOut" }} style={{ borderRadius: "2px" }} /> </motion.button> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/two-factor/types.ts: -------------------------------------------------------------------------------- ```typescript import type { User } from "../../types"; import type { AuthEndpoint } from "@better-auth/core/middleware"; import type { LiteralString } from "../../types/helper"; import type { BackupCodeOptions } from "./backup-codes"; import type { OTPOptions } from "./otp"; import type { TOTPOptions } from "./totp"; import type { InferOptionSchema } from "../../types"; import type { schema } from "./schema"; export interface TwoFactorOptions { /** * Application Name */ issuer?: string; /** * TOTP OPtions */ totpOptions?: Omit<TOTPOptions, "issuer">; /** * OTP Options */ otpOptions?: OTPOptions; /** * Backup code options */ backupCodeOptions?: BackupCodeOptions; /** * Skip verification on enabling two factor authentication. * @default false */ skipVerificationOnEnable?: boolean; /** * Custom schema for the two factor plugin */ schema?: InferOptionSchema<typeof schema>; } export interface UserWithTwoFactor extends User { /** * If the user has enabled two factor authentication. */ twoFactorEnabled: boolean; } export interface TwoFactorProvider { id: LiteralString; endpoints?: Record<string, AuthEndpoint>; } export interface TwoFactorTable { userId: string; secret: string; backupCodes: string; enabled: boolean; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/permission.ts: -------------------------------------------------------------------------------- ```typescript import type { Role } from "../access"; import type { OrganizationOptions } from "./types"; export const hasPermissionFn = ( input: HasPermissionBaseInput, acRoles: { [x: string]: Role<any> | undefined; }, ) => { if (!input.permissions && !input.permission) return false; const roles = input.role.split(","); const creatorRole = input.options.creatorRole || "owner"; const isCreator = roles.includes(creatorRole); const allowCreatorsAllPermissions = input.allowCreatorAllPermissions || false; if (isCreator && allowCreatorsAllPermissions) return true; for (const role of roles) { const _role = acRoles[role as keyof typeof acRoles]; const result = _role?.authorize(input.permissions ?? input.permission); if (result?.success) { return true; } } return false; }; export type PermissionExclusive = | { /** * @deprecated Use `permissions` instead */ permission: { [key: string]: string[] }; permissions?: never; } | { permissions: { [key: string]: string[] }; permission?: never; }; export let cacheAllRoles = new Map< string, { [x: string]: Role<any> | undefined; } >(); export type HasPermissionBaseInput = { role: string; options: OrganizationOptions; allowCreatorAllPermissions?: boolean; } & PermissionExclusive; ``` -------------------------------------------------------------------------------- /docs/app/api/support/route.ts: -------------------------------------------------------------------------------- ```typescript import { NextResponse } from "next/server"; export async function POST(request: Request) { try { const body = await request.json(); const { name, email, company, website, userCount, interest, features, additional, } = body ?? {}; if (!name || !email) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, ); } const payload = { name, email, company: company ?? "", website: website ?? "", userCount: userCount ?? "", interest: interest ?? "", features: features ?? "", additional: additional ?? "", submittedAt: new Date().toISOString(), userAgent: request.headers.get("user-agent") ?? undefined, referer: request.headers.get("referer") ?? undefined, }; const webhook = process.env.SUPPORT_WEBHOOK_URL; if (webhook) { try { await fetch(webhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } catch (e) { console.error("Support webhook failed", e); } } else { console.log("[support] submission", payload); } return NextResponse.json({ ok: true }); } catch (e) { console.error(e); return NextResponse.json({ error: "Invalid request" }, { status: 400 }); } } ``` -------------------------------------------------------------------------------- /packages/stripe/package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/stripe", "author": "Bereket Engida", "version": "1.4.0-beta.10", "type": "module", "main": "dist/index.js", "license": "MIT", "keywords": [ "stripe", "auth", "stripe" ], "module": "dist/index.js", "description": "Stripe plugin for Better Auth", "scripts": { "test": "vitest", "build": "tsdown", "dev": "tsdown --watch", "typecheck": "tsc --project tsconfig.json" }, "publishConfig": { "access": "public" }, "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" ] } }, "dependencies": { "defu": "^6.1.4", "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/core": "workspace:*", "better-auth": "workspace:*", "stripe": "^18" }, "devDependencies": { "@better-auth/core": "workspace:*", "better-auth": "workspace:*", "better-call": "catalog:", "stripe": "^18.5.0", "tsdown": "catalog:" } } ``` -------------------------------------------------------------------------------- /packages/cli/src/generators/index.ts: -------------------------------------------------------------------------------- ```typescript import { logger, type Adapter, type BetterAuthOptions } from "better-auth"; import { generateDrizzleSchema } from "./drizzle"; import { generatePrismaSchema } from "./prisma"; import { generateMigrations } from "./kysely"; export const adapters = { prisma: generatePrismaSchema, drizzle: generateDrizzleSchema, kysely: generateMigrations, }; export const generateSchema = (opts: { adapter: Adapter; file?: string; options: BetterAuthOptions; }) => { const adapter = opts.adapter; const generator = adapter.id in adapters ? adapters[adapter.id as keyof typeof adapters] : null; if (generator) { // generator from the built-in list above return generator(opts); } if (adapter.createSchema) { // use the custom adapter's createSchema method return adapter .createSchema(opts.options, opts.file) .then(({ code, path: fileName, overwrite }) => ({ code, fileName, overwrite, })); } logger.error( `${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`, ); process.exit(1); }; /** * @deprecated getGenerator is a misnomer as this function gets a generator AND uses it to generate * and return the schema. Use generateSchema instead */ export const getGenerator = generateSchema; ``` -------------------------------------------------------------------------------- /demo/expo-example/metro.config.js: -------------------------------------------------------------------------------- ```javascript // Learn more: https://docs.expo.dev/guides/monorepos/ const { getDefaultConfig } = require("expo/metro-config"); const { FileStore } = require("metro-cache"); const { withNativeWind } = require("nativewind/metro"); const path = require("path"); const config = withMonorepoPaths( withNativeWind(getDefaultConfig(__dirname), { input: "./src/global.css" }), ); // XXX: Resolve our exports in workspace packages // https://github.com/expo/expo/issues/26926 config.resolver.unstable_enablePackageExports = true; module.exports = config; /** * Add the monorepo paths to the Metro config. * This allows Metro to resolve modules from the monorepo. * * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config * @param {import('expo/metro-config').MetroConfig} config * @returns {import('expo/metro-config').MetroConfig} */ function withMonorepoPaths(config) { const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, "../.."); // #1 - Watch all files in the monorepo config.watchFolders = [workspaceRoot]; // #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(workspaceRoot, "node_modules"), ]; return config; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/copy-button.tsx: -------------------------------------------------------------------------------- ```typescript import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Copy, Check } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; interface CopyButtonProps { textToCopy: string; } export default function CopyButton({ textToCopy }: CopyButtonProps) { const [isCopied, setIsCopied] = useState(false); useEffect(() => { if (isCopied) { const timer = setTimeout(() => setIsCopied(false), 2000); return () => clearTimeout(timer); } }, [isCopied]); const handleCopy = async () => { try { await navigator.clipboard.writeText(textToCopy); setIsCopied(true); } catch (err) { console.error("Failed to copy text: ", err); } }; return ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button variant="link" size="icon" onClick={handleCopy} className="h-8 w-8" > {isCopied ? ( <Check className="h-4 w-4 " /> ) : ( <Copy className="h-4 w-4" /> )} <span className="sr-only">Copy to clipboard</span> </Button> </TooltipTrigger> <TooltipContent> <p>{isCopied ? "Copied!" : "Copy to clipboard"}</p> </TooltipContent> </Tooltip> </TooltipProvider> ); } ``` -------------------------------------------------------------------------------- /e2e/smoke/test/bun.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it } from "node:test"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { join } from "node:path"; import assert from "node:assert"; const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); describe("(bun) simple server", () => { it("run server", async (t) => { const cp = spawn("bun", [join(fixturesDir, "bun-simple.ts")], { stdio: "pipe", }); t.after(() => { cp.kill("SIGINT"); }); cp.stdout.on("data", (data) => { console.log(data.toString()); }); cp.stderr.on("data", (data) => { console.error(data.toString()); }); const port = await new Promise<number>((resolve) => { cp.stdout.once("data", (data) => { // Bun outputs colored string, we need to remove it const port = +data.toString().replace(/\u001b\[[0-9;]*m/g, ""); assert.ok(port > 0); assert.ok(!isNaN(port)); assert.ok(isFinite(port)); resolve(port); }); }); const response = await fetch( `http://localhost:${port}/api/auth/sign-up/email`, { method: "POST", body: JSON.stringify({ email: "[email protected]", password: "password", name: "test-2", }), headers: { "content-type": "application/json", }, }, ); assert.ok(response.ok); }); }); ``` -------------------------------------------------------------------------------- /docs/components/ui/tooltip-docs.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import { cn } from "@/lib/utils"; const TooltipProvider = TooltipPrimitive.Provider; const Tooltip = TooltipPrimitive.Root; const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipContent = React.forwardRef< React.ElementRef<typeof TooltipPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> >(({ className, sideOffset = 4, ...props }, ref) => ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content ref={ref} sideOffset={sideOffset} className={cn( "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs 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", className, )} {...props} > {props.children} <TooltipPrimitive.Arrow className="fill-primary -mt-[1px]" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; ``` -------------------------------------------------------------------------------- /docs/components/builder/code-tabs/theme.ts: -------------------------------------------------------------------------------- ```typescript import { PrismTheme } from "prism-react-renderer"; const theme: PrismTheme = { plain: { color: "#d0d0d0", backgroundColor: "#000000", // Changed to true black }, styles: [ { types: ["comment", "prolog", "doctype", "cdata"], style: { color: "#555555", fontStyle: "italic", }, }, { types: ["namespace"], style: { opacity: 0.7, }, }, { types: ["string", "attr-value"], style: { color: "#8ab4f8", // Darker soft blue for strings }, }, { types: ["punctuation", "operator"], style: { color: "#888888", }, }, { types: [ "entity", "url", "symbol", "number", "boolean", "variable", "constant", "property", "regex", "inserted", ], style: { color: "#a0a0a0", }, }, { types: ["atrule", "keyword", "attr-name", "selector"], style: { color: "#c5c5c5", fontWeight: "bold", }, }, { types: ["function", "deleted", "tag"], style: { color: "#7aa2f7", // Darker soft blue for functions }, }, { types: ["function-variable"], style: { color: "#9e9e9e", }, }, { types: ["tag", "selector", "keyword"], style: { color: "#cccccc", // Adjusted to a slightly lighter gray for better contrast on true black }, }, ], }; export default theme; ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/remix.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Remix Example description: Better Auth Remix example. --- This is an example of how to use Better Auth with Remix. **Implements the following features:** Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management <ForkButton url="better-auth/better-auth/tree/main/examples/remix-example" /> <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/remix-example?codemirror=1&fontsize=14&hidedevtools=1&hidenavigation=1&runonclick=1" style={{ width: "100%", height: "500px", border: 0, borderRadius: "4px", overflow: "hidden" }} title="Better Auth Remix Example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > </iframe> ## How to run 1. Clone the code sandbox (or the repo) and open it in your code editor 2. Provide .env file with by copying the `.env.example` file and adding the variables 3. Run the following commands ```bash pnpm install pnpm run dev ``` 4. Open the browser and navigate to `http://localhost:3000` ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/svelte-kit.mdx: -------------------------------------------------------------------------------- ```markdown --- title: SvelteKit Example description: Better Auth SvelteKit example. --- This is an example of how to use Better Auth with SvelteKit. **Implements the following features:** Email & Password . <u>Social Sign-in with Google</u> . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management <ForkButton url="better-auth/better-auth/tree/main/examples/svelte-kit-example" /> <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/svelte-kit-example?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" style={{ width: "100%", height: "500px", border: 0, borderRadius: "4px", overflow: "hidden" }} title="Better Auth SvelteKit Example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > </iframe> ## How to run 1. Clone the code sandbox (or the repo) and open it in your code editor 2. Move .env.example to .env and provide necessary variables 3. Run the following commands ```bash pnpm install pnpm dev ``` 4. Open the browser and navigate to `http://localhost:3000` ``` -------------------------------------------------------------------------------- /docs/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import type * as React from "react"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function absoluteUrl(path: string) { return `${process.env.NEXT_PUBLIC_APP_URL}${path}`; } export function kFormatter(num: number) { const absNum = Math.abs(num); const sign = Math.sign(num); if (absNum >= 1000000000) { return sign * parseFloat((absNum / 1000000000).toFixed(1)) + "B+"; } else if (absNum >= 1000000) { return sign * parseFloat((absNum / 1000000).toFixed(1)) + "M+"; } else if (absNum >= 1000) { return sign * parseFloat((absNum / 1000).toFixed(1)) + "K+"; } return sign * absNum; } export const baseUrl = process.env.NODE_ENV === "development" || !process.env.VERCEL_URL ? new URL("http://localhost:3000") : new URL(`https://${process.env.VERCEL_URL}`); export function formatDate(date: Date) { let d = new Date(date); return d .toLocaleDateString("en-US", { month: "short", day: "numeric" }) .replace(",", ""); } export function mergeRefs<T>( ...refs: (React.Ref<T> | undefined)[] ): React.RefCallback<T> { return (value) => { refs.forEach((ref) => { if (typeof ref === "function") { ref(value); } else if (ref) { ref.current = value; } }); }; } ``` -------------------------------------------------------------------------------- /docs/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; import { cn } from "@/lib/utils"; function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) { return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />; } function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { return ( <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> ); } function HoverCardContent({ className, align = "center", sideOffset = 4, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>) { return ( <HoverCardPrimitive.Content data-slot="hover-card-content" align={align} sideOffset={sideOffset} className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden", className, )} {...props} /> ); } export { HoverCard, HoverCardTrigger, HoverCardContent }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/wrapper.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import Link from "next/link"; import { ThemeToggle } from "./theme-toggle"; import { Logo } from "./logo"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export function Wrapper(props: { children: React.ReactNode }) { return ( <div className="min-h-screen w-full dark:bg-black bg-white dark:bg-grid-small-white/[0.2] bg-grid-small-black/[0.2] relative flex justify-center"> <div className="absolute pointer-events-none inset-0 md:flex items-center justify-center dark:bg-black bg-white mask-[radial-gradient(ellipse_at_center,transparent_20%,black)] hidden"></div> <div className="bg-white dark:bg-black border-b py-2 flex justify-between items-center border-border absolute z-50 w-full lg:w-8/12 px-4 md:px-1"> <Link href="/"> <div className="flex gap-2 cursor-pointer"> <Logo /> <p className="dark:text-white text-black">BETTER-AUTH.</p> </div> </Link> <div className="z-50 flex items-center"> <ThemeToggle /> </div> </div> <div className="mt-20 lg:w-7/12 w-full">{props.children}</div> </div> ); } const queryClient = new QueryClient(); export function WrapperWithQuery(props: { children: React.ReactNode | any }) { return ( <QueryClientProvider client={queryClient}> {props.children} </QueryClientProvider> ); } ``` -------------------------------------------------------------------------------- /docs/app/not-found.tsx: -------------------------------------------------------------------------------- ```typescript import Section from "@/components/landing/section"; import Link from "next/link"; import { Logo } from "@/components/logo"; export default function NotFound() { return ( <div className="h-full relative overflow-hidden"> <Section className="mb-1 h-[92.3vh] overflow-y-hidden" crosses crossesOffset="lg:translate-y-[5.25rem]" customPaddings id="404" > <div className="relative flex flex-col h-full items-center justify-center dark:bg-black bg-white text-black dark:text-white"> <div className="relative mb-8"> <Logo className="w-10 h-10" /> </div> <h1 className="text-8xl font-normal">404</h1> <p className="text-sm mb-8">Need help? Visit the docs</p> <div className="flex flex-col items-center gap-6"> <Link href="/docs" className="hover:shadow-sm dark:border-stone-100 dark:hover:shadow-sm border-2 border-black bg-white px-4 py-1.5 text-sm uppercase text-black shadow-[1px_1px_rgba(0,0,0),2px_2px_rgba(0,0,0),3px_3px_rgba(0,0,0),4px_4px_rgba(0,0,0),5px_5px_0px_0px_rgba(0,0,0)] transition duration-200 md:px-8 dark:shadow-[1px_1px_rgba(255,255,255),2px_2px_rgba(255,255,255),3px_3px_rgba(255,255,255),4px_4px_rgba(255,255,255),5px_5px_0px_0px_rgba(255,255,255)]" > Go to docs </Link> </div> </div> </Section> </div> ); } ``` -------------------------------------------------------------------------------- /demo/nextjs/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", }); const ToggleGroup = ({ ref, className, variant, size, children, ...props }) => ( <ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props} > <ToggleGroupContext.Provider value={{ variant, size }}> {children} </ToggleGroupContext.Provider> </ToggleGroupPrimitive.Root> ); ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; const ToggleGroupItem = ({ ref, className, children, variant, size, ...props }) => { const context = React.useContext(ToggleGroupContext); return ( <ToggleGroupPrimitive.Item ref={ref} className={cn( toggleVariants({ variant: context.variant || variant, size: context.size || size, }), className, )} {...props} > {children} </ToggleGroupPrimitive.Item> ); }; ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; export { ToggleGroup, ToggleGroupItem }; ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/app.ts: -------------------------------------------------------------------------------- ```typescript import { createServer } from "node:http"; import { betterAuth } from "better-auth"; import { toNodeHandler } from "better-auth/node"; import Database from "better-sqlite3"; import { getMigrations } from "better-auth/db"; export async function createAuthServer( baseURL: string = "http://localhost:3000", ) { const database = new Database(":memory:"); const auth = betterAuth({ database, baseURL, emailAndPassword: { enabled: true, }, }); const { runMigrations } = await getMigrations(auth.options); await runMigrations(); // Create an example user await auth.api.signUpEmail({ body: { name: "Test User", email: "[email protected]", password: "password123", }, }); const authHandler = toNodeHandler(auth); return createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); res.setHeader("Access-Control-Allow-Credentials", "true"); if (req.method === "OPTIONS") { res.statusCode = 200; res.end(); return; } const isAuthRoute = req.url?.startsWith("/api/auth"); if (isAuthRoute) { return authHandler(req, res); } res.statusCode = 404; res.end(JSON.stringify({ error: "Not found" })); }); } ``` -------------------------------------------------------------------------------- /docs/components/ripple.tsx: -------------------------------------------------------------------------------- ```typescript import React, { CSSProperties } from "react"; interface RippleProps { mainCircleSize?: number; mainCircleOpacity?: number; numCircles?: number; } export const Ripple = React.memo(function Ripple({ mainCircleSize = 180, mainCircleOpacity = 0.2, numCircles = 10, }: RippleProps) { return ( <div className="absolute opacity-65 w-full inset-0 flex items-center justify-center bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)] dark:[box-shadow:0_-20px_80px_-20px_#8686f01f_inset]"> {Array.from({ length: numCircles }, (_, i) => { const size = mainCircleSize + i * 70; const opacity = mainCircleOpacity - i * 0.03; const animationDelay = `${i * 0.06}s`; const borderStyle = i === numCircles - 1 ? "dashed" : "solid"; const borderOpacity = 5 + i * 5; return ( <div key={i} className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border [--i:${i}]`} style={ { width: `${size}px`, height: `${size}px`, opacity, animationDelay, borderStyle, borderWidth: "1px", borderColor: `hsl(var(--foreground), ${borderOpacity / 100})`, top: "50%", left: "50%", transform: "translate(-50%, -50%) scale(1)", } as CSSProperties } /> ); })} </div> ); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/password.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import type { GenericEndpointContext } from "@better-auth/core"; export async function validatePassword( ctx: GenericEndpointContext, data: { password: string; userId: string; }, ) { const accounts = await ctx.context.internalAdapter.findAccounts(data.userId); const credentialAccount = accounts?.find( (account) => account.providerId === "credential", ); const currentPassword = credentialAccount?.password; if (!credentialAccount || !currentPassword) { return false; } const compare = await ctx.context.password.verify({ hash: currentPassword, password: data.password, }); return compare; } export async function checkPassword(userId: string, c: GenericEndpointContext) { const accounts = await c.context.internalAdapter.findAccounts(userId); const credentialAccount = accounts?.find( (account) => account.providerId === "credential", ); const currentPassword = credentialAccount?.password; if (!credentialAccount || !currentPassword || !c.body.password) { throw new APIError("BAD_REQUEST", { message: "No password credential found", }); } const compare = await c.context.password.verify({ hash: currentPassword, password: c.body.password, }); if (!compare) { throw new APIError("BAD_REQUEST", { message: "Invalid password", }); } return true; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/tests/transactions.ts: -------------------------------------------------------------------------------- ```typescript import { expect } from "vitest"; import { createTestSuite } from "../create-test-suite"; import type { User } from "../../types"; /** * This test suite tests the transaction functionality of the adapter. */ export const transactionsTestSuite = createTestSuite( "transactions", {}, ({ adapter, generate, hardCleanup }) => ({ "transaction - should rollback failing transaction": async ({ skip }) => { const isEnabled = adapter.options?.adapterConfig.transaction; if (!isEnabled) { skip( `Skipping test: ${adapter.options?.adapterConfig.adapterName} does not support transactions`, ); return; } const user1 = await generate("user"); const user2 = await generate("user"); await expect( adapter.transaction(async (tx) => { await tx.create({ model: "user", data: user1, forceAllowId: true }); const users = await tx.findMany({ model: "user" }); expect(users).toHaveLength(1); throw new Error("Simulated failure"); await tx.create({ model: "user", data: user2, forceAllowId: true }); }), ).rejects.toThrow("Simulated failure"); const result = await adapter.findMany<User>({ model: "user", }); //Transactions made rows are unable to be automatically cleaned up, so we need to clean them up manually await hardCleanup(); expect(result.length).toBe(0); }, }), ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/react-start.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPlugin } from "@better-auth/core"; import { parseSetCookieHeader } from "../cookies"; import { createAuthMiddleware } from "@better-auth/core/middleware"; export const reactStartCookies = () => { return { id: "react-start-cookies", hooks: { after: [ { matcher(ctx) { return true; }, handler: createAuthMiddleware(async (ctx) => { const returned = ctx.context.responseHeaders; if ("_flag" in ctx && ctx._flag === "router") { return; } if (returned instanceof Headers) { const setCookies = returned?.get("set-cookie"); if (!setCookies) return; const parsed = parseSetCookieHeader(setCookies); const { setCookie } = await import("@tanstack/start-server-core"); parsed.forEach((value, key) => { if (!key) return; const opts = { sameSite: value.samesite, secure: value.secure, maxAge: value["max-age"], httpOnly: value.httponly, domain: value.domain, path: value.path, } as const; try { setCookie(key, decodeURIComponent(value.value), opts); } catch (e) { // this will fail if the cookie is being set on server component } }); return; } }), }, ], }, } satisfies BetterAuthPlugin; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/auth.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expectTypeOf, test } from "vitest"; import { betterAuth, type Auth } from "./auth"; import { router } from "better-auth/api"; import { createAuthEndpoint } from "@better-auth/core/middleware"; describe("auth type", () => { test("default auth type should be okay", () => { const auth = betterAuth({}); type T = typeof auth; expectTypeOf<T>().toEqualTypeOf<Auth>(); }); test("$ERROR_CODES in auth", () => { const auth = betterAuth({ plugins: [ { id: "custom-plugin", $ERROR_CODES: { CUSTOM_ERROR: "Custom error message", }, }, ], }); type T = typeof auth.$ERROR_CODES; expectTypeOf<T>().toEqualTypeOf< { CUSTOM_ERROR: string; } & typeof import("@better-auth/core/error").BASE_ERROR_CODES >(); }); test("plugin endpoints", () => { const endpoints = { getSession: createAuthEndpoint( "/get-session", { method: "GET" }, async () => { return { data: { message: "Hello, World!", }, }; }, ), }; const auth = betterAuth({ plugins: [ { id: "custom-plugin", endpoints, }, ], }); type T = typeof auth; type E = ReturnType<typeof router<T["options"]>>["endpoints"]; type G = E["getSession"]; type R = Awaited<ReturnType<G>>; expectTypeOf<R>().toEqualTypeOf<{ data: { message: string } }>(); }); }); ``` -------------------------------------------------------------------------------- /packages/core/src/context/transaction.ts: -------------------------------------------------------------------------------- ```typescript import { getAsyncLocalStorage, type AsyncLocalStorage } from "../async_hooks"; import type { DBTransactionAdapter, DBAdapter } from "../db/adapter"; let currentAdapterAsyncStorage: AsyncLocalStorage<DBTransactionAdapter> | null = null; const ensureAsyncStorage = async () => { if (!currentAdapterAsyncStorage) { const AsyncLocalStorage = await getAsyncLocalStorage(); currentAdapterAsyncStorage = new AsyncLocalStorage(); } return currentAdapterAsyncStorage; }; export const getCurrentAdapter = async ( fallback: DBTransactionAdapter, ): Promise<DBTransactionAdapter> => { return ensureAsyncStorage() .then((als) => { return als.getStore() || fallback; }) .catch(() => { return fallback; }); }; export const runWithAdapter = async <R>( adapter: DBAdapter, fn: () => R, ): Promise<R> => { let called = true; return ensureAsyncStorage() .then((als) => { called = true; return als.run(adapter, fn); }) .catch((err) => { if (!called) { return fn(); } throw err; }); }; export const runWithTransaction = async <R>( adapter: DBAdapter, fn: () => R, ): Promise<R> => { let called = true; return ensureAsyncStorage() .then((als) => { called = true; return adapter.transaction(async (trx) => { return als.run(trx, fn); }); }) .catch((err) => { if (!called) { return fn(); } throw err; }); }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import { CircleIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { return ( <RadioGroupPrimitive.Root data-slot="radio-group" className={cn("grid gap-3", className)} {...props} /> ); } function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { return ( <RadioGroupPrimitive.Item data-slot="radio-group-item" className={cn( "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", className, )} {...props} > <RadioGroupPrimitive.Indicator data-slot="radio-group-indicator" className="relative flex items-center justify-center" > <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> </RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Item> ); } export { RadioGroup, RadioGroupItem }; ``` -------------------------------------------------------------------------------- /docs/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import { CircleIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { return ( <RadioGroupPrimitive.Root data-slot="radio-group" className={cn("grid gap-3", className)} {...props} /> ); } function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { return ( <RadioGroupPrimitive.Item data-slot="radio-group-item" className={cn( "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", className, )} {...props} > <RadioGroupPrimitive.Indicator data-slot="radio-group-indicator" className="relative flex items-center justify-center" > <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> </RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Item> ); } export { RadioGroup, RadioGroupItem }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.pg.test.ts: -------------------------------------------------------------------------------- ```typescript import { Kysely, PostgresDialect } from "kysely"; import { testAdapter } from "../../test-adapter"; import { kyselyAdapter } from "../kysely-adapter"; import { Pool } from "pg"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import { getMigrations } from "../../../db"; import type { BetterAuthOptions } from "@better-auth/core"; const pgDB = new Pool({ connectionString: "postgres://user:password@localhost:5433/better_auth", }); let kyselyDB = new Kysely({ dialect: new PostgresDialect({ pool: pgDB }), }); const cleanupDatabase = async () => { await pgDB.query(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`); }; const { execute } = await testAdapter({ adapter: () => kyselyAdapter(kyselyDB, { type: "postgres", debugLogs: { isRunningAdapterTests: true }, }), prefixTests: "pg", async runMigrations(betterAuthOptions) { await cleanupDatabase(); const opts = Object.assign(betterAuthOptions, { database: pgDB, } satisfies BetterAuthOptions); const { runMigrations } = await getMigrations(opts); await runMigrations(); }, tests: [ normalTestSuite(), transactionsTestSuite({ disableTests: { ALL: true } }), authFlowTestSuite(), numberIdTestSuite(), performanceTestSuite({ dialect: "pg" }), ], async onFinish() { await pgDB.end(); }, }); execute(); ``` -------------------------------------------------------------------------------- /docs/content/docs/comparison.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Comparison description: Comparison of Better Auth versus over other auth libraries and services. --- > <p className="text-orange-200">Comparison is the thief of joy.</p> Here are non detailed reasons why you may want to use Better Auth over other auth libraries and services. ### vs Other Auth Libraries - **Framework agnostic** - Works with any framework, not just specific ones - **Advanced features built-in** - 2FA, multi-tenancy, multi-session, rate limiting, and many more - **Plugin system** - Extend functionality without forking or complex workarounds - **Full control** - Customize auth flows exactly how you want ### vs Self-Hosted Auth Servers - **No separate infrastructure** - Runs in your app, users stay in your database - **Zero server maintenance** - No auth servers to deploy, monitor, or update - **Complete feature set** - Everything you need without the operational overhead ### vs Managed Auth Services - **Keep your data** - Users stay in your database, not a third-party service - **No per-user costs** - Scale without worrying about auth billing - **Single source of truth** - All user data in one place ### vs Rolling Your Own - **Security handled** - Battle-tested auth flows and security practices - **Focus on your product** - Spend time on features that matter to your business - **Plugin extensibility** - Add custom features without starting from scratch ``` -------------------------------------------------------------------------------- /packages/core/src/error/codes.ts: -------------------------------------------------------------------------------- ```typescript import { defineErrorCodes } from "../utils"; export const BASE_ERROR_CODES = defineErrorCodes({ USER_NOT_FOUND: "User not found", FAILED_TO_CREATE_USER: "Failed to create user", FAILED_TO_CREATE_SESSION: "Failed to create session", FAILED_TO_UPDATE_USER: "Failed to update user", FAILED_TO_GET_SESSION: "Failed to get session", INVALID_PASSWORD: "Invalid password", INVALID_EMAIL: "Invalid email", INVALID_EMAIL_OR_PASSWORD: "Invalid email or password", SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked", PROVIDER_NOT_FOUND: "Provider not found", INVALID_TOKEN: "Invalid token", ID_TOKEN_NOT_SUPPORTED: "id_token not supported", FAILED_TO_GET_USER_INFO: "Failed to get user info", USER_EMAIL_NOT_FOUND: "User email not found", EMAIL_NOT_VERIFIED: "Email not verified", PASSWORD_TOO_SHORT: "Password too short", PASSWORD_TOO_LONG: "Password too long", USER_ALREADY_EXISTS: "User already exists.", USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.", EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated", CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found", SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.", FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account", ACCOUNT_NOT_FOUND: "Account not found", USER_ALREADY_HAS_PASSWORD: "User already has a password. Provide that to delete the account.", }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/api.ts: -------------------------------------------------------------------------------- ```typescript import type { Endpoint } from "better-call"; import type { PrettifyDeep, UnionToIntersection } from "../types/helper"; export type FilteredAPI<API> = Omit< API, API extends { [key in infer K]: Endpoint } ? K extends string ? K extends "getSession" ? K : API[K]["options"]["metadata"] extends { isAction: false } ? K : never : never : never >; export type FilterActions<API> = Omit< API, API extends { [key in infer K]: Endpoint } ? K extends string ? API[K]["options"]["metadata"] extends { isAction: false } ? K : never : never : never >; export type InferSessionAPI<API> = API extends { [key: string]: infer E; } ? UnionToIntersection< E extends Endpoint ? E["path"] extends "/get-session" ? { getSession: < R extends boolean, H extends boolean = false, >(context: { headers: Headers; query?: { disableCookieCache?: boolean; disableRefresh?: boolean; }; asResponse?: R; returnHeaders?: H; }) => false extends R ? H extends true ? Promise<{ headers: Headers; response: PrettifyDeep<Awaited<ReturnType<E>>> | null; }> : Promise<PrettifyDeep<Awaited<ReturnType<E>>> | null> : Promise<Response>; } : never : never > : never; export type InferAPI<API> = InferSessionAPI<API> & API; ``` -------------------------------------------------------------------------------- /demo/nextjs/app/dashboard/page.tsx: -------------------------------------------------------------------------------- ```typescript import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; import UserCard from "./user-card"; import { OrganizationCard } from "./organization-card"; import AccountSwitcher from "@/components/account-switch"; export default async function DashboardPage() { const [session, activeSessions, deviceSessions, organization, subscriptions] = await Promise.all([ auth.api.getSession({ headers: await headers(), }), auth.api.listSessions({ headers: await headers(), }), auth.api.listDeviceSessions({ headers: await headers(), }), auth.api.getFullOrganization({ headers: await headers(), }), auth.api.listActiveSubscriptions({ headers: await headers(), }), ]).catch((e) => { console.log(e); throw redirect("/sign-in"); }); return ( <div className="w-full"> <div className="flex gap-4 flex-col"> <AccountSwitcher sessions={JSON.parse(JSON.stringify(deviceSessions))} /> <UserCard session={JSON.parse(JSON.stringify(session))} activeSessions={JSON.parse(JSON.stringify(activeSessions))} subscription={subscriptions.find( (sub) => sub.status === "active" || sub.status === "trialing", )} /> <OrganizationCard session={JSON.parse(JSON.stringify(session))} activeOrganization={JSON.parse(JSON.stringify(organization))} /> </div> </div> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.sqlite.test.ts: -------------------------------------------------------------------------------- ```typescript import { Kysely, SqliteDialect } from "kysely"; import { testAdapter } from "../../test-adapter"; import { kyselyAdapter } from "../kysely-adapter"; import Database from "better-sqlite3"; import { authFlowTestSuite, normalTestSuite, numberIdTestSuite, performanceTestSuite, transactionsTestSuite, } from "../../tests"; import path from "path"; import { getMigrations } from "../../../db"; import fs from "fs/promises"; const dbPath = path.join(__dirname, "test.db"); let database = new Database(dbPath); let kyselyDB = new Kysely({ dialect: new SqliteDialect({ database }), }); const { execute } = await testAdapter({ adapter: () => { return kyselyAdapter(kyselyDB, { type: "sqlite", debugLogs: { isRunningAdapterTests: true }, }); }, prefixTests: "sqlite", async runMigrations(betterAuthOptions) { database.close(); try { await fs.unlink(dbPath); } catch { console.log("db doesnt exist"); } database = new Database(dbPath); kyselyDB = new Kysely({ dialect: new SqliteDialect({ database }) }); const opts = Object.assign(betterAuthOptions, { database }); const { runMigrations } = await getMigrations(opts); await runMigrations(); }, tests: [ normalTestSuite({}), transactionsTestSuite({ disableTests: { ALL: true } }), authFlowTestSuite(), numberIdTestSuite(), performanceTestSuite({ dialect: "sqlite" }), ], async onFinish() { database.close(); }, }); execute(); ``` -------------------------------------------------------------------------------- /docs/components/ui/card.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { cn } from "@/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card" className={cn( "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className, )} {...props} /> ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-header" className={cn("flex flex-col gap-1.5 px-6", className)} {...props} /> ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} /> ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-content" className={cn("px-6", className)} {...props} /> ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="card-footer" className={cn("flex items-center px-6", className)} {...props} /> ); } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/time.ts: -------------------------------------------------------------------------------- ```typescript const minute = 60; const hour = minute * 60; const day = hour * 24; const week = day * 7; const year = day * 365.25; const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i; /** * https://github.com/panva/jose/blob/723ee6152d7ee2fc81852d2d26777e86df6fce01/src/lib/secs.ts */ export function joseSecs(str: string): number { const matched = REGEX.exec(str); if (!matched || (matched[4]! && matched[1]!)) { throw new TypeError("Invalid time period format"); } const value = parseFloat(matched[2]!); const unit = matched[3]!.toLowerCase(); let numericDate: number; switch (unit) { case "sec": case "secs": case "second": case "seconds": case "s": numericDate = Math.round(value); break; case "minute": case "minutes": case "min": case "mins": case "m": numericDate = Math.round(value * minute); break; case "hour": case "hours": case "hr": case "hrs": case "h": numericDate = Math.round(value * hour); break; case "day": case "days": case "d": numericDate = Math.round(value * day); break; case "week": case "weeks": case "w": numericDate = Math.round(value * week); break; // years matched default: numericDate = Math.round(value * year); break; } if (matched[1]! === "-" || matched[4]! === "ago") { return -numericDate; } return numericDate; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/index.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, vi } from "vitest"; import { getEndpoints } from "./index"; import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core"; import { createAuthMiddleware } from "@better-auth/core/middleware"; import type { AuthContext } from "@better-auth/core"; describe("getEndpoints", () => { it("should await promise-based context before passing to middleware", async () => { const mockContext: AuthContext = { baseURL: "http://localhost:3000", options: {}, } as any; const middlewareFn = vi.fn().mockResolvedValue({}); const testPlugin: BetterAuthPlugin = { id: "test-plugin", middlewares: [ { path: "/test", middleware: createAuthMiddleware(async (ctx) => { middlewareFn(ctx); return {}; }), }, ], }; const options: BetterAuthOptions = { plugins: [testPlugin], }; const promiseContext = new Promise<AuthContext>((resolve) => { setTimeout(() => resolve(mockContext), 10); }); const { middlewares } = getEndpoints(promiseContext, options); const testCtx = { request: new Request("http://localhost:3000/test"), context: { customProp: "value" }, }; await middlewares[0]!.middleware(testCtx); expect(middlewareFn).toHaveBeenCalled(); const call = middlewareFn.mock.calls[0]![0]; expect(call.context).toMatchObject({ baseURL: "http://localhost:3000", options: {}, customProp: "value", }); }); }); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/root", "private": true, "type": "module", "packageManager": "[email protected]", "scripts": { "build": "turbo --filter \"./packages/*\" build", "dev": "turbo --filter \"./packages/*\" dev", "dev:dts": "turbo --filter \"./packages/*\" dev:dts", "clean": "turbo --filter \"./packages/*\" clean && rm -rf node_modules", "format": "biome format . --write", "lint": "biome check .", "lint:fix": "biome check . --fix --unsafe", "release": "turbo --filter \"./packages/*\" build && bumpp && pnpm -r publish --access public --no-git-checks", "release:no-build": "bumpp && pnpm -r publish --access public --no-git-checks --tag next", "release:canary": "turbo --filter \"./packages/*\" build && bumpp && pnpm -r publish --access public --tag canary --no-git-checks", "bump": "bumpp", "test": "turbo --filter \"./packages/*\" test", "e2e:smoke": "turbo --filter \"./e2e/*\" e2e:smoke", "e2e:integration": "turbo --filter \"./e2e/*\" e2e:integration", "typecheck": "turbo --filter \"./packages/*\" typecheck" }, "devDependencies": { "@biomejs/biome": "2.2.4", "@types/bun": "^1.2.23", "@types/node": "^24.7.1", "bumpp": "^10.2.3", "tinyglobby": "^0.2.15", "turbo": "^2.5.6", "typescript": "catalog:", "vitest": "catalog:" }, "resolutions": { "zod": "^4.1.5", "miniflare>zod": "^3.25.1", "vinxi>zod": "^3.24.3" } } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/card.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { cn } from "@/lib/utils"; const Card = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn( "rounded-xl border bg-card text-card-foreground shadow", className, )} {...props} /> ); Card.displayName = "Card"; const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> ); CardHeader.displayName = "CardHeader"; const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => ( <h3 className={cn("font-semibold leading-none tracking-tight", className)} {...props} /> ); CardTitle.displayName = "CardTitle"; const CardDescription = ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => ( <p className={cn("text-sm text-muted-foreground", className)} {...props} /> ); CardDescription.displayName = "CardDescription"; const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("p-6 pt-0", className)} {...props} /> ); CardContent.displayName = "CardContent"; const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex items-center p-6 pt-0", className)} {...props} /> ); CardFooter.displayName = "CardFooter"; export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, }; ``` -------------------------------------------------------------------------------- /e2e/smoke/test/fixtures/cloudflare/drizzle/0000_clean_vector.sql: -------------------------------------------------------------------------------- ```sql CREATE TABLE `account` ( `id` text PRIMARY KEY NOT NULL, `account_id` text NOT NULL, `provider_id` text NOT NULL, `user_id` text NOT NULL, `access_token` text, `refresh_token` text, `id_token` text, `access_token_expires_at` integer, `refresh_token_expires_at` integer, `scope` text, `password` text, `created_at` integer NOT NULL, `updated_at` integer NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `session` ( `id` text PRIMARY KEY NOT NULL, `expires_at` integer NOT NULL, `token` text NOT NULL, `created_at` integer NOT NULL, `updated_at` integer NOT NULL, `ip_address` text, `user_agent` text, `user_id` text NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint CREATE TABLE `user` ( `id` text PRIMARY KEY NOT NULL, `name` text NOT NULL, `email` text NOT NULL, `email_verified` integer NOT NULL, `image` text, `created_at` integer NOT NULL, `updated_at` integer NOT NULL ); --> statement-breakpoint CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint CREATE TABLE `verification` ( `id` text PRIMARY KEY NOT NULL, `identifier` text NOT NULL, `value` text NOT NULL, `expires_at` integer NOT NULL, `created_at` integer, `updated_at` integer ); ``` -------------------------------------------------------------------------------- /docs/components/theme-toggler.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import type { ComponentProps } from "react"; import { cn } from "@/lib/utils"; export function ThemeToggle(props: ComponentProps<typeof Button>) { const { setTheme } = useTheme(); return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" aria-label="Toggle Theme" {...props} className={cn( "flex ring-0 shrink-0 md:w-[3.56rem] md:h-14 md:border-l md:text-muted-foreground max-md:-mr-1.5 max-md:hover:bg-transparent", props.className, )} > <Sun className="size-4 fill-current dark:hidden md:size-5" /> <Moon className="absolute fill-current size-4 hidden dark:block md:size-5" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent className="rounded-none" align="end"> <DropdownMenuItem className="rounded-none" onClick={() => setTheme("light")} > Light </DropdownMenuItem> <DropdownMenuItem className="rounded-none" onClick={() => setTheme("dark")} > Dark </DropdownMenuItem> <DropdownMenuItem className="rounded-none" onClick={() => setTheme("system")} > System </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); } ``` -------------------------------------------------------------------------------- /docs/components/ui/toggle.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as TogglePrimitive from "@radix-ui/react-toggle"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const toggleVariants = cva( "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", { variants: { variant: { default: "bg-transparent", outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-9 px-2 min-w-9", sm: "h-8 px-1.5 min-w-8", lg: "h-10 px-2.5 min-w-10", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); function Toggle({ className, variant, size, ...props }: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) { return ( <TogglePrimitive.Root data-slot="toggle" className={cn(toggleVariants({ variant, size, className }))} {...props} /> ); } export { Toggle, toggleVariants }; ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/forget-password.tsx: -------------------------------------------------------------------------------- ```typescript import { Button } from "@/components/ui/button"; import { Card, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Text } from "@/components/ui/text"; import { authClient } from "@/lib/auth-client"; import { useState } from "react"; import { View } from "react-native"; import Icons from "@expo/vector-icons/AntDesign"; import { router } from "expo-router"; export default function ForgetPassword() { const [email, setEmail] = useState(""); return ( <Card className="w-10/12 "> <CardHeader> <CardTitle>Forget Password</CardTitle> <CardDescription> Enter your email to reset your password </CardDescription> </CardHeader> <View className="px-6 mb-2"> <Input autoCapitalize="none" placeholder="Email" value={email} onChangeText={(text) => setEmail(text)} /> </View> <CardFooter> <View className="w-full gap-2"> <Button onPress={() => { authClient.forgetPassword({ email, redirectTo: "/reset-password", }); }} className="w-full" variant="default" > <Text>Send Email</Text> </Button> <Button onPress={() => { router.push("/"); }} className="w-full flex-row gap-4 items-center" variant="outline" > <Icons name="arrowleft" size={18} /> <Text>Back to Sign In</Text> </Button> </View> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/utils.ts: -------------------------------------------------------------------------------- ```typescript import type { DBFieldAttribute } from "@better-auth/core/db"; export function withApplyDefault( value: any, field: DBFieldAttribute, action: "create" | "update", ) { if (action === "update") { // Apply onUpdate if value is undefined if (value === undefined && field.onUpdate !== undefined) { if (typeof field.onUpdate === "function") { return field.onUpdate(); } return field.onUpdate; } return value; } if (value === undefined || value === null) { if (field.defaultValue !== undefined) { if (typeof field.defaultValue === "function") { return field.defaultValue(); } return field.defaultValue; } } return value; } function isObject(item: unknown): item is Record<string, unknown> { return item !== null && typeof item === "object" && !Array.isArray(item); } export function deepmerge<T>(target: T, source: Partial<T>): T { if (Array.isArray(target) && Array.isArray(source)) { // merge arrays by concatenation return [...target, ...source] as T; } else if (isObject(target) && isObject(source)) { const result: Record<string, unknown> = { ...target }; for (const [key, value] of Object.entries(source)) { if (value === undefined) continue; // skip undefineds if (key in target) { result[key] = deepmerge( (target as Record<string, unknown>)[key], value as unknown as Partial<T>, ); } else { result[key] = value; } } return result as T; } // primitives and fallback: source overrides target return source as T; } ``` -------------------------------------------------------------------------------- /packages/sso/package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/sso", "author": "Bereket Engida", "version": "1.4.0-beta.10", "type": "module", "main": "dist/index.js", "license": "MIT", "keywords": [ "sso", "auth", "sso", "saml", "oauth", "oidc", "openid", "openid connect", "openid connect", "single sign on" ], "publishConfig": { "access": "public" }, "module": "dist/index.js", "description": "SSO plugin for Better Auth", "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" ] } }, "dependencies": { "@better-fetch/fetch": "catalog:", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "oauth2-mock-server": "^7.2.1", "samlify": "^2.10.1", "zod": "^4.1.5" }, "devDependencies": { "@types/body-parser": "^1.19.6", "@types/express": "^5.0.3", "better-auth": "workspace:^", "better-call": "catalog:", "body-parser": "^2.2.0", "express": "^5.1.0", "tsdown": "catalog:" }, "peerDependencies": { "better-auth": "workspace:*" } } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/error-codes.ts: -------------------------------------------------------------------------------- ```typescript // NOTE: Error code const must be all capital of string (ref https://github.com/better-auth/better-auth/issues/4386) import { defineErrorCodes } from "@better-auth/core/utils"; export const ADMIN_ERROR_CODES = defineErrorCodes({ FAILED_TO_CREATE_USER: "Failed to create user", USER_ALREADY_EXISTS: "User already exists.", USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.", YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself", YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE: "You are not allowed to change users role", YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users", YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS: "You are not allowed to list users sessions", YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users", YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS: "You are not allowed to impersonate users", YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS: "You are not allowed to revoke users sessions", YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users", YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD: "You are not allowed to set users password", BANNED_USER: "You have been banned from this application", YOU_ARE_NOT_ALLOWED_TO_GET_USER: "You are not allowed to get user", NO_DATA_TO_UPDATE: "No data to update", YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS: "You are not allowed to update users", YOU_CANNOT_REMOVE_YOURSELF: "You cannot remove yourself", }); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/alert.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { default: "bg-background text-foreground", destructive: "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", }, }, defaultVariants: { variant: "default", }, }, ); function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { return ( <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} /> ); } function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-title" className={cn( "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className, )} {...props} /> ); } function AlertDescription({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-description" className={cn( "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", className, )} {...props} /> ); } export { Alert, AlertTitle, AlertDescription }; ``` -------------------------------------------------------------------------------- /docs/components/ui/alert.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { default: "bg-background text-foreground", destructive: "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", }, }, defaultVariants: { variant: "default", }, }, ); function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { return ( <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} /> ); } function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-title" className={cn( "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className, )} {...props} /> ); } function AlertDescription({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-description" className={cn( "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", className, )} {...props} /> ); } export { Alert, AlertTitle, AlertDescription }; ``` -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- ```json { "root": true, "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "formatter": { "enabled": true, "indentStyle": "tab" }, "assist": { "actions": { "source": { "organizeImports": "off" } } }, "linter": { "enabled": true, "rules": { "recommended": false, "suspicious": { "noImplicitAnyLet": "warn", "noDuplicateObjectKeys": "warn", "noTsIgnore": "error", "noDebugger": "error" }, "performance": { "noDelete": "error" }, "complexity": { "noUselessSwitchCase": "warn", "noUselessTypeConstraint": "warn" }, "correctness": { "noUnusedImports": "warn" }, "nursery": { "noMisusedPromises": "error", "noFloatingPromises": "error" } } }, "overrides": [ { "includes": ["**/examples/svelte-kit-example/**"], "linter": { "rules": { "correctness": { "noUnusedImports": "off" } } } }, { "includes": ["**/*.json"], "formatter": { "indentStyle": "space", "indentWidth": 2 } } ], "files": { "includes": [ "**", "!**/dist", "!**/build", "!**/.next", "!**/.svelte-kit", "!**/.contentlayer", "!**/.turbo", "!**/.nuxt", "!**/.source", "!**/.expo", "!**/.cache", "!**/dev/cloudflare/drizzle", "!**/playwright-report", "!**/.output", "!**/.tmp" ] } } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/popover.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import { cn } from "@/lib/utils"; function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) { return <PopoverPrimitive.Root data-slot="popover" {...props} />; } function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; } function PopoverContent({ className, align = "center", sideOffset = 4, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) { return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Content data-slot="popover-content" align={align} sideOffset={sideOffset} className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden", className, )} {...props} /> </PopoverPrimitive.Portal> ); } function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; } export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; ``` -------------------------------------------------------------------------------- /docs/components/ui/popover.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import { cn } from "@/lib/utils"; function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) { return <PopoverPrimitive.Root data-slot="popover" {...props} />; } function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; } function PopoverContent({ className, align = "center", sideOffset = 4, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) { return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Content data-slot="popover-content" align={align} sideOffset={sideOffset} className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden", className, )} {...props} /> </PopoverPrimitive.Portal> ); } function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; } export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; ``` -------------------------------------------------------------------------------- /docs/components/ui/badge.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 badgeVariants = cva( "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { variant: "default", }, }, ); function Badge({ className, variant, asChild = false, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "span"; return ( <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} /> ); } export { Badge, badgeVariants }; ``` -------------------------------------------------------------------------------- /packages/core/src/db/index.ts: -------------------------------------------------------------------------------- ```typescript import type { DBFieldAttribute, DBFieldAttributeConfig, DBFieldType, DBPrimitive, BetterAuthDBSchema, DBPreservedModels, } from "./type"; import type { BetterAuthPluginDBSchema } from "./plugin"; export type { BetterAuthPluginDBSchema } from "./plugin"; export type { SecondaryStorage } from "./type"; export { coreSchema } from "./schema/shared"; export { userSchema, type User } from "./schema/user"; export { accountSchema, type Account } from "./schema/account"; export { sessionSchema, type Session } from "./schema/session"; export { verificationSchema, type Verification } from "./schema/verification"; export { rateLimitSchema, type RateLimit } from "./schema/rate-limit"; export type { DBFieldAttribute, DBFieldAttributeConfig, DBFieldType, DBPrimitive, BetterAuthDBSchema, DBPreservedModels, }; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type AuthPluginSchema = BetterAuthPluginDBSchema; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type FieldAttribute = DBFieldAttribute; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type FieldAttributeConfig = DBFieldAttributeConfig; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type FieldType = DBFieldType; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type Primitive = DBPrimitive; /** * @deprecated Backport for 1.3.x, we will remove this in 1.4.x */ export type BetterAuthDbSchema = BetterAuthDBSchema; ``` -------------------------------------------------------------------------------- /docs/components/resource-card.tsx: -------------------------------------------------------------------------------- ```typescript import { ArrowUpRight } from "lucide-react"; import { cn } from "@/lib/utils"; interface ResourceCardProps { title: string; description: string; href: string; tags?: string[]; className?: string; } export function ResourceCard({ title, description, href, tags, className, }: ResourceCardProps) { return ( <div className={cn( "relative flex justify-between rounded-none flex-col group space-y-1 border transition-colors hover:bg-muted/80", className, )} > <div> <ArrowUpRight className="absolute top-3 right-3 h-4 w-4 group-hover:opacity-100 opacity-80 text-muted-foreground transition-colors group-hover:text-foreground no-underline underline-offset-0" /> <div className="p-4 py-0 flex items-start justify-between"> <a href={href} target="_blank" rel="noopener noreferrer"> <h3 className="font-semibold text-md tracking-tight no-underline"> {title} </h3> </a> </div> <p dangerouslySetInnerHTML={{ __html: `${description}` }} className="p-4 py-0 text-sm md:decoration-none text-muted-foreground" ></p> </div> <div> {tags && tags.length > 0 && ( <div className="py-3 border-zinc-700/80 border-t-[1.2px] flex flex-wrap items-end gap-2"> {tags.map((tag) => ( <span key={tag} className="inline-flex items-end underline underline-offset-2 rounded-md bg-secondary/10 px-2 py-1 text-xs font-medium text-secondary-foreground" > {tag} </span> ))} </div> )} </div> </div> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/additional-fields/client.ts: -------------------------------------------------------------------------------- ```typescript import type { DBFieldAttribute } from "@better-auth/core/db"; import type { BetterAuthOptions } from "../../types"; import type { BetterAuthClientPlugin } from "@better-auth/core"; import type { BetterAuthPlugin } from "../../types"; export const inferAdditionalFields = < T, S extends { user?: { [key: string]: DBFieldAttribute; }; session?: { [key: string]: DBFieldAttribute; }; } = {}, >( schema?: S, ) => { type Opts = T extends BetterAuthOptions ? T : T extends { options: BetterAuthOptions; } ? T["options"] : never; type Plugin = Opts extends never ? S extends { user?: { [key: string]: DBFieldAttribute; }; session?: { [key: string]: DBFieldAttribute; }; } ? { id: "additional-fields-client"; schema: { user: { fields: S["user"] extends object ? S["user"] : {}; }; session: { fields: S["session"] extends object ? S["session"] : {}; }; }; } : never : Opts extends BetterAuthOptions ? { id: "additional-fields"; schema: { user: { fields: Opts["user"] extends { additionalFields: infer U; } ? U : {}; }; session: { fields: Opts["session"] extends { additionalFields: infer U; } ? U : {}; }; }; } : never; return { id: "additional-fields-client", $InferServerPlugin: {} as Plugin extends BetterAuthPlugin ? Plugin : undefined, } satisfies BetterAuthClientPlugin; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/verify-handlers/captchafox.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; "error-codes": | Array< | "missing-input-secret" | "invalid-input-secret" | "invalid-input-sitekey" | "missing-input-response" | "invalid-input-response" | "expired-input-response" | "timeout-or-duplicate" | "bad-request" > | undefined; insights: Record<string, unknown> | undefined; // ENTERPRISE feature: insights into verification. }; export const captchaFox = 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; }; ``` -------------------------------------------------------------------------------- /packages/core/src/env/logger.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect } from "vitest"; import { shouldPublishLog, type LogLevel } from "./logger"; describe("shouldPublishLog", () => { const testCases: { currentLogLevel: LogLevel; logLevel: LogLevel; expected: boolean; }[] = [ { currentLogLevel: "info", logLevel: "info", expected: true }, { currentLogLevel: "info", logLevel: "warn", expected: false }, { currentLogLevel: "info", logLevel: "error", expected: false }, { currentLogLevel: "info", logLevel: "debug", expected: false }, { currentLogLevel: "warn", logLevel: "info", expected: true }, { currentLogLevel: "warn", logLevel: "warn", expected: true }, { currentLogLevel: "warn", logLevel: "error", expected: false }, { currentLogLevel: "warn", logLevel: "debug", expected: false }, { currentLogLevel: "error", logLevel: "info", expected: true }, { currentLogLevel: "error", logLevel: "warn", expected: true }, { currentLogLevel: "error", logLevel: "error", expected: true }, { currentLogLevel: "error", logLevel: "debug", expected: false }, { currentLogLevel: "debug", logLevel: "info", expected: true }, { currentLogLevel: "debug", logLevel: "warn", expected: true }, { currentLogLevel: "debug", logLevel: "error", expected: true }, { currentLogLevel: "debug", logLevel: "debug", expected: true }, ]; testCases.forEach(({ currentLogLevel, logLevel, expected }) => { it(`should return "${expected}" when currentLogLevel is "${currentLogLevel}" and logLevel is "${logLevel}"`, () => { expect(shouldPublishLog(currentLogLevel, logLevel)).toBe(expected); }); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/astro.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Astro Example description: Better Auth Astro example. --- This is an example of how to use Better Auth with Astro. It uses Solid for building the components. **Implements the following features:** Email & Password . Social Sign-in with Google . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management <ForkButton url="better-auth/examples/tree/main/astro-example" /> <iframe src="https://stackblitz.com/github/better-auth/examples/tree/main/astro-example?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" style={{ width: "100%", height: "500px", border: 0, borderRadius: "4px", overflow: "hidden" }} title="Better Auth Astro+Solid Example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > </iframe> ## How to run 1. Clone the code sandbox (or the repo) and open it in your code editor 2. Provide .env file with the following variables ```txt GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= BETTER_AUTH_SECRET= ``` //if you don't have these, you can get them from the google developer console. If you don't want to use google sign-in, you can remove the google config from the `auth.ts` file. 3. Run the following commands ```bash pnpm install pnpm run dev ``` 4. Open the browser and navigate to `http://localhost:3000` ``` -------------------------------------------------------------------------------- /docs/content/docs/examples/next-js.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Next.js Example description: Better Auth Next.js example. --- This is an example of how to use Better Auth with Next. **Implements the following features:** Email & Password . Social Sign-in . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management . Organization, Members and Roles See [Demo](https://demo.better-auth.com) <ForkButton url="better-auth/better-auth/tree/main/demo/nextjs" /> <iframe src="https://stackblitz.com/github/better-auth/better-auth/tree/main/demo/nextjs?codemirror=1&fontsize=14&hidenavigation=1&runonclick=1&hidedevtools=1" style={{ width: "100%", height: "500px", border: 0, borderRadius: "4px", overflow: "hidden" }} title="Better Auth Next.js Example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > </iframe> ## How to run 1. Clone the code sandbox (or the repo) and open it in your code editor 2. Move .env.example to .env and provide necessary variables 3. Run the following commands ```bash pnpm install pnpm dev ``` 4. Open the browser and navigate to `http://localhost:3000` ### SSO Login Example For this example, we utilize DummyIDP. Initiate the login from the [DummyIDP login](https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/login), click "Proceed", and from here it will direct you to user's dashboard. ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/password-input.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { EyeIcon, EyeOffIcon } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; const PasswordInput = ({ ref, className, ...props }: any & { ref: React.RefObject<HTMLInputElement>; }) => { const [showPassword, setShowPassword] = React.useState(false); const disabled = props.value === "" || props.value === undefined || props.disabled; return ( <div className="relative"> <Input {...props} type={showPassword ? "text" : "password"} name="password_fake" className={cn("hide-password-toggle pr-10", className)} ref={ref} /> <Button type="button" variant="ghost" size="sm" className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent" onClick={() => setShowPassword((prev) => !prev)} disabled={disabled} > {showPassword && !disabled ? ( <EyeIcon className="h-4 w-4" aria-hidden="true" /> ) : ( <EyeOffIcon className="h-4 w-4" aria-hidden="true" /> )} <span className="sr-only"> {showPassword ? "Hide password" : "Show password"} </span> </Button> {/* hides browsers password toggles */} <style>{` .hide-password-toggle::-ms-reveal, .hide-password-toggle::-ms-clear { visibility: hidden; pointer-events: none; display: none; } `}</style> </div> ); }; PasswordInput.displayName = "PasswordInput"; export { PasswordInput }; ``` -------------------------------------------------------------------------------- /e2e/integration/solid-vinxi/e2e/utils.ts: -------------------------------------------------------------------------------- ```typescript import type { Page } from "@playwright/test"; import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { terminate } from "@better-auth/test-utils/playwright"; const root = fileURLToPath(new URL("../", import.meta.url)); export async function runClient<R>( page: Page, fn: ({ client }: { client: Window["client"] }) => R, ): Promise<R> { const client = await page.evaluateHandle<Window["client"]>("window.client"); return page.evaluate(fn, { client }); } export function setup() { let clientChild: ChildProcessWithoutNullStreams; const ref: { clientPort: number; } = { clientPort: -1, }; return { ref, start: async () => { clientChild = spawn("pnpm", ["run", "dev"], { cwd: root, stdio: "pipe", env: { ...process.env, NO_COLOR: "1", }, }); clientChild.stderr.on("data", (data) => { const message = data.toString(); console.error(message); }); clientChild.stdout.on("data", (data) => { const message = data.toString(); console.log(message); }); await Promise.all([ new Promise<void>((resolve) => { clientChild.stdout.on("data", (data) => { const message = data.toString(); // find: http://localhost:XXXX/ for vinxi dev server if (message.includes("http://localhost:")) { const match = message.match(/http:\/\/localhost:(\d+)/); if (match) { ref.clientPort = Number(match[1]); resolve(); } } }); }), ]); }, clean: async () => { await terminate(clientChild.pid!); }, }; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; import { cn } from "@/lib/utils"; const ScrollArea = ({ ref, className, children, ...props }: React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & { ref: React.RefObject<React.ElementRef<typeof ScrollAreaPrimitive.Root>>; }) => ( <ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props} > <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> {children} </ScrollAreaPrimitive.Viewport> <ScrollBar /> <ScrollAreaPrimitive.Corner /> </ScrollAreaPrimitive.Root> ); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = ({ ref, className, orientation = "vertical", ...props }: React.ComponentPropsWithoutRef< typeof ScrollAreaPrimitive.ScrollAreaScrollbar > & { ref: React.RefObject< React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> >; }) => ( <ScrollAreaPrimitive.ScrollAreaScrollbar ref={ref} orientation={orientation} className={cn( "flex touch-none select-none transition-colors", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-px", className, )} {...props} > <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> </ScrollAreaPrimitive.ScrollAreaScrollbar> ); ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; export { ScrollArea, ScrollBar }; ```