This is page 17 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/adapter-factory/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { 2 | DBFieldAttribute, 3 | BetterAuthDBSchema, 4 | } from "@better-auth/core/db"; 5 | import type { 6 | DBAdapterFactoryConfig, 7 | CustomAdapter as CoreCustomAdapter, 8 | } from "@better-auth/core/db/adapter"; 9 | import type { 10 | AdapterSchemaCreation, 11 | TransactionAdapter, 12 | Where, 13 | } from "../../types"; 14 | import type { BetterAuthOptions } from "@better-auth/core"; 15 | import type { Prettify } from "../../types/helper"; 16 | 17 | export type AdapterFactoryOptions = { 18 | config: AdapterFactoryConfig; 19 | adapter: AdapterFactoryCustomizeAdapterCreator; 20 | }; 21 | 22 | /** 23 | * @deprecated Use `DBAdapterFactoryConfig` from `@better-auth/core/db/adapter` instead. 24 | */ 25 | export interface AdapterFactoryConfig 26 | extends Omit<DBAdapterFactoryConfig<BetterAuthOptions>, "transaction"> { 27 | /** 28 | * Execute multiple operations in a transaction. 29 | * 30 | * If the database doesn't support transactions, set this to `false` and operations will be executed sequentially. 31 | * 32 | * @default false 33 | */ 34 | transaction?: 35 | | false 36 | | (<R>(callback: (trx: TransactionAdapter) => Promise<R>) => Promise<R>); 37 | } 38 | 39 | export type AdapterFactoryCustomizeAdapterCreator = (config: { 40 | options: BetterAuthOptions; 41 | /** 42 | * The schema of the user's Better-Auth instance. 43 | */ 44 | schema: BetterAuthDBSchema; 45 | /** 46 | * The debug log function. 47 | * 48 | * If the config has defined `debugLogs` as `false`, no logs will be shown. 49 | */ 50 | debugLog: (...args: any[]) => void; 51 | /** 52 | * Get the model name which is expected to be saved in the database based on the user's schema. 53 | */ 54 | getModelName: (model: string) => string; 55 | /** 56 | * Get the field name which is expected to be saved in the database based on the user's schema. 57 | */ 58 | getFieldName: ({ model, field }: { model: string; field: string }) => string; 59 | /** 60 | * This function helps us get the default model name from the schema defined by devs. 61 | * Often times, the user will be using the `modelName` which could had been customized by the users. 62 | * This function helps us get the actual model name useful to match against the schema. (eg: schema[model]) 63 | * 64 | * If it's still unclear what this does: 65 | * 66 | * 1. User can define a custom modelName. 67 | * 2. When using a custom modelName, doing something like `schema[model]` will not work. 68 | * 3. Using this function helps us get the actual model name based on the user's defined custom modelName. 69 | * 4. Thus allowing us to use `schema[model]`. 70 | */ 71 | getDefaultModelName: (model: string) => string; 72 | /** 73 | * This function helps us get the default field name from the schema defined by devs. 74 | * Often times, the user will be using the `fieldName` which could had been customized by the users. 75 | * This function helps us get the actual field name useful to match against the schema. (eg: schema[model].fields[field]) 76 | * 77 | * If it's still unclear what this does: 78 | * 79 | * 1. User can define a custom fieldName. 80 | * 2. When using a custom fieldName, doing something like `schema[model].fields[field]` will not work. 81 | * 82 | */ 83 | getDefaultFieldName: ({ 84 | model, 85 | field, 86 | }: { 87 | model: string; 88 | field: string; 89 | }) => string; 90 | /** 91 | * Get the field attributes for a given model and field. 92 | * 93 | * Note: any model name or field name is allowed, whether default to schema or not. 94 | */ 95 | getFieldAttributes: ({ 96 | model, 97 | field, 98 | }: { 99 | model: string; 100 | field: string; 101 | }) => DBFieldAttribute; 102 | // The following functions are exposed primarily for the purpose of having wrapper adapters. 103 | transformInput: ( 104 | data: Record<string, any>, 105 | defaultModelName: string, 106 | action: "create" | "update", 107 | forceAllowId?: boolean, 108 | ) => Promise<Record<string, any>>; 109 | transformOutput: ( 110 | data: Record<string, any>, 111 | defaultModelName: string, 112 | select?: string[], 113 | ) => Promise<Record<string, any>>; 114 | transformWhereClause: <W extends Where[] | undefined>({ 115 | model, 116 | where, 117 | }: { 118 | where: W; 119 | model: string; 120 | }) => W extends undefined ? undefined : CleanedWhere[]; 121 | }) => CustomAdapter; 122 | 123 | /** 124 | * @deprecated Use `CustomAdapter` from `@better-auth/core/db/adapter` instead. 125 | */ 126 | export interface CustomAdapter extends Omit<CoreCustomAdapter, "createSchema"> { 127 | createSchema?: (props: { 128 | /** 129 | * The file the user may have passed in to the `generate` command as the expected schema file output path. 130 | */ 131 | file?: string; 132 | /** 133 | * The tables from the user's Better-Auth instance schema. 134 | */ 135 | tables: BetterAuthDBSchema; 136 | }) => Promise<AdapterSchemaCreation>; 137 | } 138 | 139 | /** 140 | * @deprecated Use `CleanedWhere` from `@better-auth/core/db/adapter` instead. 141 | */ 142 | export type CleanedWhere = Prettify<Required<Where>>; 143 | 144 | export type AdapterTestDebugLogs = { 145 | resetDebugLogs: () => void; 146 | printDebugLogs: () => void; 147 | }; 148 | 149 | /** 150 | * @deprecated Use `AdapterFactoryOptions` instead. This export will be removed in a future version. 151 | */ 152 | export type CreateAdapterOptions = AdapterFactoryOptions; 153 | 154 | /** 155 | * @deprecated Use `AdapterFactoryConfig` instead. This export will be removed in a future version. 156 | */ 157 | export type AdapterConfig = AdapterFactoryConfig; 158 | 159 | /** 160 | * @deprecated Use `AdapterFactoryCustomizeAdapterCreator` instead. This export will be removed in a future version. 161 | */ 162 | export type CreateCustomAdapter = AdapterFactoryCustomizeAdapterCreator; 163 | ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/resources.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Resources 3 | description: A curated collection of resources to help you learn and master Better Auth. 4 | --- 5 | 6 | import { Resource } from "@/components/resource-section"; 7 | 8 | A curated collection of resources to help you learn and master Better Auth. From blog posts to video tutorials, find everything you need to get started. 9 | 10 | ## Video tutorials 11 | 12 | <Resource resources={ 13 | [ 14 | { 15 | title: "The State of Authentication", 16 | description: 17 | "<strong>Theo(t3.gg)</strong> explores the current landscape of authentication, discussing trends, challenges, and where the industry is heading.", 18 | href: "https://www.youtube.com/watch?v=lxslnp-ZEMw", 19 | tags: ["trends", "showcase", "review"], 20 | }, 21 | { 22 | title: "Last Authentication You Will Ever Need", 23 | description: 24 | "A comprehensive tutorial demonstrating why Better Auth could be the final authentication solution you'll need for your projects.", 25 | href: "https://www.youtube.com/watch?v=hFtufpaMcLM", 26 | tags: ["implementation", "showcase"], 27 | }, 28 | { 29 | title: "This Might Be My New Favourite Auth Library", 30 | description: 31 | "<strong>developedbyed</strong> explores the features and capabilities of Better Auth, explaining why it stands out among authentication libraries.", 32 | href: "https://www.youtube.com/watch?v=Hjs3zM7o7NE", 33 | tags: ["review", "showcase"], 34 | }, 35 | { 36 | title: "8 Reasons To Try Better Auth", 37 | description: 38 | "<strong>CJ</strong> presents 8 compelling reasons why Better Auth is the BEST auth framework he's ever used, demonstrating its superior features and ease of implementation.", 39 | href: "https://www.youtube.com/watch?v=_OApmLmex14", 40 | tags: ["review", "showcase", "implementation"], 41 | }, 42 | { 43 | title: "Better Auth is so good that I almost switched programming languages", 44 | description: 45 | "<strong>Dreams of Code</strong> reviews Better Auth's features that nearly made them switch languages.", 46 | href: "https://www.youtube.com/watch?v=dNY4FKXwTsM", 47 | tags: ["review", "showcase", "implementation"], 48 | }, 49 | { 50 | title: "Best authentication framework for next.js", 51 | description: 52 | "A detailed comparison of authentication frameworks for Next.js, highlighting why Better Auth might be your best choice.", 53 | href: "https://www.youtube.com/watch?v=V--T0q9FrEw", 54 | tags: ["nextjs", "comparison"], 55 | }, 56 | { 57 | title: "Better-Auth: A First Look", 58 | description: 59 | "An introductory overview and demonstration of Better Auth's core features and capabilities.", 60 | href: "https://www.youtube.com/watch?v=2cQTV6NYxis", 61 | tags: ["implementation", "showcase"], 62 | }, 63 | { 64 | title: "Stripe was never so easy (with better auth)", 65 | description: "A tutorial on how to integrate Stripe with Better Auth.", 66 | href: "https://www.youtube.com/watch?v=g-RIrzBEX6M", 67 | tags: ["implementation"], 68 | }, 69 | { 70 | title: "Nextjs 15 Authentication Made EASY with Better Auth", 71 | description: 72 | "A practical guide showing how to seamlessly integrate Better Auth with Next.js 15 for robust authentication.", 73 | href: "https://www.youtube.com/watch?v=lxslnp-ZEMw", 74 | tags: ["nextjs", "implementation", "tutorial"], 75 | }, 76 | { 77 | title: "Better Auth: Headless Authentication for Your TanStack Start App", 78 | description: "<strong>Jack</strong> demonstrates how to implement headless authentication in your TanStack Start application using Better Auth, providing a modern approach to auth.", 79 | href: "https://www.youtube.com/watch?v=Atev8Nxpw7c", 80 | tags: ["tanstack", "implementation"], 81 | }, 82 | { 83 | title: "Goodbye Clerk, Hello Better Auth – Full Migration Guide!", 84 | description: "A comprehensive guide showing how to migrate your authentication from Clerk to Better Auth, with step-by-step instructions and best practices.", 85 | href: "https://www.youtube.com/watch?v=Za_QihbDSuk", 86 | tags: ["migration", "clerk", "tutorial"], 87 | }, 88 | ] 89 | } /> 90 | 91 | ## Blog posts 92 | 93 | <Resource resources={ 94 | [ 95 | { 96 | title: "Better Auth with Hono, Bun, TypeScript, React and Vite", 97 | description: 98 | "You'll learn how to implement authentication with Better Auth in a client - server architecture, where the frontend is separate from the backend.", 99 | href: "https://catalins.tech/better-auth-with-hono-bun-typescript-react-vite", 100 | tags: ["typescript", "react", "bun", "vite"], 101 | }, 102 | { 103 | title: "Polar.sh + BetterAuth for Organizations", 104 | description: 105 | "Polar.sh is a platform for building payment integrations. This article will show you how to use Better Auth to authenticate your users.", 106 | href: "https://dev.to/phumudzosly/polarsh-betterauth-for-organizations-1j1b", 107 | tags: ["organizations", "integration", "payments"], 108 | }, 109 | { 110 | title: "Authenticating users in Astro with Better Auth", 111 | description: 112 | "Step by step guide on how to authenticate users in Astro with Better Auth.", 113 | href: "https://www.launchfa.st/blog/astro-better-auth", 114 | tags: ["astro", "integration", "tutorial"], 115 | }, 116 | { 117 | title: "Building Multi-Tenant Apps With Better-Auth and ZenStack", 118 | description: 119 | "Learn how to build multi-tenant apps with Better-Auth and ZenStack.", 120 | href: "https://zenstack.dev/blog/better-auth", 121 | tags: ["multi-tenant", "zenstack", "architecture"], 122 | }, 123 | ] 124 | } /> ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/changelog-layout.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | import { useId } from "react"; 3 | 4 | import clsx from "clsx"; 5 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 6 | 7 | function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { 8 | return ( 9 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 10 | <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> 11 | </svg> 12 | ); 13 | } 14 | 15 | function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { 16 | return ( 17 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 18 | <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> 19 | </svg> 20 | ); 21 | } 22 | 23 | function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { 24 | return ( 25 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 26 | <path 27 | fillRule="evenodd" 28 | clipRule="evenodd" 29 | d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" 30 | /> 31 | </svg> 32 | ); 33 | } 34 | 35 | function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { 36 | return ( 37 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 38 | <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> 39 | </svg> 40 | ); 41 | } 42 | 43 | export function Intro() { 44 | return ( 45 | <> 46 | <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> 47 | All of the changes made will be{" "} 48 | <span className="">available here.</span> 49 | </h1> 50 | <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> 51 | Better Auth is comprehensive authentication library for TypeScript that 52 | provides a wide range of features to make authentication easier and more 53 | secure. 54 | </p> 55 | <hr className="h-px bg-gray-300 mt-5" /> 56 | <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 justify-center gap-x-1 gap-y-3 sm:gap-x-2 lg:justify-start"> 57 | <IconLink 58 | href="/docs" 59 | icon={BookIcon} 60 | className="flex-none text-gray-600 dark:text-gray-300" 61 | > 62 | Documentation 63 | </IconLink> 64 | <IconLink 65 | href="https://github.com/better-auth/better-auth" 66 | icon={GitHubIcon} 67 | className="flex-none text-gray-600 dark:text-gray-300" 68 | > 69 | GitHub 70 | </IconLink> 71 | <IconLink 72 | href="https://discord.gg/better-auth" 73 | icon={DiscordLogoIcon} 74 | className="flex-none text-gray-600 dark:text-gray-300" 75 | > 76 | Community 77 | </IconLink> 78 | </div> 79 | </> 80 | ); 81 | } 82 | 83 | export function IntroFooter() { 84 | return ( 85 | <p className="flex items-baseline gap-x-2 text-[0.8125rem]/6 text-gray-500"> 86 | Brought to you by{" "} 87 | <IconLink href="#" icon={XIcon} compact> 88 | BETTER-AUTH. 89 | </IconLink> 90 | </p> 91 | ); 92 | } 93 | 94 | export function SignUpForm() { 95 | let id = useId(); 96 | 97 | return ( 98 | <form className="relative isolate mt-8 flex items-center pr-1"> 99 | <label htmlFor={id} className="sr-only"> 100 | Email address 101 | </label> 102 | 103 | <div className="absolute inset-0 -z-10 rounded-lg transition peer-focus:ring-4 peer-focus:ring-sky-300/15" /> 104 | <div className="absolute inset-0 -z-10 rounded-lg bg-white/2.5 ring-1 ring-white/15 transition peer-focus:ring-sky-300" /> 105 | </form> 106 | ); 107 | } 108 | 109 | export function IconLink({ 110 | children, 111 | className, 112 | compact = false, 113 | icon: Icon, 114 | ...props 115 | }: React.ComponentPropsWithoutRef<typeof Link> & { 116 | compact?: boolean; 117 | icon?: React.ComponentType<{ className?: string }>; 118 | }) { 119 | return ( 120 | <Link 121 | {...props} 122 | className={clsx( 123 | className, 124 | "group relative isolate flex items-center px-2 py-0.5 text-[0.8125rem]/6 font-medium text-black/70 dark:text-white/30 transition-colors hover:text-stone-300 rounded-none", 125 | compact ? "gap-x-2" : "gap-x-3", 126 | )} 127 | > 128 | <span className="absolute inset-0 -z-10 scale-75 rounded-lg bg-white/5 opacity-0 transition group-hover:scale-100 group-hover:opacity-100" /> 129 | {Icon && <Icon className="h-4 w-4 flex-none" />} 130 | <span className="self-baseline text-black/70 dark:text-white"> 131 | {children} 132 | </span> 133 | </Link> 134 | ); 135 | } 136 | ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/contributing.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Contributing to BetterAuth 3 | description: A concise guide to contributing to BetterAuth 4 | --- 5 | 6 | Thank you for your interest in contributing to Better Auth! This guide is a concise guide to contributing to Better Auth. 7 | 8 | ## Getting Started 9 | 10 | Before diving in, here are a few important resources: 11 | 12 | - Take a look at our existing <Link href="https://github.com/better-auth/better-auth/issues">issues</Link> and <Link href="https://github.com/better-auth/better-auth/pulls">pull requests</Link> 13 | - Join our community discussions in <Link href="https://discord.gg/better-auth">Discord</Link> 14 | 15 | 16 | ## Development Setup 17 | 18 | To get started with development: 19 | 20 | <Callout type="warn"> 21 | Make sure you have <Link href="https://nodejs.org/en/download">Node.JS</Link>{" "} 22 | installed, preferably on LTS. 23 | </Callout> 24 | 25 | <Steps> 26 | 27 | <Step> 28 | ### 1. Fork the repository 29 | 30 | Visit https://github.com/better-auth/better-auth 31 | 32 | Click the "Fork" button in the top right. 33 | 34 | </Step> 35 | 36 | <Step> 37 | ### 2. Clone your fork 38 | 39 | ```bash 40 | # Replace YOUR-USERNAME with your GitHub username 41 | git clone https://github.com/YOUR-USERNAME/better-auth.git 42 | cd better-auth 43 | ``` 44 | </Step> 45 | 46 | <Step> 47 | ### 3. Install dependencies 48 | 49 | Make sure you have <Link href="https://pnpm.io/installation">pnpm</Link> installed! 50 | 51 | ```bash 52 | pnpm install 53 | ``` 54 | </Step> 55 | 56 | <Step> 57 | ### 4. Prepare ENV files 58 | 59 | Copy the example env file to create your new `.env` file. 60 | 61 | ```bash 62 | cp -n ./docs/.env.example ./docs/.env 63 | ``` 64 | </Step> 65 | 66 | </Steps> 67 | 68 | ## Making changes 69 | 70 | Once you have an idea of what you want to contribute, you can start making changes. Here are some steps to get started: 71 | 72 | <Steps> 73 | <Step> 74 | ### 1. Create a new branch 75 | 76 | ```bash 77 | # Make sure you're on main 78 | git checkout main 79 | 80 | # Pull latest changes 81 | git pull upstream main 82 | 83 | # Create and switch to a new branch 84 | git checkout -b feature/your-feature-name 85 | ``` 86 | </Step> 87 | <Step> 88 | ### 2. Start development server 89 | 90 | Start the development server: 91 | 92 | ```bash 93 | pnpm dev 94 | ``` 95 | 96 | To start the docs server: 97 | 98 | ```bash 99 | pnpm -F docs dev 100 | ``` 101 | </Step> 102 | <Step> 103 | ### 3. Make Your Changes 104 | 105 | * Make your changes to the codebase. 106 | 107 | * Write tests if needed. (Read more about testing <Link href="/docs/reference/contributing#testing">here</Link>) 108 | 109 | * Update documentation. (Read more about documenting <Link href="/docs/reference/contributing#documentation">here</Link>) 110 | 111 | </Step> 112 | 113 | </Steps> 114 | 115 | 116 | ### Issues and Bug Fixes 117 | 118 | - Check our [GitHub issues](https://github.com/better-auth/better-auth/issues) for tasks labeled `good first issue` 119 | - When reporting bugs, include steps to reproduce and expected behavior 120 | - Comment on issues you'd like to work on to avoid duplicate efforts 121 | 122 | ### Framework Integrations 123 | 124 | We welcome contributions to support more frameworks: 125 | 126 | - Focus on framework-agnostic solutions where possible 127 | - Keep integrations minimal and maintainable 128 | - All integrations currently live in the main package 129 | 130 | ### Plugin Development 131 | 132 | - For core plugins: Open an issue first to discuss your idea 133 | - For community plugins: Feel free to develop independently 134 | - Follow our plugin architecture guidelines 135 | 136 | ### Documentation 137 | 138 | - Fix typos and errors 139 | - Add examples and clarify existing content 140 | - Ensure documentation is up to date with code changes 141 | 142 | ## Testing 143 | 144 | We use Vitest for testing. Place test files next to the source files they test: 145 | 146 | ```ts 147 | import { describe, it, expect } from "vitest"; 148 | import { getTestInstance } from "./test-utils/test-instance"; 149 | 150 | describe("Feature", () => { 151 | it("should work as expected", async () => { 152 | const { client } = await getTestInstance(); 153 | // Test code here 154 | expect(result).toBeDefined(); 155 | }); 156 | }); 157 | ``` 158 | 159 | ### Using the Test Instance Helper 160 | 161 | The test instance helper now includes improved async context support for managing user sessions: 162 | 163 | ```ts 164 | const { client, runWithUser, signInWithTestUser } = await getTestInstance(); 165 | 166 | // Run tests with a specific user context 167 | await runWithUser("[email protected]", "password", async (headers) => { 168 | // All client calls within this block will use the user's session 169 | const response = await client.getSession(); 170 | // headers are automatically applied 171 | }); 172 | 173 | // Or use the test user with async context 174 | const { runWithDefaultUser } = await signInWithTestUser(); 175 | await runWithDefaultUser(async (headers) => { 176 | // Code here runs with the test user's session context 177 | }); 178 | ``` 179 | 180 | ### Testing Best Practices 181 | 182 | - Write clear commit messages 183 | - Update documentation to reflect your changes 184 | - Add tests for new features 185 | - Follow our coding standards 186 | - Keep pull requests focused on a single change 187 | 188 | ## Need Help? 189 | 190 | Don't hesitate to ask for help! You can: 191 | 192 | - Open an <Link href="https://github.com/better-auth/better-auth/issues">issue</Link> with questions 193 | - Join our <Link href="https://discord.gg/better-auth">community discussions</Link> 194 | - Reach out to project maintainers 195 | 196 | Thank you for contributing to Better Auth! ``` -------------------------------------------------------------------------------- /docs/components/ui/carousel.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import useEmblaCarousel, { 5 | type UseEmblaCarouselType, 6 | } from "embla-carousel-react"; 7 | import { ArrowLeft, ArrowRight } from "lucide-react"; 8 | 9 | import { cn } from "@/lib/utils"; 10 | import { Button } from "@/components/ui/button"; 11 | 12 | type CarouselApi = UseEmblaCarouselType[1]; 13 | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>; 14 | type CarouselOptions = UseCarouselParameters[0]; 15 | type CarouselPlugin = UseCarouselParameters[1]; 16 | 17 | type CarouselProps = { 18 | opts?: CarouselOptions; 19 | plugins?: CarouselPlugin; 20 | orientation?: "horizontal" | "vertical"; 21 | setApi?: (api: CarouselApi) => void; 22 | }; 23 | 24 | type CarouselContextProps = { 25 | carouselRef: ReturnType<typeof useEmblaCarousel>[0]; 26 | api: ReturnType<typeof useEmblaCarousel>[1]; 27 | scrollPrev: () => void; 28 | scrollNext: () => void; 29 | canScrollPrev: boolean; 30 | canScrollNext: boolean; 31 | } & CarouselProps; 32 | 33 | const CarouselContext = React.createContext<CarouselContextProps | null>(null); 34 | 35 | function useCarousel() { 36 | const context = React.useContext(CarouselContext); 37 | 38 | if (!context) { 39 | throw new Error("useCarousel must be used within a <Carousel />"); 40 | } 41 | 42 | return context; 43 | } 44 | 45 | function Carousel({ 46 | orientation = "horizontal", 47 | opts, 48 | setApi, 49 | plugins, 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps<"div"> & CarouselProps) { 54 | const [carouselRef, api] = useEmblaCarousel( 55 | { 56 | ...opts, 57 | axis: orientation === "horizontal" ? "x" : "y", 58 | }, 59 | plugins, 60 | ); 61 | const [canScrollPrev, setCanScrollPrev] = React.useState(false); 62 | const [canScrollNext, setCanScrollNext] = React.useState(false); 63 | 64 | const onSelect = React.useCallback((api: CarouselApi) => { 65 | if (!api) return; 66 | setCanScrollPrev(api.canScrollPrev()); 67 | setCanScrollNext(api.canScrollNext()); 68 | }, []); 69 | 70 | const scrollPrev = React.useCallback(() => { 71 | api?.scrollPrev(); 72 | }, [api]); 73 | 74 | const scrollNext = React.useCallback(() => { 75 | api?.scrollNext(); 76 | }, [api]); 77 | 78 | const handleKeyDown = React.useCallback( 79 | (event: React.KeyboardEvent<HTMLDivElement>) => { 80 | if (event.key === "ArrowLeft") { 81 | event.preventDefault(); 82 | scrollPrev(); 83 | } else if (event.key === "ArrowRight") { 84 | event.preventDefault(); 85 | scrollNext(); 86 | } 87 | }, 88 | [scrollPrev, scrollNext], 89 | ); 90 | 91 | React.useEffect(() => { 92 | if (!api || !setApi) return; 93 | setApi(api); 94 | }, [api, setApi]); 95 | 96 | React.useEffect(() => { 97 | if (!api) return; 98 | onSelect(api); 99 | api.on("reInit", onSelect); 100 | api.on("select", onSelect); 101 | 102 | return () => { 103 | api?.off("select", onSelect); 104 | }; 105 | }, [api, onSelect]); 106 | 107 | return ( 108 | <CarouselContext.Provider 109 | value={{ 110 | carouselRef, 111 | api: api, 112 | opts, 113 | orientation: 114 | orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), 115 | scrollPrev, 116 | scrollNext, 117 | canScrollPrev, 118 | canScrollNext, 119 | }} 120 | > 121 | <div 122 | onKeyDownCapture={handleKeyDown} 123 | className={cn("relative", className)} 124 | role="region" 125 | aria-roledescription="carousel" 126 | data-slot="carousel" 127 | {...props} 128 | > 129 | {children} 130 | </div> 131 | </CarouselContext.Provider> 132 | ); 133 | } 134 | 135 | function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { 136 | const { carouselRef, orientation } = useCarousel(); 137 | 138 | return ( 139 | <div 140 | ref={carouselRef} 141 | className="overflow-hidden" 142 | data-slot="carousel-content" 143 | > 144 | <div 145 | className={cn( 146 | "flex", 147 | orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", 148 | className, 149 | )} 150 | {...props} 151 | /> 152 | </div> 153 | ); 154 | } 155 | 156 | function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { 157 | const { orientation } = useCarousel(); 158 | 159 | return ( 160 | <div 161 | role="group" 162 | aria-roledescription="slide" 163 | data-slot="carousel-item" 164 | className={cn( 165 | "min-w-0 shrink-0 grow-0 basis-full", 166 | orientation === "horizontal" ? "pl-4" : "pt-4", 167 | className, 168 | )} 169 | {...props} 170 | /> 171 | ); 172 | } 173 | 174 | function CarouselPrevious({ 175 | className, 176 | variant = "outline", 177 | size = "icon", 178 | ...props 179 | }: React.ComponentProps<typeof Button>) { 180 | const { orientation, scrollPrev, canScrollPrev } = useCarousel(); 181 | 182 | return ( 183 | <Button 184 | data-slot="carousel-previous" 185 | variant={variant} 186 | size={size} 187 | className={cn( 188 | "absolute size-8 rounded-full", 189 | orientation === "horizontal" 190 | ? "top-1/2 -left-12 -translate-y-1/2" 191 | : "-top-12 left-1/2 -translate-x-1/2 rotate-90", 192 | className, 193 | )} 194 | disabled={!canScrollPrev} 195 | onClick={scrollPrev} 196 | {...props} 197 | > 198 | <ArrowLeft /> 199 | <span className="sr-only">Previous slide</span> 200 | </Button> 201 | ); 202 | } 203 | 204 | function CarouselNext({ 205 | className, 206 | variant = "outline", 207 | size = "icon", 208 | ...props 209 | }: React.ComponentProps<typeof Button>) { 210 | const { orientation, scrollNext, canScrollNext } = useCarousel(); 211 | 212 | return ( 213 | <Button 214 | data-slot="carousel-next" 215 | variant={variant} 216 | size={size} 217 | className={cn( 218 | "absolute size-8 rounded-full", 219 | orientation === "horizontal" 220 | ? "top-1/2 -right-12 -translate-y-1/2" 221 | : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", 222 | className, 223 | )} 224 | disabled={!canScrollNext} 225 | onClick={scrollNext} 226 | {...props} 227 | > 228 | <ArrowRight /> 229 | <span className="sr-only">Next slide</span> 230 | </Button> 231 | ); 232 | } 233 | 234 | export { 235 | type CarouselApi, 236 | Carousel, 237 | CarouselContent, 238 | CarouselItem, 239 | CarouselPrevious, 240 | CarouselNext, 241 | }; 242 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/discord.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { refreshAccessToken, validateAuthorizationCode } from "../oauth2"; 4 | export interface DiscordProfile extends Record<string, any> { 5 | /** the user's id (i.e. the numerical snowflake) */ 6 | id: string; 7 | /** the user's username, not unique across the platform */ 8 | username: string; 9 | /** the user's Discord-tag */ 10 | discriminator: string; 11 | /** the user's display name, if it is set */ 12 | global_name: string | null; 13 | /** 14 | * the user's avatar hash: 15 | * https://discord.com/developers/docs/reference#image-formatting 16 | */ 17 | avatar: string | null; 18 | /** whether the user belongs to an OAuth2 application */ 19 | bot?: boolean; 20 | /** 21 | * whether the user is an Official Discord System user (part of the urgent 22 | * message system) 23 | */ 24 | system?: boolean; 25 | /** whether the user has two factor enabled on their account */ 26 | mfa_enabled: boolean; 27 | /** 28 | * the user's banner hash: 29 | * https://discord.com/developers/docs/reference#image-formatting 30 | */ 31 | banner: string | null; 32 | 33 | /** the user's banner color encoded as an integer representation of hexadecimal color code */ 34 | accent_color: number | null; 35 | 36 | /** 37 | * the user's chosen language option: 38 | * https://discord.com/developers/docs/reference#locales 39 | */ 40 | locale: string; 41 | /** whether the email on this account has been verified */ 42 | verified: boolean; 43 | /** the user's email */ 44 | email: string; 45 | /** 46 | * the flags on a user's account: 47 | * https://discord.com/developers/docs/resources/user#user-object-user-flags 48 | */ 49 | flags: number; 50 | /** 51 | * the type of Nitro subscription on a user's account: 52 | * https://discord.com/developers/docs/resources/user#user-object-premium-types 53 | */ 54 | premium_type: number; 55 | /** 56 | * the public flags on a user's account: 57 | * https://discord.com/developers/docs/resources/user#user-object-user-flags 58 | */ 59 | public_flags: number; 60 | /** undocumented field; corresponds to the user's custom nickname */ 61 | display_name: string | null; 62 | /** 63 | * undocumented field; corresponds to the Discord feature where you can e.g. 64 | * put your avatar inside of an ice cube 65 | */ 66 | avatar_decoration: string | null; 67 | /** 68 | * undocumented field; corresponds to the premium feature where you can 69 | * select a custom banner color 70 | */ 71 | banner_color: string | null; 72 | /** undocumented field; the CDN URL of their profile picture */ 73 | image_url: string; 74 | } 75 | 76 | export interface DiscordOptions extends ProviderOptions<DiscordProfile> { 77 | clientId: string; 78 | prompt?: "none" | "consent"; 79 | permissions?: number; 80 | } 81 | 82 | export const discord = (options: DiscordOptions) => { 83 | return { 84 | id: "discord", 85 | name: "Discord", 86 | createAuthorizationURL({ state, scopes, redirectURI }) { 87 | const _scopes = options.disableDefaultScope ? [] : ["identify", "email"]; 88 | scopes && _scopes.push(...scopes); 89 | options.scope && _scopes.push(...options.scope); 90 | const hasBotScope = _scopes.includes("bot"); 91 | const permissionsParam = 92 | hasBotScope && options.permissions !== undefined 93 | ? `&permissions=${options.permissions}` 94 | : ""; 95 | return new URL( 96 | `https://discord.com/api/oauth2/authorize?scope=${_scopes.join( 97 | "+", 98 | )}&response_type=code&client_id=${ 99 | options.clientId 100 | }&redirect_uri=${encodeURIComponent( 101 | options.redirectURI || redirectURI, 102 | )}&state=${state}&prompt=${ 103 | options.prompt || "none" 104 | }${permissionsParam}`, 105 | ); 106 | }, 107 | validateAuthorizationCode: async ({ code, redirectURI }) => { 108 | return validateAuthorizationCode({ 109 | code, 110 | redirectURI, 111 | options, 112 | tokenEndpoint: "https://discord.com/api/oauth2/token", 113 | }); 114 | }, 115 | refreshAccessToken: options.refreshAccessToken 116 | ? options.refreshAccessToken 117 | : async (refreshToken) => { 118 | return refreshAccessToken({ 119 | refreshToken, 120 | options: { 121 | clientId: options.clientId, 122 | clientKey: options.clientKey, 123 | clientSecret: options.clientSecret, 124 | }, 125 | tokenEndpoint: "https://discord.com/api/oauth2/token", 126 | }); 127 | }, 128 | async getUserInfo(token) { 129 | if (options.getUserInfo) { 130 | return options.getUserInfo(token); 131 | } 132 | const { data: profile, error } = await betterFetch<DiscordProfile>( 133 | "https://discord.com/api/users/@me", 134 | { 135 | headers: { 136 | authorization: `Bearer ${token.accessToken}`, 137 | }, 138 | }, 139 | ); 140 | 141 | if (error) { 142 | return null; 143 | } 144 | if (profile.avatar === null) { 145 | const defaultAvatarNumber = 146 | profile.discriminator === "0" 147 | ? Number(BigInt(profile.id) >> BigInt(22)) % 6 148 | : parseInt(profile.discriminator) % 5; 149 | profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`; 150 | } else { 151 | const format = profile.avatar.startsWith("a_") ? "gif" : "png"; 152 | profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; 153 | } 154 | const userMap = await options.mapProfileToUser?.(profile); 155 | return { 156 | user: { 157 | id: profile.id, 158 | name: profile.global_name || profile.username || "", 159 | email: profile.email, 160 | emailVerified: profile.verified, 161 | image: profile.image_url, 162 | ...userMap, 163 | }, 164 | data: profile, 165 | }; 166 | }, 167 | options, 168 | } satisfies OAuthProvider<DiscordProfile>; 169 | }; 170 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/db.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it, vi } from "vitest"; 2 | import { getTestInstance } from "../test-utils/test-instance"; 3 | 4 | describe("db", async () => { 5 | it("should work with custom model names", async () => { 6 | const { client, db } = await getTestInstance({ 7 | user: { 8 | modelName: "users", 9 | }, 10 | session: { 11 | modelName: "sessions", 12 | }, 13 | account: { 14 | modelName: "accounts", 15 | }, 16 | }); 17 | const res = await client.signUp.email({ 18 | email: "[email protected]", 19 | password: "password", 20 | name: "Test User", 21 | }); 22 | const users = await db.findMany({ 23 | model: "user", 24 | }); 25 | const session = await db.findMany({ 26 | model: "session", 27 | }); 28 | const accounts = await db.findMany({ 29 | model: "account", 30 | }); 31 | expect(res.data).toBeDefined(); 32 | //including the user that was created in the test instance 33 | expect(users).toHaveLength(2); 34 | expect(session).toHaveLength(2); 35 | expect(accounts).toHaveLength(2); 36 | }); 37 | 38 | it("db hooks", async () => { 39 | let callback = false; 40 | const { client, db } = await getTestInstance({ 41 | databaseHooks: { 42 | user: { 43 | create: { 44 | async before(user) { 45 | return { 46 | data: { 47 | ...user, 48 | image: "test-image", 49 | }, 50 | }; 51 | }, 52 | async after(user) { 53 | callback = true; 54 | }, 55 | }, 56 | }, 57 | }, 58 | }); 59 | const res = await client.signUp.email({ 60 | email: "[email protected]", 61 | name: "test", 62 | password: "password", 63 | }); 64 | const session = await client.getSession({ 65 | fetchOptions: { 66 | headers: { 67 | Authorization: `Bearer ${res.data?.token}`, 68 | }, 69 | throw: true, 70 | }, 71 | }); 72 | expect(session?.user?.image).toBe("test-image"); 73 | expect(callback).toBe(true); 74 | }); 75 | 76 | it("should work with custom field names", async () => { 77 | const { client } = await getTestInstance({ 78 | user: { 79 | fields: { 80 | email: "email_address", 81 | }, 82 | }, 83 | }); 84 | const res = await client.signUp.email({ 85 | email: "[email protected]", 86 | password: "password", 87 | name: "Test User", 88 | }); 89 | const session = await client.getSession({ 90 | fetchOptions: { 91 | headers: { 92 | Authorization: `Bearer ${res.data?.token}`, 93 | }, 94 | throw: true, 95 | }, 96 | }); 97 | expect(session?.user.email).toBe("[email protected]"); 98 | }); 99 | 100 | it("delete hooks", async () => { 101 | const hookUserDeleteBefore = vi.fn(); 102 | const hookUserDeleteAfter = vi.fn(); 103 | const hookSessionDeleteBefore = vi.fn(); 104 | const hookSessionDeleteAfter = vi.fn(); 105 | 106 | const { client } = await getTestInstance({ 107 | session: { 108 | storeSessionInDatabase: true, 109 | }, 110 | user: { 111 | deleteUser: { 112 | enabled: true, 113 | }, 114 | }, 115 | databaseHooks: { 116 | user: { 117 | delete: { 118 | async before(user, context) { 119 | hookUserDeleteBefore(user, context); 120 | return true; 121 | }, 122 | async after(user, context) { 123 | hookUserDeleteAfter(user, context); 124 | }, 125 | }, 126 | }, 127 | session: { 128 | delete: { 129 | async before(session, context) { 130 | hookSessionDeleteBefore(session, context); 131 | return true; 132 | }, 133 | async after(session, context) { 134 | hookSessionDeleteAfter(session, context); 135 | }, 136 | }, 137 | }, 138 | }, 139 | }); 140 | 141 | const res = await client.signUp.email({ 142 | email: "[email protected]", 143 | password: "password", 144 | name: "Delete Test User", 145 | }); 146 | 147 | expect(res.data).toBeDefined(); 148 | const userId = res.data?.user.id; 149 | 150 | await client.deleteUser({ 151 | fetchOptions: { 152 | headers: { 153 | Authorization: `Bearer ${res.data?.token}`, 154 | }, 155 | throw: true, 156 | }, 157 | }); 158 | 159 | expect(hookUserDeleteBefore).toHaveBeenCalledOnce(); 160 | expect(hookUserDeleteAfter).toHaveBeenCalledOnce(); 161 | expect(hookSessionDeleteBefore).toHaveBeenCalledOnce(); 162 | expect(hookSessionDeleteAfter).toHaveBeenCalledOnce(); 163 | 164 | expect(hookUserDeleteBefore).toHaveBeenCalledWith( 165 | expect.objectContaining({ 166 | id: userId, 167 | email: "[email protected]", 168 | name: "Delete Test User", 169 | }), 170 | expect.any(Object), 171 | ); 172 | 173 | expect(hookUserDeleteAfter).toHaveBeenCalledWith( 174 | expect.objectContaining({ 175 | id: userId, 176 | email: "[email protected]", 177 | name: "Delete Test User", 178 | }), 179 | expect.any(Object), 180 | ); 181 | }); 182 | 183 | it("delete hooks abort", async () => { 184 | const hookUserDeleteBefore = vi.fn(); 185 | const hookUserDeleteAfter = vi.fn(); 186 | 187 | const { client } = await getTestInstance({ 188 | user: { 189 | deleteUser: { 190 | enabled: true, 191 | }, 192 | }, 193 | databaseHooks: { 194 | user: { 195 | delete: { 196 | async before(user, context) { 197 | hookUserDeleteBefore(user, context); 198 | return false; 199 | }, 200 | async after(user, context) { 201 | hookUserDeleteAfter(user, context); 202 | }, 203 | }, 204 | }, 205 | }, 206 | }); 207 | 208 | const res = await client.signUp.email({ 209 | email: "[email protected]", 210 | password: "password", 211 | name: "Abort Delete Test User", 212 | }); 213 | 214 | expect(res.data).toBeDefined(); 215 | const userId = res.data?.user.id; 216 | 217 | try { 218 | await client.deleteUser({ 219 | fetchOptions: { 220 | headers: { 221 | Authorization: `Bearer ${res.data?.token}`, 222 | }, 223 | throw: true, 224 | }, 225 | }); 226 | } catch (error) { 227 | // Expected to fail due to hook returning false 228 | } 229 | 230 | expect(hookUserDeleteBefore).toHaveBeenCalledOnce(); 231 | expect(hookUserDeleteAfter).not.toHaveBeenCalled(); 232 | 233 | expect(hookUserDeleteBefore).toHaveBeenCalledWith( 234 | expect.objectContaining({ 235 | id: userId, 236 | email: "[email protected]", 237 | name: "Abort Delete Test User", 238 | }), 239 | expect.any(Object), 240 | ); 241 | }); 242 | }); 243 | ``` -------------------------------------------------------------------------------- /packages/cli/src/utils/add-svelte-kit-env-modules.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | /** 5 | * Adds SvelteKit environment modules and path aliases 6 | * @param aliases - The aliases object to populate 7 | * @param cwd - Current working directory (optional, defaults to process.cwd()) 8 | */ 9 | export function addSvelteKitEnvModules( 10 | aliases: Record<string, string>, 11 | cwd?: string, 12 | ) { 13 | const workingDir = cwd || process.cwd(); 14 | 15 | // Add SvelteKit environment modules 16 | aliases["$env/dynamic/private"] = createDataUriModule( 17 | createDynamicEnvModule(), 18 | ); 19 | aliases["$env/dynamic/public"] = createDataUriModule( 20 | createDynamicEnvModule(), 21 | ); 22 | aliases["$env/static/private"] = createDataUriModule( 23 | createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")), 24 | ); 25 | aliases["$env/static/public"] = createDataUriModule( 26 | createStaticEnvModule(filterPublicEnv("PUBLIC_", "")), 27 | ); 28 | 29 | const svelteKitAliases = getSvelteKitPathAliases(workingDir); 30 | Object.assign(aliases, svelteKitAliases); 31 | } 32 | 33 | function getSvelteKitPathAliases(cwd: string): Record<string, string> { 34 | const aliases: Record<string, string> = {}; 35 | 36 | const packageJsonPath = path.join(cwd, "package.json"); 37 | const svelteConfigPath = path.join(cwd, "svelte.config.js"); 38 | const svelteConfigTsPath = path.join(cwd, "svelte.config.ts"); 39 | 40 | let isSvelteKitProject = false; 41 | 42 | if (fs.existsSync(packageJsonPath)) { 43 | try { 44 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); 45 | const deps = { 46 | ...packageJson.dependencies, 47 | ...packageJson.devDependencies, 48 | }; 49 | isSvelteKitProject = !!deps["@sveltejs/kit"]; 50 | } catch { 51 | // Ignore JSON parse errors 52 | } 53 | } 54 | 55 | if (!isSvelteKitProject) { 56 | isSvelteKitProject = 57 | fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath); 58 | } 59 | 60 | if (!isSvelteKitProject) { 61 | return aliases; 62 | } 63 | 64 | const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")]; 65 | 66 | for (const libPath of libPaths) { 67 | if (fs.existsSync(libPath)) { 68 | aliases["$lib"] = libPath; 69 | // handles a common subpaths 70 | const commonSubPaths = ["server", "utils", "components", "stores"]; 71 | for (const subPath of commonSubPaths) { 72 | const subDir = path.join(libPath, subPath); 73 | if (fs.existsSync(subDir)) { 74 | aliases[`$lib/${subPath}`] = subDir; 75 | } 76 | } 77 | break; 78 | } 79 | } 80 | // Add simple stub for $app/server to prevent CLI errors 81 | aliases["$app/server"] = createDataUriModule(createAppServerModule()); 82 | 83 | const customAliases = getSvelteConfigAliases(cwd); 84 | Object.assign(aliases, customAliases); 85 | 86 | return aliases; 87 | } 88 | // for custom aliases in svelte.config.js/ts 89 | function getSvelteConfigAliases(cwd: string): Record<string, string> { 90 | const aliases: Record<string, string> = {}; 91 | const configPaths = [ 92 | path.join(cwd, "svelte.config.js"), 93 | path.join(cwd, "svelte.config.ts"), 94 | ]; 95 | 96 | for (const configPath of configPaths) { 97 | if (fs.existsSync(configPath)) { 98 | try { 99 | const content = fs.readFileSync(configPath, "utf-8"); 100 | const aliasMatch = content.match(/alias\s*:\s*\{([^}]+)\}/); 101 | if (aliasMatch && aliasMatch[1]) { 102 | const aliasContent = aliasMatch[1]; 103 | const aliasMatches = aliasContent.matchAll( 104 | /['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g, 105 | ); 106 | 107 | for (const match of aliasMatches) { 108 | const [, alias, target] = match; 109 | if (alias && target) { 110 | aliases[alias + "/*"] = path.resolve(cwd, target) + "/*"; 111 | aliases[alias] = path.resolve(cwd, target); 112 | } 113 | } 114 | } 115 | } catch { 116 | // Ignore file reading/parsing errors 117 | } 118 | break; 119 | } 120 | } 121 | 122 | return aliases; 123 | } 124 | 125 | function createAppServerModule(): string { 126 | return ` 127 | // $app/server stub for CLI compatibility 128 | export default {}; 129 | // jiti dirty hack: .unknown 130 | `; 131 | } 132 | 133 | function createDataUriModule(module: string) { 134 | return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`; 135 | } 136 | 137 | function createStaticEnvModule(env: Record<string, string>) { 138 | const declarations = Object.keys(env) 139 | .filter((k) => validIdentifier.test(k) && !reserved.has(k)) 140 | .map((k) => `export const ${k} = ${JSON.stringify(env[k])};`); 141 | 142 | return ` 143 | ${declarations.join("\n")} 144 | // jiti dirty hack: .unknown 145 | `; 146 | } 147 | 148 | function createDynamicEnvModule() { 149 | return ` 150 | export const env = process.env; 151 | // jiti dirty hack: .unknown 152 | `; 153 | } 154 | 155 | export function filterPrivateEnv(publicPrefix: string, privatePrefix: string) { 156 | return Object.fromEntries( 157 | Object.entries(process.env).filter( 158 | ([k]) => 159 | k.startsWith(privatePrefix) && 160 | (publicPrefix === "" || !k.startsWith(publicPrefix)), 161 | ), 162 | ) as Record<string, string>; 163 | } 164 | 165 | export function filterPublicEnv(publicPrefix: string, privatePrefix: string) { 166 | return Object.fromEntries( 167 | Object.entries(process.env).filter( 168 | ([k]) => 169 | k.startsWith(publicPrefix) && 170 | (privatePrefix === "" || !k.startsWith(privatePrefix)), 171 | ), 172 | ) as Record<string, string>; 173 | } 174 | 175 | const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; 176 | const reserved = new Set([ 177 | "do", 178 | "if", 179 | "in", 180 | "for", 181 | "let", 182 | "new", 183 | "try", 184 | "var", 185 | "case", 186 | "else", 187 | "enum", 188 | "eval", 189 | "null", 190 | "this", 191 | "true", 192 | "void", 193 | "with", 194 | "await", 195 | "break", 196 | "catch", 197 | "class", 198 | "const", 199 | "false", 200 | "super", 201 | "throw", 202 | "while", 203 | "yield", 204 | "delete", 205 | "export", 206 | "import", 207 | "public", 208 | "return", 209 | "static", 210 | "switch", 211 | "typeof", 212 | "default", 213 | "extends", 214 | "finally", 215 | "package", 216 | "private", 217 | "continue", 218 | "debugger", 219 | "function", 220 | "arguments", 221 | "interface", 222 | "protected", 223 | "implements", 224 | "instanceof", 225 | ]); 226 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/paypal.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: PayPal 3 | description: Paypal provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your PayPal Credentials 9 | To integrate with PayPal, you need to obtain API credentials by creating an application in the [PayPal Developer Portal](https://developer.paypal.com/dashboard). 10 | 11 | Follow these steps: 12 | 1. Create an account on the PayPal Developer Portal 13 | 2. Create a new application, [official docs]( https://developer.paypal.com/developer/applications/) 14 | 3. Configure Log in with PayPal under "Other features" 15 | 4. Set up your Return URL (redirect URL) 16 | 5. Configure user information permissions 17 | 6. Note your Client ID and Client Secret 18 | 19 | <Callout type="info"> 20 | - PayPal has two environments: Sandbox (for testing) and Live (for production) 21 | - For testing, create sandbox test accounts in the Developer Dashboard under "Sandbox" → "Accounts" 22 | - You cannot use your real PayPal account to test in sandbox mode - you must use the generated test accounts 23 | - The Return URL in your PayPal app settings must exactly match your redirect URI 24 | - 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. 25 | </Callout> 26 | Make sure to configure "Log in with PayPal" in your app settings: 27 | 1. Go to your app in the Developer Dashboard 28 | 2. Under "Other features", check "Log in with PayPal" 29 | 3. Click "Advanced Settings" 30 | 4. Enter your Return URL 31 | 5. Select the user information you want to access (email, name, etc.) 32 | 6. Enter Privacy Policy and User Agreement URLs 33 | 34 | <Callout type="info"> 35 | - PayPal doesn't use traditional OAuth2 scopes in the authorization URL. Instead, you configure permissions directly in the Developer Dashboard 36 | - For live apps, PayPal must review and approve your application before it can go live, which typically takes a few weeks 37 | </Callout> 38 | </Step> 39 | 40 | <Step> 41 | ### Configure the provider 42 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 43 | 44 | ```ts title="auth.ts" 45 | import { betterAuth } from "better-auth" 46 | 47 | export const auth = betterAuth({ 48 | socialProviders: { 49 | paypal: { // [!code highlight] 50 | clientId: process.env.PAYPAL_CLIENT_ID as string, // [!code highlight] 51 | clientSecret: process.env.PAYPAL_CLIENT_SECRET as string, // [!code highlight] 52 | environment: "sandbox", // or "live" for production //, // [!code highlight] 53 | }, // [!code highlight] 54 | }, 55 | }) 56 | ``` 57 | #### Options 58 | The PayPal provider accepts the following options: 59 | 60 | - `environment`: `'sandbox' | 'live'` - PayPal environment to use (default: `'sandbox'`) 61 | - `requestShippingAddress`: `boolean` - Whether to request shipping address information (default: `false`) 62 | 63 | ```ts title="auth.ts" 64 | export const auth = betterAuth({ 65 | socialProviders: { 66 | paypal: { 67 | clientId: process.env.PAYPAL_CLIENT_ID as string, 68 | clientSecret: process.env.PAYPAL_CLIENT_SECRET as string, 69 | environment: "live", // Use "live" for production 70 | requestShippingAddress: true, // Request address info 71 | }, 72 | }, 73 | }) 74 | ``` 75 | </Step> 76 | <Step> 77 | ### Sign In with PayPal 78 | 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: 79 | - `provider`: The provider to use. It should be set to `paypal`. 80 | 81 | ```ts title="auth-client.ts" 82 | import { createAuthClient } from "better-auth/client" 83 | const authClient = createAuthClient() 84 | 85 | const signIn = async () => { 86 | const data = await authClient.signIn.social({ 87 | provider: "paypal" 88 | }) 89 | } 90 | ``` 91 | ### Additional Options: 92 | - `environment`: PayPal environment to use. 93 | - Default: `"sandbox"` 94 | - Options: `"sandbox"` | `"live"` 95 | - `requestShippingAddress`: Whether to request shipping address information. 96 | - Default: `false` 97 | - `scope`: Additional scopes to request (combined with default permissions). 98 | - Default: Configured in PayPal Developer Dashboard 99 | - Note: PayPal doesn't use traditional OAuth2 scopes - permissions are set in the Dashboard 100 | For more details refer to the [Scopes Reference](https://developer.paypal.com/docs/log-in-with-paypal/integrate/reference/#scope-attributes) 101 | - `mapProfileToUser`: Custom function to map PayPal profile data to user object. 102 | - `getUserInfo`: Custom function to retrieve user information. 103 | For more details refer to the [User Reference](https://developer.paypal.com/docs/api/identity/v1/#userinfo_get) 104 | - `verifyIdToken`: Custom ID token verification function. 105 | </Step> 106 | 107 | </Steps> 108 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import type { RateLimit } from "../../types"; 4 | 5 | describe( 6 | "rate-limiter", 7 | { 8 | timeout: 10000, 9 | }, 10 | async () => { 11 | const { client, testUser } = await getTestInstance({ 12 | rateLimit: { 13 | enabled: true, 14 | window: 10, 15 | max: 20, 16 | }, 17 | }); 18 | 19 | it("should return 429 after 3 request for sign-in", async () => { 20 | for (let i = 0; i < 5; i++) { 21 | const response = await client.signIn.email({ 22 | email: testUser.email, 23 | password: testUser.password, 24 | }); 25 | if (i >= 3) { 26 | expect(response.error?.status).toBe(429); 27 | } else { 28 | expect(response.error).toBeNull(); 29 | } 30 | } 31 | }); 32 | 33 | it("should reset the limit after the window period", async () => { 34 | vi.useFakeTimers(); 35 | vi.advanceTimersByTime(11000); 36 | for (let i = 0; i < 5; i++) { 37 | const res = await client.signIn.email({ 38 | email: testUser.email, 39 | password: testUser.password, 40 | }); 41 | if (i >= 3) { 42 | expect(res.error?.status).toBe(429); 43 | } else { 44 | expect(res.error).toBeNull(); 45 | } 46 | } 47 | }); 48 | 49 | it("should respond the correct retry-after header", async () => { 50 | vi.useFakeTimers(); 51 | vi.advanceTimersByTime(3000); 52 | let retryAfter = ""; 53 | await client.signIn.email( 54 | { 55 | email: testUser.email, 56 | password: testUser.password, 57 | }, 58 | { 59 | onError(context) { 60 | retryAfter = context.response.headers.get("X-Retry-After") ?? ""; 61 | }, 62 | }, 63 | ); 64 | expect(retryAfter).toBe("7"); 65 | }); 66 | 67 | it("should rate limit based on the path", async () => { 68 | const signInRes = await client.signIn.email({ 69 | email: testUser.email, 70 | password: testUser.password, 71 | }); 72 | expect(signInRes.error?.status).toBe(429); 73 | 74 | const signUpRes = await client.signUp.email({ 75 | email: "[email protected]", 76 | password: testUser.password, 77 | name: "test", 78 | }); 79 | expect(signUpRes.error).toBeNull(); 80 | }); 81 | 82 | it("non-special-rules limits", async () => { 83 | for (let i = 0; i < 25; i++) { 84 | const response = await client.getSession(); 85 | expect(response.error?.status).toBe(i >= 20 ? 429 : undefined); 86 | } 87 | }); 88 | 89 | it("query params should be ignored", async () => { 90 | for (let i = 0; i < 25; i++) { 91 | const response = await client.listSessions({ 92 | fetchOptions: { 93 | query: { 94 | "test-query": Math.random().toString(), 95 | }, 96 | }, 97 | }); 98 | 99 | if (i >= 20) { 100 | expect(response.error?.status).toBe(429); 101 | } else { 102 | expect(response.error?.status).toBe(401); 103 | } 104 | } 105 | }); 106 | }, 107 | ); 108 | 109 | describe("custom rate limiting storage", async () => { 110 | let store = new Map<string, string>(); 111 | const expirationMap = new Map<string, number>(); 112 | const { client, testUser } = await getTestInstance({ 113 | rateLimit: { 114 | enabled: true, 115 | }, 116 | secondaryStorage: { 117 | set(key, value, ttl) { 118 | store.set(key, value); 119 | if (ttl) expirationMap.set(key, ttl); 120 | }, 121 | get(key) { 122 | return store.get(key) || null; 123 | }, 124 | delete(key) { 125 | store.delete(key); 126 | expirationMap.delete(key); 127 | }, 128 | }, 129 | }); 130 | 131 | it("should use custom storage", async () => { 132 | await client.getSession(); 133 | expect(store.size).toBe(3); 134 | let lastRequest = Date.now(); 135 | for (let i = 0; i < 4; i++) { 136 | const response = await client.signIn.email({ 137 | email: testUser.email, 138 | password: testUser.password, 139 | }); 140 | const rateLimitData: RateLimit = JSON.parse( 141 | store.get("127.0.0.1/sign-in/email") ?? "{}", 142 | ); 143 | expect(rateLimitData.lastRequest).toBeGreaterThanOrEqual(lastRequest); 144 | lastRequest = rateLimitData.lastRequest; 145 | if (i >= 3) { 146 | expect(response.error?.status).toBe(429); 147 | expect(rateLimitData.count).toBe(3); 148 | } else { 149 | expect(response.error).toBeNull(); 150 | expect(rateLimitData.count).toBe(i + 1); 151 | } 152 | const rateLimitExp = expirationMap.get("127.0.0.1/sign-in/email"); 153 | expect(rateLimitExp).toBe(10); 154 | } 155 | }); 156 | }); 157 | 158 | describe("should work with custom rules", async () => { 159 | const { client, testUser } = await getTestInstance({ 160 | rateLimit: { 161 | enabled: true, 162 | storage: "database", 163 | customRules: { 164 | "/sign-in/*": { 165 | window: 10, 166 | max: 2, 167 | }, 168 | "/sign-up/email": { 169 | window: 10, 170 | max: 3, 171 | }, 172 | "/get-session": false, 173 | }, 174 | }, 175 | }); 176 | 177 | it("should use custom rules", async () => { 178 | for (let i = 0; i < 4; i++) { 179 | const response = await client.signIn.email({ 180 | email: testUser.email, 181 | password: testUser.password, 182 | }); 183 | if (i >= 2) { 184 | expect(response.error?.status).toBe(429); 185 | } else { 186 | expect(response.error).toBeNull(); 187 | } 188 | } 189 | 190 | for (let i = 0; i < 5; i++) { 191 | const response = await client.signUp.email({ 192 | email: `${Math.random()}@test.com`, 193 | password: testUser.password, 194 | name: "test", 195 | }); 196 | if (i >= 3) { 197 | expect(response.error?.status).toBe(429); 198 | } else { 199 | expect(response.error).toBeNull(); 200 | } 201 | } 202 | }); 203 | 204 | it("should use default rules if custom rules are not defined", async () => { 205 | for (let i = 0; i < 5; i++) { 206 | const response = await client.getSession(); 207 | if (i >= 20) { 208 | expect(response.error?.status).toBe(429); 209 | } else { 210 | expect(response.error).toBeNull(); 211 | } 212 | } 213 | }); 214 | 215 | it("should not rate limit if custom rule is false", async () => { 216 | let i = 0; 217 | let response = null; 218 | for (; i < 110; i++) { 219 | response = await client.getSession().then((res) => res.error); 220 | } 221 | expect(response).toBeNull(); 222 | expect(i).toBe(110); 223 | }); 224 | }); 225 | ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/saml-sso-with-okta.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: SAML SSO with Okta 3 | description: A guide to integrating SAML Single Sign-On (SSO) with Better Auth, featuring Okta 4 | --- 5 | 6 | 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). 7 | 8 | ## What is SAML? 9 | 10 | 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). 11 | 12 | In this setup: 13 | 14 | - **IdP (Okta)**: Authenticates users and sends assertions about their identity. 15 | - **SP (Better Auth)**: Validates assertions and logs the user in.up. 16 | 17 | ### Step 1: Create a SAML Application in Okta 18 | 19 | 1. Log in to your Okta Admin Console 20 | 2. Navigate to Applications > Applications 21 | 3. Click "Create App Integration" 22 | 4. Select "SAML 2.0" as the Sign-in method 23 | 5. Configure the following settings: 24 | 25 | - **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 26 | - **Audience URI (SP Entity ID)**: Your Better Auth metadata URL (e.g., `http://localhost:3000/api/auth/sso/saml2/sp/metadata`) 27 | - **Name ID format**: Email Address or any of your choice. 28 | 29 | 6. Download the IdP metadata XML file and certificate 30 | 31 | ### Step 2: Configure Better Auth 32 | 33 | Here’s an example configuration for Okta in a dev environment: 34 | 35 | ```typescript 36 | const ssoConfig = { 37 | defaultSSO: [{ 38 | domain: "localhost:3000", // Your domain 39 | providerId: "sso", 40 | samlConfig: { 41 | // SP Configuration 42 | issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata", 43 | entryPoint: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml", 44 | callbackUrl: "/dashboard", // Redirect after successful authentication 45 | 46 | // IdP Configuration 47 | idpMetadata: { 48 | entityID: "https://trial-1076874.okta.com/app/exktofb0a62hqLAUL697/sso/saml/metadata", 49 | singleSignOnService: [{ 50 | Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", 51 | Location: "https://trial-1076874.okta.com/app/trial-1076874_samltest_1/exktofb0a62hqLAUL697/sso/saml" 52 | }], 53 | cert: `-----BEGIN CERTIFICATE----- 54 | MIIDqjCCApKgAwIBAgIGAZhVGMeUMA0GCSqGSIb3DQEBCwUAMIGVMQswCQYDVQQGEwJVUzETMBEG 55 | ... 56 | [Your Okta Certificate] 57 | ... 58 | -----END CERTIFICATE-----` 59 | }, 60 | 61 | // SP Metadata 62 | spMetadata: { 63 | metadata: `<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" 64 | entityID="http://localhost:3000/api/sso/saml2/sp/metadata"> 65 | ... 66 | [Your SP Metadata XML] 67 | ... 68 | </md:EntityDescriptor>` 69 | } 70 | } 71 | }] 72 | } 73 | ``` 74 | 75 | ### Step 3: Multiple Default Providers (Optional) 76 | 77 | You can configure multiple SAML providers for different domains: 78 | 79 | ```typescript 80 | const ssoConfig = { 81 | defaultSSO: [ 82 | { 83 | domain: "company.com", 84 | providerId: "company-okta", 85 | samlConfig: { 86 | // Okta SAML configuration for company.com 87 | } 88 | }, 89 | { 90 | domain: "partner.com", 91 | providerId: "partner-adfs", 92 | samlConfig: { 93 | // ADFS SAML configuration for partner.com 94 | } 95 | }, 96 | { 97 | domain: "contractor.org", 98 | providerId: "contractor-azure", 99 | samlConfig: { 100 | // Azure AD SAML configuration for contractor.org 101 | } 102 | } 103 | ] 104 | } 105 | ``` 106 | 107 | <Callout type="info"> 108 | **Explicit**: Pass providerId directly when signing in. 109 | **Domain fallback:** Matches based on the user’s email domain. e.g. [email protected] → matches `company-okta` provider. 110 | </Callout> 111 | 112 | 113 | ### Step 4: Initiating Sign-In 114 | 115 | You can start an SSO flow in three ways: 116 | 117 | **1. Explicitly by `providerId` (recommended):** 118 | 119 | ```typescript 120 | // Explicitly specify which provider to use 121 | await authClient.signIn.sso({ 122 | providerId: "company-okta", 123 | callbackURL: "/dashboard" 124 | }); 125 | ``` 126 | 127 | **2. By email domain matching:** 128 | 129 | ```typescript 130 | // Automatically matches provider based on email domain 131 | await authClient.signIn.sso({ 132 | email: "[email protected]", 133 | callbackURL: "/dashboard" 134 | }); 135 | ``` 136 | 137 | **3. By specifying domain:** 138 | 139 | ```typescript 140 | // Explicitly specify domain for matching 141 | await authClient.signIn.sso({ 142 | domain: "partner.com", 143 | callbackURL: "/dashboard" 144 | }); 145 | ``` 146 | 147 | **Important Notes**: 148 | - DummyIDP should ONLY be used for development and testing 149 | - Never use these certificates in production 150 | - The example uses `localhost:3000` - adjust URLs for your environment 151 | - For production, always use proper IdP providers like Okta, Azure AD, or OneLogin 152 | 153 | ### Step 5: Dynamically Registering SAML Providers 154 | 155 | 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. 156 | 157 | Example registration: 158 | 159 | ```typescript 160 | await authClient.sso.register({ 161 | providerId: "okta-prod", 162 | issuer: "https://your-domain.com", 163 | domain: "your-domain.com", 164 | samlConfig: { 165 | // Your production SAML configuration 166 | } 167 | }); 168 | ``` 169 | 170 | ## Additional Resources 171 | 172 | - [SSO Plugin Documentation](/docs/plugins/sso) 173 | - [Okta SAML Documentation](https://developer.okta.com/docs/concepts/saml/) 174 | - [SAML 2.0 Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf) 175 | ``` -------------------------------------------------------------------------------- /packages/cli/src/commands/generate.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Command } from "commander"; 2 | import { getConfig } from "../utils/get-config"; 3 | import * as z from "zod/v4"; 4 | import { existsSync } from "fs"; 5 | import path from "path"; 6 | import { logger, createTelemetry, getTelemetryAuthConfig } from "better-auth"; 7 | import yoctoSpinner from "yocto-spinner"; 8 | import prompts from "prompts"; 9 | import fs from "fs/promises"; 10 | import chalk from "chalk"; 11 | import { getAdapter } from "better-auth/db"; 12 | import { generateSchema } from "../generators"; 13 | 14 | export async function generateAction(opts: any) { 15 | const options = z 16 | .object({ 17 | cwd: z.string(), 18 | config: z.string().optional(), 19 | output: z.string().optional(), 20 | y: z.boolean().optional(), 21 | yes: z.boolean().optional(), 22 | }) 23 | .parse(opts); 24 | 25 | const cwd = path.resolve(options.cwd); 26 | if (!existsSync(cwd)) { 27 | logger.error(`The directory "${cwd}" does not exist.`); 28 | process.exit(1); 29 | } 30 | const config = await getConfig({ 31 | cwd, 32 | configPath: options.config, 33 | }); 34 | if (!config) { 35 | logger.error( 36 | "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.", 37 | ); 38 | return; 39 | } 40 | 41 | const adapter = await getAdapter(config).catch((e) => { 42 | logger.error(e.message); 43 | process.exit(1); 44 | }); 45 | 46 | const spinner = yoctoSpinner({ text: "preparing schema..." }).start(); 47 | 48 | const schema = await generateSchema({ 49 | adapter, 50 | file: options.output, 51 | options: config, 52 | }); 53 | 54 | spinner.stop(); 55 | if (!schema.code) { 56 | logger.info("Your schema is already up to date."); 57 | // telemetry: track generate attempted, no changes 58 | try { 59 | const telemetry = await createTelemetry(config); 60 | await telemetry.publish({ 61 | type: "cli_generate", 62 | payload: { 63 | outcome: "no_changes", 64 | config: getTelemetryAuthConfig(config, { 65 | adapter: adapter.id, 66 | database: 67 | typeof config.database === "function" ? "adapter" : "kysely", 68 | }), 69 | }, 70 | }); 71 | } catch {} 72 | process.exit(0); 73 | } 74 | if (schema.overwrite) { 75 | let confirm = options.y || options.yes; 76 | if (!confirm) { 77 | const response = await prompts({ 78 | type: "confirm", 79 | name: "confirm", 80 | message: `The file ${ 81 | schema.fileName 82 | } already exists. Do you want to ${chalk.yellow( 83 | `${schema.overwrite ? "overwrite" : "append"}`, 84 | )} the schema to the file?`, 85 | }); 86 | confirm = response.confirm; 87 | } 88 | 89 | if (confirm) { 90 | const exist = existsSync(path.join(cwd, schema.fileName)); 91 | if (!exist) { 92 | await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), { 93 | recursive: true, 94 | }); 95 | } 96 | if (schema.overwrite) { 97 | await fs.writeFile(path.join(cwd, schema.fileName), schema.code); 98 | } else { 99 | await fs.appendFile(path.join(cwd, schema.fileName), schema.code); 100 | } 101 | logger.success( 102 | `🚀 Schema was ${ 103 | schema.overwrite ? "overwritten" : "appended" 104 | } successfully!`, 105 | ); 106 | // telemetry: track generate success overwrite/append 107 | try { 108 | const telemetry = await createTelemetry(config); 109 | await telemetry.publish({ 110 | type: "cli_generate", 111 | payload: { 112 | outcome: schema.overwrite ? "overwritten" : "appended", 113 | config: getTelemetryAuthConfig(config), 114 | }, 115 | }); 116 | } catch {} 117 | process.exit(0); 118 | } else { 119 | logger.error("Schema generation aborted."); 120 | // telemetry: track generate aborted 121 | try { 122 | const telemetry = await createTelemetry(config); 123 | await telemetry.publish({ 124 | type: "cli_generate", 125 | payload: { 126 | outcome: "aborted", 127 | config: getTelemetryAuthConfig(config), 128 | }, 129 | }); 130 | } catch {} 131 | process.exit(1); 132 | } 133 | } 134 | 135 | if (options.y) { 136 | console.warn("WARNING: --y is deprecated. Consider -y or --yes"); 137 | options.yes = true; 138 | } 139 | 140 | let confirm = options.yes; 141 | 142 | if (!confirm) { 143 | const response = await prompts({ 144 | type: "confirm", 145 | name: "confirm", 146 | message: `Do you want to generate the schema to ${chalk.yellow( 147 | schema.fileName, 148 | )}?`, 149 | }); 150 | confirm = response.confirm; 151 | } 152 | 153 | if (!confirm) { 154 | logger.error("Schema generation aborted."); 155 | // telemetry: track generate aborted before write 156 | try { 157 | const telemetry = await createTelemetry(config); 158 | await telemetry.publish({ 159 | type: "cli_generate", 160 | payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) }, 161 | }); 162 | } catch {} 163 | process.exit(1); 164 | } 165 | 166 | if (!options.output) { 167 | const dirExist = existsSync(path.dirname(path.join(cwd, schema.fileName))); 168 | if (!dirExist) { 169 | await fs.mkdir(path.dirname(path.join(cwd, schema.fileName)), { 170 | recursive: true, 171 | }); 172 | } 173 | } 174 | await fs.writeFile( 175 | options.output || path.join(cwd, schema.fileName), 176 | schema.code, 177 | ); 178 | logger.success(`🚀 Schema was generated successfully!`); 179 | // telemetry: track generate success 180 | try { 181 | const telemetry = await createTelemetry(config); 182 | await telemetry.publish({ 183 | type: "cli_generate", 184 | payload: { outcome: "generated", config: getTelemetryAuthConfig(config) }, 185 | }); 186 | } catch {} 187 | process.exit(0); 188 | } 189 | 190 | export const generate = new Command("generate") 191 | .option( 192 | "-c, --cwd <cwd>", 193 | "the working directory. defaults to the current directory.", 194 | process.cwd(), 195 | ) 196 | .option( 197 | "--config <config>", 198 | "the path to the configuration file. defaults to the first configuration file found.", 199 | ) 200 | .option("--output <output>", "the file to output to the generated schema") 201 | .option("-y, --yes", "automatically answer yes to all prompts", false) 202 | .option("--y", "(deprecated) same as --yes", false) 203 | .action(generateAction); 204 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/select.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SelectPrimitive from "@radix-ui/react-select"; 5 | import { Check, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Select = SelectPrimitive.Root; 10 | 11 | const SelectGroup = SelectPrimitive.Group; 12 | 13 | const SelectValue = SelectPrimitive.Value; 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef<typeof SelectPrimitive.Trigger>, 17 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> 18 | >(({ className, children, ...props }, ref) => ( 19 | <SelectPrimitive.Trigger 20 | ref={ref} 21 | className={cn( 22 | "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", 23 | className, 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | <SelectPrimitive.Icon asChild> 29 | <ChevronsUpDown className="size-4 opacity-50" /> 30 | </SelectPrimitive.Icon> 31 | </SelectPrimitive.Trigger> 32 | )); 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, 37 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> 38 | >(({ className, ...props }, ref) => ( 39 | <SelectPrimitive.ScrollUpButton 40 | ref={ref} 41 | className={cn( 42 | "flex cursor-default items-center justify-center py-1", 43 | className, 44 | )} 45 | {...props} 46 | > 47 | <ChevronUp className="h-4 w-4" /> 48 | </SelectPrimitive.ScrollUpButton> 49 | )); 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, 54 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> 55 | >(({ className, ...props }, ref) => ( 56 | <SelectPrimitive.ScrollDownButton 57 | ref={ref} 58 | className={cn( 59 | "flex cursor-default items-center justify-center py-1", 60 | className, 61 | )} 62 | {...props} 63 | > 64 | <ChevronDown className="h-4 w-4" /> 65 | </SelectPrimitive.ScrollDownButton> 66 | )); 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName; 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef<typeof SelectPrimitive.Content>, 72 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | <SelectPrimitive.Portal> 75 | <SelectPrimitive.Content 76 | ref={ref} 77 | className={cn( 78 | "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", 79 | position === "popper" && 80 | "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 81 | className, 82 | )} 83 | position={position} 84 | {...props} 85 | > 86 | <SelectScrollUpButton /> 87 | <SelectPrimitive.Viewport 88 | className={cn( 89 | "p-1", 90 | position === "popper" && 91 | "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)", 92 | )} 93 | > 94 | {children} 95 | </SelectPrimitive.Viewport> 96 | <SelectScrollDownButton /> 97 | </SelectPrimitive.Content> 98 | </SelectPrimitive.Portal> 99 | )); 100 | SelectContent.displayName = SelectPrimitive.Content.displayName; 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef<typeof SelectPrimitive.Label>, 104 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> 105 | >(({ className, ...props }, ref) => ( 106 | <SelectPrimitive.Label 107 | ref={ref} 108 | className={cn("px-2 py-1.5 text-sm font-semibold", className)} 109 | {...props} 110 | /> 111 | )); 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef<typeof SelectPrimitive.Item>, 116 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> 117 | >(({ className, children, ...props }, ref) => ( 118 | <SelectPrimitive.Item 119 | ref={ref} 120 | className={cn( 121 | "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", 122 | className, 123 | )} 124 | {...props} 125 | > 126 | <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> 127 | <SelectPrimitive.ItemIndicator> 128 | <Check className="h-4 w-4" /> 129 | </SelectPrimitive.ItemIndicator> 130 | </span> 131 | <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 132 | </SelectPrimitive.Item> 133 | )); 134 | SelectItem.displayName = SelectPrimitive.Item.displayName; 135 | 136 | const SelectSeparator = React.forwardRef< 137 | React.ElementRef<typeof SelectPrimitive.Separator>, 138 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> 139 | >(({ className, ...props }, ref) => ( 140 | <SelectPrimitive.Separator 141 | ref={ref} 142 | className={cn("-mx-1 my-1 h-px bg-muted", className)} 143 | {...props} 144 | /> 145 | )); 146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 147 | 148 | export { 149 | Select, 150 | SelectGroup, 151 | SelectValue, 152 | SelectTrigger, 153 | SelectContent, 154 | SelectLabel, 155 | SelectItem, 156 | SelectSeparator, 157 | SelectScrollUpButton, 158 | SelectScrollDownButton, 159 | }; 160 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/last-login-method/custom-prefix.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { lastLoginMethod } from "."; 4 | import { lastLoginMethodClient } from "./client"; 5 | import { parseCookies } from "../../cookies"; 6 | 7 | describe("lastLoginMethod custom cookie prefix", async () => { 8 | it("should work with default cookie name regardless of custom prefix", async () => { 9 | const { client, cookieSetter, testUser } = await getTestInstance( 10 | { 11 | advanced: { 12 | cookiePrefix: "custom-auth", 13 | }, 14 | plugins: [lastLoginMethod()], 15 | }, 16 | { 17 | clientOptions: { 18 | plugins: [lastLoginMethodClient()], 19 | }, 20 | }, 21 | ); 22 | 23 | const headers = new Headers(); 24 | await client.signIn.email( 25 | { 26 | email: testUser.email, 27 | password: testUser.password, 28 | }, 29 | { 30 | onSuccess(context) { 31 | cookieSetter(headers)(context); 32 | }, 33 | }, 34 | ); 35 | const cookies = parseCookies(headers.get("cookie") || ""); 36 | // Uses exact cookie name from config, not affected by cookiePrefix 37 | expect(cookies.get("better-auth.last_used_login_method")).toBe("email"); 38 | }); 39 | 40 | it("should work with custom cookie name and prefix", async () => { 41 | const { client, cookieSetter, testUser } = await getTestInstance( 42 | { 43 | advanced: { 44 | cookiePrefix: "my-app", 45 | }, 46 | plugins: [lastLoginMethod({ cookieName: "my-app.last_method" })], 47 | }, 48 | { 49 | clientOptions: { 50 | plugins: [lastLoginMethodClient()], 51 | }, 52 | }, 53 | ); 54 | 55 | const headers = new Headers(); 56 | await client.signIn.email( 57 | { 58 | email: testUser.email, 59 | password: testUser.password, 60 | }, 61 | { 62 | onSuccess(context) { 63 | cookieSetter(headers)(context); 64 | }, 65 | }, 66 | ); 67 | const cookies = parseCookies(headers.get("cookie") || ""); 68 | expect(cookies.get("my-app.last_method")).toBe("email"); 69 | }); 70 | 71 | it("should work with custom cookie name regardless of prefix", async () => { 72 | const { client, cookieSetter, testUser } = await getTestInstance( 73 | { 74 | advanced: { 75 | cookiePrefix: "my-app", 76 | }, 77 | plugins: [lastLoginMethod({ cookieName: "last_login_method" })], 78 | }, 79 | { 80 | clientOptions: { 81 | plugins: [lastLoginMethodClient()], 82 | }, 83 | }, 84 | ); 85 | 86 | const headers = new Headers(); 87 | await client.signIn.email( 88 | { 89 | email: testUser.email, 90 | password: testUser.password, 91 | }, 92 | { 93 | onSuccess(context) { 94 | cookieSetter(headers)(context); 95 | }, 96 | }, 97 | ); 98 | const cookies = parseCookies(headers.get("cookie") || ""); 99 | // Uses exact cookie name from config, not affected by cookiePrefix 100 | expect(cookies.get("last_login_method")).toBe("email"); 101 | }); 102 | 103 | it("should work with cross-subdomain and custom prefix", async () => { 104 | const { client, testUser } = await getTestInstance( 105 | { 106 | baseURL: "https://auth.example.com", 107 | advanced: { 108 | cookiePrefix: "custom-auth", 109 | crossSubDomainCookies: { 110 | enabled: true, 111 | domain: "example.com", 112 | }, 113 | }, 114 | plugins: [lastLoginMethod()], 115 | }, 116 | { 117 | clientOptions: { 118 | plugins: [lastLoginMethodClient()], 119 | }, 120 | }, 121 | ); 122 | 123 | await client.signIn.email( 124 | { 125 | email: testUser.email, 126 | password: testUser.password, 127 | }, 128 | { 129 | onResponse(context) { 130 | const setCookie = context.response.headers.get("set-cookie"); 131 | expect(setCookie).toContain("Domain=example.com"); 132 | expect(setCookie).toContain("SameSite=Lax"); 133 | // Uses exact cookie name from config, not affected by cookiePrefix 134 | expect(setCookie).toContain( 135 | "better-auth.last_used_login_method=email", 136 | ); 137 | }, 138 | }, 139 | ); 140 | }); 141 | 142 | it("should work with cross-origin cookies", async () => { 143 | const { client, testUser } = await getTestInstance( 144 | { 145 | baseURL: "https://api.example.com", 146 | advanced: { 147 | crossOriginCookies: { 148 | enabled: true, 149 | }, 150 | defaultCookieAttributes: { 151 | sameSite: "none", 152 | secure: true, 153 | }, 154 | }, 155 | plugins: [lastLoginMethod()], 156 | }, 157 | { 158 | clientOptions: { 159 | plugins: [lastLoginMethodClient()], 160 | }, 161 | }, 162 | ); 163 | 164 | await client.signIn.email( 165 | { 166 | email: testUser.email, 167 | password: testUser.password, 168 | }, 169 | { 170 | onResponse(context) { 171 | const setCookie = context.response.headers.get("set-cookie"); 172 | expect(setCookie).toContain("SameSite=None"); 173 | expect(setCookie).toContain("Secure"); 174 | // Should not contain Domain attribute for cross-origin 175 | expect(setCookie).not.toContain("Domain="); 176 | expect(setCookie).toContain( 177 | "better-auth.last_used_login_method=email", 178 | ); 179 | }, 180 | }, 181 | ); 182 | }); 183 | 184 | it("should handle cross-origin on localhost for development", async () => { 185 | const { client, testUser } = await getTestInstance( 186 | { 187 | baseURL: "http://localhost:3000", 188 | advanced: { 189 | crossOriginCookies: { 190 | enabled: true, 191 | allowLocalhostUnsecure: true, 192 | }, 193 | defaultCookieAttributes: { 194 | sameSite: "none", 195 | secure: false, 196 | }, 197 | }, 198 | plugins: [lastLoginMethod()], 199 | }, 200 | { 201 | clientOptions: { 202 | plugins: [lastLoginMethodClient()], 203 | }, 204 | }, 205 | ); 206 | 207 | await client.signIn.email( 208 | { 209 | email: testUser.email, 210 | password: testUser.password, 211 | }, 212 | { 213 | onResponse(context) { 214 | const setCookie = context.response.headers.get("set-cookie"); 215 | expect(setCookie).toContain("SameSite=None"); 216 | // Should not contain Secure on localhost when allowLocalhostUnsecure is true 217 | expect(setCookie).not.toContain("Secure"); 218 | expect(setCookie).toContain( 219 | "better-auth.last_used_login_method=email", 220 | ); 221 | }, 222 | }, 223 | ); 224 | }); 225 | }); 226 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/dodopayments.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Dodo Payments 3 | description: Better Auth Plugin for Dodo Payments 4 | --- 5 | 6 | [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. 7 | 8 | <Card 9 | href="https://discord.gg/bYqAp4ayYh" 10 | title="Get support on Dodo Payments' Discord" 11 | > 12 | This plugin is maintained by the Dodo Payments team.<br /> 13 | Have questions? Our team is available on Discord to assist you anytime. 14 | </Card> 15 | 16 | ## Features 17 | 18 | - Automatic customer creation on sign-up 19 | - Type-safe checkout flows with product slug mapping 20 | - Self-service customer portal 21 | - Real-time webhook event processing with signature verification 22 | 23 | <Card href="https://app.dodopayments.com" title="Get started with Dodo Payments"> 24 | You need a Dodo Payments account and API keys to use this integration. 25 | </Card> 26 | 27 | ## Installation 28 | 29 | <Steps> 30 | <Step title="Install dependencies"> 31 | Run the following command in your project root: 32 | ```bash 33 | npm install @dodopayments/better-auth dodopayments better-auth zod 34 | ``` 35 | 36 | </Step> 37 | <Step title="Configure environment variables"> 38 | Add these to your `.env` file: 39 | ```txt 40 | DODO_PAYMENTS_API_KEY=your_api_key_here 41 | DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here 42 | ``` 43 | </Step> 44 | 45 | <Step title="Set up server-side integration"> 46 | Create or update `src/lib/auth.ts`: 47 | ```typescript 48 | import { betterAuth } from "better-auth"; 49 | import { 50 | dodopayments, 51 | checkout, 52 | portal, 53 | webhooks, 54 | } from "@dodopayments/better-auth"; 55 | import DodoPayments from "dodopayments"; 56 | 57 | export const dodoPayments = new DodoPayments({ 58 | bearerToken: process.env.DODO_PAYMENTS_API_KEY!, 59 | environment: "test_mode" 60 | }); 61 | 62 | export const auth = betterAuth({ 63 | plugins: [ 64 | dodopayments({ 65 | client: dodoPayments, 66 | createCustomerOnSignUp: true, 67 | use: [ 68 | checkout({ 69 | products: [ 70 | { 71 | productId: "pdt_xxxxxxxxxxxxxxxxxxxxx", 72 | slug: "premium-plan", 73 | }, 74 | ], 75 | successUrl: "/dashboard/success", 76 | authenticatedUsersOnly: true, 77 | }), 78 | portal(), 79 | webhooks({ 80 | webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, 81 | onPayload: async (payload) => { 82 | console.log("Received webhook:", payload.event_type); 83 | }, 84 | }), 85 | ], 86 | }), 87 | ], 88 | }); 89 | ``` 90 | <Card> 91 | Set `environment` to `live_mode` for production. 92 | </Card> 93 | </Step> 94 | 95 | <Step title="Set up client-side integration"> 96 | Create or update `src/lib/auth-client.ts`: 97 | ```typescript 98 | import { dodopaymentsClient } from "@dodopayments/better-auth"; 99 | 100 | export const authClient = createAuthClient({ 101 | baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", 102 | plugins: [dodopaymentsClient()], 103 | }); 104 | ``` 105 | </Step> 106 | </Steps> 107 | 108 | ## Usage 109 | 110 | ### Creating a Checkout Session 111 | 112 | ```typescript 113 | const { data: checkout, error } = await authClient.dodopayments.checkout({ 114 | slug: "premium-plan", 115 | customer: { 116 | email: "[email protected]", 117 | name: "John Doe", 118 | }, 119 | billing: { 120 | city: "San Francisco", 121 | country: "US", 122 | state: "CA", 123 | street: "123 Market St", 124 | zipcode: "94103", 125 | }, 126 | referenceId: "order_123", 127 | }); 128 | 129 | if (checkout) { 130 | window.location.href = checkout.url; 131 | } 132 | ``` 133 | 134 | ### Accessing the Customer Portal 135 | 136 | ```typescript 137 | const { data: customerPortal, error } = await authClient.dodopayments.customer.portal(); 138 | if (customerPortal && customerPortal.redirect) { 139 | window.location.href = customerPortal.url; 140 | } 141 | ``` 142 | 143 | ### Listing Customer Data 144 | 145 | ```typescript 146 | // Get subscriptions 147 | const { data: subscriptions, error } = 148 | await authClient.dodopayments.customer.subscriptions.list({ 149 | query: { 150 | limit: 10, 151 | page: 1, 152 | active: true, 153 | }, 154 | }); 155 | 156 | // Get payment history 157 | const { data: payments, error } = await authClient.dodopayments.customer.payments.list({ 158 | query: { 159 | limit: 10, 160 | page: 1, 161 | status: "succeeded", 162 | }, 163 | }); 164 | ``` 165 | 166 | ### Webhooks 167 | 168 | <Card> 169 | The webhooks plugin processes real-time payment events from Dodo Payments with secure signature verification. The default endpoint is `/api/auth/dodopayments/webhooks`. 170 | </Card> 171 | 172 | <Steps> 173 | <Step title="Generate and set webhook secret"> 174 | 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: 175 | ```txt 176 | DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here 177 | ``` 178 | </Step> 179 | 180 | <Step title="Handle webhook events"> 181 | Example handler: 182 | ```typescript 183 | webhooks({ 184 | webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, 185 | onPayload: async (payload) => { 186 | console.log("Received webhook:", payload.event_type); 187 | }, 188 | }); 189 | ``` 190 | </Step> 191 | </Steps> 192 | 193 | ## Configuration Reference 194 | 195 | ### Plugin Options 196 | 197 | - **client** (required): DodoPayments client instance 198 | - **createCustomerOnSignUp** (optional): Auto-create customers on user signup 199 | - **use** (required): Array of plugins to enable (checkout, portal, webhooks) 200 | 201 | ### Checkout Plugin Options 202 | 203 | - **products**: Array of products or async function returning products 204 | - **successUrl**: URL to redirect after successful payment 205 | - **authenticatedUsersOnly**: Require user authentication (default: false) 206 | 207 | If you encounter any issues, please refer to the [Dodo Payments documentation](https://docs.dodopayments.com) for troubleshooting steps. 208 | ``` -------------------------------------------------------------------------------- /docs/components/ui/select.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SelectPrimitive from "@radix-ui/react-select"; 5 | import { Check, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Select = SelectPrimitive.Root; 10 | 11 | const SelectGroup = SelectPrimitive.Group; 12 | 13 | const SelectValue = SelectPrimitive.Value; 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef<typeof SelectPrimitive.Trigger>, 17 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> 18 | >(({ className, children, ...props }, ref) => ( 19 | <SelectPrimitive.Trigger 20 | ref={ref} 21 | className={cn( 22 | "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", 23 | className, 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | <SelectPrimitive.Icon asChild> 29 | <ChevronsUpDown className="size-4 opacity-50" /> 30 | </SelectPrimitive.Icon> 31 | </SelectPrimitive.Trigger> 32 | )); 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, 37 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> 38 | >(({ className, ...props }, ref) => ( 39 | <SelectPrimitive.ScrollUpButton 40 | ref={ref} 41 | className={cn( 42 | "flex cursor-default items-center justify-center py-1", 43 | className, 44 | )} 45 | {...props} 46 | > 47 | <ChevronUp className="h-4 w-4" /> 48 | </SelectPrimitive.ScrollUpButton> 49 | )); 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, 54 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> 55 | >(({ className, ...props }, ref) => ( 56 | <SelectPrimitive.ScrollDownButton 57 | ref={ref} 58 | className={cn( 59 | "flex cursor-default items-center justify-center py-1", 60 | className, 61 | )} 62 | {...props} 63 | > 64 | <ChevronDown className="h-4 w-4" /> 65 | </SelectPrimitive.ScrollDownButton> 66 | )); 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName; 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef<typeof SelectPrimitive.Content>, 72 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | <SelectPrimitive.Portal> 75 | <SelectPrimitive.Content 76 | ref={ref} 77 | className={cn( 78 | "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", 79 | position === "popper" && 80 | "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 81 | className, 82 | )} 83 | position={position} 84 | {...props} 85 | > 86 | <SelectScrollUpButton /> 87 | <SelectPrimitive.Viewport 88 | className={cn( 89 | "p-1", 90 | position === "popper" && 91 | "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", 92 | )} 93 | > 94 | {children} 95 | </SelectPrimitive.Viewport> 96 | <SelectScrollDownButton /> 97 | </SelectPrimitive.Content> 98 | </SelectPrimitive.Portal> 99 | )); 100 | SelectContent.displayName = SelectPrimitive.Content.displayName; 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef<typeof SelectPrimitive.Label>, 104 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> 105 | >(({ className, ...props }, ref) => ( 106 | <SelectPrimitive.Label 107 | ref={ref} 108 | className={cn("px-2 py-1.5 text-sm font-semibold", className)} 109 | {...props} 110 | /> 111 | )); 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef<typeof SelectPrimitive.Item>, 116 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> 117 | >(({ className, children, ...props }, ref) => ( 118 | <SelectPrimitive.Item 119 | ref={ref} 120 | className={cn( 121 | "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", 122 | className, 123 | )} 124 | {...props} 125 | > 126 | <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> 127 | <SelectPrimitive.ItemIndicator> 128 | <Check className="h-4 w-4" /> 129 | </SelectPrimitive.ItemIndicator> 130 | </span> 131 | <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 132 | </SelectPrimitive.Item> 133 | )); 134 | SelectItem.displayName = SelectPrimitive.Item.displayName; 135 | 136 | const SelectSeparator = React.forwardRef< 137 | React.ElementRef<typeof SelectPrimitive.Separator>, 138 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> 139 | >(({ className, ...props }, ref) => ( 140 | <SelectPrimitive.Separator 141 | ref={ref} 142 | className={cn("-mx-1 my-1 h-px bg-muted", className)} 143 | {...props} 144 | /> 145 | )); 146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 147 | 148 | export { 149 | Select, 150 | SelectGroup, 151 | SelectValue, 152 | SelectTrigger, 153 | SelectContent, 154 | SelectLabel, 155 | SelectItem, 156 | SelectSeparator, 157 | SelectScrollUpButton, 158 | SelectScrollDownButton, 159 | }; 160 | ```