This is page 19 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /e2e/smoke/test/fixtures/cloudflare/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- ```json { "version": "6", "dialect": "sqlite", "id": "dde09aa0-ff07-4e38-a49a-742a3c2b7af4", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { "name": "account", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false }, "account_id": { "name": "account_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "provider_id": { "name": "provider_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "user_id": { "name": "user_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "access_token": { "name": "access_token", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "refresh_token": { "name": "refresh_token", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "id_token": { "name": "id_token", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "access_token_expires_at": { "name": "access_token_expires_at", "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false }, "refresh_token_expires_at": { "name": "refresh_token_expires_at", "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false }, "scope": { "name": "scope", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "password": { "name": "password", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "created_at": { "name": "created_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false } }, "indexes": {}, "foreignKeys": { "account_user_id_user_id_fk": { "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", "columnsFrom": ["user_id"], "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, "session": { "name": "session", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false }, "expires_at": { "name": "expires_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "token": { "name": "token", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "created_at": { "name": "created_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "ip_address": { "name": "ip_address", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "user_agent": { "name": "user_agent", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "user_id": { "name": "user_id", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false } }, "indexes": { "session_token_unique": { "name": "session_token_unique", "columns": ["token"], "isUnique": true } }, "foreignKeys": { "session_user_id_user_id_fk": { "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", "columnsFrom": ["user_id"], "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, "user": { "name": "user", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false }, "name": { "name": "name", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "email": { "name": "email", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "email_verified": { "name": "email_verified", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "image": { "name": "image", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, "created_at": { "name": "created_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false } }, "indexes": { "user_email_unique": { "name": "user_email_unique", "columns": ["email"], "isUnique": true } }, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, "verification": { "name": "verification", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, "notNull": true, "autoincrement": false }, "identifier": { "name": "identifier", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "value": { "name": "value", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }, "expires_at": { "name": "expires_at", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, "created_at": { "name": "created_at", "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false }, "updated_at": { "name": "updated_at", "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false } }, "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} } }, "views": {}, "enums": {}, "_meta": { "schemas": {}, "tables": {}, "columns": {} }, "internal": { "indexes": {} } } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/chart.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as RechartsPrimitive from "recharts"; import { cn } from "@/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType; } & ( | { color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> } ); }; type ChartContextProps = { config: ChartConfig; }; const ChartContext = React.createContext<ChartContextProps | null>(null); function useChart() { const context = React.useContext(ChartContext); if (!context) { throw new Error("useChart must be used within a <ChartContainer />"); } return context; } const ChartContainer = ({ ref, id, className, children, config, ...props }) => { const uniqueId = React.useId(); const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; return ( <ChartContext.Provider value={{ config }}> <div data-chart={chartId} ref={ref} className={cn( "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", className, )} {...props} > <ChartStyle id={chartId} config={config} /> <RechartsPrimitive.ResponsiveContainer> {children} </RechartsPrimitive.ResponsiveContainer> </div> </ChartContext.Provider> ); }; ChartContainer.displayName = "Chart"; const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([_, config]) => config.theme || config.color, ); if (!colorConfig.length) { return null; } return ( <style dangerouslySetInnerHTML={{ __html: Object.entries(THEMES) .map( ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color; return color ? ` --color-${key}: ${color};` : null; }) .join("\n")} } `, ) .join("\n"), }} /> ); }; const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltipContent = ({ ref, active, payload, className, indicator = "dot", hideLabel = false, hideIndicator = false, label, labelFormatter, labelClassName, formatter, color, nameKey, labelKey, }) => { const { config } = useChart(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { return null; } const [item] = payload; const key = `${labelKey || item.dataKey || item.name || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label; if (labelFormatter) { return ( <div className={cn("font-medium", labelClassName)}> {labelFormatter(value, payload)} </div> ); } if (!value) { return null; } return <div className={cn("font-medium", labelClassName)}>{value}</div>; }, [ label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey, ]); if (!active || !payload?.length) { return null; } const nestLabel = payload.length === 1 && indicator !== "dot"; return ( <div ref={ref} className={cn( "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", className, )} > {!nestLabel ? tooltipLabel : null} <div className="grid gap-1.5"> {payload.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = color || item.payload.fill || item.color; return ( <div key={item.dataKey} className={cn( "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", indicator === "dot" && "items-center", )} > {formatter && item?.value !== undefined && item.name ? ( formatter(item.value, item.name, item, index, item.payload) ) : ( <> {itemConfig?.icon ? ( <itemConfig.icon /> ) : ( !hideIndicator && ( <div className={cn( "shrink-0 rounded-[2px] border-border bg-(--color-bg)", { "h-2.5 w-2.5": indicator === "dot", "w-1": indicator === "line", "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed", "my-0.5": nestLabel && indicator === "dashed", }, )} style={ { "--color-bg": indicatorColor, "--color-border": indicatorColor, } as React.CSSProperties } /> ) )} <div className={cn( "flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center", )} > <div className="grid gap-1.5"> {nestLabel ? tooltipLabel : null} <span className="text-muted-foreground"> {itemConfig?.label || item.name} </span> </div> {item.value && ( <span className="font-mono font-medium tabular-nums text-foreground"> {item.value.toLocaleString()} </span> )} </div> </> )} </div> ); })} </div> </div> ); }; ChartTooltipContent.displayName = "ChartTooltip"; const ChartLegend = RechartsPrimitive.Legend; const ChartLegendContent = ({ ref, className, hideIcon = false, payload, verticalAlign = "bottom", nameKey, }) => { const { config } = useChart(); if (!payload?.length) { return null; } return ( <div ref={ref} className={cn( "flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className, )} > {payload.map((item) => { const key = `${nameKey || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); return ( <div key={item.value} className={cn( "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", )} > {itemConfig?.icon && !hideIcon ? ( <itemConfig.icon /> ) : ( <div className="h-2 w-2 shrink-0 rounded-[2px]" style={{ backgroundColor: item.color, }} /> )} {itemConfig?.label} </div> ); })} </div> ); }; ChartLegendContent.displayName = "ChartLegend"; // Helper to extract item config from a payload. function getPayloadConfigFromPayload( config: ChartConfig, payload: unknown, key: string, ) { if (typeof payload !== "object" || payload === null) { return undefined; } const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined; let configLabelKey: string = key; if ( key in payload && typeof payload[key as keyof typeof payload] === "string" ) { configLabelKey = payload[key as keyof typeof payload] as string; } else if ( payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string" ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload ] as string; } return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; } export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, }; ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/username.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Username description: Username plugin --- The username plugin is a lightweight plugin that adds username support to the email and password authenticator. This allows users to sign in and sign up with their username instead of their email. ## Installation <Steps> <Step> ### Add Plugin to the server ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" export const auth = betterAuth({ plugins: [ // [!code highlight] username() // [!code highlight] ] // [!code highlight] }) ``` </Step> <Step> ### Migrate the database Run the migration or generate the schema to add the necessary fields and tables to the database. <Tabs items={["migrate", "generate"]}> <Tab value="migrate"> ```bash npx @better-auth/cli migrate ``` </Tab> <Tab value="generate"> ```bash npx @better-auth/cli generate ``` </Tab> </Tabs> See the [Schema](#schema) section to add the fields manually. </Step> <Step> ### Add the client plugin ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { usernameClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [ // [!code highlight] usernameClient() // [!code highlight] ] // [!code highlight] }) ``` </Step> </Steps> ## Usage ### Sign up To sign up a user with username, you can use the existing `signUp.email` function provided by the client. The `signUp` function should take a new `username` property in the object. <APIMethod path="/sign-up/email" method="POST"> ```ts type signUpEmail = { /** * The email of the user. */ email: string = "[email protected]" /** * The name of the user. */ name: string = "Test User" /** * The password of the user. */ password: string = "password1234" /** * The username of the user. */ username: string = "test" /** * An optional display username of the user. */ displayUsername?: string = "Test User123" } ``` </APIMethod> <Callout type="info"> If only `username` is provided, the `displayUsername` will be set to the pre normalized version of the `username`. You can see the [Username Normalization](#username-normalization) and [Display Username Normalization](#display-username-normalization) sections for more details. </Callout> ### Sign in To sign in a user with username, you can use the `signIn.username` function provided by the client. <APIMethod path="/sign-in/username" method="POST"> ```ts type signInUsername = { /** * The username of the user. */ username: string = "test" /** * The password of the user. */ password: string = "password1234" } ``` </APIMethod> ### Update username To update the username of a user, you can use the `updateUser` function provided by the client. <APIMethod path="/update-user" method="POST"> ```ts type updateUser = { /** * The username to update. */ username?: string = "new-username" } ``` </APIMethod> ### Check if username is available To check if a username is available, you can use the `isUsernameAvailable` function provided by the client. <APIMethod path="/is-username-available" method="POST" resultVariable="response"> ```ts type isUsernameAvailable = { /** * The username to check. */ username: string = "new-username" } if(response?.available) { console.log("Username is available"); } else { console.log("Username is not available"); } ``` </APIMethod> ## Options ### Min Username Length The minimum length of the username. Default is `3`. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ minUsernameLength: 5 }) ] }) ``` ### Max Username Length The maximum length of the username. Default is `30`. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ maxUsernameLength: 100 }) ] }) ``` ### Username Validator A function that validates the username. The function should return false if the username is invalid. By default, the username should only contain alphanumeric characters, underscores, and dots. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ usernameValidator: (username) => { if (username === "admin") { return false } return true } }) ] }) ``` ### Display Username Validator A function that validates the display username. The function should return false if the display username is invalid. By default, no validation is applied to display username. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ displayUsernameValidator: (displayUsername) => { // Allow only alphanumeric characters, underscores, and hyphens return /^[a-zA-Z0-9_-]+$/.test(displayUsername) } }) ] }) ``` ### Username Normalization A function that normalizes the username, or `false` if you want to disable normalization. By default, usernames are normalized to lowercase, so "TestUser" and "testuser", for example, are considered the same username. The `username` field will contain the normalized (lower case) username, while `displayUsername` will contain the original `username`. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ usernameNormalization: (username) => { return username.toLowerCase() .replaceAll("0", "o") .replaceAll("3", "e") .replaceAll("4", "a"); } }) ] }) ``` ### Display Username Normalization A function that normalizes the display username, or `false` to disable normalization. By default, display usernames are not normalized. When only `username` is provided during signup or update, the `displayUsername` will be set to match the original `username` value (before normalization). You can also explicitly set a `displayUsername` which will be preserved as-is. For custom normalization, provide a function that takes the display username as input and returns the normalized version. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ displayUsernameNormalization: (displayUsername) => displayUsername.toLowerCase(), }) ] }) ``` ### Validation Order By default, username and display username are validated before normalization. You can change this behavior by setting `validationOrder` to `post-normalization`. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { username } from "better-auth/plugins" const auth = betterAuth({ plugins: [ username({ validationOrder: { username: "post-normalization", displayUsername: "post-normalization", } }) ] }) ``` ## Schema The plugin requires 2 fields to be added to the user table: <DatabaseTable fields={[ { name: "username", type: "string", description: "The username of the user", isUnique: true }, { name: "displayUsername", type: "string", description: "Non normalized username of the user", isUnique: true }, ]} /> ``` -------------------------------------------------------------------------------- /docs/components/ui/sparkles.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useId } from "react"; import { useEffect, useState } from "react"; import Particles, { initParticlesEngine } from "@tsparticles/react"; import type { Container, SingleOrMultiple } from "@tsparticles/engine"; import { loadSlim } from "@tsparticles/slim"; import { cn } from "@/lib/utils"; import { motion, useAnimation } from "framer-motion"; type ParticlesProps = { id?: string; className?: string; background?: string; particleSize?: number; minSize?: number; maxSize?: number; speed?: number; particleColor?: string; particleDensity?: number; }; export const SparklesCore = (props: ParticlesProps) => { const { id, className, background, minSize, maxSize, speed, particleColor, particleDensity, } = props; const [init, setInit] = useState(false); useEffect(() => { initParticlesEngine(async (engine) => { await loadSlim(engine); }).then(() => { setInit(true); }); }, []); const controls = useAnimation(); const particlesLoaded = async (container?: Container) => { if (container) { console.log(container); // biome-ignore lint/nursery/noFloatingPromises: add error handling is not important controls.start({ opacity: 1, transition: { duration: 1, }, }); } }; const generatedId = useId(); return ( <motion.div animate={controls} className={cn("opacity-0", className)}> {init && ( <Particles id={id || generatedId} className={cn("h-full w-full")} particlesLoaded={particlesLoaded} options={{ background: { color: { value: background || "#0d47a1", }, }, fullScreen: { enable: false, zIndex: 1, }, fpsLimit: 120, interactivity: { events: { onClick: { enable: true, mode: "push", }, onHover: { enable: false, mode: "repulse", }, resize: true as any, }, modes: { push: { quantity: 4, }, repulse: { distance: 200, duration: 0.4, }, }, }, particles: { bounce: { horizontal: { value: 1, }, vertical: { value: 1, }, }, collisions: { absorb: { speed: 2, }, bounce: { horizontal: { value: 1, }, vertical: { value: 1, }, }, enable: false, maxSpeed: 50, mode: "bounce", overlap: { enable: true, retries: 0, }, }, color: { value: particleColor || "#ffffff", animation: { h: { count: 0, enable: false, speed: 1, decay: 0, delay: 0, sync: true, offset: 0, }, s: { count: 0, enable: false, speed: 1, decay: 0, delay: 0, sync: true, offset: 0, }, l: { count: 0, enable: false, speed: 1, decay: 0, delay: 0, sync: true, offset: 0, }, }, }, effect: { close: true, fill: true, options: {}, type: {} as SingleOrMultiple<string> | undefined, }, groups: {}, move: { angle: { offset: 0, value: 90, }, attract: { distance: 200, enable: false, rotate: { x: 3000, y: 3000, }, }, center: { x: 50, y: 50, mode: "percent", radius: 0, }, decay: 0, distance: {}, direction: "none", drift: 0, enable: true, gravity: { acceleration: 9.81, enable: false, inverse: false, maxSpeed: 50, }, path: { clamp: true, delay: { value: 0, }, enable: false, options: {}, }, outModes: { default: "out", }, random: false, size: false, speed: { min: 0.1, max: 1, }, spin: { acceleration: 0, enable: false, }, straight: false, trail: { enable: false, length: 10, fill: {}, }, vibrate: false, warp: false, }, number: { density: { enable: true, width: 400, height: 400, }, limit: { mode: "delete", value: 0, }, value: particleDensity || 120, }, opacity: { value: { min: 0.1, max: 1, }, animation: { count: 0, enable: true, speed: speed || 4, decay: 0, delay: 0, sync: false, mode: "auto", startValue: "random", destroy: "none", }, }, reduceDuplicates: false, shadow: { blur: 0, color: { value: "#000", }, enable: false, offset: { x: 0, y: 0, }, }, shape: { close: true, fill: true, options: {}, type: "circle", }, size: { value: { min: minSize || 1, max: maxSize || 3, }, animation: { count: 0, enable: false, speed: 5, decay: 0, delay: 0, sync: false, mode: "auto", startValue: "random", destroy: "none", }, }, stroke: { width: 0, }, zIndex: { value: 0, opacityRate: 1, sizeRate: 1, velocityRate: 1, }, destroy: { bounds: {}, mode: "none", split: { count: 1, factor: { value: 3, }, rate: { value: { min: 4, max: 9, }, }, sizeOffset: true, }, }, roll: { darken: { enable: false, value: 0, }, enable: false, enlighten: { enable: false, value: 0, }, mode: "vertical", speed: 25, }, tilt: { value: 0, animation: { enable: false, speed: 0, decay: 0, sync: false, }, direction: "clockwise", enable: false, }, twinkle: { lines: { enable: false, frequency: 0.05, opacity: 1, }, particles: { enable: false, frequency: 0.05, opacity: 1, }, }, wobble: { distance: 5, enable: false, speed: { angle: 50, move: 10, }, }, life: { count: 0, delay: { value: 0, sync: false, }, duration: { value: 0, sync: false, }, }, rotate: { value: 0, animation: { enable: false, speed: 0, decay: 0, sync: false, }, direction: "clockwise", path: false, }, orbit: { animation: { count: 0, enable: false, speed: 1, decay: 0, delay: 0, sync: false, }, enable: false, opacity: 1, rotation: { value: 45, }, width: 1, }, links: { blink: false, color: { value: "#fff", }, consent: false, distance: 100, enable: false, frequency: 1, opacity: 1, shadow: { blur: 5, color: { value: "#000", }, enable: false, }, triangles: { enable: false, frequency: 1, }, width: 1, warp: false, }, repulse: { value: 0, enabled: false, distance: 1, duration: 1, factor: 1, speed: 1, }, }, detectRetina: true, }} /> )} </motion.div> ); }; ``` -------------------------------------------------------------------------------- /docs/components/ui/code-block.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Check, Copy } from "lucide-react"; import { ButtonHTMLAttributes, type ComponentProps, createContext, forwardRef, type HTMLAttributes, ReactElement, type ReactNode, type RefObject, useCallback, useContext, useMemo, useRef, } from "react"; import { cn } from "@/lib/utils"; import { useCopyButton } from "./use-copy-button"; import { buttonVariants } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { mergeRefs } from "@/lib/utils"; import { ScrollArea, ScrollBar, ScrollViewport } from "./scroll-area"; export interface CodeBlockProps extends ComponentProps<"figure"> { /** * Icon of code block * * When passed as a string, it assumes the value is the HTML of icon */ icon?: ReactNode; /** * Allow to copy code with copy button * * @defaultValue true */ allowCopy?: boolean; /** * Keep original background color generated by Shiki or Rehype Code * * @defaultValue false */ keepBackground?: boolean; viewportProps?: HTMLAttributes<HTMLElement>; /** * show line numbers */ "data-line-numbers"?: boolean; /** * @defaultValue 1 */ "data-line-numbers-start"?: number; Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode; } const TabsContext = createContext<{ containerRef: RefObject<HTMLDivElement | null>; nested: boolean; } | null>(null); export function Pre(props: ComponentProps<"pre">) { return ( <pre {...props} className={cn("min-w-full w-max *:flex *:flex-col", props.className)} > {props.children} </pre> ); } export function CodeBlock({ ref, title, allowCopy, keepBackground = false, icon, viewportProps = {}, children, Actions = (props) => ( <div {...props} className={cn("empty:hidden", props.className)} /> ), ...props }: CodeBlockProps) { const isTab = useContext(TabsContext) !== null; const areaRef = useRef<HTMLDivElement>(null); allowCopy ??= !isTab; const bg = cn( "bg-fd-secondary", keepBackground && "bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)", ); const onCopy = useCallback(() => { const pre = areaRef.current?.getElementsByTagName("pre").item(0); if (!pre) return; const clone = pre.cloneNode(true) as HTMLElement; clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { node.remove(); }); void navigator.clipboard.writeText(clone.textContent ?? ""); }, []); return ( <figure ref={ref} dir="ltr" {...props} className={cn( isTab ? [bg, "rounded-lg"] : "my-4 rounded-lg bg-fd-card", "group shiki relative border shadow-sm outline-none not-prose overflow-hidden text-sm", props.className, )} > {title ? ( <div className={cn( "group flex text-fd-muted-foreground items-center gap-2 ps-3 h-9.5 pr-1 bg-fd-muted", isTab && "border-b", )} > {typeof icon === "string" ? ( <div className="[&_svg]:size-3.5" dangerouslySetInnerHTML={{ __html: icon, }} /> ) : ( icon )} <figcaption className="flex-1 truncate">{title}</figcaption> {Actions({ children: allowCopy && <CopyButton onCopy={onCopy} />, })} </div> ) : ( Actions({ className: "absolute top-1 right-1 z-2 text-fd-muted-foreground", children: allowCopy && <CopyButton onCopy={onCopy} />, }) )} <div ref={areaRef} {...viewportProps} className={cn( !isTab && [bg, "rounded-none border border-x-0 border-b-0"], "text-[13px] overflow-auto max-h-[600px] bg-fd-muted/50 fd-scroll-container", viewportProps.className, !title && "border-t-0", )} style={ { // space for toolbar "--padding-right": !title ? "calc(var(--spacing) * 8)" : undefined, counterSet: props["data-line-numbers"] ? `line ${Number(props["data-line-numbers-start"] ?? 1) - 1}` : undefined, ...viewportProps.style, } as object } > {children} </div> </figure> ); } function CopyButton({ className, onCopy, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & { onCopy: () => void; }): ReactElement { const [checked, onClick] = useCopyButton(onCopy); return ( <button type="button" className={cn( buttonVariants({ variant: "ghost", size: "icon", }), "transition-opacity size-7 border-none group-hover:opacity-100", "opacity-0 group-hover:opacity-100", "group-hover:opacity-100", className, )} aria-label="Copy Text" onClick={onClick} {...props} > <Check className={cn("size-3.5 transition-transform", !checked && "scale-0")} /> <Copy className={cn( "absolute size-3.5 transition-transform", checked && "scale-0", )} /> </button> ); } export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) { const containerRef = useRef<HTMLDivElement>(null); const nested = useContext(TabsContext) !== null; return ( <Tabs ref={mergeRefs(containerRef, ref)} {...props} className={cn( "bg-fd-card p-1 rounded-xl border overflow-hidden", !nested && "my-4", props.className, )} > <TabsContext.Provider value={useMemo( () => ({ containerRef, nested, }), [nested], )} > {props.children} </TabsContext.Provider> </Tabs> ); } export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) { const { containerRef, nested } = useContext(TabsContext)!; return ( <TabsList {...props} className={cn( "flex flex-row overflow-x-auto px-1 -mx-1 text-fd-muted-foreground", props.className, )} > {props.children} </TabsList> ); } export function CodeBlockTabsTrigger({ children, ...props }: ComponentProps<typeof TabsTrigger>) { return ( <TabsTrigger {...props} className={cn( "relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 first:ms-1 py-1.5 hover:text-fd-accent-foreground data-[state=active]:text-fd-primary [&_svg]:size-3.5", props.className, )} > <div className="absolute inset-x-2 bottom-0 h-px group-data-[state=active]:bg-fd-primary" /> {children} </TabsTrigger> ); } // TODO: currently Vite RSC plugin has problem with adding `asChild` here, maybe revisit this in future export const CodeBlockTab = TabsContent; export const CodeBlockOld = forwardRef<HTMLElement, CodeBlockProps>( ( { title, allowCopy = true, keepBackground = false, icon, viewportProps, ...props }, ref, ) => { const areaRef = useRef<HTMLDivElement>(null); const onCopy = useCallback(() => { const pre = areaRef.current?.getElementsByTagName("pre").item(0); if (!pre) return; const clone = pre.cloneNode(true) as HTMLElement; clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { node.remove(); }); void navigator.clipboard.writeText(clone.textContent ?? ""); }, []); return ( <figure ref={ref} {...props} className={cn( "not-prose group fd-codeblock relative my-6 overflow-hidden rounded-lg border bg-fd-secondary/50 text-sm", keepBackground && "bg-[var(--shiki-light-bg)] dark:bg-[var(--shiki-dark-bg)]", props.className, )} > {title ? ( <div className="flex flex-row items-center gap-2 border-b bg-fd-muted px-4 py-1.5"> {icon ? ( <div className="text-fd-muted-foreground [&_svg]:size-3.5" dangerouslySetInnerHTML={ typeof icon === "string" ? { __html: icon, } : undefined } > {typeof icon !== "string" ? icon : null} </div> ) : null} <figcaption className="flex-1 truncate text-fd-muted-foreground"> {title} </figcaption> {allowCopy ? ( <CopyButton className="-me-2" onCopy={onCopy} /> ) : null} </div> ) : ( allowCopy && ( <CopyButton className="absolute right-2 top-2 z-[2] backdrop-blur-md" onCopy={onCopy} /> ) )} <ScrollArea ref={areaRef} dir="ltr"> <ScrollViewport {...viewportProps} className={cn("max-h-[600px]", viewportProps?.className)} > {props.children} </ScrollViewport> <ScrollBar orientation="horizontal" /> </ScrollArea> </figure> ); }, ); CodeBlockOld.displayName = "CodeBlockOld"; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/organization-hook.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { organization } from "."; describe("organization creation in database hooks", async () => { it("should create organization in user creation after hook within transaction", async () => { let hookCalledForTestEmail = false; let orgCreated: any = null; let errorInHook: any = null; const { auth, client, db } = await getTestInstance({ plugins: [organization()], databaseHooks: { user: { create: { after: async (user) => { // Only run for our specific test user if (user.email !== "[email protected]") { return; } hookCalledForTestEmail = true; try { // This should work now that the adapter uses getCurrentAdapter const org = await auth.api.createOrganization({ body: { name: `${user.email}'s Organization`, slug: `org-${user.id.substring(0, 8)}`, userId: user.id, }, }); orgCreated = org; } catch (error) { errorInHook = error; throw error; } }, }, }, }, }); // Create a user which should trigger the hook const result = await client.signUp.email({ email: "[email protected]", password: "password123", name: "Test Hook User", }); // Verify the user was created expect(result.data).toBeDefined(); expect(result.data?.user).toBeDefined(); expect(result.data?.user?.email).toBe("[email protected]"); // Verify the hook was called expect(hookCalledForTestEmail).toBe(true); expect(errorInHook).toBeNull(); // Verify organization was created successfully expect(orgCreated).not.toBeNull(); expect(orgCreated?.name).toBe("[email protected]'s Organization"); expect(orgCreated?.slug).toMatch(/^org-/); // Verify the organization exists in the database const orgs = await db.findMany({ model: "organization", }); // Should have the test user's org from getTestInstance plus our new one expect(orgs.length).toBeGreaterThanOrEqual(1); const createdOrg = orgs.find((o: any) => o.slug?.startsWith("org-")); expect(createdOrg).toBeDefined(); expect((createdOrg as any)?.name).toBe( "[email protected]'s Organization", ); // Verify the user is a member of the organization const members = await db.findMany({ model: "member", where: [ { field: "organizationId", value: orgCreated?.id, }, ], }); expect(members).toHaveLength(1); expect(members[0]).toMatchObject({ userId: result.data?.user?.id, organizationId: orgCreated?.id, role: "owner", }); }); it("should handle errors gracefully when organization creation fails in hook", async ({ skip, }) => { let firstUserCreated = false; let errorOnSecondUser: any = null; const { auth, client, db } = await getTestInstance({ plugins: [organization()], databaseHooks: { user: { create: { after: async (user) => { // Skip test instance default user if (!user.email?.includes("-hook@")) { return; } // Try to create an org with duplicate slug (will fail on second user) await auth.api.createOrganization({ body: { name: "Test Org", slug: "duplicate-test-org", // Same slug for all users userId: user.id, }, }); if (!firstUserCreated) { firstUserCreated = true; } }, }, }, }, }); if (!db.options?.adapterConfig.transaction) { skip( "Skipping since transactions are enabled and will rollback automatically", ); } // First user should succeed const result1 = await client.signUp.email({ email: "[email protected]", password: "password123", name: "User 1", }); expect(result1.data).toBeDefined(); expect(result1.data?.user?.email).toBe("[email protected]"); expect(firstUserCreated).toBe(true); // Second user should fail due to duplicate org slug try { await client.signUp.email({ email: "[email protected]", password: "password123", name: "User 2", }); } catch (error) { errorOnSecondUser = error; } expect(errorOnSecondUser).toBeDefined(); // Verify only one organization with our test slug was created const orgs = await db.findMany({ model: "organization", where: [ { field: "slug", value: "duplicate-test-org", }, ], }); expect(orgs).toHaveLength(1); // Verify only the first user exists (transaction should have rolled back for second user) const users = await db.findMany({ model: "user", where: [ { field: "email", value: "[email protected]", }, ], }); expect(users).toHaveLength(0); }); it("should work with multiple async operations in the hook", async () => { let asyncOperationsCompleted = 0; let foundUserInTransaction = false; const { auth, client, db } = await getTestInstance({ plugins: [organization()], databaseHooks: { user: { create: { after: async (user, ctx): Promise<any> => { // Skip test instance default user if (user.email !== "[email protected]") { return; } // Simulate some async operation await new Promise((resolve) => setTimeout(resolve, 10)); asyncOperationsCompleted++; // Check if user exists in the transaction context // This should work because we're in the same transaction const foundUser = await ctx?.context?.internalAdapter?.findUserById?.(user.id); foundUserInTransaction = !!foundUser; // Create organization const org = await auth.api.createOrganization({ body: { name: `Async Org for ${user.name}`, slug: `async-${user.id.substring(0, 8)}`, userId: user.id, }, }); // Another async operation await new Promise((resolve) => setTimeout(resolve, 10)); asyncOperationsCompleted++; return org; }, }, }, }, }); const result = await client.signUp.email({ email: "[email protected]", password: "password123", name: "Async User", }); expect(result.data).toBeDefined(); expect(result.data?.user?.email).toBe("[email protected]"); // Verify async operations completed expect(asyncOperationsCompleted).toBe(2); expect(foundUserInTransaction).toBe(true); // Verify organization was created const orgs = await db.findMany({ model: "organization", where: [ { field: "slug", operator: "contains", value: "async-", }, ], }); expect(orgs.length).toBeGreaterThanOrEqual(1); const asyncOrg = orgs.find((o: any) => o.name?.includes("Async Org")); expect(asyncOrg).toBeDefined(); expect((asyncOrg as any)?.name).toBe("Async Org for Async User"); }); it("should work when creating organization from before hook", async () => { let orgId: string | null = null; const { auth, client, db } = await getTestInstance({ plugins: [organization()], databaseHooks: { user: { create: { before: async (user) => { // We can't create the org here since user doesn't have an ID yet // But we can prepare the data return { data: { ...user, image: "prepared-in-before-hook", }, }; }, after: async (user) => { // Skip test instance default user if (user.email !== "[email protected]") { return; } // Now we can create the org with the user ID const org = await auth.api.createOrganization({ body: { name: `Before-After Org`, slug: `before-after-${user.id.substring(0, 8)}`, userId: user.id, }, }); orgId = org?.id || null; }, }, }, }, }); const result = await client.signUp.email({ email: "[email protected]", password: "password123", name: "Before Hook User", }); expect(result.data).toBeDefined(); expect(result.data?.user?.image).toBe("prepared-in-before-hook"); expect(orgId).not.toBeNull(); // Verify organization was created const org = await db.findOne({ model: "organization", where: [ { field: "id", value: orgId!, }, ], }); expect(org).toBeDefined(); expect((org as any)?.name).toBe("Before-After Org"); }); }); ``` -------------------------------------------------------------------------------- /docs/components/side-bar.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { AsideLink } from "@/components/ui/aside-link"; import { cn } from "@/lib/utils"; import { AnimatePresence, motion, MotionConfig } from "framer-motion"; import { useSearchContext } from "fumadocs-ui/provider"; import { ChevronDownIcon, Search } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; import { contents, examples } from "./sidebar-content"; import { Badge } from "./ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; export default function ArticleLayout() { const [currentOpen, setCurrentOpen] = useState<number>(0); const { setOpenSearch } = useSearchContext(); const pathname = usePathname(); function getDefaultValue() { const defaultValue = contents.findIndex((item) => item.list.some((listItem) => listItem.href === pathname), ); return defaultValue === -1 ? 0 : defaultValue; } const [group, setGroup] = useState("docs"); useEffect(() => { const grp = pathname.includes("examples") ? "examples" : "docs"; setGroup(grp); setCurrentOpen(getDefaultValue()); }, [pathname]); const cts = group === "docs" ? contents : examples; return ( <div className={cn("fixed start-0 top-0")}> <aside className={cn( "md:transition-all", "border-r border-lines top-[55px] md:flex hidden md:w-[268px] lg:w-[286px] overflow-y-auto absolute h-[calc(100dvh-55px)] pb-2 flex-col justify-between w-[var(--fd-sidebar-width)]", )} > <div> <SidebarTab group={group} setGroup={setGroup} /> <button className="flex w-full items-center gap-2 px-5 py-2.5 border-b text-muted-foreground dark:bg-zinc-950 dark:border-t-zinc-900/30 dark:border-t" onClick={() => { setOpenSearch(true); }} > <Search className="size-4 mx-0.5" /> <p className="text-sm">Search documentation...</p> </button> <MotionConfig transition={{ duration: 0.4, type: "spring", bounce: 0 }} > <div className="flex flex-col"> {cts.map((item, index) => ( <div key={item.title}> <button className="border-b w-full hover:underline border-lines text-sm px-5 py-2.5 text-left flex items-center gap-2" onClick={() => { if (currentOpen === index) { setCurrentOpen(-1); } else { setCurrentOpen(index); } }} > <item.Icon className="size-5" /> <span className="grow">{item.title}</span> {item.isNew && <NewBadge />} <motion.div animate={{ rotate: currentOpen === index ? 180 : 0 }} > <ChevronDownIcon className={cn( "h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200", )} /> </motion.div> </button> <AnimatePresence initial={false}> {currentOpen === index && ( <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="relative overflow-hidden" > <motion.div className="text-sm"> {item.list.map((listItem, j) => ( <div key={listItem.title}> <Suspense fallback={<>Loading...</>}> {listItem.group ? ( <div className="flex flex-row items-center gap-2 mx-5 my-1 "> <p className="text-sm text-transparent bg-gradient-to-tr dark:from-gray-100 dark:to-stone-200 bg-clip-text from-gray-900 to-stone-900"> {listItem.title} </p> <div className="flex-grow h-px bg-gradient-to-r from-stone-800/90 to-stone-800/60" /> </div> ) : ( <AsideLink href={listItem.href} startWith="/docs" title={listItem.title} className="break-words text-nowrap w-[--fd-sidebar-width] [&>div>div]:hover:!bg-fd-muted" activeClassName="[&>div>div]:!bg-fd-muted" > <div className="min-w-4"> <listItem.icon className="text-stone-950 dark:text-white" /> </div> {listItem.title} {listItem.isNew && <NewBadge />} </AsideLink> )} </Suspense> </div> ))} </motion.div> </motion.div> )} </AnimatePresence> </div> ))} </div> </MotionConfig> </div> </aside> </div> ); } function NewBadge({ isSelected }: { isSelected?: boolean }) { return ( <div className="flex items-center justify-end w-full"> <Badge className={cn( " pointer-events-none !no-underline border-dashed !decoration-transparent", isSelected && "!border-solid", )} variant={isSelected ? "default" : "outline"} > New </Badge> </div> ); } const tabs = [ { value: "docs", icon: ( <svg xmlns="http://www.w3.org/2000/svg" width="1.4em" height="1.4em" viewBox="0 0 24 24" > <path fill="currentColor" d="M4.727 2.733c.306-.308.734-.508 1.544-.618C7.105 2.002 8.209 2 9.793 2h4.414c1.584 0 2.688.002 3.522.115c.81.11 1.238.31 1.544.618c.305.308.504.74.613 1.557c.112.84.114 1.955.114 3.552V18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505A1.3 1.3 0 0 0 4 19.7V7.842c0-1.597.002-2.711.114-3.552c.109-.816.308-1.249.613-1.557" opacity=".5" ></path> <path fill="currentColor" d="M20 18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505s-.107.489-.066.78l.022.15c.11.653.31.998.616 1.244c.307.246.737.407 1.55.494c.837.09 1.946.092 3.536.092h4.43c1.59 0 2.7-.001 3.536-.092c.813-.087 1.243-.248 1.55-.494c.2-.16.354-.362.467-.664H8a.75.75 0 0 1 0-1.5h11.975c.018-.363.023-.776.025-1.25M7.25 7A.75.75 0 0 1 8 6.25h8a.75.75 0 0 1 0 1.5H8A.75.75 0 0 1 7.25 7M8 9.75a.75.75 0 0 0 0 1.5h5a.75.75 0 0 0 0-1.5z" ></path> </svg> ), title: "Docs", description: "get started, concepts, and plugins", }, { value: "examples", icon: ( <svg xmlns="http://www.w3.org/2000/svg" width="1.4em" height="1.4em" viewBox="0 0 24 24" > <path fill="currentColor" d="M12 2c4.714 0 7.071 0 8.535 1.464c1.08 1.08 1.364 2.647 1.439 5.286L22 9.5H2.026v-.75c.075-2.64.358-4.205 1.438-5.286C4.93 2 7.286 2 12 2" opacity=".5" ></path> <path fill="currentColor" d="M13 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-3 0a1 1 0 1 1-2 0a1 1 0 0 1 2 0M7 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0" ></path> <path fill="currentColor" d="M2 12c0 4.714 0 7.071 1.464 8.535c1.01 1.01 2.446 1.324 4.786 1.421L9 22V9.5H2.026l-.023.75Q2 11.066 2 12" opacity=".7" ></path> <path fill="currentColor" d="M22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22c-.819 0-2.316 0-3-.008V9.5h13l-.003.75Q22 11.066 22 12" ></path> </svg> ), title: "Examples", description: "examples and guides", }, ]; function SidebarTab({ group, setGroup, }: { group: string; setGroup: (group: string) => void; }) { const router = useRouter(); const selected = tabs.find((tab) => tab.value === group); return ( <Select value={group} onValueChange={(val) => { setGroup(val); if (val === "docs") { router.push("/docs"); } else { router.push("/docs/examples"); } }} > <SelectTrigger className="h-16 border border-b border-none rounded-none px-5"> {selected ? ( <div className="flex flex-col gap-1 items-start"> <div className="flex items-center gap-1 -ml-0.5"> {selected.icon} {selected.title} </div> <p className="text-xs text-muted-foreground"> {selected.description} </p> </div> ) : null} </SelectTrigger> <SelectContent> {tabs.map((tab) => ( <SelectItem key={tab.value} value={tab.value} className="h-12 flex flex-col items-start gap-1" > <div className="flex items-center gap-1"> {tab.icon} {tab.title} </div> <p className="text-xs text-muted-foreground">{tab.description}</p> </SelectItem> ))} </SelectContent> </Select> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/autumn.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Autumn Billing description: Better Auth Plugin for Autumn Billing --- import { HomeIcon } from "lucide-react"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; [Autumn](https://useautumn.com) is open source infrastructure to run SaaS pricing plans. It sits between your app and Stripe, and acts as the database for your customers' subscription status, usage metering and feature permissions. <Card href="https://discord.gg/STqxY92zuS" title="Get help on Autumn's Discord"> We're online to help you with any questions you have. </Card> ## Features - One function for all checkout, subscription and payment flows - No webhooks required: query Autumn for the data you need - Manages your application's free and paid plans - Usage tracking for usage billing and periodic limits - Custom plans and pricing changes through Autumn's dashboard <Steps> <Step> ### Setup Autumn Account First, create your pricing plans in Autumn's [dashboard](https://app.useautumn.com), where you define what each plan and product gets access to and how it should be billed. In this example, we're handling the free and pro plans for an AI chatbot, which comes with a number of `messages` per month. </Step> <Step> ### Install Autumn SDK ```package-install autumn-js ``` <Callout> If you're using a separate client and server setup, make sure to install the plugin in both parts of your project. </Callout> </Step> <Step> ### Add `AUTUMN_SECRET_KEY` to your environment variables You can find it in Autumn's dashboard under "[Developer](https://app.useautumn.com/sandbox/onboarding)". ```bash title=".env" AUTUMN_SECRET_KEY=am_sk_xxxxxxxxxx ``` </Step> <Step> ### Add the Autumn plugin to your `auth` config <Tabs items={["User", "Organization", "User & Organization", "Custom"]}> <Tab value="User"> ```ts title="auth.ts" import { autumn } from "autumn-js/better-auth"; export const auth = betterAuth({ // ... plugins: [autumn()], }); ``` </Tab> <Tab value="Organization"> ```ts title="auth.ts" import { autumn } from "autumn-js/better-auth"; import { organization } from "better-auth/plugins"; export const auth = betterAuth({ // ... plugins: [organization(), autumn({ customerScope: "organization" })], }); ``` </Tab> <Tab value="User & Organization"> ```ts title="auth.ts" import { autumn } from "autumn-js/better-auth"; import { organization } from "better-auth/plugins"; export const auth = betterAuth({ // ... plugins: [ organization(), autumn({ customerScope: "user_and_organization" }) ], }); ``` </Tab> <Tab value="Custom"> ```ts title="auth.ts" import { autumn } from "autumn-js/better-auth"; import { organization } from "better-auth/plugins"; export const auth = betterAuth({ // ... plugins: [ organization(), autumn({ identify: async ({ session, organization }) => { return { customerId: "your_customer_id", customerData: { name: "Customer Name", email: "[email protected]", }, }; }, }), ], }); ``` </Tab> </Tabs> <Callout> Autumn will auto-create your customers when they sign up, and assign them any default plans you created (eg your Free plan). You can choose who becomes a customer: individual users, organizations, both, or something custom like workspaces. </Callout> </Step> <Step> ### Add `<AutumnProvider />` Client side, wrap your application with the AutumnProvider component, and pass in the `baseUrl` that you define within better-auth's `authClient`. ```tsx title="app/layout.tsx" import { AutumnProvider } from "autumn-js/react"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html> <body> {/* or meta.env.BETTER_AUTH_URL for vite */} <AutumnProvider betterAuthUrl={process.env.NEXT_PUBLIC_BETTER_AUTH_URL}> {children} </AutumnProvider> </body> </html> ); } ``` </Step> </Steps> ## Usage ### Handle payments Call `attach` to redirect the customer to a Stripe checkout page when they want to purchase the Pro plan. If their payment method is already on file, `AttachDialog` will open instead to let the customer confirm their new subscription or purchase, and handle the payment. <Callout type="warn"> {" "} Make sure you've pasted in your [Stripe test secret key](https://dashboard.stripe.com/test/apikeys) in the [Autumn dashboard](https://app.useautumn.com/integrations/stripe). </Callout> ```tsx import { useCustomer, AttachDialog } from "autumn-js/react"; export default function PurchaseButton() { const { attach } = useCustomer(); return ( <button onClick={async () => { await attach({ productId: "pro", dialog: AttachDialog, }); }} > Upgrade to Pro </button> ); } ``` The AttachDialog component can be used directly from the `autumn-js/react` library (as shown in the example above), or downloaded as a [shadcn/ui component](https://docs.useautumn.com/quickstart/shadcn) to customize. ### Integrate Pricing Logic Integrate your client and server pricing tiers logic with the following functions: - `check` to see if the customer is `allowed` to send a message. - `track` a usage event in Autumn (typically done server-side) - `customer` to display any relevant billing data in your UI (subscriptions, feature balances) Server-side, you can access Autumn's functions through the `auth` object. <Tabs items={["Client", "Server"]}> <Tab value="Client"> ```jsx import { useCustomer } from "autumn-js/react"; export default function SendChatMessage() { const { customer, allowed, refetch } = useCustomer(); return ( <> <button onClick={async () => { if (allowed({ featureId: "messages" })) { //... send chatbot message server-side, then await refetch(); // refetch customer usage data alert( "Remaining messages: " + customer?.features.messages?.balance ); } else { alert("You're out of messages"); } }} > Send Message </button> </> ); } ``` </Tab> <Tab value="Server"> ```typescript Server import { auth } from "@/lib/auth"; // check on the backend if the customer can send a message const { allowed } = await auth.api.check({ headers: await headers(), // pass the request headers body: { featureId: "messages", }, }); // server-side function to send the message // then track the usage await auth.api.track({ headers: await headers(), body: { featureId: "messages", value: 2, }, }); ``` </Tab> </Tabs> ### Additional Functions #### openBillingPortal() Opens a billing portal where the customer can update their payment method or cancel their plan. ```tsx import { useCustomer } from "autumn-js/react"; export default function BillingSettings() { const { openBillingPortal } = useCustomer(); return ( <button onClick={async () => { await openBillingPortal({ returnUrl: "/settings/billing", }); }} > Manage Billing </button> ); } ``` #### cancel() Cancel a product or subscription. ```tsx import { useCustomer } from "autumn-js/react"; export default function CancelSubscription() { const { cancel } = useCustomer(); return ( <button onClick={async () => { await cancel({ productId: "pro" }); }} > Cancel Subscription </button> ); } ``` #### Get invoice history Pass in an `expand` param into `useCustomer` to get additional information. You can expand `invoices`, `trials_used`, `payment_method`, or `rewards`. ```tsx import { useCustomer } from "autumn-js/react"; export default function CustomerProfile() { const { customer } = useCustomer({ expand: ["invoices"] }); return ( <div> <h2>Customer Profile</h2> <p>Name: {customer?.name}</p> <p>Email: {customer?.email}</p> <p>Balance: {customer?.features.chat_messages?.balance}</p> </div> ); } ``` ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/index.ts: -------------------------------------------------------------------------------- ```typescript import { APIError, type Middleware, createRouter, type Endpoint, } from "better-call"; import type { BetterAuthOptions } from "@better-auth/core"; import type { UnionToIntersection } from "../types/helper"; import { originCheckMiddleware } from "./middlewares"; import { callbackOAuth, forgetPassword, forgetPasswordCallback, getSession, listSessions, resetPassword, revokeSession, revokeSessions, sendVerificationEmail, changeEmail, signInEmail, signInSocial, signOut, verifyEmail, linkSocialAccount, revokeOtherSessions, listUserAccounts, changePassword, deleteUser, setPassword, updateUser, deleteUserCallback, unlinkAccount, refreshToken, getAccessToken, accountInfo, requestPasswordReset, requestPasswordResetCallback, } from "./routes"; import { ok } from "./routes"; import { signUpEmail } from "./routes"; import { error } from "./routes"; import { type InternalLogger, logger } from "@better-auth/core/env"; import type { BetterAuthPlugin } from "@better-auth/core"; import { onRequestRateLimit } from "./rate-limiter"; import { toAuthEndpoints } from "./to-auth-endpoints"; import type { AuthContext } from "@better-auth/core"; export function checkEndpointConflicts( options: BetterAuthOptions, logger: InternalLogger, ) { const endpointRegistry = new Map< string, { pluginId: string; endpointKey: string; methods: string[] }[] >(); options.plugins?.forEach((plugin) => { if (plugin.endpoints) { for (const [key, endpoint] of Object.entries(plugin.endpoints)) { if (endpoint && "path" in endpoint) { const path = endpoint.path; let methods: string[] = []; if (endpoint.options && "method" in endpoint.options) { if (Array.isArray(endpoint.options.method)) { methods = endpoint.options.method; } else if (typeof endpoint.options.method === "string") { methods = [endpoint.options.method]; } } if (methods.length === 0) { methods = ["*"]; } if (!endpointRegistry.has(path)) { endpointRegistry.set(path, []); } endpointRegistry.get(path)!.push({ pluginId: plugin.id, endpointKey: key, methods, }); } } } }); const conflicts: { path: string; plugins: string[]; conflictingMethods: string[]; }[] = []; for (const [path, entries] of endpointRegistry.entries()) { if (entries.length > 1) { const methodMap = new Map<string, string[]>(); let hasConflict = false; for (const entry of entries) { for (const method of entry.methods) { if (!methodMap.has(method)) { methodMap.set(method, []); } methodMap.get(method)!.push(entry.pluginId); if (methodMap.get(method)!.length > 1) { hasConflict = true; } if (method === "*" && entries.length > 1) { hasConflict = true; } else if (method !== "*" && methodMap.has("*")) { hasConflict = true; } } } if (hasConflict) { const uniquePlugins = [...new Set(entries.map((e) => e.pluginId))]; const conflictingMethods: string[] = []; for (const [method, plugins] of methodMap.entries()) { if ( plugins.length > 1 || (method === "*" && entries.length > 1) || (method !== "*" && methodMap.has("*")) ) { conflictingMethods.push(method); } } conflicts.push({ path, plugins: uniquePlugins, conflictingMethods, }); } } } if (conflicts.length > 0) { const conflictMessages = conflicts .map( (conflict) => ` - "${conflict.path}" [${conflict.conflictingMethods.join(", ")}] used by plugins: ${conflict.plugins.join(", ")}`, ) .join("\n"); logger.error( `Endpoint path conflicts detected! Multiple plugins are trying to use the same endpoint paths with conflicting HTTP methods: ${conflictMessages} To resolve this, you can: 1. Use only one of the conflicting plugins 2. Configure the plugins to use different paths (if supported) 3. Ensure plugins use different HTTP methods for the same path `, ); } } export function getEndpoints<Option extends BetterAuthOptions>( ctx: Promise<AuthContext> | AuthContext, options: Option, ) { const pluginEndpoints = options.plugins?.reduce<Record<string, Endpoint>>((acc, plugin) => { return { ...acc, ...plugin.endpoints, }; }, {}) ?? {}; type PluginEndpoint = UnionToIntersection< Option["plugins"] extends Array<infer T> ? T extends BetterAuthPlugin ? T extends { endpoints: infer E; } ? E : {} : {} : {} >; const middlewares = options.plugins ?.map((plugin) => plugin.middlewares?.map((m) => { const middleware = (async (context: any) => { const authContext = await ctx; return m.middleware({ ...context, context: { ...authContext, ...context.context, }, }); }) as Middleware; middleware.options = m.middleware.options; return { path: m.path, middleware, }; }), ) .filter((plugin) => plugin !== undefined) .flat() || []; const baseEndpoints = { signInSocial, callbackOAuth, getSession: getSession<Option>(), signOut, signUpEmail: signUpEmail<Option>(), signInEmail, forgetPassword, resetPassword, verifyEmail, sendVerificationEmail, changeEmail, changePassword, setPassword, updateUser: updateUser<Option>(), deleteUser, forgetPasswordCallback, requestPasswordReset, requestPasswordResetCallback, listSessions: listSessions<Option>(), revokeSession, revokeSessions, revokeOtherSessions, linkSocialAccount, listUserAccounts, deleteUserCallback, unlinkAccount, refreshToken, getAccessToken, accountInfo, }; const endpoints = { ...baseEndpoints, ...pluginEndpoints, ok, error, } as const; const api = toAuthEndpoints(endpoints, ctx); return { api: api as typeof endpoints & PluginEndpoint, middlewares, }; } export const router = <Option extends BetterAuthOptions>( ctx: AuthContext, options: Option, ) => { const { api, middlewares } = getEndpoints(ctx, options); const basePath = new URL(ctx.baseURL).pathname; return createRouter(api, { routerContext: ctx, openapi: { disabled: true, }, basePath, routerMiddleware: [ { path: "/**", middleware: originCheckMiddleware, }, ...middlewares, ], async onRequest(req) { //handle disabled paths const disabledPaths = ctx.options.disabledPaths || []; const path = new URL(req.url).pathname.replace(basePath, ""); if (disabledPaths.includes(path)) { return new Response("Not Found", { status: 404 }); } for (const plugin of ctx.options.plugins || []) { if (plugin.onRequest) { const response = await plugin.onRequest(req, ctx); if (response && "response" in response) { return response.response; } } } return onRequestRateLimit(req, ctx); }, async onResponse(res) { for (const plugin of ctx.options.plugins || []) { if (plugin.onResponse) { const response = await plugin.onResponse(res, ctx); if (response) { return response.response; } } } return res; }, onError(e) { if (e instanceof APIError && e.status === "FOUND") { return; } if (options.onAPIError?.throw) { throw e; } if (options.onAPIError?.onError) { options.onAPIError.onError(e, ctx); return; } const optLogLevel = options.logger?.level; const log = optLogLevel === "error" || optLogLevel === "warn" || optLogLevel === "debug" ? logger : undefined; if (options.logger?.disabled !== true) { if ( e && typeof e === "object" && "message" in e && typeof e.message === "string" ) { if ( e.message.includes("no column") || e.message.includes("column") || e.message.includes("relation") || e.message.includes("table") || e.message.includes("does not exist") ) { ctx.logger?.error(e.message); return; } } if (e instanceof APIError) { if (e.status === "INTERNAL_SERVER_ERROR") { ctx.logger.error(e.status, e); } log?.error(e.message); } else { ctx.logger?.error( e && typeof e === "object" && "name" in e ? (e.name as string) : "", e, ); } } }, }); }; export * from "./routes"; export * from "./middlewares"; export { APIError } from "better-call"; export { createAuthEndpoint, createAuthMiddleware, optionsMiddleware, type AuthEndpoint, type AuthMiddleware, } from "@better-auth/core/api"; ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/default-changelog.tsx: -------------------------------------------------------------------------------- ```typescript import Link from "next/link"; import { useId } from "react"; import { cn } from "@/lib/utils"; import { IconLink } from "./changelog-layout"; import { BookIcon, GitHubIcon, XIcon } from "./icons"; import { DiscordLogoIcon } from "@radix-ui/react-icons"; import { StarField } from "./stat-field"; import { betterFetch } from "@better-fetch/fetch"; import Markdown from "react-markdown"; import defaultMdxComponents from "fumadocs-ui/mdx"; import rehypeHighlight from "rehype-highlight"; import "highlight.js/styles/dark.css"; export const dynamic = "force-static"; const ChangelogPage = async () => { const { data: releases } = await betterFetch< { id: number; tag_name: string; name: string; body: string; html_url: string; prerelease: boolean; published_at: string; }[] >("https://api.github.com/repos/better-auth/better-auth/releases"); const messages = releases ?.filter((release) => !release.prerelease) .map((release) => ({ tag: release.tag_name, title: release.name, content: getContent(release.body), date: new Date(release.published_at).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }), url: release.html_url, })); function getContent(content: string) { const lines = content.split("\n"); const newContext = lines.map((line) => { if (line.startsWith("- ")) { const mainContent = line.split(";")[0]; const context = line.split(";")[2]; const mentionMatches = (context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? []; if (mentionMatches.length === 0) { return (mainContent || line).replace(/ /g, ""); } const mentions = mentionMatches.map((match) => { const username = match.slice(1); const avatarUrl = `https://github.com/${username}.png`; return `[](https://github.com/${username})`; }); // Remove   return ( (mainContent || line).replace(/ /g, "") + " – " + mentions.join(" ") ); } return line; }); return newContext.join("\n"); } return ( <div className="grid md:grid-cols-2 items-start"> <div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> <StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" /> <Glow /> <div className="flex flex-col md:justify-center max-w-xl mx-auto h-full"> <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> All of the changes made will be{" "} <span className="">available here.</span> </h1> <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> Better Auth is comprehensive authentication library for TypeScript that provides a wide range of features to make authentication easier and more secure. </p> <hr className="h-px bg-gray-300 mt-5" /> <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 gap-x-1 gap-y-3 sm:gap-x-2"> <IconLink href="/docs" icon={BookIcon} className="flex-none text-gray-600 dark:text-gray-300" > Documentation </IconLink> <IconLink href="https://github.com/better-auth/better-auth" icon={GitHubIcon} className="flex-none text-gray-600 dark:text-gray-300" > GitHub </IconLink> <IconLink href="https://discord.gg/better-auth" icon={DiscordLogoIcon} className="flex-none text-gray-600 dark:text-gray-300" > Community </IconLink> </div> <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> <IconLink href="https://x.com/better_auth" icon={XIcon} compact> BETTER-AUTH. </IconLink> </p> </div> </div> <div className="px-4 relative md:px-8 pb-12 md:py-12"> <div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div> <div className="max-w-2xl relative"> <Markdown rehypePlugins={[[rehypeHighlight]]} components={{ pre: (props) => ( <defaultMdxComponents.pre {...props} className={cn(props.className, " ml-10 my-2")} /> ), h2: (props) => ( <h2 id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]" {...props} > <div className="sticky top-0 left-[-9.9rem] hidden md:block"> <time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal"> {props.children?.toString().includes("date=") && props.children?.toString().split("date=")[1]} <div className="w-4 h-[1px] dark:bg-white/60 bg-black" /> </time> </div> <Link href={ props.children ?.toString() .split("date=")[0] .trim() .endsWith(".00") ? `/changelogs/${props.children ?.toString() .split("date=")[0] .trim()}` : `#${props.children ?.toString() .split("date=")[0] .trim()}` } > {props.children?.toString().split("date=")[0].trim()} </Link> <p className="text-xs font-normal opacity-60 hidden"> {props.children?.toString().includes("date=") && props.children?.toString().split("date=")[1]} </p> </h2> ), h3: (props) => ( <h3 className="text-xl tracking-tighter py-1" {...props}> {props.children?.toString()?.trim()} <hr className="h-[1px] my-1 mb-2 bg-input" /> </h3> ), p: (props) => <p className="my-0 ml-10 text-sm" {...props} />, ul: (props) => ( <ul className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300" {...props} /> ), li: (props) => <li className="my-1" {...props} />, a: ({ className, ...props }: any) => ( <Link target="_blank" className={cn("font-medium underline", className)} {...props} /> ), strong: (props) => ( <strong className="font-semibold" {...props} /> ), img: (props) => ( <img className="rounded-full w-6 h-6 border opacity-70 inline-block" {...props} style={{ maxWidth: "100%" }} /> ), }} > {messages ?.map((message) => { return ` ## ${message.title} date=${message.date} ${message.content} `; }) .join("\n")} </Markdown> </div> </div> </div> ); }; export default ChangelogPage; export function Glow() { let id = useId(); return ( <div className="absolute inset-0 -z-10 overflow-hidden bg-gradient-to-tr from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> <svg className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]" aria-hidden="true" > <defs> <radialGradient id={`${id}-desktop`} cx="100%"> <stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" /> <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> </radialGradient> <radialGradient id={`${id}-mobile`} cy="100%"> <stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" /> <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> </radialGradient> </defs> <rect width="100%" height="100%" fill={`url(#${id}-desktop)`} className="hidden lg:block" /> <rect width="100%" height="100%" fill={`url(#${id}-mobile)`} className="lg:hidden" /> </svg> <div className="absolute inset-x-0 bottom-0 right-0 h-px dark:bg-white/5 mix-blend-overlay lg:left-auto lg:top-0 lg:h-auto lg:w-px" /> </div> ); } ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/default-changelog.tsx: -------------------------------------------------------------------------------- ```typescript import Link from "next/link"; import { useId } from "react"; import { cn } from "@/lib/utils"; import { IconLink } from "./changelog-layout"; import { BookIcon, GitHubIcon, XIcon } from "./icons"; import { DiscordLogoIcon } from "@radix-ui/react-icons"; import { StarField } from "./stat-field"; import { betterFetch } from "@better-fetch/fetch"; import Markdown from "react-markdown"; import defaultMdxComponents from "fumadocs-ui/mdx"; import rehypeHighlight from "rehype-highlight"; import "highlight.js/styles/dark.css"; export const dynamic = "force-static"; const ChangelogPage = async () => { const { data: releases } = await betterFetch< { id: number; tag_name: string; name: string; body: string; html_url: string; prerelease: boolean; published_at: string; }[] >("https://api.github.com/repos/better-auth/better-auth/releases"); const messages = releases ?.filter((release) => !release.prerelease) .map((release) => ({ tag: release.tag_name, title: release.name, content: getContent(release.body), date: new Date(release.published_at).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }), url: release.html_url, })); function getContent(content: string) { const lines = content.split("\n"); const newContext = lines.map((line) => { if (line.trim().startsWith("- ")) { const mainContent = line.split(";")[0]; const context = line.split(";")[2]; const mentionMatches = (context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? []; if (mentionMatches.length === 0) { return (mainContent || line).replace(/ /g, ""); } const mentions = mentionMatches.map((match) => { const username = match.slice(1); const avatarUrl = `https://github.com/${username}.png`; return `[](https://github.com/${username})`; }); // Remove   return ( (mainContent || line).replace(/ /g, "") + " – " + mentions.join(" ") ); } return line; }); return newContext.join("\n"); } return ( <div className="grid items-start md:grid-cols-2"> <div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> <StarField className="top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" /> <Glow /> <div className="flex flex-col mx-auto max-w-xl h-full md:justify-center"> <h1 className="mt-14 font-sans text-5xl font-semibold tracking-tighter"> All of the changes made will be{" "} <span className="">available here.</span> </h1> <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> Better Auth is comprehensive authentication library for TypeScript that provides a wide range of features to make authentication easier and more secure. </p> <hr className="mt-5 h-px bg-gray-300" /> <div className="flex flex-wrap gap-x-1 gap-y-3 mt-8 text-gray-600 dark:text-gray-300 sm:gap-x-2"> <IconLink href="/docs" icon={BookIcon} className="flex-none text-gray-600 dark:text-gray-300" > Documentation </IconLink> <IconLink href="https://github.com/better-auth/better-auth" icon={GitHubIcon} className="flex-none text-gray-600 dark:text-gray-300" > GitHub </IconLink> <IconLink href="https://discord.gg/better-auth" icon={DiscordLogoIcon} className="flex-none text-gray-600 dark:text-gray-300" > Community </IconLink> </div> <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> <IconLink href="https://x.com/better_auth" icon={XIcon} compact> BETTER-AUTH. </IconLink> </p> </div> </div> <div className="relative px-4 pb-12 md:px-8 md:py-12"> <div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div> <div className="relative max-w-2xl"> <Markdown rehypePlugins={[[rehypeHighlight]]} components={{ pre: (props) => ( <defaultMdxComponents.pre {...props} className={cn(props.className, " ml-10 my-2")} /> ), h2: (props) => ( <h2 id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]" {...props} > <div className="sticky top-0 left-[-9.9rem] hidden md:block"> <time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal"> {props.children?.toString().includes("date=") && props.children?.toString().split("date=")[1]} <div className="w-4 h-[1px] dark:bg-white/60 bg-black" /> </time> </div> <Link href={ props.children ?.toString() .split("date=")[0] .trim() .endsWith(".00") ? `/changelogs/${props.children ?.toString() .split("date=")[0] .trim()}` : `#${props.children ?.toString() .split("date=")[0] .trim()}` } > {props.children?.toString().split("date=")[0].trim()} </Link> <p className="hidden text-xs font-normal opacity-60"> {props.children?.toString().includes("date=") && props.children?.toString().split("date=")[1]} </p> </h2> ), h3: (props) => ( <h3 className="py-1 text-xl tracking-tighter" {...props}> {props.children?.toString()?.trim()} <hr className="h-[1px] my-1 mb-2 bg-input" /> </h3> ), p: (props) => <p className="my-0 ml-10 text-sm" {...props} />, ul: (props) => ( <ul className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300" {...props} /> ), li: (props) => <li className="my-1" {...props} />, a: ({ className, ...props }: any) => ( <Link target="_blank" className={cn("font-medium underline", className)} {...props} /> ), strong: (props) => ( <strong className="font-semibold" {...props} /> ), img: (props) => ( <img className="inline-block w-6 h-6 rounded-full border opacity-70" {...props} style={{ maxWidth: "100%" }} /> ), }} > {messages ?.map((message) => { return ` ## ${message.title} date=${message.date} ${message.content} `; }) .join("\n")} </Markdown> </div> </div> </div> ); }; export default ChangelogPage; export function Glow() { let id = useId(); return ( <div className="overflow-hidden absolute inset-0 bg-gradient-to-tr from-transparent -z-10 dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> <svg className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]" aria-hidden="true" > <defs> <radialGradient id={`${id}-desktop`} cx="100%"> <stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" /> <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> </radialGradient> <radialGradient id={`${id}-mobile`} cy="100%"> <stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" /> <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> </radialGradient> </defs> <rect width="100%" height="100%" fill={`url(#${id}-desktop)`} className="hidden lg:block" /> <rect width="100%" height="100%" fill={`url(#${id}-mobile)`} className="lg:hidden" /> </svg> <div className="absolute inset-x-0 right-0 bottom-0 h-px mix-blend-overlay dark:bg-white/5 lg:left-auto lg:top-0 lg:h-auto lg:w-px" /> </div> ); } ``` -------------------------------------------------------------------------------- /docs/components/ui/chart.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as RechartsPrimitive from "recharts"; import { cn } from "@/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType; } & ( | { color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> } ); }; type ChartContextProps = { config: ChartConfig; }; const ChartContext = React.createContext<ChartContextProps | null>(null); function useChart() { const context = React.useContext(ChartContext); if (!context) { throw new Error("useChart must be used within a <ChartContainer />"); } return context; } function ChartContainer({ id, className, children, config, ...props }: React.ComponentProps<"div"> & { config: ChartConfig; children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer >["children"]; }) { const uniqueId = React.useId(); const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; return ( <ChartContext.Provider value={{ config }}> <div data-slot="chart" data-chart={chartId} className={cn( "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", className, )} {...props} > <ChartStyle id={chartId} config={config} /> <RechartsPrimitive.ResponsiveContainer> {children} </RechartsPrimitive.ResponsiveContainer> </div> </ChartContext.Provider> ); } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([, config]) => config.theme || config.color, ); if (!colorConfig.length) { return null; } return ( <style dangerouslySetInnerHTML={{ __html: Object.entries(THEMES) .map( ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color; return color ? ` --color-${key}: ${color};` : null; }) .join("\n")} } `, ) .join("\n"), }} /> ); }; const ChartTooltip = RechartsPrimitive.Tooltip; function ChartTooltipContent({ active, payload, className, indicator = "dot", hideLabel = false, hideIndicator = false, label, labelFormatter, labelClassName, formatter, color, nameKey, labelKey, }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<"div"> & { hideLabel?: boolean; hideIndicator?: boolean; indicator?: "line" | "dot" | "dashed"; nameKey?: string; labelKey?: string; }) { const { config } = useChart(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { return null; } const [item] = payload; const key = `${labelKey || item?.dataKey || item?.name || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label; if (labelFormatter) { return ( <div className={cn("font-medium", labelClassName)}> {labelFormatter(value, payload)} </div> ); } if (!value) { return null; } return <div className={cn("font-medium", labelClassName)}>{value}</div>; }, [ label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey, ]); if (!active || !payload?.length) { return null; } const nestLabel = payload.length === 1 && indicator !== "dot"; return ( <div className={cn( "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl", className, )} > {!nestLabel ? tooltipLabel : null} <div className="grid gap-1.5"> {payload.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = color || item.payload.fill || item.color; return ( <div key={item.dataKey} className={cn( "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", indicator === "dot" && "items-center", )} > {formatter && item?.value !== undefined && item.name ? ( formatter(item.value, item.name, item, index, item.payload) ) : ( <> {itemConfig?.icon ? ( <itemConfig.icon /> ) : ( !hideIndicator && ( <div className={cn( "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", { "h-2.5 w-2.5": indicator === "dot", "w-1": indicator === "line", "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed", "my-0.5": nestLabel && indicator === "dashed", }, )} style={ { "--color-bg": indicatorColor, "--color-border": indicatorColor, } as React.CSSProperties } /> ) )} <div className={cn( "flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center", )} > <div className="grid gap-1.5"> {nestLabel ? tooltipLabel : null} <span className="text-muted-foreground"> {itemConfig?.label || item.name} </span> </div> {item.value && ( <span className="text-foreground font-mono font-medium tabular-nums"> {item.value.toLocaleString()} </span> )} </div> </> )} </div> ); })} </div> </div> ); } const ChartLegend = RechartsPrimitive.Legend; function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey, }: React.ComponentProps<"div"> & Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { hideIcon?: boolean; nameKey?: string; }) { const { config } = useChart(); if (!payload?.length) { return null; } return ( <div className={cn( "flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className, )} > {payload.map((item) => { const key = `${nameKey || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); return ( <div key={item.value} className={cn( "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3", )} > {itemConfig?.icon && !hideIcon ? ( <itemConfig.icon /> ) : ( <div className="h-2 w-2 shrink-0 rounded-[2px]" style={{ backgroundColor: item.color, }} /> )} {itemConfig?.label} </div> ); })} </div> ); } // Helper to extract item config from a payload. function getPayloadConfigFromPayload( config: ChartConfig, payload: unknown, key: string, ) { if (typeof payload !== "object" || payload === null) { return undefined; } const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined; let configLabelKey: string = key; if ( key in payload && typeof payload[key as keyof typeof payload] === "string" ) { configLabelKey = payload[key as keyof typeof payload] as string; } else if ( payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string" ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload ] as string; } return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; } export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/siwe/index.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "../../api"; import { createAuthEndpoint } from "@better-auth/core/api"; import { setSessionCookie } from "../../cookies"; import * as z from "zod"; import type { InferOptionSchema } from "../../types"; import type { BetterAuthPlugin } from "@better-auth/core"; import type { ENSLookupArgs, ENSLookupResult, SIWEVerifyMessageArgs, WalletAddress, } from "./types"; import type { User } from "../../types"; import { schema } from "./schema"; import { getOrigin } from "../../utils/url"; import { toChecksumAddress } from "../../utils/hashing"; import { mergeSchema } from "../../db/schema"; export interface SIWEPluginOptions { domain: string; emailDomainName?: string; anonymous?: boolean; getNonce: () => Promise<string>; verifyMessage: (args: SIWEVerifyMessageArgs) => Promise<boolean>; ensLookup?: (args: ENSLookupArgs) => Promise<ENSLookupResult>; schema?: InferOptionSchema<typeof schema>; } export const siwe = (options: SIWEPluginOptions) => ({ id: "siwe", schema: mergeSchema(schema, options?.schema), endpoints: { getSiweNonce: createAuthEndpoint( "/siwe/nonce", { method: "POST", body: z.object({ walletAddress: z .string() .regex(/^0[xX][a-fA-F0-9]{40}$/i) .length(42), chainId: z .number() .int() .positive() .max(2147483647) .optional() .default(1), // Default to Ethereum mainnet }), }, async (ctx) => { const { walletAddress: rawWalletAddress, chainId } = ctx.body; const walletAddress = toChecksumAddress(rawWalletAddress); const nonce = await options.getNonce(); // Store nonce with wallet address and chain ID context await ctx.context.internalAdapter.createVerificationValue({ identifier: `siwe:${walletAddress}:${chainId}`, value: nonce, expiresAt: new Date(Date.now() + 15 * 60 * 1000), }); return ctx.json({ nonce }); }, ), verifySiweMessage: createAuthEndpoint( "/siwe/verify", { method: "POST", body: z .object({ message: z.string().min(1), signature: z.string().min(1), walletAddress: z .string() .regex(/^0[xX][a-fA-F0-9]{40}$/i) .length(42), chainId: z .number() .int() .positive() .max(2147483647) .optional() .default(1), email: z.string().email().optional(), }) .refine((data) => options.anonymous !== false || !!data.email, { message: "Email is required when the anonymous plugin option is disabled.", path: ["email"], }), requireRequest: true, }, async (ctx) => { const { message, signature, walletAddress: rawWalletAddress, chainId, email, } = ctx.body; const walletAddress = toChecksumAddress(rawWalletAddress); const isAnon = options.anonymous ?? true; if (!isAnon && !email) { throw new APIError("BAD_REQUEST", { message: "Email is required when anonymous is disabled.", status: 400, }); } try { // Find stored nonce with wallet address and chain ID context const verification = await ctx.context.internalAdapter.findVerificationValue( `siwe:${walletAddress}:${chainId}`, ); // Ensure nonce is valid and not expired if (!verification || new Date() > verification.expiresAt) { throw new APIError("UNAUTHORIZED", { message: "Unauthorized: Invalid or expired nonce", status: 401, code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE", }); } // Verify SIWE message with enhanced parameters const { value: nonce } = verification; const verified = await options.verifyMessage({ message, signature, address: walletAddress, chainId, cacao: { h: { t: "caip122" }, p: { domain: options.domain, aud: options.domain, nonce, iss: options.domain, version: "1", }, s: { t: "eip191", s: signature }, }, }); if (!verified) { throw new APIError("UNAUTHORIZED", { message: "Unauthorized: Invalid SIWE signature", status: 401, }); } // Clean up used nonce await ctx.context.internalAdapter.deleteVerificationValue( verification.id, ); // Look for existing user by their wallet addresses let user: User | null = null; // Check if there's a wallet address record for this exact address+chainId combination const existingWalletAddress: WalletAddress | null = await ctx.context.adapter.findOne({ model: "walletAddress", where: [ { field: "address", operator: "eq", value: walletAddress }, { field: "chainId", operator: "eq", value: chainId }, ], }); if (existingWalletAddress) { // Get the user associated with this wallet address user = await ctx.context.adapter.findOne({ model: "user", where: [ { field: "id", operator: "eq", value: existingWalletAddress.userId, }, ], }); } else { // No exact match found, check if this address exists on any other chain const anyWalletAddress: WalletAddress | null = await ctx.context.adapter.findOne({ model: "walletAddress", where: [ { field: "address", operator: "eq", value: walletAddress }, ], }); if (anyWalletAddress) { // Same address exists on different chain, get that user user = await ctx.context.adapter.findOne({ model: "user", where: [ { field: "id", operator: "eq", value: anyWalletAddress.userId, }, ], }); } } // Create new user if none exists if (!user) { const domain = options.emailDomainName ?? getOrigin(ctx.context.baseURL); // Use checksummed address for email generation const userEmail = !isAnon && email ? email : `${walletAddress}@${domain}`; const { name, avatar } = (await options.ensLookup?.({ walletAddress })) ?? {}; user = await ctx.context.internalAdapter.createUser({ name: name ?? walletAddress, email: userEmail, image: avatar ?? "", }); // Create wallet address record await ctx.context.adapter.create({ model: "walletAddress", data: { userId: user.id, address: walletAddress, chainId, isPrimary: true, // First address is primary createdAt: new Date(), }, }); // Create account record for wallet authentication await ctx.context.internalAdapter.createAccount({ userId: user.id, providerId: "siwe", accountId: `${walletAddress}:${chainId}`, createdAt: new Date(), updatedAt: new Date(), }); } else { // User exists, but check if this specific address/chain combo exists if (!existingWalletAddress) { // Add this new chainId to existing user's addresses await ctx.context.adapter.create({ model: "walletAddress", data: { userId: user.id, address: walletAddress, chainId, isPrimary: false, // Additional addresses are not primary by default createdAt: new Date(), }, }); // Create account record for this new wallet+chain combination await ctx.context.internalAdapter.createAccount({ userId: user.id, providerId: "siwe", accountId: `${walletAddress}:${chainId}`, createdAt: new Date(), updatedAt: new Date(), }); } } const session = await ctx.context.internalAdapter.createSession( user.id, ); if (!session) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "Internal Server Error", status: 500, }); } await setSessionCookie(ctx, { session, user }); return ctx.json({ token: session.token, success: true, user: { id: user.id, walletAddress, chainId, }, }); } catch (error: unknown) { if (error instanceof APIError) throw error; throw new APIError("UNAUTHORIZED", { message: "Something went wrong. Please try again later.", error: error instanceof Error ? error.message : "Unknown error", status: 401, }); } }, ), }, }) satisfies BetterAuthPlugin; ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/generic-oauth.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Generic OAuth description: Authenticate users with any OAuth provider --- The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth provider. It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application. ## Installation <Steps> <Step> ### Add the plugin to your auth config To use the Generic OAuth plugin, add it to your auth config. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { genericOAuth } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ // ... other config options plugins: [ genericOAuth({ // [!code highlight] config: [ // [!code highlight] { // [!code highlight] providerId: "provider-id", // [!code highlight] clientId: "test-client-id", // [!code highlight] clientSecret: "test-client-secret", // [!code highlight] discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight] // ... other config options // [!code highlight] }, // [!code highlight] // Add more providers as needed // [!code highlight] ] // [!code highlight] }) // [!code highlight] ] }) ``` </Step> <Step> ### Add the client plugin Include the Generic OAuth client plugin in your authentication client instance. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { genericOAuthClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [ genericOAuthClient() ] }) ``` </Step> </Steps> ## Usage The Generic OAuth plugin provides endpoints for initiating the OAuth flow and handling the callback. Here's how to use them: ### Initiate OAuth Sign-In To start the OAuth sign-in process: <APIMethod path="/sign-in/oauth2" method="POST"> ```ts type signInWithOAuth2 = { /** * The provider ID for the OAuth provider. */ providerId: string = "provider-id" /** * The URL to redirect to after sign in. */ callbackURL?: string = "/dashboard" /** * The URL to redirect to if an error occurs. */ errorCallbackURL?: string = "/error-page" /** * The URL to redirect to after login if the user is new. */ newUserCallbackURL?: string = "/welcome" /** * Disable redirect. */ disableRedirect?: boolean = false /** * Scopes to be passed to the provider authorization request. */ scopes?: string[] = ["my-scope"] /** * Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. */ requestSignUp?: boolean = false } ``` </APIMethod> ### Linking OAuth Accounts To link an OAuth account to an existing user: <APIMethod path="/oauth2/link" method="POST" requireSession > ```ts type oAuth2LinkAccount = { /** * The OAuth provider ID. */ providerId: string = "my-provider-id" /** * The URL to redirect to once the account linking was complete. */ callbackURL: string = "/successful-link" } ``` </APIMethod> ### Handle OAuth Callback The plugin mounts a route to handle the OAuth callback `/oauth2/callback/:providerId`. This means by default `${baseURL}/api/auth/oauth2/callback/:providerId` will be used as the callback URL. Make sure your OAuth provider is configured to use this URL. ## Configuration When adding the plugin to your auth config, you can configure multiple OAuth providers. Each provider configuration object supports the following options: ```ts interface GenericOAuthConfig { providerId: string; discoveryUrl?: string; authorizationUrl?: string; tokenUrl?: string; userInfoUrl?: string; clientId: string; clientSecret: string; scopes?: string[]; redirectURI?: string; responseType?: string; prompt?: string; pkce?: boolean; accessType?: string; getUserInfo?: (tokens: OAuth2Tokens) => Promise<User | null>; } ``` ### Other Provider Configurations **providerId**: A unique string to identify the OAuth provider configuration. **discoveryUrl**: (Optional) URL to fetch the provider's OAuth 2.0/OIDC configuration. If provided, endpoints like `authorizationUrl`, `tokenUrl`, and `userInfoUrl` can be auto-discovered. **authorizationUrl**: (Optional) The OAuth provider's authorization endpoint. Not required if using `discoveryUrl`. **tokenUrl**: (Optional) The OAuth provider's token endpoint. Not required if using `discoveryUrl`. **userInfoUrl**: (Optional) The endpoint to fetch user profile information. Not required if using `discoveryUrl`. **clientId**: The OAuth client ID issued by your provider. **clientSecret**: The OAuth client secret issued by your provider. **scopes**: (Optional) An array of scopes to request from the provider (e.g., `["openid", "email", "profile"]`). **redirectURI**: (Optional) The redirect URI to use for the OAuth flow. If not set, a default is constructed based on your app's base URL. **responseType**: (Optional) The OAuth response type. Defaults to `"code"` for authorization code flow. **responseMode**: (Optional) The response mode for the authorization code request, such as `"query"` or `"form_post"`. **prompt**: (Optional) Controls the authentication experience (e.g., force login, consent, etc.). **pkce**: (Optional) If true, enables PKCE (Proof Key for Code Exchange) for enhanced security. Defaults to `false`. **accessType**: (Optional) The access type for the authorization request. Use `"offline"` to request a refresh token. **getUserInfo**: (Optional) A custom function to fetch user info from the provider, given the OAuth tokens. If not provided, a default fetch is used. **mapProfileToUser**: (Optional) A function to map the provider's user profile to your app's user object. Useful for custom field mapping or transformations. **authorizationUrlParams**: (Optional) Additional query parameters to add to the authorization URL. These can override default parameters. You can also provide a function that returns the parameters. **tokenUrlParams**: (Optional) Additional query parameters to add to the token URL. These can override default parameters. You can also provide a function that returns the parameters. **disableImplicitSignUp**: (Optional) If true, disables automatic sign-up for new users. Sign-in must be explicitly requested with sign-up intent. **disableSignUp**: (Optional) If true, disables sign-up for new users entirely. Only existing users can sign in. **authentication**: (Optional) The authentication method for token requests. Can be `'basic'` or `'post'`. Defaults to `'post'`. **discoveryHeaders**: (Optional) Custom headers to include in the discovery request. Useful for providers that require special headers. **authorizationHeaders**: (Optional) Custom headers to include in the authorization request. Useful for providers that require special headers. **overrideUserInfo**: (Optional) If true, the user's info in your database will be updated with the provider's info every time they sign in. Defaults to `false`. ## Advanced Usage ### Custom User Info Fetching You can provide a custom `getUserInfo` function to handle specific provider requirements: ```ts genericOAuth({ config: [ { providerId: "custom-provider", // ... other config options getUserInfo: async (tokens) => { // Custom logic to fetch and return user info const userInfo = await fetchUserInfoFromCustomProvider(tokens); return { id: userInfo.sub, email: userInfo.email, name: userInfo.name, // ... map other fields as needed }; } } ] }) ``` ### Map User Info Fields If the user info returned by the provider does not match the expected format, or you need to map additional fields, you can use the `mapProfileToUser`: ```ts genericOAuth({ config: [ { providerId: "custom-provider", // ... other config options mapProfileToUser: async (profile) => { return { firstName: profile.given_name, // ... map other fields as needed }; } } ] }) ``` ### Error Handling The plugin includes built-in error handling for common OAuth issues. Errors are typically redirected to your application's error page with an appropriate error message in the URL parameters. If the callback URL is not provided, the user will be redirected to Better Auth's default error page. ```