This is page 13 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&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 -------------------------------------------------------------------------------- /docs/components/ui/carousel.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import useEmblaCarousel, { type UseEmblaCarouselType, } from "embla-carousel-react"; import { ArrowLeft, ArrowRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; type CarouselApi = UseEmblaCarouselType[1]; type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; type CarouselOptions = UseCarouselParameters[0]; type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { opts?: CarouselOptions; plugins?: CarouselPlugin; orientation?: "horizontal" | "vertical"; setApi?: (api: CarouselApi) => void; }; type CarouselContextProps = { carouselRef: ReturnType<typeof useEmblaCarousel>[0]; api: ReturnType<typeof useEmblaCarousel>[1]; scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean; } & CarouselProps; const CarouselContext = React.createContext<CarouselContextProps | null>(null); function useCarousel() { const context = React.useContext(CarouselContext); if (!context) { throw new Error("useCarousel must be used within a <Carousel />"); } return context; } function Carousel({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }: React.ComponentProps<"div"> & CarouselProps) { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, plugins, ); const [canScrollPrev, setCanScrollPrev] = React.useState(false); const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: CarouselApi) => { if (!api) return; setCanScrollPrev(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }, []); const scrollPrev = React.useCallback(() => { api?.scrollPrev(); }, [api]); const scrollNext = React.useCallback(() => { api?.scrollNext(); }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === "ArrowLeft") { event.preventDefault(); scrollPrev(); } else if (event.key === "ArrowRight") { event.preventDefault(); scrollNext(); } }, [scrollPrev, scrollNext], ); React.useEffect(() => { if (!api || !setApi) return; setApi(api); }, [api, setApi]); React.useEffect(() => { if (!api) return; onSelect(api); api.on("reInit", onSelect); api.on("select", onSelect); return () => { api?.off("select", onSelect); }; }, [api, onSelect]); return ( <CarouselContext.Provider value={{ carouselRef, api: api, opts, orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), scrollPrev, scrollNext, canScrollPrev, canScrollNext, }} > <div onKeyDownCapture={handleKeyDown} className={cn("relative", className)} role="region" aria-roledescription="carousel" data-slot="carousel" {...props} > {children} </div> </CarouselContext.Provider> ); } function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { const { carouselRef, orientation } = useCarousel(); return ( <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content" > <div className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className, )} {...props} /> </div> ); } function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { const { orientation } = useCarousel(); return ( <div role="group" aria-roledescription="slide" data-slot="carousel-item" className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className, )} {...props} /> ); } function CarouselPrevious({ className, variant = "outline", size = "icon", ...props }: React.ComponentProps<typeof Button>) { const { orientation, scrollPrev, canScrollPrev } = useCarousel(); return ( <Button data-slot="carousel-previous" variant={variant} size={size} className={cn( "absolute size-8 rounded-full", orientation === "horizontal" ? "top-1/2 -left-12 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > <ArrowLeft /> <span className="sr-only">Previous slide</span> </Button> ); } function CarouselNext({ className, variant = "outline", size = "icon", ...props }: React.ComponentProps<typeof Button>) { const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( <Button data-slot="carousel-next" variant={variant} size={size} className={cn( "absolute size-8 rounded-full", orientation === "horizontal" ? "top-1/2 -right-12 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollNext} onClick={scrollNext} {...props} > <ArrowRight /> <span className="sr-only">Next slide</span> </Button> ); } export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, }; ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/discord.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { refreshAccessToken, validateAuthorizationCode } from "../oauth2"; export interface DiscordProfile extends Record<string, any> { /** the user's id (i.e. the numerical snowflake) */ id: string; /** the user's username, not unique across the platform */ username: string; /** the user's Discord-tag */ discriminator: string; /** the user's display name, if it is set */ global_name: string | null; /** * the user's avatar hash: * https://discord.com/developers/docs/reference#image-formatting */ avatar: string | null; /** whether the user belongs to an OAuth2 application */ bot?: boolean; /** * whether the user is an Official Discord System user (part of the urgent * message system) */ system?: boolean; /** whether the user has two factor enabled on their account */ mfa_enabled: boolean; /** * the user's banner hash: * https://discord.com/developers/docs/reference#image-formatting */ banner: string | null; /** the user's banner color encoded as an integer representation of hexadecimal color code */ accent_color: number | null; /** * the user's chosen language option: * https://discord.com/developers/docs/reference#locales */ locale: string; /** whether the email on this account has been verified */ verified: boolean; /** the user's email */ email: string; /** * the flags on a user's account: * https://discord.com/developers/docs/resources/user#user-object-user-flags */ flags: number; /** * the type of Nitro subscription on a user's account: * https://discord.com/developers/docs/resources/user#user-object-premium-types */ premium_type: number; /** * the public flags on a user's account: * https://discord.com/developers/docs/resources/user#user-object-user-flags */ public_flags: number; /** undocumented field; corresponds to the user's custom nickname */ display_name: string | null; /** * undocumented field; corresponds to the Discord feature where you can e.g. * put your avatar inside of an ice cube */ avatar_decoration: string | null; /** * undocumented field; corresponds to the premium feature where you can * select a custom banner color */ banner_color: string | null; /** undocumented field; the CDN URL of their profile picture */ image_url: string; } export interface DiscordOptions extends ProviderOptions<DiscordProfile> { clientId: string; prompt?: "none" | "consent"; permissions?: number; } export const discord = (options: DiscordOptions) => { return { id: "discord", name: "Discord", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["identify", "email"]; scopes && _scopes.push(...scopes); options.scope && _scopes.push(...options.scope); const hasBotScope = _scopes.includes("bot"); const permissionsParam = hasBotScope && options.permissions !== undefined ? `&permissions=${options.permissions}` : ""; return new URL( `https://discord.com/api/oauth2/authorize?scope=${_scopes.join( "+", )}&response_type=code&client_id=${ options.clientId }&redirect_uri=${encodeURIComponent( options.redirectURI || redirectURI, )}&state=${state}&prompt=${ options.prompt || "none" }${permissionsParam}`, ); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://discord.com/api/oauth2/token", }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint: "https://discord.com/api/oauth2/token", }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<DiscordProfile>( "https://discord.com/api/users/@me", { headers: { authorization: `Bearer ${token.accessToken}`, }, }, ); if (error) { return null; } if (profile.avatar === null) { const defaultAvatarNumber = profile.discriminator === "0" ? Number(BigInt(profile.id) >> BigInt(22)) % 6 : parseInt(profile.discriminator) % 5; profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`; } else { const format = profile.avatar.startsWith("a_") ? "gif" : "png"; profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.global_name || profile.username || "", email: profile.email, emailVerified: profile.verified, image: profile.image_url, ...userMap, }, data: profile, }; }, options, } satisfies OAuthProvider<DiscordProfile>; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/db.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, vi } from "vitest"; import { getTestInstance } from "../test-utils/test-instance"; describe("db", async () => { it("should work with custom model names", async () => { const { client, db } = await getTestInstance({ user: { modelName: "users", }, session: { modelName: "sessions", }, account: { modelName: "accounts", }, }); const res = await client.signUp.email({ email: "[email protected]", password: "password", name: "Test User", }); const users = await db.findMany({ model: "user", }); const session = await db.findMany({ model: "session", }); const accounts = await db.findMany({ model: "account", }); expect(res.data).toBeDefined(); //including the user that was created in the test instance expect(users).toHaveLength(2); expect(session).toHaveLength(2); expect(accounts).toHaveLength(2); }); it("db hooks", async () => { let callback = false; const { client, db } = await getTestInstance({ databaseHooks: { user: { create: { async before(user) { return { data: { ...user, image: "test-image", }, }; }, async after(user) { callback = true; }, }, }, }, }); const res = await client.signUp.email({ email: "[email protected]", name: "test", password: "password", }); const session = await client.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${res.data?.token}`, }, throw: true, }, }); expect(session?.user?.image).toBe("test-image"); expect(callback).toBe(true); }); it("should work with custom field names", async () => { const { client } = await getTestInstance({ user: { fields: { email: "email_address", }, }, }); const res = await client.signUp.email({ email: "[email protected]", password: "password", name: "Test User", }); const session = await client.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${res.data?.token}`, }, throw: true, }, }); expect(session?.user.email).toBe("[email protected]"); }); it("delete hooks", async () => { const hookUserDeleteBefore = vi.fn(); const hookUserDeleteAfter = vi.fn(); const hookSessionDeleteBefore = vi.fn(); const hookSessionDeleteAfter = vi.fn(); const { client } = await getTestInstance({ session: { storeSessionInDatabase: true, }, user: { deleteUser: { enabled: true, }, }, databaseHooks: { user: { delete: { async before(user, context) { hookUserDeleteBefore(user, context); return true; }, async after(user, context) { hookUserDeleteAfter(user, context); }, }, }, session: { delete: { async before(session, context) { hookSessionDeleteBefore(session, context); return true; }, async after(session, context) { hookSessionDeleteAfter(session, context); }, }, }, }, }); const res = await client.signUp.email({ email: "[email protected]", password: "password", name: "Delete Test User", }); expect(res.data).toBeDefined(); const userId = res.data?.user.id; await client.deleteUser({ fetchOptions: { headers: { Authorization: `Bearer ${res.data?.token}`, }, throw: true, }, }); expect(hookUserDeleteBefore).toHaveBeenCalledOnce(); expect(hookUserDeleteAfter).toHaveBeenCalledOnce(); expect(hookSessionDeleteBefore).toHaveBeenCalledOnce(); expect(hookSessionDeleteAfter).toHaveBeenCalledOnce(); expect(hookUserDeleteBefore).toHaveBeenCalledWith( expect.objectContaining({ id: userId, email: "[email protected]", name: "Delete Test User", }), expect.any(Object), ); expect(hookUserDeleteAfter).toHaveBeenCalledWith( expect.objectContaining({ id: userId, email: "[email protected]", name: "Delete Test User", }), expect.any(Object), ); }); it("delete hooks abort", async () => { const hookUserDeleteBefore = vi.fn(); const hookUserDeleteAfter = vi.fn(); const { client } = await getTestInstance({ user: { deleteUser: { enabled: true, }, }, databaseHooks: { user: { delete: { async before(user, context) { hookUserDeleteBefore(user, context); return false; }, async after(user, context) { hookUserDeleteAfter(user, context); }, }, }, }, }); const res = await client.signUp.email({ email: "[email protected]", password: "password", name: "Abort Delete Test User", }); expect(res.data).toBeDefined(); const userId = res.data?.user.id; try { await client.deleteUser({ fetchOptions: { headers: { Authorization: `Bearer ${res.data?.token}`, }, throw: true, }, }); } catch (error) { // Expected to fail due to hook returning false } expect(hookUserDeleteBefore).toHaveBeenCalledOnce(); expect(hookUserDeleteAfter).not.toHaveBeenCalled(); expect(hookUserDeleteBefore).toHaveBeenCalledWith( expect.objectContaining({ id: userId, email: "[email protected]", name: "Abort Delete Test User", }), expect.any(Object), ); }); }); ``` -------------------------------------------------------------------------------- /packages/cli/src/utils/add-svelte-kit-env-modules.ts: -------------------------------------------------------------------------------- ```typescript import path from "path"; import fs from "fs"; /** * Adds SvelteKit environment modules and path aliases * @param aliases - The aliases object to populate * @param cwd - Current working directory (optional, defaults to process.cwd()) */ export function addSvelteKitEnvModules( aliases: Record<string, string>, cwd?: string, ) { const workingDir = cwd || process.cwd(); // Add SvelteKit environment modules aliases["$env/dynamic/private"] = createDataUriModule( createDynamicEnvModule(), ); aliases["$env/dynamic/public"] = createDataUriModule( createDynamicEnvModule(), ); aliases["$env/static/private"] = createDataUriModule( createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")), ); aliases["$env/static/public"] = createDataUriModule( createStaticEnvModule(filterPublicEnv("PUBLIC_", "")), ); const svelteKitAliases = getSvelteKitPathAliases(workingDir); Object.assign(aliases, svelteKitAliases); } function getSvelteKitPathAliases(cwd: string): Record<string, string> { const aliases: Record<string, string> = {}; const packageJsonPath = path.join(cwd, "package.json"); const svelteConfigPath = path.join(cwd, "svelte.config.js"); const svelteConfigTsPath = path.join(cwd, "svelte.config.ts"); let isSvelteKitProject = false; if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies, }; isSvelteKitProject = !!deps["@sveltejs/kit"]; } catch { // Ignore JSON parse errors } } if (!isSvelteKitProject) { isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath); } if (!isSvelteKitProject) { return aliases; } const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")]; for (const libPath of libPaths) { if (fs.existsSync(libPath)) { aliases["$lib"] = libPath; // handles a common subpaths const commonSubPaths = ["server", "utils", "components", "stores"]; for (const subPath of commonSubPaths) { const subDir = path.join(libPath, subPath); if (fs.existsSync(subDir)) { aliases[`$lib/${subPath}`] = subDir; } } break; } } // Add simple stub for $app/server to prevent CLI errors aliases["$app/server"] = createDataUriModule(createAppServerModule()); const customAliases = getSvelteConfigAliases(cwd); Object.assign(aliases, customAliases); return aliases; } // for custom aliases in svelte.config.js/ts function getSvelteConfigAliases(cwd: string): Record<string, string> { const aliases: Record<string, string> = {}; const configPaths = [ path.join(cwd, "svelte.config.js"), path.join(cwd, "svelte.config.ts"), ]; for (const configPath of configPaths) { if (fs.existsSync(configPath)) { try { const content = fs.readFileSync(configPath, "utf-8"); const aliasMatch = content.match(/alias\s*:\s*\{([^}]+)\}/); if (aliasMatch && aliasMatch[1]) { const aliasContent = aliasMatch[1]; const aliasMatches = aliasContent.matchAll( /['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g, ); for (const match of aliasMatches) { const [, alias, target] = match; if (alias && target) { aliases[alias + "/*"] = path.resolve(cwd, target) + "/*"; aliases[alias] = path.resolve(cwd, target); } } } } catch { // Ignore file reading/parsing errors } break; } } return aliases; } function createAppServerModule(): string { return ` // $app/server stub for CLI compatibility export default {}; // jiti dirty hack: .unknown `; } function createDataUriModule(module: string) { return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`; } function createStaticEnvModule(env: Record<string, string>) { const declarations = Object.keys(env) .filter((k) => validIdentifier.test(k) && !reserved.has(k)) .map((k) => `export const ${k} = ${JSON.stringify(env[k])};`); return ` ${declarations.join("\n")} // jiti dirty hack: .unknown `; } function createDynamicEnvModule() { return ` export const env = process.env; // jiti dirty hack: .unknown `; } export function filterPrivateEnv(publicPrefix: string, privatePrefix: string) { return Object.fromEntries( Object.entries(process.env).filter( ([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix)), ), ) as Record<string, string>; } export function filterPublicEnv(publicPrefix: string, privatePrefix: string) { return Object.fromEntries( Object.entries(process.env).filter( ([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix)), ), ) as Record<string, string>; } const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; const reserved = new Set([ "do", "if", "in", "for", "let", "new", "try", "var", "case", "else", "enum", "eval", "null", "this", "true", "void", "with", "await", "break", "catch", "class", "const", "false", "super", "throw", "while", "yield", "delete", "export", "import", "public", "return", "static", "switch", "typeof", "default", "extends", "finally", "package", "private", "continue", "debugger", "function", "arguments", "interface", "protected", "implements", "instanceof", ]); ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/paypal.mdx: -------------------------------------------------------------------------------- ```markdown --- title: PayPal description: Paypal provider setup and usage. --- <Steps> <Step> ### Get your PayPal Credentials To integrate with PayPal, you need to obtain API credentials by creating an application in the [PayPal Developer Portal](https://developer.paypal.com/dashboard). Follow these steps: 1. Create an account on the PayPal Developer Portal 2. Create a new application, [official docs]( https://developer.paypal.com/developer/applications/) 3. Configure Log in with PayPal under "Other features" 4. Set up your Return URL (redirect URL) 5. Configure user information permissions 6. Note your Client ID and Client Secret <Callout type="info"> - PayPal has two environments: Sandbox (for testing) and Live (for production) - For testing, create sandbox test accounts in the Developer Dashboard under "Sandbox" → "Accounts" - You cannot use your real PayPal account to test in sandbox mode - you must use the generated test accounts - The Return URL in your PayPal app settings must exactly match your redirect URI - The PayPal API does not work with localhost. You need to use a public domain for the redirect URL and HTTPS for local testing. You can use [NGROK](https://ngrok.com/) or another similar tool for this. </Callout> Make sure to configure "Log in with PayPal" in your app settings: 1. Go to your app in the Developer Dashboard 2. Under "Other features", check "Log in with PayPal" 3. Click "Advanced Settings" 4. Enter your Return URL 5. Select the user information you want to access (email, name, etc.) 6. Enter Privacy Policy and User Agreement URLs <Callout type="info"> - PayPal doesn't use traditional OAuth2 scopes in the authorization URL. Instead, you configure permissions directly in the Developer Dashboard - For live apps, PayPal must review and approve your application before it can go live, which typically takes a few weeks </Callout> </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { paypal: { // [!code highlight] clientId: process.env.PAYPAL_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.PAYPAL_CLIENT_SECRET as string, // [!code highlight] environment: "sandbox", // or "live" for production //, // [!code highlight] }, // [!code highlight] }, }) ``` #### Options The PayPal provider accepts the following options: - `environment`: `'sandbox' | 'live'` - PayPal environment to use (default: `'sandbox'`) - `requestShippingAddress`: `boolean` - Whether to request shipping address information (default: `false`) ```ts title="auth.ts" export const auth = betterAuth({ socialProviders: { paypal: { clientId: process.env.PAYPAL_CLIENT_ID as string, clientSecret: process.env.PAYPAL_CLIENT_SECRET as string, environment: "live", // Use "live" for production requestShippingAddress: true, // Request address info }, }, }) ``` </Step> <Step> ### Sign In with PayPal To sign in with PayPal, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `paypal`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "paypal" }) } ``` ### Additional Options: - `environment`: PayPal environment to use. - Default: `"sandbox"` - Options: `"sandbox"` | `"live"` - `requestShippingAddress`: Whether to request shipping address information. - Default: `false` - `scope`: Additional scopes to request (combined with default permissions). - Default: Configured in PayPal Developer Dashboard - Note: PayPal doesn't use traditional OAuth2 scopes - permissions are set in the Dashboard For more details refer to the [Scopes Reference](https://developer.paypal.com/docs/log-in-with-paypal/integrate/reference/#scope-attributes) - `mapProfileToUser`: Custom function to map PayPal profile data to user object. - `getUserInfo`: Custom function to retrieve user information. For more details refer to the [User Reference](https://developer.paypal.com/docs/api/identity/v1/#userinfo_get) - `verifyIdToken`: Custom ID token verification function. </Step> </Steps> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import type { RateLimit } from "../../types"; describe( "rate-limiter", { timeout: 10000, }, async () => { const { client, testUser } = await getTestInstance({ rateLimit: { enabled: true, window: 10, max: 20, }, }); it("should return 429 after 3 request for sign-in", async () => { for (let i = 0; i < 5; i++) { const response = await client.signIn.email({ email: testUser.email, password: testUser.password, }); if (i >= 3) { expect(response.error?.status).toBe(429); } else { expect(response.error).toBeNull(); } } }); it("should reset the limit after the window period", async () => { vi.useFakeTimers(); vi.advanceTimersByTime(11000); for (let i = 0; i < 5; i++) { const res = await client.signIn.email({ email: testUser.email, password: testUser.password, }); if (i >= 3) { expect(res.error?.status).toBe(429); } else { expect(res.error).toBeNull(); } } }); it("should respond the correct retry-after header", async () => { vi.useFakeTimers(); vi.advanceTimersByTime(3000); let retryAfter = ""; await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onError(context) { retryAfter = context.response.headers.get("X-Retry-After") ?? ""; }, }, ); expect(retryAfter).toBe("7"); }); it("should rate limit based on the path", async () => { const signInRes = await client.signIn.email({ email: testUser.email, password: testUser.password, }); expect(signInRes.error?.status).toBe(429); const signUpRes = await client.signUp.email({ email: "[email protected]", password: testUser.password, name: "test", }); expect(signUpRes.error).toBeNull(); }); it("non-special-rules limits", async () => { for (let i = 0; i < 25; i++) { const response = await client.getSession(); expect(response.error?.status).toBe(i >= 20 ? 429 : undefined); } }); it("query params should be ignored", async () => { for (let i = 0; i < 25; i++) { const response = await client.listSessions({ fetchOptions: { query: { "test-query": Math.random().toString(), }, }, }); if (i >= 20) { expect(response.error?.status).toBe(429); } else { expect(response.error?.status).toBe(401); } } }); }, ); describe("custom rate limiting storage", async () => { let store = new Map<string, string>(); const expirationMap = new Map<string, number>(); const { client, testUser } = await getTestInstance({ rateLimit: { enabled: true, }, secondaryStorage: { set(key, value, ttl) { store.set(key, value); if (ttl) expirationMap.set(key, ttl); }, get(key) { return store.get(key) || null; }, delete(key) { store.delete(key); expirationMap.delete(key); }, }, }); it("should use custom storage", async () => { await client.getSession(); expect(store.size).toBe(3); let lastRequest = Date.now(); for (let i = 0; i < 4; i++) { const response = await client.signIn.email({ email: testUser.email, password: testUser.password, }); const rateLimitData: RateLimit = JSON.parse( store.get("127.0.0.1/sign-in/email") ?? "{}", ); expect(rateLimitData.lastRequest).toBeGreaterThanOrEqual(lastRequest); lastRequest = rateLimitData.lastRequest; if (i >= 3) { expect(response.error?.status).toBe(429); expect(rateLimitData.count).toBe(3); } else { expect(response.error).toBeNull(); expect(rateLimitData.count).toBe(i + 1); } const rateLimitExp = expirationMap.get("127.0.0.1/sign-in/email"); expect(rateLimitExp).toBe(10); } }); }); describe("should work with custom rules", async () => { const { client, testUser } = await getTestInstance({ rateLimit: { enabled: true, storage: "database", customRules: { "/sign-in/*": { window: 10, max: 2, }, "/sign-up/email": { window: 10, max: 3, }, "/get-session": false, }, }, }); it("should use custom rules", async () => { for (let i = 0; i < 4; i++) { const response = await client.signIn.email({ email: testUser.email, password: testUser.password, }); if (i >= 2) { expect(response.error?.status).toBe(429); } else { expect(response.error).toBeNull(); } } for (let i = 0; i < 5; i++) { const response = await client.signUp.email({ email: `${Math.random()}@test.com`, password: testUser.password, name: "test", }); if (i >= 3) { expect(response.error?.status).toBe(429); } else { expect(response.error).toBeNull(); } } }); it("should use default rules if custom rules are not defined", async () => { for (let i = 0; i < 5; i++) { const response = await client.getSession(); if (i >= 20) { expect(response.error?.status).toBe(429); } else { expect(response.error).toBeNull(); } } }); it("should not rate limit if custom rule is false", async () => { let i = 0; let response = null; for (; i < 110; i++) { response = await client.getSession().then((res) => res.error); } expect(response).toBeNull(); expect(i).toBe(110); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/saml-sso-with-okta.mdx: -------------------------------------------------------------------------------- ```markdown --- title: SAML SSO with Okta description: A guide to integrating SAML Single Sign-On (SSO) with Better Auth, featuring Okta --- This guide walks you through setting up SAML Single Sign-On (SSO) with your Identity Provider (IdP), using Okta as an example. For advanced configuration details and the full API reference, check out the [SSO Plugin Documentation](/docs/plugins/sso). ## What is SAML? SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) (e.g., Okta, Azure AD, OneLogin) and a Service Provider (SP) (in this case, Better Auth). In this setup: - **IdP (Okta)**: Authenticates users and sends assertions about their identity. - **SP (Better Auth)**: Validates assertions and logs the user in.up. ### Step 1: Create a SAML Application in Okta 1. Log in to your Okta Admin Console 2. Navigate to Applications > Applications 3. Click "Create App Integration" 4. Select "SAML 2.0" as the Sign-in method 5. Configure the following settings: - **Single Sign-on URL**: Your Better Auth ACS endpoint (e.g., `http://localhost:3000/api/auth/sso/saml2/sp/acs/sso`). while `sso` being your providerId - **Audience URI (SP Entity ID)**: Your Better Auth metadata URL (e.g., `http://localhost:3000/api/auth/sso/saml2/sp/metadata`) - **Name ID format**: Email Address or any of your choice. 6. Download the IdP metadata XML file and certificate ### Step 2: Configure Better Auth Here’s an example configuration for Okta in a dev environment: ```typescript const ssoConfig = { defaultSSO: [{ domain: "localhost:3000", // Your domain providerId: "sso", samlConfig: { // SP Configuration issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata", entryPoint: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml", callbackUrl: "/dashboard", // Redirect after successful authentication // IdP Configuration idpMetadata: { entityID: "https://trial-1076874.okta.com/app/exktofb0a62hqLAUL697/sso/saml/metadata", singleSignOnService: [{ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Location: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml" }], cert: `-----BEGIN CERTIFICATE----- MIIDqjCCApKgAwIBAgIGAZhVGMeUMA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG ... [Your Okta Certificate] ... -----END CERTIFICATE-----` }, // SP Metadata spMetadata: { metadata: `<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/api/sso/saml2/sp/metadata"> ... [Your SP Metadata XML] ... </md:EntityDescriptor>` } } }] } ``` ### Step 3: Multiple Default Providers (Optional) You can configure multiple SAML providers for different domains: ```typescript const ssoConfig = { defaultSSO: [ { domain: "company.com", providerId: "company-okta", samlConfig: { // Okta SAML configuration for company.com } }, { domain: "partner.com", providerId: "partner-adfs", samlConfig: { // ADFS SAML configuration for partner.com } }, { domain: "contractor.org", providerId: "contractor-azure", samlConfig: { // Azure AD SAML configuration for contractor.org } } ] } ``` <Callout type="info"> **Explicit**: Pass providerId directly when signing in. **Domain fallback:** Matches based on the user’s email domain. e.g. [email protected] → matches `company-okta` provider. </Callout> ### Step 4: Initiating Sign-In You can start an SSO flow in three ways: **1. Explicitly by `providerId` (recommended):** ```typescript // Explicitly specify which provider to use await authClient.signIn.sso({ providerId: "company-okta", callbackURL: "/dashboard" }); ``` **2. By email domain matching:** ```typescript // Automatically matches provider based on email domain await authClient.signIn.sso({ email: "[email protected]", callbackURL: "/dashboard" }); ``` **3. By specifying domain:** ```typescript // Explicitly specify domain for matching await authClient.signIn.sso({ domain: "partner.com", callbackURL: "/dashboard" }); ``` **Important Notes**: - DummyIDP should ONLY be used for development and testing - Never use these certificates in production - The example uses `localhost:3000` - adjust URLs for your environment - For production, always use proper IdP providers like Okta, Azure AD, or OneLogin ### Step 5: Dynamically Registering SAML Providers For dynamic registration, you should register SAML providers using the API. See the [SSO Plugin Documentation](/docs/plugins/sso#register-a-saml-provider) for detailed registration instructions. Example registration: ```typescript await authClient.sso.register({ providerId: "okta-prod", issuer: "https://your-domain.com", domain: "your-domain.com", samlConfig: { // Your production SAML configuration } }); ``` ## Additional Resources - [SSO Plugin Documentation](/docs/plugins/sso) - [Okta SAML Documentation](https://developer.okta.com/docs/concepts/saml/) - [SAML 2.0 Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf) ``` -------------------------------------------------------------------------------- /packages/cli/src/commands/generate.ts: -------------------------------------------------------------------------------- ```typescript import { Command } from "commander"; import { getConfig } from "../utils/get-config"; import * as z from "zod/v4"; import { existsSync } from "fs"; import path from "path"; import { logger, createTelemetry, getTelemetryAuthConfig } from "better-auth"; import yoctoSpinner from "yocto-spinner"; import prompts from "prompts"; import fs from "fs/promises"; import chalk from "chalk"; import { getAdapter } from "better-auth/db"; import { generateSchema } from "../generators"; export async function generateAction(opts: any) { const options = z .object({ cwd: z.string(), config: z.string().optional(), output: z.string().optional(), y: z.boolean().optional(), yes: z.boolean().optional(), }) .parse(opts); const cwd = path.resolve(options.cwd); if (!existsSync(cwd)) { logger.error(`The directory "${cwd}" does not exist.`); process.exit(1); } const config = await getConfig({ cwd, configPath: options.config, }); if (!config) { logger.error( "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.", ); return; } const adapter = await getAdapter(config).catch((e) => { logger.error(e.message); process.exit(1); }); const spinner = yoctoSpinner({ text: "preparing schema..." }).start(); const schema = await generateSchema({ adapter, file: options.output, options: config, }); spinner.stop(); if (!schema.code) { logger.info("Your schema is already up to date."); // telemetry: track generate attempted, no changes try { const telemetry = await createTelemetry(config); await telemetry.publish({ type: "cli_generate", payload: { outcome: "no_changes", config: getTelemetryAuthConfig(config, { adapter: adapter.id, database: typeof config.database === "function" ? "adapter" : "kysely", }), }, }); } catch {} process.exit(0); } if (schema.overwrite) { let confirm = options.y || options.yes; if (!confirm) { const response = await prompts({ type: "confirm", name: "confirm", message: `The file ${ schema.fileName } already exists. Do you want to ${chalk.yellow( `${schema.overwrite ? "overwrite" : "append"}`, )} the schema to the file?`, }); confirm = response.confirm; } if (confirm) { const exist = existsSync(path.join(cwd, schema.fileName)); if (!exist) { await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true, }); } if (schema.overwrite) { await fs.writeFile(path.join(cwd, schema.fileName), schema.code); } else { await fs.appendFile(path.join(cwd, schema.fileName), schema.code); } logger.success( `🚀 Schema was ${ schema.overwrite ? "overwritten" : "appended" } successfully!`, ); // telemetry: track generate success overwrite/append try { const telemetry = await createTelemetry(config); await telemetry.publish({ type: "cli_generate", payload: { outcome: schema.overwrite ? "overwritten" : "appended", config: getTelemetryAuthConfig(config), }, }); } catch {} process.exit(0); } else { logger.error("Schema generation aborted."); // telemetry: track generate aborted try { const telemetry = await createTelemetry(config); await telemetry.publish({ type: "cli_generate", payload: { outcome: "aborted", config: getTelemetryAuthConfig(config), }, }); } catch {} process.exit(1); } } if (options.y) { console.warn("WARNING: --y is deprecated. Consider -y or --yes"); options.yes = true; } let confirm = options.yes; if (!confirm) { const response = await prompts({ type: "confirm", name: "confirm", message: `Do you want to generate the schema to ${chalk.yellow( schema.fileName, )}?`, }); confirm = response.confirm; } if (!confirm) { logger.error("Schema generation aborted."); // telemetry: track generate aborted before write try { const telemetry = await createTelemetry(config); await telemetry.publish({ type: "cli_generate", payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) }, }); } catch {} process.exit(1); } if (!options.output) { const dirExist = existsSync(path.dirname(path.join(cwd, schema.fileName))); if (!dirExist) { await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true, }); } } await fs.writeFile( options.output || path.join(cwd, schema.fileName), schema.code, ); logger.success(`🚀 Schema was generated successfully!`); // telemetry: track generate success try { const telemetry = await createTelemetry(config); await telemetry.publish({ type: "cli_generate", payload: { outcome: "generated", config: getTelemetryAuthConfig(config) }, }); } catch {} process.exit(0); } export const generate = new Command("generate") .option( "-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd(), ) .option( "--config <config>", "the path to the configuration file. defaults to the first configuration file found.", ) .option("--output <output>", "the file to output to the generated schema") .option("-y, --yes", "automatically answer yes to all prompts", false) .option("--y", "(deprecated) same as --yes", false) .action(generateAction); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/select.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SelectPrimitive from "@radix-ui/react-select"; import { Check, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; const Select = SelectPrimitive.Root; const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> >(({ className, children, ...props }, ref) => ( <SelectPrimitive.Trigger ref={ref} className={cn( "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} > {children} <SelectPrimitive.Icon asChild> <ChevronsUpDown className="size-4 opacity-50" /> </SelectPrimitive.Icon> </SelectPrimitive.Trigger> )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> >(({ className, ...props }, ref) => ( <SelectPrimitive.ScrollUpButton ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", className, )} {...props} > <ChevronUp className="h-4 w-4" /> </SelectPrimitive.ScrollUpButton> )); SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> >(({ className, ...props }, ref) => ( <SelectPrimitive.ScrollDownButton ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", className, )} {...props} > <ChevronDown className="h-4 w-4" /> </SelectPrimitive.ScrollDownButton> )); SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Content>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> >(({ className, children, position = "popper", ...props }, ref) => ( <SelectPrimitive.Portal> <SelectPrimitive.Content ref={ref} className={cn( "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className, )} position={position} {...props} > <SelectScrollUpButton /> <SelectPrimitive.Viewport className={cn( "p-1", position === "popper" && "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)", )} > {children} </SelectPrimitive.Viewport> <SelectScrollDownButton /> </SelectPrimitive.Content> </SelectPrimitive.Portal> )); SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> >(({ className, ...props }, ref) => ( <SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} /> )); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> >(({ className, children, ...props }, ref) => ( <SelectPrimitive.Item ref={ref} className={cn( "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", className, )} {...props} > <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> <SelectPrimitive.ItemIndicator> <Check className="h-4 w-4" /> </SelectPrimitive.ItemIndicator> </span> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> </SelectPrimitive.Item> )); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> >(({ className, ...props }, ref) => ( <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/last-login-method/custom-prefix.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { lastLoginMethod } from "."; import { lastLoginMethodClient } from "./client"; import { parseCookies } from "../../cookies"; describe("lastLoginMethod custom cookie prefix", async () => { it("should work with default cookie name regardless of custom prefix", async () => { const { client, cookieSetter, testUser } = await getTestInstance( { advanced: { cookiePrefix: "custom-auth", }, plugins: [lastLoginMethod()], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess(context) { cookieSetter(headers)(context); }, }, ); const cookies = parseCookies(headers.get("cookie") || ""); // Uses exact cookie name from config, not affected by cookiePrefix expect(cookies.get("better-auth.last_used_login_method")).toBe("email"); }); it("should work with custom cookie name and prefix", async () => { const { client, cookieSetter, testUser } = await getTestInstance( { advanced: { cookiePrefix: "my-app", }, plugins: [lastLoginMethod({ cookieName: "my-app.last_method" })], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess(context) { cookieSetter(headers)(context); }, }, ); const cookies = parseCookies(headers.get("cookie") || ""); expect(cookies.get("my-app.last_method")).toBe("email"); }); it("should work with custom cookie name regardless of prefix", async () => { const { client, cookieSetter, testUser } = await getTestInstance( { advanced: { cookiePrefix: "my-app", }, plugins: [lastLoginMethod({ cookieName: "last_login_method" })], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); const headers = new Headers(); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onSuccess(context) { cookieSetter(headers)(context); }, }, ); const cookies = parseCookies(headers.get("cookie") || ""); // Uses exact cookie name from config, not affected by cookiePrefix expect(cookies.get("last_login_method")).toBe("email"); }); it("should work with cross-subdomain and custom prefix", async () => { const { client, testUser } = await getTestInstance( { baseURL: "https://auth.example.com", advanced: { cookiePrefix: "custom-auth", crossSubDomainCookies: { enabled: true, domain: "example.com", }, }, plugins: [lastLoginMethod()], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("Domain=example.com"); expect(setCookie).toContain("SameSite=Lax"); // Uses exact cookie name from config, not affected by cookiePrefix expect(setCookie).toContain( "better-auth.last_used_login_method=email", ); }, }, ); }); it("should work with cross-origin cookies", async () => { const { client, testUser } = await getTestInstance( { baseURL: "https://api.example.com", advanced: { crossOriginCookies: { enabled: true, }, defaultCookieAttributes: { sameSite: "none", secure: true, }, }, plugins: [lastLoginMethod()], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("SameSite=None"); expect(setCookie).toContain("Secure"); // Should not contain Domain attribute for cross-origin expect(setCookie).not.toContain("Domain="); expect(setCookie).toContain( "better-auth.last_used_login_method=email", ); }, }, ); }); it("should handle cross-origin on localhost for development", async () => { const { client, testUser } = await getTestInstance( { baseURL: "http://localhost:3000", advanced: { crossOriginCookies: { enabled: true, allowLocalhostUnsecure: true, }, defaultCookieAttributes: { sameSite: "none", secure: false, }, }, plugins: [lastLoginMethod()], }, { clientOptions: { plugins: [lastLoginMethodClient()], }, }, ); await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookie = context.response.headers.get("set-cookie"); expect(setCookie).toContain("SameSite=None"); // Should not contain Secure on localhost when allowLocalhostUnsecure is true expect(setCookie).not.toContain("Secure"); expect(setCookie).toContain( "better-auth.last_used_login_method=email", ); }, }, ); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/dodopayments.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Dodo Payments description: Better Auth Plugin for Dodo Payments --- [Dodo Payments](https://dodopayments.com) is a global Merchant-of-Record platform that lets AI, SaaS and digital businesses sell in 150+ countries without touching tax, fraud, or compliance. A single, developer-friendly API powers checkout, billing, and payouts so you can launch worldwide in minutes. <Card href="https://discord.gg/bYqAp4ayYh" title="Get support on Dodo Payments' Discord" > This plugin is maintained by the Dodo Payments team.<br /> Have questions? Our team is available on Discord to assist you anytime. </Card> ## Features - Automatic customer creation on sign-up - Type-safe checkout flows with product slug mapping - Self-service customer portal - Real-time webhook event processing with signature verification <Card href="https://app.dodopayments.com" title="Get started with Dodo Payments"> You need a Dodo Payments account and API keys to use this integration. </Card> ## Installation <Steps> <Step title="Install dependencies"> Run the following command in your project root: ```bash npm install @dodopayments/better-auth dodopayments better-auth zod ``` </Step> <Step title="Configure environment variables"> Add these to your `.env` file: ```txt DODO_PAYMENTS_API_KEY=your_api_key_here DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here ``` </Step> <Step title="Set up server-side integration"> Create or update `src/lib/auth.ts`: ```typescript import { betterAuth } from "better-auth"; import { dodopayments, checkout, portal, webhooks, } from "@dodopayments/better-auth"; import DodoPayments from "dodopayments"; export const dodoPayments = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, environment: "test_mode" }); export const auth = betterAuth({ plugins: [ dodopayments({ client: dodoPayments, createCustomerOnSignUp: true, use: [ checkout({ products: [ { productId: "pdt_xxxxxxxxxxxxxxxxxxxxx", slug: "premium-plan", }, ], successUrl: "/dashboard/success", authenticatedUsersOnly: true, }), portal(), webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, onPayload: async (payload) => { console.log("Received webhook:", payload.event_type); }, }), ], }), ], }); ``` <Card> Set `environment` to `live_mode` for production. </Card> </Step> <Step title="Set up client-side integration"> Create or update `src/lib/auth-client.ts`: ```typescript import { dodopaymentsClient } from "@dodopayments/better-auth"; export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", plugins: [dodopaymentsClient()], }); ``` </Step> </Steps> ## Usage ### Creating a Checkout Session ```typescript const { data: checkout, error } = await authClient.dodopayments.checkout({ slug: "premium-plan", customer: { email: "[email protected]", name: "John Doe", }, billing: { city: "San Francisco", country: "US", state: "CA", street: "123 Market St", zipcode: "94103", }, referenceId: "order_123", }); if (checkout) { window.location.href = checkout.url; } ``` ### Accessing the Customer Portal ```typescript const { data: customerPortal, error } = await authClient.dodopayments.customer.portal(); if (customerPortal && customerPortal.redirect) { window.location.href = customerPortal.url; } ``` ### Listing Customer Data ```typescript // Get subscriptions const { data: subscriptions, error } = await authClient.dodopayments.customer.subscriptions.list({ query: { limit: 10, page: 1, active: true, }, }); // Get payment history const { data: payments, error } = await authClient.dodopayments.customer.payments.list({ query: { limit: 10, page: 1, status: "succeeded", }, }); ``` ### Webhooks <Card> The webhooks plugin processes real-time payment events from Dodo Payments with secure signature verification. The default endpoint is `/api/auth/dodopayments/webhooks`. </Card> <Steps> <Step title="Generate and set webhook secret"> Generate a webhook secret for your endpoint URL (e.g., `https://your-domain.com/api/auth/dodopayments/webhooks`) in the Dodo Payments Dashboard and set it in your .env file: ```txt DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here ``` </Step> <Step title="Handle webhook events"> Example handler: ```typescript webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, onPayload: async (payload) => { console.log("Received webhook:", payload.event_type); }, }); ``` </Step> </Steps> ## Configuration Reference ### Plugin Options - **client** (required): DodoPayments client instance - **createCustomerOnSignUp** (optional): Auto-create customers on user signup - **use** (required): Array of plugins to enable (checkout, portal, webhooks) ### Checkout Plugin Options - **products**: Array of products or async function returning products - **successUrl**: URL to redirect after successful payment - **authenticatedUsersOnly**: Require user authentication (default: false) If you encounter any issues, please refer to the [Dodo Payments documentation](https://docs.dodopayments.com) for troubleshooting steps. ``` -------------------------------------------------------------------------------- /docs/components/ui/select.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SelectPrimitive from "@radix-ui/react-select"; import { Check, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; const Select = SelectPrimitive.Root; const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> >(({ className, children, ...props }, ref) => ( <SelectPrimitive.Trigger ref={ref} className={cn( "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} > {children} <SelectPrimitive.Icon asChild> <ChevronsUpDown className="size-4 opacity-50" /> </SelectPrimitive.Icon> </SelectPrimitive.Trigger> )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> >(({ className, ...props }, ref) => ( <SelectPrimitive.ScrollUpButton ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", className, )} {...props} > <ChevronUp className="h-4 w-4" /> </SelectPrimitive.ScrollUpButton> )); SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> >(({ className, ...props }, ref) => ( <SelectPrimitive.ScrollDownButton ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", className, )} {...props} > <ChevronDown className="h-4 w-4" /> </SelectPrimitive.ScrollDownButton> )); SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Content>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> >(({ className, children, position = "popper", ...props }, ref) => ( <SelectPrimitive.Portal> <SelectPrimitive.Content ref={ref} className={cn( "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className, )} position={position} {...props} > <SelectScrollUpButton /> <SelectPrimitive.Viewport className={cn( "p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", )} > {children} </SelectPrimitive.Viewport> <SelectScrollDownButton /> </SelectPrimitive.Content> </SelectPrimitive.Portal> )); SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> >(({ className, ...props }, ref) => ( <SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} /> )); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> >(({ className, children, ...props }, ref) => ( <SelectPrimitive.Item ref={ref} className={cn( "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className, )} {...props} > <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> <SelectPrimitive.ItemIndicator> <Check className="h-4 w-4" /> </SelectPrimitive.ItemIndicator> </span> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> </SelectPrimitive.Item> )); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> >(({ className, ...props }, ref) => ( <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/rate-limiter/index.ts: -------------------------------------------------------------------------------- ```typescript import type { RateLimit } from "../../types"; import { safeJSONParse } from "../../utils/json"; import { getIp } from "../../utils/get-request-ip"; import { wildcardMatch } from "../../utils/wildcard"; import type { AuthContext } from "@better-auth/core"; function shouldRateLimit( max: number, window: number, rateLimitData: RateLimit, ) { const now = Date.now(); const windowInMs = window * 1000; const timeSinceLastRequest = now - rateLimitData.lastRequest; return timeSinceLastRequest < windowInMs && rateLimitData.count >= max; } function rateLimitResponse(retryAfter: number) { return new Response( JSON.stringify({ message: "Too many requests. Please try again later.", }), { status: 429, statusText: "Too Many Requests", headers: { "X-Retry-After": retryAfter.toString(), }, }, ); } function getRetryAfter(lastRequest: number, window: number) { const now = Date.now(); const windowInMs = window * 1000; return Math.ceil((lastRequest + windowInMs - now) / 1000); } function createDBStorage(ctx: AuthContext) { const model = "rateLimit"; const db = ctx.adapter; return { get: async (key: string) => { const res = await db.findMany<RateLimit>({ model, where: [{ field: "key", value: key }], }); const data = res[0]; if (typeof data?.lastRequest === "bigint") { data.lastRequest = Number(data.lastRequest); } return data; }, set: async (key: string, value: RateLimit, _update?: boolean) => { try { if (_update) { await db.updateMany({ model, where: [{ field: "key", value: key }], update: { count: value.count, lastRequest: value.lastRequest, }, }); } else { await db.create({ model, data: { key, count: value.count, lastRequest: value.lastRequest, }, }); } } catch (e) { ctx.logger.error("Error setting rate limit", e); } }, }; } const memory = new Map<string, RateLimit>(); export function getRateLimitStorage( ctx: AuthContext, rateLimitSettings?: { window?: number; }, ) { if (ctx.options.rateLimit?.customStorage) { return ctx.options.rateLimit.customStorage; } const storage = ctx.rateLimit.storage; if (storage === "secondary-storage") { return { get: async (key: string) => { const data = await ctx.options.secondaryStorage?.get(key); return data ? safeJSONParse<RateLimit>(data) : undefined; }, set: async (key: string, value: RateLimit, _update?: boolean) => { const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10; await ctx.options.secondaryStorage?.set?.( key, JSON.stringify(value), ttl, ); }, }; } else if (storage === "memory") { return { async get(key: string) { return memory.get(key); }, async set(key: string, value: RateLimit, _update?: boolean) { memory.set(key, value); }, }; } return createDBStorage(ctx); } export async function onRequestRateLimit(req: Request, ctx: AuthContext) { if (!ctx.rateLimit.enabled) { return; } const path = new URL(req.url).pathname.replace( ctx.options.basePath || "/api/auth", "", ); let window = ctx.rateLimit.window; let max = ctx.rateLimit.max; const ip = getIp(req, ctx.options); if (!ip) { return; } const key = ip + path; const specialRules = getDefaultSpecialRules(); const specialRule = specialRules.find((rule) => rule.pathMatcher(path)); if (specialRule) { window = specialRule.window; max = specialRule.max; } for (const plugin of ctx.options.plugins || []) { if (plugin.rateLimit) { const matchedRule = plugin.rateLimit.find((rule) => rule.pathMatcher(path), ); if (matchedRule) { window = matchedRule.window; max = matchedRule.max; break; } } } if (ctx.rateLimit.customRules) { const _path = Object.keys(ctx.rateLimit.customRules).find((p) => { if (p.includes("*")) { const isMatch = wildcardMatch(p)(path); return isMatch; } return p === path; }); if (_path) { const customRule = ctx.rateLimit.customRules[_path]; const resolved = typeof customRule === "function" ? await customRule(req) : customRule; if (resolved) { window = resolved.window; max = resolved.max; } if (resolved === false) { return; } } } const storage = getRateLimitStorage(ctx, { window, }); const data = await storage.get(key); const now = Date.now(); if (!data) { await storage.set(key, { key, count: 1, lastRequest: now, }); } else { const timeSinceLastRequest = now - data.lastRequest; if (shouldRateLimit(max, window, data)) { const retryAfter = getRetryAfter(data.lastRequest, window); return rateLimitResponse(retryAfter); } else if (timeSinceLastRequest > window * 1000) { // Reset the count if the window has passed since the last request await storage.set( key, { ...data, count: 1, lastRequest: now, }, true, ); } else { await storage.set( key, { ...data, count: data.count + 1, lastRequest: now, }, true, ); } } } function getDefaultSpecialRules() { const specialRules = [ { pathMatcher(path: string) { return ( path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email") ); }, window: 10, max: 3, }, ]; return specialRules; } ``` -------------------------------------------------------------------------------- /docs/app/changelogs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- ```typescript import { changelogs } from "@/lib/source"; import { notFound } from "next/navigation"; import { absoluteUrl, formatDate } from "@/lib/utils"; import DatabaseTable from "@/components/mdx/database-tables"; import { cn } from "@/lib/utils"; import { Step, Steps } from "fumadocs-ui/components/steps"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { GenerateSecret } from "@/components/generate-secret"; import { AnimatePresence } from "@/components/ui/fade-in"; import { TypeTable } from "fumadocs-ui/components/type-table"; import { Features } from "@/components/blocks/features"; import { ForkButton } from "@/components/fork-button"; import Link from "next/link"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { File, Folder, Files } from "fumadocs-ui/components/files"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Pre } from "fumadocs-ui/components/codeblock"; import ChangelogPage, { Glow } from "../_components/default-changelog"; import { IconLink } from "../_components/changelog-layout"; import { XIcon } from "../_components/icons"; import { StarField } from "../_components/stat-field"; import { GridPatterns } from "../_components/grid-pattern"; import { Callout } from "@/components/ui/callout"; const metaTitle = "Changelogs"; const metaDescription = "Latest changes , fixes and updates."; const ogImage = "https://better-auth.com/release-og/changelog-og.png"; export default async function Page({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; const page = changelogs.getPage(slug); if (!slug) { return <ChangelogPage />; } if (!page) { notFound(); } const MDX = page.data?.body; const toc = page.data?.toc; const { title, description, date } = page.data; return ( <div className="md:grid md:grid-cols-2 items-start"> <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"> <StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" /> <Glow /> <GridPatterns /> <div className="z-20 flex flex-col md:justify-center max-w-xl mx-auto h-full"> <div className="mt-14 mb-2 text-gray-600 dark:text-gray-300 flex items-center gap-x-1"> <p className="text-[12px] uppercase font-mono"> {formatDate(date)} </p> </div> <h1 className=" font-sans mb-2 font-semibold tracking-tighter text-5xl"> {title}{" "} </h1> <p className="text-sm text-gray-600 mb-2 dark:text-gray-300"> {description} </p> <hr className="mt-4" /> <p className="absolute bottom-10 text-[0.8125rem]/6 text-gray-500"> <IconLink href="https://x.com/better_auth" icon={XIcon} compact> BETTER-AUTH. </IconLink> </p> </div> </div> <div className="px-4 relative md:px-8 pb-12 md:py-12"> <div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div> <div className="prose pt-8 md:pt-0"> <MDX components={{ ...defaultMdxComponents, Link: ({ className, ...props }: React.ComponentProps<typeof Link>) => ( <Link className={cn( "font-medium underline underline-offset-4", className, )} {...props} /> ), Step, Steps, File, Folder, Files, Tab, Tabs, Pre: Pre, GenerateSecret, AnimatePresence, TypeTable, Features, ForkButton, DatabaseTable, Accordion, Accordions, Callout: ({ children, type, ...props }: { children: React.ReactNode; type?: "info" | "warn" | "error" | "success" | "warning"; [key: string]: any; }) => ( <Callout type={type} {...props}> {children} </Callout> ), }} /> </div> </div> </div> ); } export async function generateMetadata({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; if (!slug) { return { metadataBase: new URL("https://better-auth.com/changelogs"), title: metaTitle, description: metaDescription, openGraph: { title: metaTitle, description: metaDescription, images: [ { url: ogImage, }, ], url: "https://better-auth.com/changelogs", }, twitter: { card: "summary_large_image", title: metaTitle, description: metaDescription, images: [ogImage], }, }; } const page = changelogs.getPage(slug); if (page == null) notFound(); const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL; const url = new URL(`${baseUrl}/release-og/${slug.join("")}.png`); const { title, description } = page.data; return { title, description, openGraph: { title, description, type: "website", url: absoluteUrl(`changelogs/${slug.join("")}`), images: [ { url: url.toString(), width: 1200, height: 630, alt: title, }, ], }, twitter: { card: "summary_large_image", title, description, images: [url.toString()], }, }; } export function generateStaticParams() { return changelogs.generateParams(); } ``` -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- ```typescript import { source } from "@/lib/source"; import { DocsPage, DocsBody, DocsTitle } from "@/components/docs/page"; import { notFound } from "next/navigation"; import { absoluteUrl } from "@/lib/utils"; import DatabaseTable from "@/components/mdx/database-tables"; import { cn } from "@/lib/utils"; import { Step, Steps } from "fumadocs-ui/components/steps"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { GenerateSecret } from "@/components/generate-secret"; import { AnimatePresence } from "@/components/ui/fade-in"; import { TypeTable } from "fumadocs-ui/components/type-table"; import { Features } from "@/components/blocks/features"; import { ForkButton } from "@/components/fork-button"; import Link from "next/link"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { CodeBlock, Pre, CodeBlockTab, CodeBlockTabsList, CodeBlockTabs, } from "@/components/ui/code-block"; import { File, Folder, Files } from "fumadocs-ui/components/files"; import { AutoTypeTable } from "fumadocs-typescript/ui"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Endpoint } from "@/components/endpoint"; import { DividerText } from "@/components/divider-text"; import { APIMethod } from "@/components/api-method"; import { LLMCopyButton, ViewOptions } from "./page.client"; import { GenerateAppleJwt } from "@/components/generate-apple-jwt"; import { Callout } from "@/components/ui/callout"; import { AddToCursor } from "@/components/mdx/add-to-cursor"; export default async function Page({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; const page = source.getPage(slug); if (!page) { notFound(); } const MDX = page.data.body; const avoidLLMHeader = ["Introduction", "Comparison"]; return ( <DocsPage toc={page.data.toc} full={page.data.full} editOnGithub={{ owner: "better-auth", repo: "better-auth", sha: process.env.VERCEL_GIT_COMMIT_SHA || "main", path: `/docs/content/docs/${page.path}`, }} tableOfContent={{ header: <div className="w-10 h-4"></div>, }} > <DocsTitle>{page.data.title}</DocsTitle> {!avoidLLMHeader.includes(page.data.title) && ( <div className="flex flex-row gap-2 items-center pb-3 border-b"> <LLMCopyButton /> <ViewOptions markdownUrl={`${page.url}.mdx`} githubUrl={`https://github.com/better-auth/better-auth/blob/main/docs/content/docs/${page.file.path}`} /> </div> )} <DocsBody> <MDX components={{ ...defaultMdxComponents, CodeBlockTabs: (props) => { return ( <CodeBlockTabs {...props} className="p-0 border-0 rounded-lg bg-fd-secondary" > <div {...props}>{props.children}</div> </CodeBlockTabs> ); }, CodeBlockTabsList: (props) => { return ( <CodeBlockTabsList {...props} className="pb-0 my-0 rounded-lg bg-fd-secondary" /> ); }, CodeBlockTab: (props) => { return <CodeBlockTab {...props} className="p-0 m-0 rounded-lg" />; }, pre: (props) => { return ( <CodeBlock className="rounded-xl bg-fd-muted" {...props}> <div style={{ minWidth: "100%", display: "table" }}> <Pre className="px-0 py-3 bg-fd-muted focus-visible:outline-none"> {props.children} </Pre> </div> </CodeBlock> ); }, Link: ({ className, ...props }: React.ComponentProps<typeof Link>) => ( <Link className={cn( "font-medium underline underline-offset-4", className, )} {...props} /> ), Step, Steps, File, Folder, Files, Tab, Tabs, AutoTypeTable, GenerateSecret, GenerateAppleJwt, AnimatePresence, TypeTable, Features, ForkButton, AddToCursor, DatabaseTable, Accordion, Accordions, Endpoint, APIMethod, Callout: ({ children, type, ...props }: { children: React.ReactNode; type?: "info" | "warn" | "error" | "success" | "warning"; [key: string]: any; }) => ( <Callout type={type} {...props}> {children} </Callout> ), DividerText, iframe: (props) => ( <iframe {...props} className="w-full h-[500px]" /> ), }} /> </DocsBody> </DocsPage> ); } export async function generateStaticParams() { return source.generateParams(); } export async function generateMetadata({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; const page = source.getPage(slug); if (page == null) notFound(); const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL; const url = new URL(`${baseUrl}/api/og`); const { title, description } = page.data; const pageSlug = page.file.path; url.searchParams.set("type", "Documentation"); url.searchParams.set("mode", "dark"); url.searchParams.set("heading", `${title}`); return { title, description, openGraph: { title, description, type: "website", url: absoluteUrl(`docs/${pageSlug}`), images: [ { url: url.toString(), width: 1200, height: 630, alt: title, }, ], }, twitter: { card: "summary_large_image", title, description, images: [url.toString()], }, }; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/passkey/client.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterFetch, BetterFetchOption } from "@better-fetch/fetch"; import { WebAuthnError, startAuthentication, startRegistration, } from "@simplewebauthn/browser"; import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, } from "@simplewebauthn/browser"; import type { User, Session } from "../../types"; import type { passkey as passkeyPl, Passkey } from "."; import type { BetterAuthClientPlugin, ClientStore } from "@better-auth/core"; import { useAuthQuery } from "../../client"; import { atom } from "nanostores"; export const getPasskeyActions = ( $fetch: BetterFetch, { $listPasskeys, $store, }: { $listPasskeys: ReturnType<typeof atom<any>>; $store: ClientStore; }, ) => { const signInPasskey = async ( opts?: { autoFill?: boolean; fetchOptions?: BetterFetchOption; }, options?: BetterFetchOption, ) => { const response = await $fetch<PublicKeyCredentialRequestOptionsJSON>( "/passkey/generate-authenticate-options", { method: "POST", throw: false, }, ); if (!response.data) { return response; } try { const res = await startAuthentication({ optionsJSON: response.data, useBrowserAutofill: opts?.autoFill, }); const verified = await $fetch<{ session: Session; user: User; }>("/passkey/verify-authentication", { body: { response: res, }, ...opts?.fetchOptions, ...options, method: "POST", throw: false, }); $listPasskeys.set(Math.random()); $store.notify("$sessionSignal"); return verified; } catch (e) { return { data: null, error: { code: "AUTH_CANCELLED", message: "auth cancelled", status: 400, statusText: "BAD_REQUEST", }, }; } }; const registerPasskey = async ( opts?: { fetchOptions?: BetterFetchOption; /** * The name of the passkey. This is used to * identify the passkey in the UI. */ name?: string; /** * The type of attachment for the passkey. Defaults to both * platform and cross-platform allowed, with platform preferred. */ authenticatorAttachment?: "platform" | "cross-platform"; /** * Try to silently create a passkey with the password manager that the user just signed * in with. * @default false */ useAutoRegister?: boolean; }, fetchOpts?: BetterFetchOption, ) => { const options = await $fetch<PublicKeyCredentialCreationOptionsJSON>( "/passkey/generate-register-options", { method: "GET", query: { ...(opts?.authenticatorAttachment && { authenticatorAttachment: opts.authenticatorAttachment, }), ...(opts?.name && { name: opts.name, }), }, throw: false, }, ); if (!options.data) { return options; } try { const res = await startRegistration({ optionsJSON: options.data, useAutoRegister: opts?.useAutoRegister, }); const verified = await $fetch<{ passkey: Passkey; }>("/passkey/verify-registration", { ...opts?.fetchOptions, ...fetchOpts, body: { response: res, name: opts?.name, }, method: "POST", throw: false, }); if (!verified.data) { return verified; } $listPasskeys.set(Math.random()); } catch (e) { if (e instanceof WebAuthnError) { if (e.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") { return { data: null, error: { code: e.code, message: "previously registered", status: 400, statusText: "BAD_REQUEST", }, }; } if (e.code === "ERROR_CEREMONY_ABORTED") { return { data: null, error: { code: e.code, message: "registration cancelled", status: 400, statusText: "BAD_REQUEST", }, }; } return { data: null, error: { code: e.code, message: e.message, status: 400, statusText: "BAD_REQUEST", }, }; } return { data: null, error: { code: "UNKNOWN_ERROR", message: e instanceof Error ? e.message : "unknown error", status: 500, statusText: "INTERNAL_SERVER_ERROR", }, }; } }; return { signIn: { /** * Sign in with a registered passkey */ passkey: signInPasskey, }, passkey: { /** * Add a passkey to the user account */ addPasskey: registerPasskey, }, /** * Inferred Internal Types */ $Infer: {} as { Passkey: Passkey; }, }; }; export const passkeyClient = () => { const $listPasskeys = atom<any>(); return { id: "passkey", $InferServerPlugin: {} as ReturnType<typeof passkeyPl>, getActions: ($fetch, $store) => getPasskeyActions($fetch, { $listPasskeys, $store, }), getAtoms($fetch) { const listPasskeys = useAuthQuery<Passkey[]>( $listPasskeys, "/passkey/list-user-passkeys", $fetch, { method: "GET", }, ); return { listPasskeys, $listPasskeys, }; }, pathMethods: { "/passkey/register": "POST", "/passkey/authenticate": "POST", }, atomListeners: [ { matcher(path) { return ( path === "/passkey/verify-registration" || path === "/passkey/delete-passkey" || path === "/passkey/update-passkey" || path === "/sign-out" ); }, signal: "$listPasskeys", }, { matcher: (path) => path === "/passkey/verify-authentication", signal: "$sessionSignal", }, ], } satisfies BetterAuthClientPlugin; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/one-tap/client.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterFetchOption } from "@better-fetch/fetch"; import type { BetterAuthClientPlugin } from "@better-auth/core"; declare global { interface Window { google?: { accounts: { id: { initialize: (config: any) => void; prompt: (callback?: (notification: any) => void) => void; }; }; }; googleScriptInitialized?: boolean; } } export interface GoogleOneTapOptions { /** * Google client ID */ clientId: string; /** * Auto select the account if the user is already signed in */ autoSelect?: boolean; /** * Cancel the flow when the user taps outside the prompt */ cancelOnTapOutside?: boolean; /** * The mode to use for the Google One Tap flow * * popup: Use a popup window * redirect: Redirect the user to the Google One Tap flow * * @default "popup" */ uxMode?: "popup" | "redirect"; /** * The context to use for the Google One Tap flow. See https://developers.google.com/identity/gsi/web/reference/js-reference * * @default "signin" */ context?: "signin" | "signup" | "use"; /** * Additional configuration options to pass to the Google One Tap API. */ additionalOptions?: Record<string, any>; /** * Configuration options for the prompt and exponential backoff behavior. */ promptOptions?: { /** * Base delay (in milliseconds) for exponential backoff. * @default 1000 */ baseDelay?: number; /** * Maximum number of prompt attempts before calling onPromptNotification. * @default 5 */ maxAttempts?: number; }; } export interface GoogleOneTapActionOptions extends Omit<GoogleOneTapOptions, "clientId" | "promptOptions"> { fetchOptions?: BetterFetchOption; /** * Callback URL. */ callbackURL?: string; /** * Optional callback that receives the prompt notification if (or when) the prompt is dismissed or skipped. * This lets you render an alternative UI (e.g. a Google Sign-In button) to restart the process. */ onPromptNotification?: (notification: any) => void; } let isRequestInProgress = false; export const oneTapClient = (options: GoogleOneTapOptions) => { return { id: "one-tap", getActions: ($fetch, _) => ({ oneTap: async ( opts?: GoogleOneTapActionOptions, fetchOptions?: BetterFetchOption, ) => { if (isRequestInProgress) { console.warn( "A Google One Tap request is already in progress. Please wait.", ); return; } isRequestInProgress = true; try { if (typeof window === "undefined" || !window.document) { console.warn( "Google One Tap is only available in browser environments", ); return; } const { autoSelect, cancelOnTapOutside, context } = opts ?? {}; const contextValue = context ?? options.context ?? "signin"; await loadGoogleScript(); await new Promise<void>((resolve, reject) => { let isResolved = false; const baseDelay = options.promptOptions?.baseDelay ?? 1000; const maxAttempts = options.promptOptions?.maxAttempts ?? 5; window.google?.accounts.id.initialize({ client_id: options.clientId, callback: async (response: { credential: string }) => { isResolved = true; try { await $fetch("/one-tap/callback", { method: "POST", body: { idToken: response.credential }, ...opts?.fetchOptions, ...fetchOptions, }); if ( (!opts?.fetchOptions && !fetchOptions) || opts?.callbackURL ) { window.location.href = opts?.callbackURL ?? "/"; } resolve(); } catch (error) { console.error("Error during One Tap callback:", error); reject(error); } }, auto_select: autoSelect, cancel_on_tap_outside: cancelOnTapOutside, context: contextValue, ...options.additionalOptions, }); const handlePrompt = (attempt: number) => { if (isResolved) return; window.google?.accounts.id.prompt((notification: any) => { if (isResolved) return; if ( notification.isDismissedMoment && notification.isDismissedMoment() ) { if (attempt < maxAttempts) { const delay = Math.pow(2, attempt) * baseDelay; setTimeout(() => handlePrompt(attempt + 1), delay); } else { opts?.onPromptNotification?.(notification); } } else if ( notification.isSkippedMoment && notification.isSkippedMoment() ) { if (attempt < maxAttempts) { const delay = Math.pow(2, attempt) * baseDelay; setTimeout(() => handlePrompt(attempt + 1), delay); } else { opts?.onPromptNotification?.(notification); } } }); }; handlePrompt(0); }); } catch (error) { console.error("Error during Google One Tap flow:", error); throw error; } finally { isRequestInProgress = false; } }, }), getAtoms($fetch) { return {}; }, } satisfies BetterAuthClientPlugin; }; const loadGoogleScript = (): Promise<void> => { return new Promise((resolve) => { if (window.googleScriptInitialized) { resolve(); return; } const script = document.createElement("script"); script.src = "https://accounts.google.com/gsi/client"; script.async = true; script.defer = true; script.onload = () => { window.googleScriptInitialized = true; resolve(); }; document.head.appendChild(script); }); }; ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/support.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import * as React from "react"; import { Card, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; export function Support() { const [open, setOpen] = React.useState(false); const [submitting, setSubmitting] = React.useState(false); const formRef = React.useRef<HTMLFormElement | null>(null); async function onSubmit(event: React.FormEvent<HTMLFormElement>) { event.preventDefault(); if (submitting) return; setSubmitting(true); const form = new FormData(event.currentTarget); const payload = { name: String(form.get("name") || ""), email: String(form.get("email") || ""), company: String(form.get("company") || ""), website: String(form.get("website") || ""), userCount: String(form.get("userCount") || ""), interest: String(form.get("interest") || ""), features: String(form.get("features") || ""), additional: String(form.get("additional") || ""), }; try { const res = await fetch("/api/support", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("Failed to submit"); setOpen(false); formRef.current?.reset(); // optionally add a toast later } catch (e) { console.error(e); // optionally add error toast } finally { setSubmitting(false); } } return ( <Card className="flex flex-col gap-3 rounded-none"> <CardHeader> <CardTitle>Dedicated Support</CardTitle> <CardDescription> We're now offering on demand support for Better Auth and Auth.js. Including help out migrations, consultations, premium dedicated support and more. If you're interested, please get in touch. </CardDescription> </CardHeader> <CardFooter> <Dialog open={open} onOpenChange={setOpen}> <div> <DialogTrigger asChild> <Button type="button" className="bg-blue-500 text-white hover:bg-blue-600 transition-colors cursor-pointer" > Request support </Button> </DialogTrigger> </div> <DialogContent> <DialogHeader> <DialogTitle>Request dedicated support</DialogTitle> <DialogDescription> Tell us about your team and what you're looking for. </DialogDescription> </DialogHeader> <form ref={formRef} className="grid gap-4" onSubmit={onSubmit}> <div className="grid gap-2"> <Label htmlFor="name">Your name</Label> <Input id="name" name="name" placeholder="Jane Doe" required /> </div> <div className="grid gap-2"> <Label htmlFor="email">Work email</Label> <Input id="email" name="email" type="email" placeholder="[email protected]" required /> </div> <div className="grid gap-2"> <Label htmlFor="company">Company</Label> <Input id="company" name="company" placeholder="Acme Inc." /> </div> <div className="grid gap-2"> <Label htmlFor="website">Website</Label> <Input id="website" name="website" placeholder="https://acme.com" /> </div> <div className="grid gap-2"> <Label htmlFor="userCount">Users</Label> <Select name="userCount"> <SelectTrigger id="userCount"> <SelectValue placeholder="Select users" /> </SelectTrigger> <SelectContent> <SelectItem value="<1k">Less than 1k</SelectItem> <SelectItem value="1k-10k">1k - 10k</SelectItem> <SelectItem value=">10k">More than 10k</SelectItem> </SelectContent> </Select> </div> <div className="grid gap-2"> <Label htmlFor="interest">What are you interested in?</Label> <Select name="interest"> <SelectTrigger id="interest"> <SelectValue placeholder="Choose a package" /> </SelectTrigger> <SelectContent> <SelectItem value="migration">Migration help</SelectItem> <SelectItem value="consultation">Consultation</SelectItem> <SelectItem value="support">Premium support</SelectItem> <SelectItem value="custom">Custom</SelectItem> </SelectContent> </Select> </div> <div className="grid gap-2"> <Label htmlFor="features"> Features or plugins of interest </Label> <Input id="features" name="features" placeholder="SAML, SIWE, WebAuthn, Organizations, ..." /> </div> <div className="grid gap-2"> <Label htmlFor="additional">Anything else?</Label> <Textarea id="additional" name="additional" placeholder="Share more context, timelines, and expectations." /> </div> <DialogFooter> <Button type="submit" disabled={submitting}> {submitting ? "Submitting..." : "Submit"} </Button> </DialogFooter> </form> </DialogContent> </Dialog> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/next-auth-migration-guide.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Migrating from NextAuth.js to Better Auth description: A step-by-step guide to transitioning from NextAuth.js to Better Auth. --- In this guide, we’ll walk through the steps to migrate a project from [NextAuth.js](https://authjs.dev/) to Better Auth, ensuring no loss of data or functionality. While this guide focuses on Next.js, it can be adapted for other frameworks as well. --- ## Before You Begin Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started. --- <Steps> <Step> ### Mapping Existing Columns Instead of altering your existing database column names, you can map them to match Better Auth's expected structure. This allows you to retain your current database schema. #### User Schema Map the following fields in the user schema: - (next-auth v4) `emailVerified`: datetime → boolean #### Session Schema Map the following fields in the session schema: - `expires` → `expiresAt` - `sessionToken` → `token` - (next-auth v4) add `createdAt` with datetime type - (next-auth v4) add `updatedAt` with datetime type ```typescript title="auth.ts" export const auth = betterAuth({ // Other configs session: { fields: { expiresAt: "expires", // Map your existing `expires` field to Better Auth's `expiresAt` token: "sessionToken" // Map your existing `sessionToken` field to Better Auth's `token` } }, }); ``` Make sure to have `createdAt` and `updatedAt` fields on your session schema. #### Account Schema Map these fields in the account schema: - (next-auth v4) `provider` → `providerId` - `providerAccountId` → `accountId` - `refresh_token` → `refreshToken` - `access_token` → `accessToken` - (next-auth v3) `access_token_expires` → `accessTokenExpiresAt` and int → datetime - (next-auth v4) `expires_at` → `accessTokenExpiresAt` and int → datetime - `id_token` → `idToken` - (next-auth v4) add `createdAt` with datetime type - (next-auth v4) add `updatedAt` with datetime type Remove the `session_state`, `type`, and `token_type` fields, as they are not required by Better Auth. ```typescript title="auth.ts" export const auth = betterAuth({ // Other configs account: { fields: { accountId: "providerAccountId", refreshToken: "refresh_token", accessToken: "access_token", accessTokenExpiresAt: "access_token_expires", idToken: "id_token", } }, }); ``` **Note:** If you use ORM adapters, you can map these fields in your schema file. **Example with Prisma:** ```prisma title="schema.prisma" model Session { id String @id @default(cuid()) expiresAt DateTime @map("expires") // Map your existing `expires` field to Better Auth's `expiresAt` token String @map("sessionToken") // Map your existing `sessionToken` field to Better Auth's `token` userId String user User @relation(fields: [userId], references: [id]) } ``` Make sure to have `createdAt` and `updatedAt` fields on your account schema. </Step> <Step> ### Update the Route Handler In the `app/api/auth` folder, rename the `[...nextauth]` file to `[...all]` to avoid confusion. Then, update the `route.ts` file as follows: ```typescript title="app/api/auth/[...all]/route.ts" import { toNextJsHandler } from "better-auth/next-js"; import { auth } from "~/server/auth"; export const { POST, GET } = toNextJsHandler(auth); ``` </Step> <Step> ### Update the Client Create a file named `auth-client.ts` in the `lib` folder. Add the following code: ```typescript title="auth-client.ts" import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ baseURL: process.env.BASE_URL! // Optional if the API base URL matches the frontend }); export const { signIn, signOut, useSession } = authClient; ``` #### Social Login Functions Update your social login functions to use Better Auth. For example, for Discord: ```typescript import { signIn } from "~/lib/auth-client"; export const signInDiscord = async () => { const data = await signIn.social({ provider: "discord" }); return data; }; ``` #### Update `useSession` Calls Replace `useSession` calls with Better Auth’s version. Example: ```typescript title="Profile.tsx" import { useSession } from "~/lib/auth-client"; export const Profile = () => { const { data } = useSession(); return ( <div> <pre> {JSON.stringify(data, null, 2)} </pre> </div> ); }; ``` </Step> <Step> ### Server-Side Session Handling Use the `auth` instance to get session data on the server: ```typescript title="actions.ts" "use server"; import { auth } from "~/server/auth"; import { headers } from "next/headers"; export const protectedAction = async () => { const session = await auth.api.getSession({ headers: await headers(), }); }; ``` </Step> <Step> ### Middleware To protect routes with middleware, refer to the [Next.js middleware guide](/docs/integrations/next#middleware). </Step> </Steps> ## Wrapping Up Congratulations! You’ve successfully migrated from NextAuth.js to Better Auth. For a complete implementation with multiple authentication methods, check out the [demo repository](https://github.com/Bekacru/t3-app-better-auth). Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential. ``` -------------------------------------------------------------------------------- /packages/better-auth/src/init.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, vi } from "vitest"; import { init } from "./init"; import Database from "better-sqlite3"; import { betterAuth } from "./auth"; import { createAuthClient } from "./client"; import { getTestInstance } from "./test-utils/test-instance"; describe("init", async () => { const database = new Database(":memory:"); it("should match config", async () => { const res = await init({ baseURL: "http://localhost:3000", database, }); expect(res).toMatchSnapshot(); }); it("should infer BASE_URL from env", async () => { vi.stubEnv("BETTER_AUTH_URL", "http://localhost:5147"); const res = await init({ database, }); expect(res.options.baseURL).toBe("http://localhost:5147"); expect(res.baseURL).toBe("http://localhost:5147/api/auth"); vi.unstubAllEnvs(); }); it("should respect base path", async () => { const res = await init({ database, basePath: "/custom-path", baseURL: "http://localhost:5147", }); expect(res.baseURL).toBe("http://localhost:5147/custom-path"); }); it("should work with base path", async () => { const { client } = await getTestInstance({ basePath: "/custom-path", }); await client.$fetch("/ok", { onSuccess: (ctx) => { expect(ctx.data).toMatchObject({ ok: true, }); }, }); }); it("should execute plugins init", async () => { const newBaseURL = "http://test.test"; const res = await init({ baseURL: "http://localhost:3000", database, plugins: [ { id: "test", init: () => { return { context: { baseURL: newBaseURL, }, }; }, }, ], }); expect(res.baseURL).toBe(newBaseURL); }); it("should work with custom path", async () => { const customPath = "/custom-path"; const ctx = await init({ database, basePath: customPath, baseURL: "http://localhost:3000", }); expect(ctx.baseURL).toBe(`http://localhost:3000${customPath}`); const res = betterAuth({ baseURL: "http://localhost:3000", database, basePath: customPath, }); const client = createAuthClient({ baseURL: `http://localhost:3000/custom-path`, fetchOptions: { customFetchImpl: async (url, init) => { return res.handler(new Request(url, init)); }, }, }); const ok = await client.$fetch("/ok"); expect(ok.data).toMatchObject({ ok: true, }); }); it("should allow plugins to set config values", async () => { const ctx = await init({ database, baseURL: "http://localhost:3000", plugins: [ { id: "test-plugin", init(ctx) { return { context: ctx, options: { emailAndPassword: { enabled: true, }, }, }; }, }, ], }); expect(ctx.options.emailAndPassword?.enabled).toBe(true); }); it("should not allow plugins to set config values if they are set in the main config", async () => { const ctx = await init({ database, baseURL: "http://localhost:3000", emailAndPassword: { enabled: false, }, plugins: [ { id: "test-plugin", init(ctx) { return { context: ctx, options: { emailAndPassword: { enabled: true, }, }, }; }, }, ], }); expect(ctx.options.emailAndPassword?.enabled).toBe(false); }); it("should properly pass modfied context from one plugin to another", async () => { const mockProvider = { id: "test-oauth-provider", name: "Test OAuth Provider", createAuthorizationURL: vi.fn(), validateAuthorizationCode: vi.fn(), refreshAccessToken: vi.fn(), getUserInfo: vi.fn(), }; const ctx = await init({ database, baseURL: "http://localhost:3000", socialProviders: { github: { clientId: "test-github-id", clientSecret: "test-github-secret", }, }, plugins: [ { id: "test-oauth-plugin", init(ctx) { return { context: { socialProviders: [mockProvider, ...ctx.socialProviders], }, }; }, }, { id: "test-oauth-plugin-2", init(ctx) { return { context: ctx, }; }, }, ], }); expect(ctx.socialProviders).toHaveLength(2); const testProvider = ctx.socialProviders.find( (p) => p.id === "test-oauth-provider", ); expect(testProvider).toBeDefined(); expect(testProvider?.refreshAccessToken).toBeDefined(); const githubProvider = ctx.socialProviders.find((p) => p.id === "github"); expect(githubProvider).toBeDefined(); }); it("should init async plugin", async () => { const initFn = vi.fn(async () => { await new Promise((r) => setTimeout(r, 100)); return { context: { baseURL: "http://async.test", }, }; }); await init({ baseURL: "http://localhost:3000", database, plugins: [ { id: "test-async", init: initFn, }, ], }); expect(initFn).toHaveBeenCalled(); }); it("handles empty basePath", async () => { const res = await init({ database, baseURL: "http://localhost:5147/", basePath: "", }); expect(res.baseURL).toBe("http://localhost:5147"); }); it("handles root basePath", async () => { const res = await init({ database, baseURL: "http://localhost:5147/", basePath: "/", }); expect(res.baseURL).toBe("http://localhost:5147"); }); it("normalizes trailing slashes with default path", async () => { const res = await init({ database, baseURL: "http://localhost:5147////", }); expect(res.baseURL).toBe("http://localhost:5147/api/auth"); }); }); ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/carousel.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"; import useEmblaCarousel, { type UseEmblaCarouselType, } from "embla-carousel-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; type CarouselApi = UseEmblaCarouselType[1]; type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; type CarouselOptions = UseCarouselParameters[0]; type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { opts?: CarouselOptions; plugins?: CarouselPlugin; orientation?: "horizontal" | "vertical"; setApi?: (api: CarouselApi) => void; }; type CarouselContextProps = { carouselRef: ReturnType<typeof useEmblaCarousel>[0]; api: ReturnType<typeof useEmblaCarousel>[1]; scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean; } & CarouselProps; const CarouselContext = React.createContext<CarouselContextProps | null>(null); function useCarousel() { const context = React.useContext(CarouselContext); if (!context) { throw new Error("useCarousel must be used within a <Carousel />"); } return context; } const Carousel = ({ ref, orientation = "horizontal", opts, setApi, plugins, className, children, ...props }) => { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, plugins, ); const [canScrollPrev, setCanScrollPrev] = React.useState(false); const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: CarouselApi) => { if (!api) { return; } setCanScrollPrev(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }, []); const scrollPrev = React.useCallback(() => { api?.scrollPrev(); }, [api]); const scrollNext = React.useCallback(() => { api?.scrollNext(); }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === "ArrowLeft") { event.preventDefault(); scrollPrev(); } else if (event.key === "ArrowRight") { event.preventDefault(); scrollNext(); } }, [scrollPrev, scrollNext], ); React.useEffect(() => { if (!api || !setApi) { return; } setApi(api); }, [api, setApi]); React.useEffect(() => { if (!api) { return; } onSelect(api); api.on("reInit", onSelect); api.on("select", onSelect); return () => { api?.off("select", onSelect); }; }, [api, onSelect]); return ( <CarouselContext.Provider value={{ carouselRef, api: api, opts, orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), scrollPrev, scrollNext, canScrollPrev, canScrollNext, }} > <div ref={ref} onKeyDownCapture={handleKeyDown} className={cn("relative", className)} role="region" aria-roledescription="carousel" {...props} > {children} </div> </CarouselContext.Provider> ); }; Carousel.displayName = "Carousel"; const CarouselContent = ({ ref, className, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref: React.RefObject<HTMLDivElement>; }) => { const { carouselRef, orientation } = useCarousel(); return ( <div ref={carouselRef} className="overflow-hidden"> <div ref={ref} className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className, )} {...props} /> </div> ); }; CarouselContent.displayName = "CarouselContent"; const CarouselItem = ({ ref, className, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref: React.RefObject<HTMLDivElement>; }) => { const { orientation } = useCarousel(); return ( <div ref={ref} role="group" aria-roledescription="slide" className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className, )} {...props} /> ); }; CarouselItem.displayName = "CarouselItem"; const CarouselPrevious = ({ ref, className, variant = "outline", size = "icon", ...props }: React.ComponentProps<typeof Button> & { ref: React.RefObject<HTMLButtonElement>; }) => { const { orientation, scrollPrev, canScrollPrev } = useCarousel(); return ( <Button ref={ref} variant={variant} size={size} className={cn( "absolute h-8 w-8 rounded-full", orientation === "horizontal" ? "-left-12 top-1/2 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > <ArrowLeftIcon className="h-4 w-4" /> <span className="sr-only">Previous slide</span> </Button> ); }; CarouselPrevious.displayName = "CarouselPrevious"; const CarouselNext = ({ ref, className, variant = "outline", size = "icon", ...props }: React.ComponentProps<typeof Button> & { ref: React.RefObject<HTMLButtonElement>; }) => { const { orientation, scrollNext, canScrollNext } = useCarousel(); return ( <Button ref={ref} variant={variant} size={size} className={cn( "absolute h-8 w-8 rounded-full", orientation === "horizontal" ? "-right-12 top-1/2 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollNext} onClick={scrollNext} {...props} > <ArrowRightIcon className="h-4 w-4" /> <span className="sr-only">Next slide</span> </Button> ); }; CarouselNext.displayName = "CarouselNext"; export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/mcp/authorize.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import { getSessionFromCtx } from "../../api"; import type { AuthorizationQuery, Client, OIDCOptions, } from "../oidc-provider/types"; import { generateRandomString } from "../../crypto"; import type { GenericEndpointContext } from "@better-auth/core"; function redirectErrorURL(url: string, error: string, description: string) { return `${ url.includes("?") ? "&" : "?" }error=${error}&error_description=${description}`; } export async function authorizeMCPOAuth( ctx: GenericEndpointContext, options: OIDCOptions, ) { ctx.setHeader("Access-Control-Allow-Origin", "*"); ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); ctx.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); ctx.setHeader("Access-Control-Max-Age", "86400"); const opts = { codeExpiresIn: 600, defaultScope: "openid", ...options, scopes: [ "openid", "profile", "email", "offline_access", ...(options?.scopes || []), ], }; if (!ctx.request) { throw new APIError("UNAUTHORIZED", { error_description: "request not found", error: "invalid_request", }); } const session = await getSessionFromCtx(ctx); if (!session) { /** * If the user is not logged in, we need to redirect them to the * login page. */ await ctx.setSignedCookie( "oidc_login_prompt", JSON.stringify(ctx.query), ctx.context.secret, { maxAge: 600, path: "/", sameSite: "lax", }, ); const queryFromURL = ctx.request.url?.split("?")[1]!; throw ctx.redirect(`${options.loginPage}?${queryFromURL}`); } const query = ctx.query as AuthorizationQuery; console.log(query); if (!query.client_id) { throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`); } if (!query.response_type) { throw ctx.redirect( redirectErrorURL( `${ctx.context.baseURL}/error`, "invalid_request", "response_type is required", ), ); } const client = await ctx.context.adapter .findOne<Record<string, any>>({ model: "oauthApplication", where: [ { field: "clientId", value: ctx.query.client_id, }, ], }) .then((res) => { if (!res) { return null; } return { ...res, redirectURLs: res.redirectURLs.split(","), metadata: res.metadata ? JSON.parse(res.metadata) : {}, } as Client; }); console.log(client); if (!client) { throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`); } const redirectURI = client.redirectURLs.find( (url) => url === ctx.query.redirect_uri, ); if (!redirectURI || !query.redirect_uri) { /** * show UI error here warning the user that the redirect URI is invalid */ throw new APIError("BAD_REQUEST", { message: "Invalid redirect URI", }); } if (client.disabled) { throw ctx.redirect(`${ctx.context.baseURL}/error?error=client_disabled`); } if (query.response_type !== "code") { throw ctx.redirect( `${ctx.context.baseURL}/error?error=unsupported_response_type`, ); } const requestScope = query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" "); const invalidScopes = requestScope.filter((scope) => { return !opts.scopes.includes(scope); }); if (invalidScopes.length) { throw ctx.redirect( redirectErrorURL( query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, ), ); } if ( (!query.code_challenge || !query.code_challenge_method) && options.requirePKCE ) { throw ctx.redirect( redirectErrorURL( query.redirect_uri, "invalid_request", "pkce is required", ), ); } if (!query.code_challenge_method) { query.code_challenge_method = "plain"; } if ( ![ "s256", options.allowPlainCodeChallengeMethod ? "plain" : "s256", ].includes(query.code_challenge_method?.toLowerCase() || "") ) { throw ctx.redirect( redirectErrorURL( query.redirect_uri, "invalid_request", "invalid code_challenge method", ), ); } const code = generateRandomString(32, "a-z", "A-Z", "0-9"); const codeExpiresInMs = opts.codeExpiresIn * 1000; const expiresAt = new Date(Date.now() + codeExpiresInMs); try { /** * Save the code in the database */ await ctx.context.internalAdapter.createVerificationValue({ value: JSON.stringify({ clientId: client.clientId, redirectURI: query.redirect_uri, scope: requestScope, userId: session.user.id, authTime: new Date(session.session.createdAt).getTime(), /** * If the prompt is set to `consent`, then we need * to require the user to consent to the scopes. * * This means the code now needs to be treated as a * consent request. * * once the user consents, the code will be updated * with the actual code. This is to prevent the * client from using the code before the user * consents. */ requireConsent: query.prompt === "consent", state: query.prompt === "consent" ? query.state : null, codeChallenge: query.code_challenge, codeChallengeMethod: query.code_challenge_method, nonce: query.nonce, }), identifier: code, expiresAt, }); } catch (e) { throw ctx.redirect( redirectErrorURL( query.redirect_uri, "server_error", "An error occurred while processing the request", ), ); } const redirectURIWithCode = new URL(redirectURI); redirectURIWithCode.searchParams.set("code", code); redirectURIWithCode.searchParams.set("state", ctx.query.state); if (query.prompt !== "consent") { throw ctx.redirect(redirectURIWithCode.toString()); } throw ctx.redirect(redirectURIWithCode.toString()); } ```