This is page 13 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-sqlite-passkey-number-id.txt: -------------------------------------------------------------------------------- ``` 1 | import { sql } from "drizzle-orm"; 2 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 3 | 4 | export const custom_user = sqliteTable("custom_user", { 5 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 6 | name: text("name").notNull(), 7 | email: text("email").notNull().unique(), 8 | emailVerified: integer("email_verified", { mode: "boolean" }) 9 | .default(false) 10 | .notNull(), 11 | image: text("image"), 12 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 13 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 14 | .notNull(), 15 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 16 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 17 | .$onUpdate(() => /* @__PURE__ */ new Date()) 18 | .notNull(), 19 | }); 20 | 21 | export const custom_session = sqliteTable("custom_session", { 22 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 23 | expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), 24 | token: text("token").notNull().unique(), 25 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 26 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 27 | .notNull(), 28 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 29 | .$onUpdate(() => /* @__PURE__ */ new Date()) 30 | .notNull(), 31 | ipAddress: text("ip_address"), 32 | userAgent: text("user_agent"), 33 | userId: integer("user_id") 34 | .notNull() 35 | .references(() => custom_user.id, { onDelete: "cascade" }), 36 | }); 37 | 38 | export const custom_account = sqliteTable("custom_account", { 39 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 40 | accountId: text("account_id").notNull(), 41 | providerId: text("provider_id").notNull(), 42 | userId: integer("user_id") 43 | .notNull() 44 | .references(() => custom_user.id, { onDelete: "cascade" }), 45 | accessToken: text("access_token"), 46 | refreshToken: text("refresh_token"), 47 | idToken: text("id_token"), 48 | accessTokenExpiresAt: integer("access_token_expires_at", { 49 | mode: "timestamp_ms", 50 | }), 51 | refreshTokenExpiresAt: integer("refresh_token_expires_at", { 52 | mode: "timestamp_ms", 53 | }), 54 | scope: text("scope"), 55 | password: text("password"), 56 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 57 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 58 | .notNull(), 59 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 60 | .$onUpdate(() => /* @__PURE__ */ new Date()) 61 | .notNull(), 62 | }); 63 | 64 | export const custom_verification = sqliteTable("custom_verification", { 65 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 66 | identifier: text("identifier").notNull(), 67 | value: text("value").notNull(), 68 | expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), 69 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 70 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 71 | .notNull(), 72 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 73 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 74 | .$onUpdate(() => /* @__PURE__ */ new Date()) 75 | .notNull(), 76 | }); 77 | 78 | export const passkey = sqliteTable("passkey", { 79 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 80 | name: text("name"), 81 | publicKey: text("public_key").notNull(), 82 | userId: integer("user_id") 83 | .notNull() 84 | .references(() => custom_user.id, { onDelete: "cascade" }), 85 | credentialID: text("credential_id").notNull(), 86 | counter: integer("counter").notNull(), 87 | deviceType: text("device_type").notNull(), 88 | backedUp: integer("backed_up", { mode: "boolean" }).notNull(), 89 | transports: text("transports"), 90 | createdAt: integer("created_at", { mode: "timestamp_ms" }), 91 | aaguid: text("aaguid"), 92 | }); 93 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/svelte-kit.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: SvelteKit Integration 3 | description: Integrate Better Auth with SvelteKit. 4 | --- 5 | 6 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). 7 | 8 | ### Mount the handler 9 | 10 | We need to mount the handler to SvelteKit server hook. 11 | 12 | ```ts title="hooks.server.ts" 13 | import { auth } from "$lib/auth"; 14 | import { svelteKitHandler } from "better-auth/svelte-kit"; 15 | import { building } from "$app/environment"; 16 | 17 | export async function handle({ event, resolve }) { 18 | return svelteKitHandler({ event, resolve, auth, building }); 19 | } 20 | ``` 21 | 22 | ### Populate session data in the event (`event.locals`) 23 | 24 | The `svelteKitHandler` does not automatically populate `event.locals.user` or `event.locals.session`. If you want to access the current session in your server code (e.g., in `+layout.server.ts`, actions, or endpoints), populate `event.locals` in your `handle` hook: 25 | 26 | ```ts title="hooks.server.ts" 27 | import { auth } from "$lib/auth"; 28 | import { svelteKitHandler } from "better-auth/svelte-kit"; 29 | import { building } from "$app/environment"; 30 | 31 | export async function handle({ event, resolve }) { 32 | // Fetch current session from Better Auth 33 | const session = await auth.api.getSession({ 34 | headers: event.request.headers, 35 | }); 36 | 37 | // Make session and user available on server 38 | if (session) { 39 | event.locals.session = session.session; 40 | event.locals.user = session.user; 41 | } 42 | 43 | return svelteKitHandler({ event, resolve, auth, building }); 44 | } 45 | ``` 46 | 47 | ### Server Action Cookies 48 | 49 | To ensure cookies are properly set when you call functions like `signInEmail` or `signUpEmail` in a server action, you should use the `sveltekitCookies` plugin. This plugin will automatically handle setting cookies for you in SvelteKit. 50 | 51 | You need to add it as a plugin to your Better Auth instance. 52 | 53 | <Callout> 54 | The `getRequestEvent` function is available in SvelteKit `2.20.0` and later. 55 | Make sure you are using a compatible version. 56 | </Callout> 57 | 58 | ```ts title="lib/auth.ts" 59 | import { betterAuth } from "better-auth"; 60 | import { sveltekitCookies } from "better-auth/svelte-kit"; 61 | import { getRequestEvent } from "$app/server"; 62 | 63 | export const auth = betterAuth({ 64 | // ... your config 65 | plugins: [sveltekitCookies(getRequestEvent)], // make sure this is the last plugin in the array 66 | }); 67 | ``` 68 | 69 | ## Create a client 70 | 71 | Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory. 72 | 73 | ```ts title="auth-client.ts" 74 | import { createAuthClient } from "better-auth/svelte"; // make sure to import from better-auth/svelte 75 | 76 | export const authClient = createAuthClient({ 77 | // you can pass client configuration here 78 | }); 79 | ``` 80 | 81 | Once you have created the client, you can use it to sign up, sign in, and perform other actions. 82 | Some of the actions are reactive. The client use [nano-store](https://github.com/nanostores/nanostores) to store the state and reflect changes when there is a change like a user signing in or out affecting the session state. 83 | 84 | ### Example usage 85 | 86 | ```svelte 87 | <script lang="ts"> 88 | import { authClient } from "$lib/client"; 89 | const session = authClient.useSession(); 90 | </script> 91 | <div> 92 | {#if $session.data} 93 | <div> 94 | <p> 95 | {$session.data.user.name} 96 | </p> 97 | <button 98 | on:click={async () => { 99 | await authClient.signOut(); 100 | }} 101 | > 102 | Sign Out 103 | </button> 104 | </div> 105 | {:else} 106 | <button 107 | on:click={async () => { 108 | await authClient.signIn.social({ 109 | provider: "github", 110 | }); 111 | }} 112 | > 113 | Continue with GitHub 114 | </button> 115 | {/if} 116 | </div> 117 | ``` 118 | ``` -------------------------------------------------------------------------------- /docs/components/ui/drawer.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Drawer({ 9 | ...props 10 | }: React.ComponentProps<typeof DrawerPrimitive.Root>) { 11 | return <DrawerPrimitive.Root data-slot="drawer" {...props} />; 12 | } 13 | 14 | function DrawerTrigger({ 15 | ...props 16 | }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { 17 | return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />; 18 | } 19 | 20 | function DrawerPortal({ 21 | ...props 22 | }: React.ComponentProps<typeof DrawerPrimitive.Portal>) { 23 | return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />; 24 | } 25 | 26 | function DrawerClose({ 27 | ...props 28 | }: React.ComponentProps<typeof DrawerPrimitive.Close>) { 29 | return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />; 30 | } 31 | 32 | function DrawerOverlay({ 33 | className, 34 | ...props 35 | }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { 36 | return ( 37 | <DrawerPrimitive.Overlay 38 | data-slot="drawer-overlay" 39 | className={cn( 40 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", 41 | className, 42 | )} 43 | {...props} 44 | /> 45 | ); 46 | } 47 | 48 | function DrawerContent({ 49 | className, 50 | children, 51 | ...props 52 | }: React.ComponentProps<typeof DrawerPrimitive.Content>) { 53 | return ( 54 | <DrawerPortal data-slot="drawer-portal"> 55 | <DrawerOverlay /> 56 | <DrawerPrimitive.Content 57 | data-slot="drawer-content" 58 | className={cn( 59 | "group/drawer-content bg-background fixed z-50 flex h-auto flex-col", 60 | "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg", 61 | "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg", 62 | "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm", 63 | "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm", 64 | className, 65 | )} 66 | {...props} 67 | > 68 | <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> 69 | {children} 70 | </DrawerPrimitive.Content> 71 | </DrawerPortal> 72 | ); 73 | } 74 | 75 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 | <div 78 | data-slot="drawer-header" 79 | className={cn("flex flex-col gap-1.5 p-4", className)} 80 | {...props} 81 | /> 82 | ); 83 | } 84 | 85 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 | <div 88 | data-slot="drawer-footer" 89 | className={cn("mt-auto flex flex-col gap-2 p-4", className)} 90 | {...props} 91 | /> 92 | ); 93 | } 94 | 95 | function DrawerTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps<typeof DrawerPrimitive.Title>) { 99 | return ( 100 | <DrawerPrimitive.Title 101 | data-slot="drawer-title" 102 | className={cn("text-foreground font-semibold", className)} 103 | {...props} 104 | /> 105 | ); 106 | } 107 | 108 | function DrawerDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps<typeof DrawerPrimitive.Description>) { 112 | return ( 113 | <DrawerPrimitive.Description 114 | data-slot="drawer-description" 115 | className={cn("text-muted-foreground text-sm", className)} 116 | {...props} 117 | /> 118 | ); 119 | } 120 | 121 | export { 122 | Drawer, 123 | DrawerPortal, 124 | DrawerOverlay, 125 | DrawerTrigger, 126 | DrawerClose, 127 | DrawerContent, 128 | DrawerHeader, 129 | DrawerFooter, 130 | DrawerTitle, 131 | DrawerDescription, 132 | }; 133 | ``` -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@better-auth/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build && pnpm run scripts:sync-orama", 8 | "dev": "next dev", 9 | "start": "next start", 10 | "postinstall": "fumadocs-mdx", 11 | "scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts", 12 | "scripts:sync-orama": "node ./scripts/sync-orama.ts" 13 | }, 14 | "dependencies": { 15 | "@ai-sdk/openai-compatible": "^1.0.20", 16 | "@ai-sdk/react": "^2.0.64", 17 | "@better-auth/utils": "0.3.0", 18 | "@better-fetch/fetch": "catalog:", 19 | "@hookform/resolvers": "^5.2.1", 20 | "@oramacloud/client": "^2.1.4", 21 | "@radix-ui/react-accordion": "^1.2.12", 22 | "@radix-ui/react-alert-dialog": "^1.1.15", 23 | "@radix-ui/react-aspect-ratio": "^1.1.7", 24 | "@radix-ui/react-avatar": "^1.1.10", 25 | "@radix-ui/react-checkbox": "^1.3.3", 26 | "@radix-ui/react-collapsible": "^1.1.12", 27 | "@radix-ui/react-context-menu": "^2.2.16", 28 | "@radix-ui/react-dialog": "^1.1.15", 29 | "@radix-ui/react-dropdown-menu": "^2.1.16", 30 | "@radix-ui/react-hover-card": "^1.1.15", 31 | "@radix-ui/react-icons": "^1.3.2", 32 | "@radix-ui/react-label": "^2.1.7", 33 | "@radix-ui/react-menubar": "^1.1.16", 34 | "@radix-ui/react-navigation-menu": "^1.2.14", 35 | "@radix-ui/react-popover": "^1.1.15", 36 | "@radix-ui/react-presence": "^1.1.5", 37 | "@radix-ui/react-progress": "^1.1.7", 38 | "@radix-ui/react-radio-group": "^1.3.8", 39 | "@radix-ui/react-scroll-area": "^1.2.10", 40 | "@radix-ui/react-select": "^2.2.6", 41 | "@radix-ui/react-separator": "^1.1.7", 42 | "@radix-ui/react-slider": "^1.3.6", 43 | "@radix-ui/react-slot": "^1.2.3", 44 | "@radix-ui/react-switch": "^1.2.6", 45 | "@radix-ui/react-tabs": "^1.1.13", 46 | "@radix-ui/react-toggle": "^1.1.10", 47 | "@radix-ui/react-toggle-group": "^1.1.11", 48 | "@radix-ui/react-tooltip": "^1.2.8", 49 | "@scalar/nextjs-api-reference": "^0.8.17", 50 | "@vercel/analytics": "^1.5.0", 51 | "@vercel/og": "^0.8.5", 52 | "ai": "^5.0.64", 53 | "class-variance-authority": "^0.7.1", 54 | "clsx": "^2.1.1", 55 | "cmdk": "1.1.1", 56 | "date-fns": "^4.1.0", 57 | "dotenv": "^17.2.2", 58 | "embla-carousel-react": "^8.6.0", 59 | "foxact": "^0.2.49", 60 | "framer-motion": "^12.23.12", 61 | "fumadocs-core": "15.8.3", 62 | "fumadocs-docgen": "2.1.0", 63 | "fumadocs-mdx": "11.8.3", 64 | "fumadocs-typescript": "^4.0.6", 65 | "fumadocs-ui": "15.8.3", 66 | "geist": "^1.4.2", 67 | "gray-matter": "^4.0.3", 68 | "hast-util-to-jsx-runtime": "^2.3.6", 69 | "highlight.js": "^11.11.1", 70 | "input-otp": "^1.4.2", 71 | "jotai": "^2.13.1", 72 | "js-beautify": "^1.15.4", 73 | "jsrsasign": "^11.1.0", 74 | "lucide-react": "^0.542.0", 75 | "motion": "^12.23.12", 76 | "next": "16.0.0-beta.0", 77 | "next-themes": "^0.4.6", 78 | "prism-react-renderer": "^2.4.1", 79 | "react": "19.2.0", 80 | "react-day-picker": "9.9.0", 81 | "react-dom": "19.2.0", 82 | "react-hook-form": "^7.62.0", 83 | "react-markdown": "^10.1.0", 84 | "react-remove-scroll": "^2.7.1", 85 | "react-resizable-panels": "^3.0.5", 86 | "react-use-measure": "^2.1.7", 87 | "recharts": "^3.1.2", 88 | "rehype-highlight": "^7.0.2", 89 | "remark": "^15.0.1", 90 | "remark-gfm": "^4.0.1", 91 | "remark-mdx": "^3.1.1", 92 | "remark-rehype": "^11.1.2", 93 | "remark-stringify": "^11.0.0", 94 | "shiki": "^3.13.0", 95 | "sonner": "^2.0.7", 96 | "tailwind-merge": "^3.3.1", 97 | "tailwindcss-animate": "^1.0.7", 98 | "unist-util-visit": "^5.0.0", 99 | "vaul": "^1.1.2", 100 | "zod": "^4.1.5" 101 | }, 102 | "devDependencies": { 103 | "@tailwindcss/postcss": "^4.1.13", 104 | "@types/jsrsasign": "^10.5.15", 105 | "@types/mdx": "^2.0.13", 106 | "@types/react": "^19.2.2", 107 | "@types/react-dom": "^19.2.1", 108 | "mini-svg-data-uri": "^1.4.4", 109 | "postcss": "^8.5.6", 110 | "tailwindcss": "^4.1.13", 111 | "typescript": "^5.9.2" 112 | } 113 | } 114 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/github.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | refreshAccessToken, 6 | validateAuthorizationCode, 7 | } from "../oauth2"; 8 | 9 | export interface GithubProfile { 10 | login: string; 11 | id: string; 12 | node_id: string; 13 | avatar_url: string; 14 | gravatar_id: string; 15 | url: string; 16 | html_url: string; 17 | followers_url: string; 18 | following_url: string; 19 | gists_url: string; 20 | starred_url: string; 21 | subscriptions_url: string; 22 | organizations_url: string; 23 | repos_url: string; 24 | events_url: string; 25 | received_events_url: string; 26 | type: string; 27 | site_admin: boolean; 28 | name: string; 29 | company: string; 30 | blog: string; 31 | location: string; 32 | email: string; 33 | hireable: boolean; 34 | bio: string; 35 | twitter_username: string; 36 | public_repos: string; 37 | public_gists: string; 38 | followers: string; 39 | following: string; 40 | created_at: string; 41 | updated_at: string; 42 | private_gists: string; 43 | total_private_repos: string; 44 | owned_private_repos: string; 45 | disk_usage: string; 46 | collaborators: string; 47 | two_factor_authentication: boolean; 48 | plan: { 49 | name: string; 50 | space: string; 51 | private_repos: string; 52 | collaborators: string; 53 | }; 54 | } 55 | 56 | export interface GithubOptions extends ProviderOptions<GithubProfile> { 57 | clientId: string; 58 | } 59 | export const github = (options: GithubOptions) => { 60 | const tokenEndpoint = "https://github.com/login/oauth/access_token"; 61 | return { 62 | id: "github", 63 | name: "GitHub", 64 | createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { 65 | const _scopes = options.disableDefaultScope 66 | ? [] 67 | : ["read:user", "user:email"]; 68 | options.scope && _scopes.push(...options.scope); 69 | scopes && _scopes.push(...scopes); 70 | return createAuthorizationURL({ 71 | id: "github", 72 | options, 73 | authorizationEndpoint: "https://github.com/login/oauth/authorize", 74 | scopes: _scopes, 75 | state, 76 | redirectURI, 77 | loginHint, 78 | prompt: options.prompt, 79 | }); 80 | }, 81 | validateAuthorizationCode: async ({ code, redirectURI }) => { 82 | return validateAuthorizationCode({ 83 | code, 84 | redirectURI, 85 | options, 86 | tokenEndpoint, 87 | }); 88 | }, 89 | refreshAccessToken: options.refreshAccessToken 90 | ? options.refreshAccessToken 91 | : async (refreshToken) => { 92 | return refreshAccessToken({ 93 | refreshToken, 94 | options: { 95 | clientId: options.clientId, 96 | clientKey: options.clientKey, 97 | clientSecret: options.clientSecret, 98 | }, 99 | tokenEndpoint: "https://github.com/login/oauth/access_token", 100 | }); 101 | }, 102 | async getUserInfo(token) { 103 | if (options.getUserInfo) { 104 | return options.getUserInfo(token); 105 | } 106 | const { data: profile, error } = await betterFetch<GithubProfile>( 107 | "https://api.github.com/user", 108 | { 109 | headers: { 110 | "User-Agent": "better-auth", 111 | authorization: `Bearer ${token.accessToken}`, 112 | }, 113 | }, 114 | ); 115 | if (error) { 116 | return null; 117 | } 118 | const { data: emails } = await betterFetch< 119 | { 120 | email: string; 121 | primary: boolean; 122 | verified: boolean; 123 | visibility: "public" | "private"; 124 | }[] 125 | >("https://api.github.com/user/emails", { 126 | headers: { 127 | Authorization: `Bearer ${token.accessToken}`, 128 | "User-Agent": "better-auth", 129 | }, 130 | }); 131 | 132 | if (!profile.email && emails) { 133 | profile.email = (emails.find((e) => e.primary) ?? emails[0]) 134 | ?.email as string; 135 | } 136 | const emailVerified = 137 | emails?.find((e) => e.email === profile.email)?.verified ?? false; 138 | 139 | const userMap = await options.mapProfileToUser?.(profile); 140 | return { 141 | user: { 142 | id: profile.id, 143 | name: profile.name || profile.login, 144 | email: profile.email, 145 | image: profile.avatar_url, 146 | emailVerified, 147 | ...userMap, 148 | }, 149 | data: profile, 150 | }; 151 | }, 152 | options, 153 | } satisfies OAuthProvider<GithubProfile>; 154 | }; 155 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/bearer.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Bearer Token Authentication 3 | description: Authenticate API requests using Bearer tokens instead of browser cookies 4 | --- 5 | 6 | The Bearer plugin enables authentication using Bearer tokens as an alternative to browser cookies. It intercepts requests, adding the Bearer token to the Authorization header before forwarding them to your API. 7 | 8 | <Callout type="warn"> 9 | Use this cautiously; it is intended only for APIs that don't support cookies or require Bearer tokens for authentication. Improper implementation could easily lead to security vulnerabilities. 10 | </Callout> 11 | 12 | ## Installing the Bearer Plugin 13 | 14 | Add the Bearer plugin to your authentication setup: 15 | 16 | ```ts title="auth.ts" 17 | import { betterAuth } from "better-auth"; 18 | import { bearer } from "better-auth/plugins"; 19 | 20 | export const auth = betterAuth({ 21 | plugins: [bearer()] 22 | }); 23 | ``` 24 | 25 | ## How to Use Bearer Tokens 26 | 27 | ### 1. Obtain the Bearer Token 28 | 29 | After a successful sign-in, you'll receive a session token in the response headers. Store this token securely (e.g., in `localStorage`): 30 | 31 | ```ts title="auth-client.ts" 32 | const { data } = await authClient.signIn.email({ 33 | email: "[email protected]", 34 | password: "securepassword" 35 | }, { 36 | onSuccess: (ctx)=>{ 37 | const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers 38 | // Store the token securely (e.g., in localStorage) 39 | localStorage.setItem("bearer_token", authToken); 40 | } 41 | }); 42 | ``` 43 | 44 | You can also set this up globally in your auth client: 45 | 46 | ```ts title="auth-client.ts" 47 | export const authClient = createAuthClient({ 48 | fetchOptions: { 49 | onSuccess: (ctx) => { 50 | const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers 51 | // Store the token securely (e.g., in localStorage) 52 | if(authToken){ 53 | localStorage.setItem("bearer_token", authToken); 54 | } 55 | } 56 | } 57 | }); 58 | ``` 59 | 60 | 61 | You may want to clear the token based on the response status code or other conditions: 62 | 63 | ### 2. Configure the Auth Client 64 | 65 | Set up your auth client to include the Bearer token in all requests: 66 | 67 | ```ts title="auth-client.ts" 68 | export const authClient = createAuthClient({ 69 | fetchOptions: { 70 | auth: { 71 | type:"Bearer", 72 | token: () => localStorage.getItem("bearer_token") || "" // get the token from localStorage 73 | } 74 | } 75 | }); 76 | ``` 77 | 78 | ### 3. Make Authenticated Requests 79 | 80 | Now you can make authenticated API calls: 81 | 82 | ```ts title="auth-client.ts" 83 | // This request is automatically authenticated 84 | const { data } = await authClient.listSessions(); 85 | ``` 86 | 87 | ### 4. Per-Request Token (Optional) 88 | 89 | You can also provide the token for individual requests: 90 | 91 | ```ts title="auth-client.ts" 92 | const { data } = await authClient.listSessions({ 93 | fetchOptions: { 94 | headers: { 95 | Authorization: `Bearer ${token}` 96 | } 97 | } 98 | }); 99 | ``` 100 | 101 | ### 5. Using Bearer Tokens Outside the Auth Client 102 | 103 | The Bearer token can be used to authenticate any request to your API, even when not using the auth client: 104 | 105 | ```ts title="api-call.ts" 106 | const token = localStorage.getItem("bearer_token"); 107 | 108 | const response = await fetch("https://api.example.com/data", { 109 | headers: { 110 | Authorization: `Bearer ${token}` 111 | } 112 | }); 113 | 114 | const data = await response.json(); 115 | ``` 116 | 117 | And in the server, you can use the `auth.api.getSession` function to authenticate requests: 118 | 119 | ```ts title="server.ts" 120 | import { auth } from "@/auth"; 121 | 122 | export async function handler(req, res) { 123 | const session = await auth.api.getSession({ 124 | headers: req.headers 125 | }); 126 | 127 | if (!session) { 128 | return res.status(401).json({ error: "Unauthorized" }); 129 | } 130 | 131 | // Process authenticated request 132 | // ... 133 | } 134 | ``` 135 | 136 | 137 | ## Options 138 | 139 | **requireSignature** (boolean): Require the token to be signed. Default: `false`. 140 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/gitlab.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | validateAuthorizationCode, 6 | refreshAccessToken, 7 | } from "../oauth2"; 8 | 9 | export interface GitlabProfile extends Record<string, any> { 10 | id: number; 11 | username: string; 12 | email: string; 13 | name: string; 14 | state: string; 15 | avatar_url: string; 16 | web_url: string; 17 | created_at: string; 18 | bio: string; 19 | location?: string; 20 | public_email: string; 21 | skype: string; 22 | linkedin: string; 23 | twitter: string; 24 | website_url: string; 25 | organization: string; 26 | job_title: string; 27 | pronouns: string; 28 | bot: boolean; 29 | work_information?: string; 30 | followers: number; 31 | following: number; 32 | local_time: string; 33 | last_sign_in_at: string; 34 | confirmed_at: string; 35 | theme_id: number; 36 | last_activity_on: string; 37 | color_scheme_id: number; 38 | projects_limit: number; 39 | current_sign_in_at: string; 40 | identities: Array<{ 41 | provider: string; 42 | extern_uid: string; 43 | }>; 44 | can_create_group: boolean; 45 | can_create_project: boolean; 46 | two_factor_enabled: boolean; 47 | external: boolean; 48 | private_profile: boolean; 49 | commit_email: string; 50 | shared_runners_minutes_limit: number; 51 | extra_shared_runners_minutes_limit: number; 52 | } 53 | 54 | export interface GitlabOptions extends ProviderOptions<GitlabProfile> { 55 | clientId: string; 56 | issuer?: string; 57 | } 58 | 59 | const cleanDoubleSlashes = (input: string = "") => { 60 | return input 61 | .split("://") 62 | .map((str) => str.replace(/\/{2,}/g, "/")) 63 | .join("://"); 64 | }; 65 | 66 | const issuerToEndpoints = (issuer?: string) => { 67 | let baseUrl = issuer || "https://gitlab.com"; 68 | return { 69 | authorizationEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/authorize`), 70 | tokenEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/token`), 71 | userinfoEndpoint: cleanDoubleSlashes(`${baseUrl}/api/v4/user`), 72 | }; 73 | }; 74 | 75 | export const gitlab = (options: GitlabOptions) => { 76 | const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } = 77 | issuerToEndpoints(options.issuer); 78 | const issuerId = "gitlab"; 79 | const issuerName = "Gitlab"; 80 | return { 81 | id: issuerId, 82 | name: issuerName, 83 | createAuthorizationURL: async ({ 84 | state, 85 | scopes, 86 | codeVerifier, 87 | loginHint, 88 | redirectURI, 89 | }) => { 90 | const _scopes = options.disableDefaultScope ? [] : ["read_user"]; 91 | options.scope && _scopes.push(...options.scope); 92 | scopes && _scopes.push(...scopes); 93 | return await createAuthorizationURL({ 94 | id: issuerId, 95 | options, 96 | authorizationEndpoint, 97 | scopes: _scopes, 98 | state, 99 | redirectURI, 100 | codeVerifier, 101 | loginHint, 102 | }); 103 | }, 104 | validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => { 105 | return validateAuthorizationCode({ 106 | code, 107 | redirectURI, 108 | options, 109 | codeVerifier, 110 | tokenEndpoint, 111 | }); 112 | }, 113 | refreshAccessToken: options.refreshAccessToken 114 | ? options.refreshAccessToken 115 | : async (refreshToken) => { 116 | return refreshAccessToken({ 117 | refreshToken, 118 | options: { 119 | clientId: options.clientId, 120 | clientKey: options.clientKey, 121 | clientSecret: options.clientSecret, 122 | }, 123 | tokenEndpoint: tokenEndpoint, 124 | }); 125 | }, 126 | async getUserInfo(token) { 127 | if (options.getUserInfo) { 128 | return options.getUserInfo(token); 129 | } 130 | const { data: profile, error } = await betterFetch<GitlabProfile>( 131 | userinfoEndpoint, 132 | { headers: { authorization: `Bearer ${token.accessToken}` } }, 133 | ); 134 | if (error || profile.state !== "active" || profile.locked) { 135 | return null; 136 | } 137 | const userMap = await options.mapProfileToUser?.(profile); 138 | return { 139 | user: { 140 | id: profile.id, 141 | name: profile.name ?? profile.username, 142 | email: profile.email, 143 | image: profile.avatar_url, 144 | emailVerified: true, 145 | ...userMap, 146 | }, 147 | data: profile, 148 | }; 149 | }, 150 | options, 151 | } satisfies OAuthProvider<GitlabProfile>; 152 | }; 153 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/drizzle.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Drizzle ORM Adapter 3 | description: Integrate Better Auth with Drizzle ORM. 4 | --- 5 | 6 | Drizzle ORM is a powerful and flexible ORM for Node.js and TypeScript. It provides a simple and intuitive API for working with databases, and supports a wide range of databases including MySQL, PostgreSQL, SQLite, and more. 7 | Read more here: [Drizzle ORM](https://orm.drizzle.team/). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have Drizzle installed and configured. 12 | Then, you can use the Drizzle adapter to connect to your database. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 17 | import { db } from "./database.ts"; 18 | 19 | export const auth = betterAuth({ 20 | database: drizzleAdapter(db, { 21 | // [!code highlight] 22 | provider: "sqlite", // or "pg" or "mysql" // [!code highlight] 23 | }), // [!code highlight] 24 | //... the rest of your config 25 | }); 26 | ``` 27 | 28 | ## Schema generation & migration 29 | 30 | The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate 31 | your database schema based on your Better Auth configuration and plugins. 32 | 33 | To generate the schema required by Better Auth, run the following command: 34 | 35 | ```bash title="Schema Generation" 36 | npx @better-auth/cli@latest generate 37 | ``` 38 | 39 | To generate and apply the migration, run the following commands: 40 | 41 | ```bash title="Schema Migration" 42 | npx drizzle-kit generate # generate the migration file 43 | npx drizzle-kit migrate # apply the migration 44 | ``` 45 | 46 | ## Modifying Table Names 47 | 48 | The Drizzle adapter expects the schema you define to match the table names. For example, if your Drizzle schema maps the `user` table to `users`, you need to manually pass the schema and map it to the user table. 49 | 50 | ```ts 51 | import { betterAuth } from "better-auth"; 52 | import { db } from "./drizzle"; 53 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 54 | import { schema } from "./schema"; 55 | 56 | export const auth = betterAuth({ 57 | database: drizzleAdapter(db, { 58 | provider: "sqlite", // or "pg" or "mysql" 59 | schema: { 60 | ...schema, 61 | user: schema.users, 62 | }, 63 | }), 64 | }); 65 | ``` 66 | 67 | You can either modify the provided schema values like the example above, 68 | or you can mutate the auth config's `modelName` property directly. 69 | For example: 70 | 71 | ```ts 72 | export const auth = betterAuth({ 73 | database: drizzleAdapter(db, { 74 | provider: "sqlite", // or "pg" or "mysql" 75 | schema, 76 | }), 77 | user: { 78 | modelName: "users", // [!code highlight] 79 | } 80 | }); 81 | ``` 82 | 83 | ## Modifying Field Names 84 | 85 | We map field names based on property you passed to your Drizzle schema. 86 | For example, if you want to modify the `email` field to `email_address`, 87 | you simply need to change the Drizzle schema to: 88 | 89 | ```ts 90 | export const user = mysqlTable("user", { 91 | // Changed field name without changing the schema property name 92 | // This allows drizzle & better-auth to still use the original field name, 93 | // while your DB uses the modified field name 94 | email: varchar("email_address", { length: 255 }).notNull().unique(), // [!code highlight] 95 | // ... others 96 | }); 97 | ``` 98 | 99 | You can either modify the Drizzle schema like the example above, 100 | or you can mutate the auth config's `fields` property directly. 101 | For example: 102 | 103 | ```ts 104 | export const auth = betterAuth({ 105 | database: drizzleAdapter(db, { 106 | provider: "sqlite", // or "pg" or "mysql" 107 | schema, 108 | }), 109 | user: { 110 | fields: { 111 | email: "email_address", // [!code highlight] 112 | } 113 | } 114 | }); 115 | ``` 116 | 117 | ## Using Plural Table Names 118 | 119 | If all your tables are using plural form, you can just pass the `usePlural` option: 120 | 121 | ```ts 122 | export const auth = betterAuth({ 123 | database: drizzleAdapter(db, { 124 | ... 125 | usePlural: true, 126 | }), 127 | }); 128 | ``` 129 | 130 | ## Performance Tips 131 | 132 | If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. 133 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/multi-session/multi-session.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { multiSession } from "."; 4 | import { multiSessionClient } from "./client"; 5 | import { parseSetCookieHeader } from "../../cookies"; 6 | 7 | describe("multi-session", async () => { 8 | const { client, testUser, cookieSetter } = await getTestInstance( 9 | { 10 | plugins: [ 11 | multiSession({ 12 | maximumSessions: 2, 13 | }), 14 | ], 15 | }, 16 | { 17 | clientOptions: { 18 | plugins: [multiSessionClient()], 19 | }, 20 | }, 21 | ); 22 | 23 | let headers = new Headers(); 24 | const testUser2 = { 25 | email: "[email protected]", 26 | password: "password", 27 | name: "Name", 28 | }; 29 | 30 | it("should set multi session when there is set-cookie header", async () => { 31 | await client.signIn.email( 32 | { 33 | email: testUser.email, 34 | password: testUser.password, 35 | }, 36 | { 37 | onResponse(context) { 38 | const setCookieString = context.response.headers.get("set-cookie"); 39 | const setCookies = parseSetCookieHeader(setCookieString || ""); 40 | const sessionToken = setCookies 41 | .get("better-auth.session_token") 42 | ?.value.split(".")[0]; 43 | const multiSession = setCookies.get( 44 | `better-auth.session_token_multi-${sessionToken?.toLowerCase()}`, 45 | )?.value; 46 | expect(sessionToken).not.toBe(null); 47 | expect(multiSession).not.toBe(null); 48 | expect(multiSession).toContain(sessionToken); 49 | expect(setCookieString).toContain("better-auth.session_token_multi-"); 50 | }, 51 | onSuccess: cookieSetter(headers), 52 | }, 53 | ); 54 | await client.signUp.email(testUser2, { 55 | onSuccess: cookieSetter(headers), 56 | }); 57 | }); 58 | 59 | it("should get active session", async () => { 60 | const session = await client.getSession({ 61 | fetchOptions: { 62 | headers, 63 | }, 64 | }); 65 | expect(session.data?.user.email).toBe(testUser2.email); 66 | }); 67 | 68 | let sessionToken = ""; 69 | it("should list all device sessions", async () => { 70 | const res = await client.multiSession.listDeviceSessions({ 71 | fetchOptions: { 72 | headers, 73 | }, 74 | }); 75 | if (res.data) { 76 | sessionToken = 77 | res.data.find((s) => s.user.email === testUser.email)?.session.token || 78 | ""; 79 | } 80 | expect(res.data).toHaveLength(2); 81 | }); 82 | 83 | it("should set active session", async () => { 84 | const res = await client.multiSession.setActive({ 85 | sessionToken, 86 | fetchOptions: { 87 | headers, 88 | }, 89 | }); 90 | expect(res.data?.user.email).toBe(testUser.email); 91 | }); 92 | 93 | it("should revoke a session and set the next active", async () => { 94 | const testUser3 = { 95 | email: "[email protected]", 96 | password: "password", 97 | name: "Name", 98 | }; 99 | let token = ""; 100 | const signUpRes = await client.signUp.email(testUser3, { 101 | onSuccess: (ctx) => { 102 | const header = ctx.response.headers.get("set-cookie"); 103 | expect(header).toContain("better-auth.session_token"); 104 | const cookies = parseSetCookieHeader(header || ""); 105 | token = 106 | cookies.get("better-auth.session_token")?.value.split(".")[0] || ""; 107 | }, 108 | }); 109 | await client.multiSession.revoke( 110 | { 111 | sessionToken: token, 112 | }, 113 | { 114 | onSuccess(context) { 115 | expect(context.response.headers.get("set-cookie")).toContain( 116 | `better-auth.session_token=`, 117 | ); 118 | }, 119 | headers, 120 | }, 121 | ); 122 | const res = await client.multiSession.listDeviceSessions({ 123 | fetchOptions: { 124 | headers, 125 | }, 126 | }); 127 | expect(res.data).toHaveLength(2); 128 | }); 129 | 130 | it("should sign-out all sessions", async () => { 131 | const newHeaders = new Headers(); 132 | await client.signOut({ 133 | fetchOptions: { 134 | headers, 135 | onSuccess: cookieSetter(newHeaders), 136 | }, 137 | }); 138 | const res = await client.multiSession.listDeviceSessions({ 139 | fetchOptions: { 140 | headers, 141 | }, 142 | }); 143 | expect(res.data).toHaveLength(0); 144 | const res2 = await client.multiSession.listDeviceSessions({ 145 | fetchOptions: { 146 | headers: newHeaders, 147 | }, 148 | }); 149 | expect(res2.data).toHaveLength(0); 150 | }); 151 | }); 152 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/passkey/passkey.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { type Passkey, passkey } from "."; 4 | import { createAuthClient } from "../../client"; 5 | import { passkeyClient } from "./client"; 6 | 7 | describe("passkey", async () => { 8 | const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({ 9 | plugins: [passkey()], 10 | }); 11 | 12 | it("should generate register options", async () => { 13 | const { headers } = await signInWithTestUser(); 14 | const options = await auth.api.generatePasskeyRegistrationOptions({ 15 | headers: headers, 16 | }); 17 | 18 | expect(options).toBeDefined(); 19 | expect(options).toHaveProperty("challenge"); 20 | expect(options).toHaveProperty("rp"); 21 | expect(options).toHaveProperty("user"); 22 | expect(options).toHaveProperty("pubKeyCredParams"); 23 | 24 | const client = createAuthClient({ 25 | plugins: [passkeyClient()], 26 | baseURL: "http://localhost:3000/api/auth", 27 | fetchOptions: { 28 | headers: headers, 29 | customFetchImpl, 30 | }, 31 | }); 32 | 33 | await client.$fetch("/passkey/generate-register-options", { 34 | headers: headers, 35 | method: "GET", 36 | onResponse(context) { 37 | const setCookie = context.response.headers.get("Set-Cookie"); 38 | expect(setCookie).toBeDefined(); 39 | expect(setCookie).toContain("better-auth-passkey"); 40 | }, 41 | }); 42 | }); 43 | 44 | it("should generate authenticate options", async () => { 45 | const { headers } = await signInWithTestUser(); 46 | const options = await auth.api.generatePasskeyAuthenticationOptions({ 47 | headers: headers, 48 | }); 49 | expect(options).toBeDefined(); 50 | expect(options).toHaveProperty("challenge"); 51 | expect(options).toHaveProperty("rpId"); 52 | expect(options).toHaveProperty("allowCredentials"); 53 | expect(options).toHaveProperty("userVerification"); 54 | }); 55 | 56 | it("should generate authenticate options without session (discoverable credentials)", async () => { 57 | // Test without any session/auth headers - simulating a new sign-in with discoverable credentials 58 | const options = await auth.api.generatePasskeyAuthenticationOptions({}); 59 | expect(options).toBeDefined(); 60 | expect(options).toHaveProperty("challenge"); 61 | expect(options).toHaveProperty("rpId"); 62 | expect(options).toHaveProperty("userVerification"); 63 | }); 64 | 65 | it("should list user passkeys", async () => { 66 | const { headers, user } = await signInWithTestUser(); 67 | const context = await auth.$context; 68 | await context.adapter.create<Omit<Passkey, "id">, Passkey>({ 69 | model: "passkey", 70 | data: { 71 | userId: user.id, 72 | publicKey: "mockPublicKey", 73 | name: "mockName", 74 | counter: 0, 75 | deviceType: "singleDevice", 76 | credentialID: "mockCredentialID", 77 | createdAt: new Date(), 78 | backedUp: false, 79 | transports: "mockTransports", 80 | aaguid: "mockAAGUID", 81 | } satisfies Omit<Passkey, "id">, 82 | }); 83 | 84 | const passkeys = await auth.api.listPasskeys({ 85 | headers: headers, 86 | }); 87 | 88 | expect(Array.isArray(passkeys)).toBe(true); 89 | expect(passkeys[0]).toHaveProperty("id"); 90 | expect(passkeys[0]).toHaveProperty("userId"); 91 | expect(passkeys[0]).toHaveProperty("publicKey"); 92 | expect(passkeys[0]).toHaveProperty("credentialID"); 93 | expect(passkeys[0]).toHaveProperty("aaguid"); 94 | }); 95 | 96 | it("should update a passkey", async () => { 97 | const { headers } = await signInWithTestUser(); 98 | const passkeys = await auth.api.listPasskeys({ 99 | headers: headers, 100 | }); 101 | const passkey = passkeys[0]!; 102 | const updateResult = await auth.api.updatePasskey({ 103 | headers: headers, 104 | body: { 105 | id: passkey.id, 106 | name: "newName", 107 | }, 108 | }); 109 | 110 | expect(updateResult.passkey.name).toBe("newName"); 111 | }); 112 | 113 | it("should delete a passkey", async () => { 114 | const { headers } = await signInWithTestUser(); 115 | const deleteResult = await auth.api.deletePasskey({ 116 | headers: headers, 117 | body: { 118 | id: "mockPasskeyId", 119 | }, 120 | }); 121 | expect(deleteResult).toBe(null); 122 | }); 123 | }); 124 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/community-adapters.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Community Adapters 3 | description: Integrate Better Auth with community made database adapters. 4 | --- 5 | 6 | This page showcases a list of recommended community made database adapters. 7 | We encourage you to create any missing database adapters and maybe get added to the list! 8 | 9 | | Adapter | Database Dialect | Author | 10 | | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | 11 | | [convex-better-auth](https://github.com/get-convex/better-auth) | [Convex Database](https://www.convex.dev/) | <img src="https://github.com/erquhart.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [erquhart](https://github.com/erquhart) | 12 | | [surreal-better-auth](https://github.com/oskar-gmerek/surreal-better-auth) | [SurrealDB](https://surrealdb.com/) | <img src="https://github.com/oskar-gmerek.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> <a href="https://oskargmerek.com" alt="Web Developer UK">Oskar Gmerek</a> | 13 | | [surrealdb-better-auth](https://github.com/Necmttn/surrealdb-better-auth) | [Surreal Database](https://surrealdb.com/) | <img src="https://github.com/Necmttn.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [Necmttn](https://github.com/Necmttn) | 14 | | [better-auth-surrealdb](https://github.com/msanchezdev/better-auth-surrealdb) | [Surreal Database](https://surrealdb.com/) | <img src="https://github.com/msanchezdev.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [msanchezdev](https://github.com/msanchezdev) | 15 | | [payload-better-auth](https://github.com/ForrestDevs/payload-better-auth/tree/main/packages/db-adapter) | [Payload CMS](https://payloadcms.com/) | <img src="https://github.com/forrestdevs.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [forrestdevs](https://github.com/forrestdevs) | 16 | | [@ronin/better-auth](https://github.com/ronin-co/better-auth) | [RONIN](https://ronin.co) | <img src="https://github.com/ronin-co.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [ronin-co](https://github.com/ronin-co) | 17 | | [better-auth-instantdb](https://github.com/daveyplate/better-auth-instantdb) | [InstantDB](https://www.instantdb.com/) | <img src="https://github.com/daveycodez.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [daveycodez](https://github.com/daveycodez) | 18 | | [@nerdfolio/remult-better-auth](https://github.com/nerdfolio/remult-better-auth) | [Remult](https://remult.dev/) | <img src="https://github.com/taivo.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [Tai Vo](https://github.com/taivo) | 19 | | [pocketbase-better-auth](https://github.com/LightInn/pocketbase-better-auth) | [PocketBase](https://pocketbase.io/) | <img src="https://github.com/LightInn.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [LightInn](https://github.com/LightInn) | 20 | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/validate-authorization-code.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { jwtVerify } from "jose"; 3 | import type { ProviderOptions } from "./index"; 4 | import { getOAuth2Tokens } from "./index"; 5 | import { base64 } from "@better-auth/utils/base64"; 6 | 7 | export function createAuthorizationCodeRequest({ 8 | code, 9 | codeVerifier, 10 | redirectURI, 11 | options, 12 | authentication, 13 | deviceId, 14 | headers, 15 | additionalParams = {}, 16 | resource, 17 | }: { 18 | code: string; 19 | redirectURI: string; 20 | options: Partial<ProviderOptions>; 21 | codeVerifier?: string; 22 | deviceId?: string; 23 | authentication?: "basic" | "post"; 24 | headers?: Record<string, string>; 25 | additionalParams?: Record<string, string>; 26 | resource?: string | string[]; 27 | }) { 28 | const body = new URLSearchParams(); 29 | const requestHeaders: Record<string, any> = { 30 | "content-type": "application/x-www-form-urlencoded", 31 | accept: "application/json", 32 | "user-agent": "better-auth", 33 | ...headers, 34 | }; 35 | body.set("grant_type", "authorization_code"); 36 | body.set("code", code); 37 | codeVerifier && body.set("code_verifier", codeVerifier); 38 | options.clientKey && body.set("client_key", options.clientKey); 39 | deviceId && body.set("device_id", deviceId); 40 | body.set("redirect_uri", options.redirectURI || redirectURI); 41 | if (resource) { 42 | if (typeof resource === "string") { 43 | body.append("resource", resource); 44 | } else { 45 | for (const _resource of resource) { 46 | body.append("resource", _resource); 47 | } 48 | } 49 | } 50 | // Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617) 51 | // Fixes compatibility with providers like Notion, Twitter, etc. 52 | if (authentication === "basic") { 53 | const primaryClientId = Array.isArray(options.clientId) 54 | ? options.clientId[0] 55 | : options.clientId; 56 | const encodedCredentials = base64.encode( 57 | `${primaryClientId}:${options.clientSecret ?? ""}`, 58 | ); 59 | requestHeaders["authorization"] = `Basic ${encodedCredentials}`; 60 | } else { 61 | const primaryClientId = Array.isArray(options.clientId) 62 | ? options.clientId[0] 63 | : options.clientId; 64 | body.set("client_id", primaryClientId); 65 | if (options.clientSecret) { 66 | body.set("client_secret", options.clientSecret); 67 | } 68 | } 69 | 70 | for (const [key, value] of Object.entries(additionalParams)) { 71 | if (!body.has(key)) body.append(key, value); 72 | } 73 | 74 | return { 75 | body, 76 | headers: requestHeaders, 77 | }; 78 | } 79 | 80 | export async function validateAuthorizationCode({ 81 | code, 82 | codeVerifier, 83 | redirectURI, 84 | options, 85 | tokenEndpoint, 86 | authentication, 87 | deviceId, 88 | headers, 89 | additionalParams = {}, 90 | resource, 91 | }: { 92 | code: string; 93 | redirectURI: string; 94 | options: Partial<ProviderOptions>; 95 | codeVerifier?: string; 96 | deviceId?: string; 97 | tokenEndpoint: string; 98 | authentication?: "basic" | "post"; 99 | headers?: Record<string, string>; 100 | additionalParams?: Record<string, string>; 101 | resource?: string | string[]; 102 | }) { 103 | const { body, headers: requestHeaders } = createAuthorizationCodeRequest({ 104 | code, 105 | codeVerifier, 106 | redirectURI, 107 | options, 108 | authentication, 109 | deviceId, 110 | headers, 111 | additionalParams, 112 | resource, 113 | }); 114 | 115 | const { data, error } = await betterFetch<object>(tokenEndpoint, { 116 | method: "POST", 117 | body: body, 118 | headers: requestHeaders, 119 | }); 120 | 121 | if (error) { 122 | throw error; 123 | } 124 | const tokens = getOAuth2Tokens(data); 125 | return tokens; 126 | } 127 | 128 | export async function validateToken(token: string, jwksEndpoint: string) { 129 | const { data, error } = await betterFetch<{ 130 | keys: { 131 | kid: string; 132 | kty: string; 133 | use: string; 134 | n: string; 135 | e: string; 136 | x5c: string[]; 137 | }[]; 138 | }>(jwksEndpoint, { 139 | method: "GET", 140 | headers: { 141 | accept: "application/json", 142 | "user-agent": "better-auth", 143 | }, 144 | }); 145 | if (error) { 146 | throw error; 147 | } 148 | const keys = data["keys"]; 149 | const header = JSON.parse(atob(token.split(".")[0]!)); 150 | const key = keys.find((key) => key.kid === header.kid); 151 | if (!key) { 152 | throw new Error("Key not found"); 153 | } 154 | const verified = await jwtVerify(token, key); 155 | return verified; 156 | } 157 | ``` -------------------------------------------------------------------------------- /docs/components/ui/sheet.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SheetPrimitive from "@radix-ui/react-dialog"; 5 | import { XIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { 10 | return <SheetPrimitive.Root data-slot="sheet" {...props} />; 11 | } 12 | 13 | function SheetTrigger({ 14 | ...props 15 | }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { 16 | return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; 17 | } 18 | 19 | function SheetClose({ 20 | ...props 21 | }: React.ComponentProps<typeof SheetPrimitive.Close>) { 22 | return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; 23 | } 24 | 25 | function SheetPortal({ 26 | ...props 27 | }: React.ComponentProps<typeof SheetPrimitive.Portal>) { 28 | return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; 29 | } 30 | 31 | function SheetOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { 35 | return ( 36 | <SheetPrimitive.Overlay 37 | data-slot="sheet-overlay" 38 | className={cn( 39 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", 40 | className, 41 | )} 42 | {...props} 43 | /> 44 | ); 45 | } 46 | 47 | function SheetContent({ 48 | className, 49 | children, 50 | side = "right", 51 | ...props 52 | }: React.ComponentProps<typeof SheetPrimitive.Content> & { 53 | side?: "top" | "right" | "bottom" | "left"; 54 | }) { 55 | return ( 56 | <SheetPortal> 57 | <SheetOverlay /> 58 | <SheetPrimitive.Content 59 | data-slot="sheet-content" 60 | className={cn( 61 | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 62 | side === "right" && 63 | "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", 64 | side === "left" && 65 | "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", 66 | side === "top" && 67 | "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", 68 | side === "bottom" && 69 | "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", 70 | className, 71 | )} 72 | {...props} 73 | > 74 | {children} 75 | <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> 76 | <XIcon className="size-4" /> 77 | <span className="sr-only">Close</span> 78 | </SheetPrimitive.Close> 79 | </SheetPrimitive.Content> 80 | </SheetPortal> 81 | ); 82 | } 83 | 84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 85 | return ( 86 | <div 87 | data-slot="sheet-header" 88 | className={cn("flex flex-col gap-1.5 p-4", className)} 89 | {...props} 90 | /> 91 | ); 92 | } 93 | 94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 95 | return ( 96 | <div 97 | data-slot="sheet-footer" 98 | className={cn("mt-auto flex flex-col gap-2 p-4", className)} 99 | {...props} 100 | /> 101 | ); 102 | } 103 | 104 | function SheetTitle({ 105 | className, 106 | ...props 107 | }: React.ComponentProps<typeof SheetPrimitive.Title>) { 108 | return ( 109 | <SheetPrimitive.Title 110 | data-slot="sheet-title" 111 | className={cn("text-foreground font-semibold", className)} 112 | {...props} 113 | /> 114 | ); 115 | } 116 | 117 | function SheetDescription({ 118 | className, 119 | ...props 120 | }: React.ComponentProps<typeof SheetPrimitive.Description>) { 121 | return ( 122 | <SheetPrimitive.Description 123 | data-slot="sheet-description" 124 | className={cn("text-muted-foreground text-sm", className)} 125 | {...props} 126 | /> 127 | ); 128 | } 129 | 130 | export { 131 | Sheet, 132 | SheetTrigger, 133 | SheetClose, 134 | SheetContent, 135 | SheetHeader, 136 | SheetFooter, 137 | SheetTitle, 138 | SheetDescription, 139 | }; 140 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/other-social-providers.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Other Social Providers 3 | description: Other social providers setup and usage. 4 | --- 5 | 6 | Better Auth provides out of the box support for a [Generic OAuth Plugin](/docs/plugins/generic-oauth) which allows you to use any social provider that implements the OAuth2 protocol or OpenID Connect (OIDC) flows. 7 | 8 | To use a provider that is not supported out of the box, you can use the [Generic OAuth Plugin](/docs/plugins/generic-oauth). 9 | 10 | ## Installation 11 | 12 | <Steps> 13 | <Step> 14 | ### Add the plugin to your auth config 15 | 16 | To use the Generic OAuth plugin, add it to your auth config. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | import { genericOAuth } from "better-auth/plugins" // [!code highlight] 21 | 22 | export const auth = betterAuth({ 23 | // ... other config options 24 | plugins: [ 25 | genericOAuth({ // [!code highlight] 26 | config: [ // [!code highlight] 27 | { // [!code highlight] 28 | providerId: "provider-id", // [!code highlight] 29 | clientId: "test-client-id", // [!code highlight] 30 | clientSecret: "test-client-secret", // [!code highlight] 31 | discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight] 32 | // ... other config options // [!code highlight] 33 | }, // [!code highlight] 34 | // Add more providers as needed // [!code highlight] 35 | ] // [!code highlight] 36 | }) // [!code highlight] 37 | ] 38 | }) 39 | ``` 40 | 41 | </Step> 42 | 43 | <Step> 44 | ### Add the client plugin 45 | 46 | Include the Generic OAuth client plugin in your authentication client instance. 47 | 48 | ```ts title="auth-client.ts" 49 | import { createAuthClient } from "better-auth/client" 50 | import { genericOAuthClient } from "better-auth/client/plugins" 51 | 52 | const authClient = createAuthClient({ 53 | plugins: [ 54 | genericOAuthClient() 55 | ] 56 | }) 57 | ``` 58 | 59 | </Step> 60 | </Steps> 61 | 62 | <Callout> 63 | Read more about installation and usage of the Generic Oauth plugin 64 | [here](/docs/plugins/generic-oauth#usage). 65 | </Callout> 66 | 67 | ## Example usage 68 | 69 | ### Instagram Example 70 | 71 | ```ts title="auth.ts" 72 | import { betterAuth } from "better-auth"; 73 | import { genericOAuth } from "better-auth/plugins"; 74 | 75 | export const auth = betterAuth({ 76 | // ... other config options 77 | plugins: [ 78 | genericOAuth({ 79 | config: [ 80 | { 81 | providerId: "instagram", 82 | clientId: process.env.INSTAGRAM_CLIENT_ID as string, 83 | clientSecret: process.env.INSTAGRAM_CLIENT_SECRET as string, 84 | authorizationUrl: "https://api.instagram.com/oauth/authorize", 85 | tokenUrl: "https://api.instagram.com/oauth/access_token", 86 | scopes: ["user_profile", "user_media"], 87 | }, 88 | ], 89 | }), 90 | ], 91 | }); 92 | ``` 93 | 94 | ```ts title="sign-in.ts" 95 | const response = await authClient.signIn.oauth2({ 96 | providerId: "instagram", 97 | callbackURL: "/dashboard", // the path to redirect to after the user is authenticated 98 | }); 99 | ``` 100 | 101 | ### Coinbase Example 102 | 103 | ```ts title="auth.ts" 104 | import { betterAuth } from "better-auth"; 105 | import { genericOAuth } from "better-auth/plugins"; 106 | 107 | export const auth = betterAuth({ 108 | // ... other config options 109 | plugins: [ 110 | genericOAuth({ 111 | config: [ 112 | { 113 | providerId: "coinbase", 114 | clientId: process.env.COINBASE_CLIENT_ID as string, 115 | clientSecret: process.env.COINBASE_CLIENT_SECRET as string, 116 | authorizationUrl: "https://www.coinbase.com/oauth/authorize", 117 | tokenUrl: "https://api.coinbase.com/oauth/token", 118 | scopes: ["wallet:user:read"], // and more... 119 | }, 120 | ], 121 | }), 122 | ], 123 | }); 124 | ``` 125 | 126 | ```ts title="sign-in.ts" 127 | const response = await authClient.signIn.oauth2({ 128 | providerId: "coinbase", 129 | callbackURL: "/dashboard", // the path to redirect to after the user is authenticated 130 | }); 131 | ``` 132 | ``` -------------------------------------------------------------------------------- /docs/components/landing/grid-pattern.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { useEffect, useId, useRef, useState } from "react"; 5 | 6 | const Block = ({ 7 | x, 8 | y, 9 | ...props 10 | }: Omit<React.ComponentPropsWithoutRef<typeof motion.path>, "x" | "y"> & { 11 | x: number; 12 | y: number; 13 | }) => { 14 | return ( 15 | <motion.path 16 | transform={`translate(${-32 * y + 96 * x} ${160 * y})`} 17 | d="M45.119 4.5a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C6.82 148.861 12.262 155.5 19.52 155.5h63.366a11.5 11.5 0 0 0 11.277-9.245l25.6-128c1.423-7.116-4.02-13.755-11.277-13.755H45.119Z" 18 | {...props} 19 | /> 20 | ); 21 | }; 22 | 23 | export const GridPattern = ({ yOffset = 0, interactive = false, ...props }) => { 24 | const id = useId(); 25 | const ref = useRef<React.ElementRef<"svg">>(null); 26 | const currentBlock = useRef<[x: number, y: number]>(); 27 | const counter = useRef(0); 28 | const [hoveredBlocks, setHoveredBlocks] = useState< 29 | Array<[x: number, y: number, key: number]> 30 | >([]); 31 | const staticBlocks = [ 32 | [1, 1], 33 | [2, 2], 34 | [4, 3], 35 | [6, 2], 36 | [7, 4], 37 | [5, 5], 38 | ]; 39 | 40 | useEffect(() => { 41 | if (!interactive) { 42 | return; 43 | } 44 | 45 | function onMouseMove(event: MouseEvent) { 46 | if (!ref.current) { 47 | return; 48 | } 49 | 50 | const rect = ref.current.getBoundingClientRect(); 51 | let x = event.clientX - rect.left; 52 | let y = event.clientY - rect.top; 53 | if (x < 0 || y < 0 || x > rect.width || y > rect.height) { 54 | return; 55 | } 56 | 57 | x = x - rect.width / 2 - 32; 58 | y = y - yOffset; 59 | x += Math.tan(32 / 160) * y; 60 | x = Math.floor(x / 96); 61 | y = Math.floor(y / 160); 62 | 63 | if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) { 64 | return; 65 | } 66 | 67 | currentBlock.current = [x, y]; 68 | 69 | setHoveredBlocks((blocks) => { 70 | const key = counter.current++; 71 | const block = [x, y, key] as (typeof hoveredBlocks)[number]; 72 | return [...blocks, block].filter( 73 | (block) => !(block[0] === x && block[1] === y && block[2] !== key), 74 | ); 75 | }); 76 | } 77 | 78 | window.addEventListener("mousemove", onMouseMove); 79 | 80 | return () => { 81 | window.removeEventListener("mousemove", onMouseMove); 82 | }; 83 | }, [yOffset, interactive]); 84 | 85 | return ( 86 | <motion.svg 87 | ref={ref} 88 | aria-hidden="true" 89 | {...props} 90 | exit={{ opacity: 0 }} 91 | animate={{ opacity: 1 }} 92 | initial={{ opacity: 0 }} 93 | > 94 | <rect width="100%" height="100%" fill={`url(#${id})`} strokeWidth="0" /> 95 | <svg x="50%" y={yOffset} strokeWidth="0" className="overflow-visible"> 96 | {staticBlocks.map((block) => ( 97 | <Block key={`${block}`} x={block[0]} y={block[1]} /> 98 | ))} 99 | {hoveredBlocks.map((block) => ( 100 | <Block 101 | key={block[2]} 102 | x={block[0]} 103 | y={block[1]} 104 | animate={{ opacity: [0, 1, 0] }} 105 | transition={{ duration: 1, times: [0, 0, 1] }} 106 | onAnimationComplete={() => { 107 | setHoveredBlocks((blocks) => 108 | blocks.filter((b) => b[2] !== block[2]), 109 | ); 110 | }} 111 | /> 112 | ))} 113 | </svg> 114 | <defs> 115 | <pattern 116 | id={id} 117 | width="96" 118 | height="480" 119 | x="50%" 120 | patternUnits="userSpaceOnUse" 121 | patternTransform={`translate(0 ${yOffset})`} 122 | fill="none" 123 | > 124 | <path d="M128 0 98.572 147.138A16 16 0 0 1 82.883 160H13.117a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-45.117 320H-116M64-160 34.572-12.862A16 16 0 0 1 18.883 0h-69.766a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-109.117 160H-180M192 160l-29.428 147.138A15.999 15.999 0 0 1 146.883 320H77.117a16 16 0 0 0-15.69 12.862L34.573 467.138A16 16 0 0 1 18.883 480H-52M-136 480h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1-18.883 320h69.766a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 109.117 160H192M-72 640h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 45.117 480h69.766a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A15.999 15.999 0 0 1 173.117 320H256M-200 320h58.883a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A16 16 0 0 1-82.883 160h69.766a16 16 0 0 0 15.69-12.862L29.427 12.862A16 16 0 0 1 45.117 0H128" /> 125 | </pattern> 126 | </defs> 127 | </motion.svg> 128 | ); 129 | }; 130 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/two-factor/verify-two-factor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { APIError } from "better-call"; 2 | import { TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant"; 3 | import { setSessionCookie } from "../../cookies"; 4 | import { getSessionFromCtx } from "../../api"; 5 | import type { UserWithTwoFactor } from "./types"; 6 | import { createHMAC } from "@better-auth/utils/hmac"; 7 | import { TWO_FACTOR_ERROR_CODES } from "./error-code"; 8 | import type { GenericEndpointContext } from "@better-auth/core"; 9 | 10 | export async function verifyTwoFactor(ctx: GenericEndpointContext) { 11 | const session = await getSessionFromCtx(ctx); 12 | if (!session) { 13 | const cookieName = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME); 14 | const twoFactorCookie = await ctx.getSignedCookie( 15 | cookieName.name, 16 | ctx.context.secret, 17 | ); 18 | if (!twoFactorCookie) { 19 | throw new APIError("UNAUTHORIZED", { 20 | message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, 21 | }); 22 | } 23 | const verificationToken = 24 | await ctx.context.internalAdapter.findVerificationValue(twoFactorCookie); 25 | if (!verificationToken) { 26 | throw new APIError("UNAUTHORIZED", { 27 | message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, 28 | }); 29 | } 30 | const user = (await ctx.context.internalAdapter.findUserById( 31 | verificationToken.value, 32 | )) as UserWithTwoFactor; 33 | if (!user) { 34 | throw new APIError("UNAUTHORIZED", { 35 | message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, 36 | }); 37 | } 38 | const dontRememberMe = await ctx.getSignedCookie( 39 | ctx.context.authCookies.dontRememberToken.name, 40 | ctx.context.secret, 41 | ); 42 | return { 43 | valid: async (ctx: GenericEndpointContext) => { 44 | const session = await ctx.context.internalAdapter.createSession( 45 | verificationToken.value, 46 | !!dontRememberMe, 47 | ); 48 | if (!session) { 49 | throw new APIError("INTERNAL_SERVER_ERROR", { 50 | message: "failed to create session", 51 | }); 52 | } 53 | await setSessionCookie(ctx, { 54 | session, 55 | user, 56 | }); 57 | if (ctx.body.trustDevice) { 58 | const trustDeviceCookie = ctx.context.createAuthCookie( 59 | TRUST_DEVICE_COOKIE_NAME, 60 | { 61 | maxAge: 30 * 24 * 60 * 60, // 30 days, it'll be refreshed on sign in requests 62 | }, 63 | ); 64 | /** 65 | * create a token that will be used to 66 | * verify the device 67 | */ 68 | const token = await createHMAC("SHA-256", "base64urlnopad").sign( 69 | ctx.context.secret, 70 | `${user.id}!${session.token}`, 71 | ); 72 | await ctx.setSignedCookie( 73 | trustDeviceCookie.name, 74 | `${token}!${session.token}`, 75 | ctx.context.secret, 76 | trustDeviceCookie.attributes, 77 | ); 78 | // delete the dont remember me cookie 79 | ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", { 80 | maxAge: 0, 81 | }); 82 | // delete the two factor cookie 83 | ctx.setCookie(cookieName.name, "", { 84 | maxAge: 0, 85 | }); 86 | } 87 | return ctx.json({ 88 | token: session.token, 89 | user: { 90 | id: user.id, 91 | email: user.email, 92 | emailVerified: user.emailVerified, 93 | name: user.name, 94 | image: user.image, 95 | createdAt: user.createdAt, 96 | updatedAt: user.updatedAt, 97 | }, 98 | }); 99 | }, 100 | invalid: async (errorKey: keyof typeof TWO_FACTOR_ERROR_CODES) => { 101 | throw new APIError("UNAUTHORIZED", { 102 | message: TWO_FACTOR_ERROR_CODES[errorKey], 103 | }); 104 | }, 105 | session: { 106 | session: null, 107 | user, 108 | }, 109 | key: twoFactorCookie, 110 | }; 111 | } 112 | return { 113 | valid: async (ctx: GenericEndpointContext) => { 114 | return ctx.json({ 115 | token: session.session.token, 116 | user: { 117 | id: session.user.id, 118 | email: session.user.email, 119 | emailVerified: session.user.emailVerified, 120 | name: session.user.name, 121 | image: session.user.image, 122 | createdAt: session.user.createdAt, 123 | updatedAt: session.user.updatedAt, 124 | }, 125 | }); 126 | }, 127 | invalid: async () => { 128 | throw new APIError("UNAUTHORIZED", { 129 | message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, 130 | }); 131 | }, 132 | session, 133 | key: `${session.user.id}!${session.session.id}`, 134 | }; 135 | } 136 | ``` -------------------------------------------------------------------------------- /packages/core/src/env/color-depth.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Source code copied & modified from node internals: https://github.com/nodejs/node/blob/5b32bb1573dace2dd058c05ac4fab1e4e446c775/lib/internal/tty.js#L123 2 | import { env, getEnvVar } from "./env-impl"; 3 | 4 | const COLORS_2 = 1; 5 | const COLORS_16 = 4; 6 | const COLORS_256 = 8; 7 | const COLORS_16m = 24; 8 | 9 | const TERM_ENVS: Record<string, number> = { 10 | eterm: COLORS_16, 11 | cons25: COLORS_16, 12 | console: COLORS_16, 13 | cygwin: COLORS_16, 14 | dtterm: COLORS_16, 15 | gnome: COLORS_16, 16 | hurd: COLORS_16, 17 | jfbterm: COLORS_16, 18 | konsole: COLORS_16, 19 | kterm: COLORS_16, 20 | mlterm: COLORS_16, 21 | mosh: COLORS_16m, 22 | putty: COLORS_16, 23 | st: COLORS_16, 24 | // http://lists.schmorp.de/pipermail/rxvt-unicode/2016q2/002261.html 25 | "rxvt-unicode-24bit": COLORS_16m, 26 | // https://bugs.launchpad.net/terminator/+bug/1030562 27 | terminator: COLORS_16m, 28 | "xterm-kitty": COLORS_16m, 29 | }; 30 | 31 | const CI_ENVS_MAP = new Map( 32 | Object.entries({ 33 | APPVEYOR: COLORS_256, 34 | BUILDKITE: COLORS_256, 35 | CIRCLECI: COLORS_16m, 36 | DRONE: COLORS_256, 37 | GITEA_ACTIONS: COLORS_16m, 38 | GITHUB_ACTIONS: COLORS_16m, 39 | GITLAB_CI: COLORS_256, 40 | TRAVIS: COLORS_256, 41 | }), 42 | ); 43 | 44 | const TERM_ENVS_REG_EXP = [ 45 | /ansi/, 46 | /color/, 47 | /linux/, 48 | /direct/, 49 | /^con[0-9]*x[0-9]/, 50 | /^rxvt/, 51 | /^screen/, 52 | /^xterm/, 53 | /^vt100/, 54 | /^vt220/, 55 | ]; 56 | 57 | // The `getColorDepth` API got inspired by multiple sources such as 58 | // https://github.com/chalk/supports-color, 59 | // https://github.com/isaacs/color-support. 60 | export function getColorDepth(): number { 61 | // Use level 0-3 to support the same levels as `chalk` does. This is done for 62 | // consistency throughout the ecosystem. 63 | if (getEnvVar("FORCE_COLOR") !== undefined) { 64 | switch (getEnvVar("FORCE_COLOR")) { 65 | case "": 66 | case "1": 67 | case "true": 68 | return COLORS_16; 69 | case "2": 70 | return COLORS_256; 71 | case "3": 72 | return COLORS_16m; 73 | default: 74 | return COLORS_2; 75 | } 76 | } 77 | 78 | if ( 79 | (getEnvVar("NODE_DISABLE_COLORS") !== undefined && 80 | getEnvVar("NODE_DISABLE_COLORS") !== "") || 81 | // See https://no-color.org/ 82 | (getEnvVar("NO_COLOR") !== undefined && getEnvVar("NO_COLOR") !== "") || 83 | // The "dumb" special terminal, as defined by terminfo, doesn't support 84 | // ANSI color control codes. 85 | // See https://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials 86 | getEnvVar("TERM") === "dumb" 87 | ) { 88 | return COLORS_2; 89 | } 90 | 91 | // Edge runtime doesn't support `process?.platform` syntax 92 | // if (typeof process !== "undefined" && process?.platform === "win32") { 93 | // // Windows 10 build 14931 (from 2016) has true color support 94 | // return COLORS_16m; 95 | // } 96 | 97 | if (getEnvVar("TMUX")) { 98 | return COLORS_16m; 99 | } 100 | 101 | // Azure DevOps 102 | if ("TF_BUILD" in env && "AGENT_NAME" in env) { 103 | return COLORS_16; 104 | } 105 | 106 | if ("CI" in env) { 107 | for (const { 0: envName, 1: colors } of CI_ENVS_MAP) { 108 | if (envName in env) { 109 | return colors; 110 | } 111 | } 112 | if (getEnvVar("CI_NAME") === "codeship") { 113 | return COLORS_256; 114 | } 115 | return COLORS_2; 116 | } 117 | 118 | if ("TEAMCITY_VERSION" in env) { 119 | return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.exec( 120 | getEnvVar("TEAMCITY_VERSION"), 121 | ) !== null 122 | ? COLORS_16 123 | : COLORS_2; 124 | } 125 | 126 | switch (getEnvVar("TERM_PROGRAM")) { 127 | case "iTerm.app": 128 | if ( 129 | !getEnvVar("TERM_PROGRAM_VERSION") || 130 | /^[0-2]\./.exec(getEnvVar("TERM_PROGRAM_VERSION")) !== null 131 | ) { 132 | return COLORS_256; 133 | } 134 | return COLORS_16m; 135 | case "HyperTerm": 136 | case "MacTerm": 137 | return COLORS_16m; 138 | case "Apple_Terminal": 139 | return COLORS_256; 140 | } 141 | 142 | if ( 143 | getEnvVar("COLORTERM") === "truecolor" || 144 | getEnvVar("COLORTERM") === "24bit" 145 | ) { 146 | return COLORS_16m; 147 | } 148 | 149 | if (getEnvVar("TERM")) { 150 | if (/truecolor/.exec(getEnvVar("TERM")) !== null) { 151 | return COLORS_16m; 152 | } 153 | 154 | if (/^xterm-256/.exec(getEnvVar("TERM")) !== null) { 155 | return COLORS_256; 156 | } 157 | 158 | const termEnv = getEnvVar("TERM").toLowerCase(); 159 | 160 | if (TERM_ENVS[termEnv]) { 161 | return TERM_ENVS[termEnv]; 162 | } 163 | if (TERM_ENVS_REG_EXP.some((term) => term.exec(termEnv) !== null)) { 164 | return COLORS_16; 165 | } 166 | } 167 | // Move 16 color COLORTERM below 16m and 256 168 | if (getEnvVar("COLORTERM")) { 169 | return COLORS_16; 170 | } 171 | return COLORS_2; 172 | } 173 | ``` -------------------------------------------------------------------------------- /docs/content/blogs/authjs-joins-better-auth.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Auth.js is now part of Better Auth 3 | description: "Auth.js, formerly known as NextAuth.js, is now being maintained and overseen by Better Auth team" 4 | date: 2025-09-22 5 | author: 6 | name: "Bereket Engida" 7 | avatar: "/avatars/beka.jpg" 8 | twitter: "imbereket" 9 | image: "/blogs/authjs-joins.png" 10 | tags: ["seed round", "authentication", "funding"] 11 | --- 12 | 13 | We’re excited to announce that [Auth.js](https://authjs.dev), formerly known as NextAuth.js, is now being maintained and overseen by Better Auth team. If you haven't heard of Auth.js, it has long been one of the most widely used open source authentication libraries in the JavaScript ecosystem. Chances are, if you’ve used [ChatGPT](https://chatgpt.com), [Google Labs](https://labs.google), [Cal.com](https://cal.com) or a million other websites, you’ve already interacted with Auth.js. 14 | 15 | ## Back Story about Better Auth and Auth.js 16 | 17 | Before Better Auth, Auth.js gave developers like us the ability to own our auth without spending months wrestling with OAuth integrations or session management. But as applications became more complex and authentication needs evolved, some of its limitations became harder to ignore. We found ourselves rebuilding the same primitives over and over. 18 | 19 | The Auth.js team recognized these challenges and had big ideas for the future, but for various reasons couldn’t execute them as fully as they hoped. 20 | 21 | That shared frustration and the vision of empowering everyone to truly own their auth started the creation of Better Auth. Since our goals aligned with the Auth.js team, we were excited to help maintain Auth.js and make auth better across the web. As we talked more, we realized that Better Auth was the best home for Auth.js. 22 | 23 | ## What does this mean for existing users? 24 | 25 | We recognize how important this project is for countless applications, companies, and developers. If you’re using Auth.js/NextAuth.js today, you can continue doing so without disruption—we’ll keep addressing security patches and urgent issues as they come up. 26 | 27 | But we strongly recommend new projects to start with Better Auth unless there are some very specific feature gaps (most notably stateless session management without a database). Our roadmap includes bringing those capabilities into Better Auth, so the ecosystem can converge rather than fragment. 28 | 29 | <Callout> 30 | For teams considering migration, we’ve prepared a [guide](/docs/guides/next-auth-migration-guide) and we’ll be adding more guides and documentation soon. 31 | </Callout> 32 | 33 | ## Final Thoughts 34 | 35 | We are deeply grateful to the Auth.js community who have carried the project to this point. In particular, the core maintainers-[Balázs](https://x.com/balazsorban44), who served as lead maintainer, [Thang Vu](https://x.com/thanghvu),[Nico Domino](https://ndo.dev), Lluis Agusti and [Falco Winkler](https://github.com/falcowinkler)-pushed through difficult phases, brought in new primitives, and kept the project alive long enough for this transition to even be possible. 36 | 37 | Better Auth beginning was inspired by Auth.js, and now, together, the two projects can carry the ecosystem further. The end goal remains unchanged: you should own your auth! 38 | 39 | 40 | <Callout type="none"> 41 | For the Auth.js team's announcement, see [GitHub discussion](https://github.com/nextauthjs/next-auth/discussions/13252). 42 | </Callout> 43 | 44 | 45 | ### Learn More 46 | 47 | <Cards> 48 | <Card 49 | href="https://www.better-auth.com/docs/introduction" 50 | title="Better Auth Setup" 51 | > 52 | Get started with installing Better Auth 53 | </Card> 54 | <Card 55 | href="https://www.better-auth.com/docs/comparison" 56 | title="Comparison" 57 | > 58 | Comparison between Better Auth and other options 59 | </Card> 60 | <Card 61 | href="https://www.better-auth.com/docs/guides/next-auth-migration-guide" 62 | title="NextAuth Migration Guide" 63 | > 64 | Migrate from NextAuth to Better Auth 65 | </Card> 66 | <Card 67 | href="https://www.better-auth.com/docs/guides/clerk-migration-guide" 68 | title="Clerk Migration Guide" 69 | > 70 | Migrate from Clerk to Better Auth 71 | </Card> 72 | </Cards> 73 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/form.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { Slot } from "@radix-ui/react-slot"; 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form"; 14 | 15 | import { cn } from "@/lib/utils"; 16 | import { Label } from "@/components/ui/label"; 17 | 18 | const Form = FormProvider; 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 23 | > = { 24 | name: TName; 25 | }; 26 | 27 | const FormFieldContext = React.createContext<FormFieldContextValue>( 28 | {} as FormFieldContextValue, 29 | ); 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 34 | >({ 35 | ...props 36 | }: ControllerProps<TFieldValues, TName>) => { 37 | return ( 38 | <FormFieldContext.Provider value={{ name: props.name }}> 39 | <Controller {...props} /> 40 | </FormFieldContext.Provider> 41 | ); 42 | }; 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext); 46 | const itemContext = React.useContext(FormItemContext); 47 | const { getFieldState, formState } = useFormContext(); 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState); 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within <FormField>"); 53 | } 54 | 55 | const { id } = itemContext; 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | }; 65 | }; 66 | 67 | type FormItemContextValue = { 68 | id: string; 69 | }; 70 | 71 | const FormItemContext = React.createContext<FormItemContextValue>( 72 | {} as FormItemContextValue, 73 | ); 74 | 75 | const FormItem = ({ 76 | ref, 77 | className, 78 | ...props 79 | }: React.HTMLAttributes<HTMLDivElement> & { 80 | ref: React.RefObject<HTMLDivElement>; 81 | }) => { 82 | const id = React.useId(); 83 | 84 | return ( 85 | <FormItemContext.Provider value={{ id }}> 86 | <div ref={ref} className={cn("space-y-2", className)} {...props} /> 87 | </FormItemContext.Provider> 88 | ); 89 | }; 90 | FormItem.displayName = "FormItem"; 91 | 92 | const FormLabel = ({ 93 | ref, 94 | className, 95 | ...props 96 | }: React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { 97 | ref: React.RefObject<React.ElementRef<typeof LabelPrimitive.Root>>; 98 | }) => { 99 | const { error, formItemId } = useFormField(); 100 | 101 | return ( 102 | <Label 103 | ref={ref} 104 | className={cn(error && "text-destructive", className)} 105 | htmlFor={formItemId} 106 | {...props} 107 | /> 108 | ); 109 | }; 110 | FormLabel.displayName = "FormLabel"; 111 | 112 | const FormControl = ({ 113 | ref, 114 | ...props 115 | }: React.ComponentPropsWithoutRef<typeof Slot> & { 116 | ref: React.RefObject<React.ElementRef<typeof Slot>>; 117 | }) => { 118 | const { error, formItemId, formDescriptionId, formMessageId } = 119 | useFormField(); 120 | 121 | return ( 122 | <Slot 123 | ref={ref} 124 | id={formItemId} 125 | aria-describedby={ 126 | !error 127 | ? `${formDescriptionId}` 128 | : `${formDescriptionId} ${formMessageId}` 129 | } 130 | aria-invalid={!!error} 131 | {...props} 132 | /> 133 | ); 134 | }; 135 | FormControl.displayName = "FormControl"; 136 | 137 | const FormDescription = ({ 138 | ref, 139 | className, 140 | ...props 141 | }: React.HTMLAttributes<HTMLParagraphElement> & { 142 | ref: React.RefObject<HTMLParagraphElement>; 143 | }) => { 144 | const { formDescriptionId } = useFormField(); 145 | 146 | return ( 147 | <p 148 | ref={ref} 149 | id={formDescriptionId} 150 | className={cn("text-[0.8rem] text-muted-foreground", className)} 151 | {...props} 152 | /> 153 | ); 154 | }; 155 | FormDescription.displayName = "FormDescription"; 156 | 157 | const FormMessage = ({ 158 | ref, 159 | className, 160 | children, 161 | ...props 162 | }: React.HTMLAttributes<HTMLParagraphElement> & { 163 | ref: React.RefObject<HTMLParagraphElement>; 164 | }) => { 165 | const { error, formMessageId } = useFormField(); 166 | const body = error ? String(error?.message) : children; 167 | 168 | if (!body) { 169 | return null; 170 | } 171 | 172 | return ( 173 | <p 174 | ref={ref} 175 | id={formMessageId} 176 | className={cn("text-[0.8rem] font-medium text-destructive", className)} 177 | {...props} 178 | > 179 | {body} 180 | </p> 181 | ); 182 | }; 183 | FormMessage.displayName = "FormMessage"; 184 | 185 | export { 186 | useFormField, 187 | Form, 188 | FormItem, 189 | FormLabel, 190 | FormControl, 191 | FormDescription, 192 | FormMessage, 193 | FormField, 194 | }; 195 | ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/blog-list.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { formatBlogDate } from "@/lib/blog"; 2 | import Link from "next/link"; 3 | import { blogs } from "@/lib/source"; 4 | import { IconLink } from "./changelog-layout"; 5 | import { GitHubIcon, BookIcon, XIcon } from "./icons"; 6 | import { Glow } from "./default-changelog"; 7 | import { StarField } from "./stat-field"; 8 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 9 | import Image from "next/image"; 10 | 11 | export async function BlogPage() { 12 | const posts = blogs.getPages().sort((a, b) => { 13 | return new Date(b.data.date).getTime() - new Date(a.data.date).getTime(); 14 | }); 15 | return ( 16 | <div className="md:grid md:grid-cols-2 items-start"> 17 | <div className="bg-gradient-to-tr hidden md:block overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 18 | <StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" /> 19 | <Glow /> 20 | 21 | <div className="flex flex-col md:justify-center max-w-xl mx-auto h-full"> 22 | <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> 23 | Blogs 24 | </h1> 25 | 26 | <p className="text-sm text-gray-600 dark:text-gray-300"> 27 | Latest updates, articles, and insights about Better Auth 28 | </p> 29 | <hr className="h-px bg-gray-300 mt-5" /> 30 | <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 gap-x-1 gap-y-3 sm:gap-x-2"> 31 | <IconLink 32 | href="/docs" 33 | icon={BookIcon} 34 | className="flex-none text-gray-600 dark:text-gray-300" 35 | > 36 | Documentation 37 | </IconLink> 38 | <IconLink 39 | href="https://github.com/better-auth/better-auth" 40 | icon={GitHubIcon} 41 | className="flex-none text-gray-600 dark:text-gray-300" 42 | > 43 | GitHub 44 | </IconLink> 45 | <IconLink 46 | href="https://discord.gg/better-auth" 47 | icon={DiscordLogoIcon} 48 | className="flex-none text-gray-600 dark:text-gray-300" 49 | > 50 | Community 51 | </IconLink> 52 | </div> 53 | <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> 54 | <IconLink href="https://x.com/better_auth" icon={XIcon} compact> 55 | BETTER-AUTH. 56 | </IconLink> 57 | </p> 58 | </div> 59 | </div> 60 | <div className="py-6 lg:py-10 px-3"> 61 | <div className="flex flex-col gap-2"> 62 | {posts.map((post) => ( 63 | <div 64 | className="group/blog flex flex-col gap-3 transition-colors p-4" 65 | key={post.slugs.join("/")} 66 | > 67 | <article className="group relative flex flex-col space-y-2 flex-3/4 py-1"> 68 | <div className="flex gap-2"> 69 | <div className="flex flex-col gap-2 border-b border-dashed pb-2"> 70 | <p className="text-xs opacity-50"> 71 | {formatBlogDate(post.data.date)} 72 | </p> 73 | <h2 className="text-2xl font-bold">{post.data?.title}</h2> 74 | </div> 75 | </div> 76 | {post.data?.image && ( 77 | <Image 78 | src={post.data.image} 79 | alt={post.data.title} 80 | width={1206} 81 | height={756} 82 | className="rounded-md w-full bg-muted transition-colors" 83 | /> 84 | )} 85 | <div className="flex gap-2"> 86 | <div className="flex flex-col gap-2 border-b border-dashed pb-2"> 87 | <p className="text-muted-foreground"> 88 | {post.data?.description.substring(0, 100)}... 89 | </p> 90 | </div> 91 | </div> 92 | <p className="text-xs opacity-50"> 93 | {post.data.structuredData.contents[0].content.substring( 94 | 0, 95 | 250, 96 | )} 97 | ... 98 | </p> 99 | <Link href={`/blog/${post.slugs.join("/")}`}> 100 | <p className="text-xs group-hover/blog:underline underline-offset-4 transition-all"> 101 | Read More 102 | </p> 103 | </Link> 104 | <Link 105 | href={`/blog/${post.slugs.join("/")}`} 106 | className="absolute inset-0" 107 | > 108 | <span className="sr-only">View Article</span> 109 | </Link> 110 | </article> 111 | </div> 112 | ))} 113 | </div> 114 | </div> 115 | </div> 116 | ); 117 | } 118 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/anonymous.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Anonymous 3 | description: Anonymous plugin for Better Auth. 4 | --- 5 | 6 | The Anonymous plugin allows users to have an authenticated experience without requiring them to provide an email address, password, OAuth provider, or any other Personally Identifiable Information (PII). Users can later link an authentication method to their account when ready. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ### Add the plugin to your auth config 13 | 14 | To enable anonymous authentication, add the anonymous plugin to your authentication configuration. 15 | 16 | ```ts title="auth.ts" 17 | import { betterAuth } from "better-auth" 18 | import { anonymous } from "better-auth/plugins" // [!code highlight] 19 | 20 | export const auth = betterAuth({ 21 | // ... other config options 22 | plugins: [ 23 | anonymous() // [!code highlight] 24 | ] 25 | }) 26 | ``` 27 | </Step> 28 | 29 | <Step> 30 | ### Migrate the database 31 | 32 | Run the migration or generate the schema to add the necessary fields and tables to the database. 33 | 34 | <Tabs items={["migrate", "generate"]}> 35 | <Tab value="migrate"> 36 | ```bash 37 | npx @better-auth/cli migrate 38 | ``` 39 | </Tab> 40 | <Tab value="generate"> 41 | ```bash 42 | npx @better-auth/cli generate 43 | ``` 44 | </Tab> 45 | </Tabs> 46 | See the [Schema](#schema) section to add the fields manually. 47 | </Step> 48 | 49 | <Step> 50 | ### Add the client plugin 51 | 52 | Next, include the anonymous client plugin in your authentication client instance. 53 | 54 | ```ts title="auth-client.ts" 55 | import { createAuthClient } from "better-auth/client" 56 | import { anonymousClient } from "better-auth/client/plugins" 57 | 58 | export const authClient = createAuthClient({ 59 | plugins: [ 60 | anonymousClient() 61 | ] 62 | }) 63 | ``` 64 | </Step> 65 | </Steps> 66 | 67 | ## Usage 68 | 69 | ### Sign In 70 | 71 | To sign in a user anonymously, use the `signIn.anonymous()` method. 72 | 73 | ```ts title="example.ts" 74 | const user = await authClient.signIn.anonymous() 75 | ``` 76 | 77 | ### Link Account 78 | 79 | If a user is already signed in anonymously and tries to `signIn` or `signUp` with another method, their anonymous activities can be linked to the new account. 80 | 81 | To do that you first need to provide `onLinkAccount` callback to the plugin. 82 | 83 | ```ts title="auth.ts" 84 | import { betterAuth } from "better-auth" 85 | 86 | export const auth = betterAuth({ 87 | plugins: [ 88 | anonymous({ 89 | onLinkAccount: async ({ anonymousUser, newUser }) => { 90 | // perform actions like moving the cart items from anonymous user to the new user 91 | } 92 | }) 93 | ] 94 | ``` 95 | 96 | Then when you call `signIn` or `signUp` with another method, the `onLinkAccount` callback will be called. And the `anonymousUser` will be deleted by default. 97 | 98 | ```ts title="example.ts" 99 | const user = await authClient.signIn.email({ 100 | email, 101 | }) 102 | ``` 103 | 104 | ## Options 105 | 106 | - `emailDomainName`: The domain name to use when generating an email address for anonymous users. Defaults to the domain name of the current site. 107 | 108 | ```ts title="auth.ts" 109 | import { betterAuth } from "better-auth" 110 | 111 | export const auth = betterAuth({ 112 | plugins: [ 113 | anonymous({ 114 | emailDomainName: "example.com" 115 | }) 116 | ] 117 | }) 118 | ``` 119 | 120 | - `onLinkAccount`: A callback function that is called when an anonymous user links their account to a new authentication method. The callback receives an object with the `anonymousUser` and the `newUser`. 121 | 122 | - `disableDeleteAnonymousUser`: By default, the anonymous user is deleted when the account is linked to a new authentication method. Set this option to `true` to disable this behavior. 123 | 124 | - `generateName`: A callback function that is called to generate a name for the anonymous user. Useful if you want to have random names for anonymous users, or if `name` is unique in your database. 125 | 126 | ## Schema 127 | 128 | The anonymous plugin requires an additional field in the user table: 129 | 130 | <DatabaseTable 131 | fields={[ 132 | { name: "isAnonymous", type: "boolean", description: "Indicates whether the user is anonymous.", isOptional: true }, 133 | ]} 134 | /> 135 | ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | push: 9 | branches: 10 | - main 11 | - canary 12 | merge_group: 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Cache turbo build setup 23 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 24 | with: 25 | path: .turbo 26 | key: ${{ runner.os }}-turbo-${{ github.sha }} 27 | restore-keys: | 28 | ${{ runner.os }}-turbo- 29 | 30 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 31 | 32 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 33 | with: 34 | node-version: 22.x 35 | registry-url: 'https://registry.npmjs.org' 36 | cache: pnpm 37 | 38 | - name: Install 39 | run: pnpm install 40 | 41 | - name: Build 42 | env: 43 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 44 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 45 | TURBO_CACHE: remote:rw 46 | run: pnpm build 47 | 48 | - name: Lint 49 | env: 50 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 51 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 52 | TURBO_CACHE: remote:rw 53 | run: pnpm lint 54 | 55 | typecheck: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 59 | with: 60 | fetch-depth: 0 61 | 62 | - name: Cache turbo build setup 63 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 64 | with: 65 | path: .turbo 66 | key: ${{ runner.os }}-turbo-${{ github.sha }} 67 | restore-keys: | 68 | ${{ runner.os }}-turbo- 69 | 70 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 71 | 72 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 73 | with: 74 | node-version: 22.x 75 | registry-url: 'https://registry.npmjs.org' 76 | cache: pnpm 77 | 78 | - name: Install 79 | run: pnpm install 80 | 81 | - name: Build 82 | env: 83 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 84 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 85 | TURBO_CACHE: remote:rw 86 | run: pnpm build 87 | 88 | - name: Typecheck 89 | env: 90 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 91 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 92 | TURBO_CACHE: remote:rw 93 | run: pnpm typecheck 94 | 95 | test: 96 | runs-on: ubuntu-latest 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | node-version: [ 22.x, 24.x, 25.x ] 101 | steps: 102 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 103 | with: 104 | fetch-depth: 0 105 | 106 | - name: Cache turbo build setup 107 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 108 | with: 109 | path: .turbo 110 | key: ${{ runner.os }}-turbo-${{ github.sha }} 111 | restore-keys: | 112 | ${{ runner.os }}-turbo- 113 | 114 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 115 | 116 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 117 | with: 118 | node-version: ${{ matrix.node-version }} 119 | registry-url: 'https://registry.npmjs.org' 120 | cache: pnpm 121 | 122 | - name: Install 123 | run: pnpm install 124 | 125 | - name: Build 126 | env: 127 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 128 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 129 | TURBO_CACHE: remote:rw 130 | run: pnpm build 131 | 132 | - name: Start Docker Containers 133 | run: | 134 | docker compose up -d 135 | # Wait for services to be ready (optional) 136 | sleep 10 137 | 138 | - name: Test 139 | env: 140 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 141 | TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} 142 | run: pnpm test 143 | 144 | - name: Stop Docker Containers 145 | run: docker compose down 146 | ```