This is page 17 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/content/docs/plugins/mcp.mdx: -------------------------------------------------------------------------------- ```markdown --- title: MCP description: MCP provider plugin for Better Auth --- `OAuth` `MCP` The **MCP** plugin lets your app act as an OAuth provider for MCP clients. It handles authentication and makes it easy to issue and manage access tokens for MCP applications. ## Installation <Steps> <Step> ### Add the Plugin Add the MCP plugin to your auth configuration and specify the login page path. ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { mcp } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ mcp({ loginPage: "/sign-in" // path to your login page }) ] }); ``` <Callout> This doesn't have a client plugin, so you don't need to make any changes to your authClient. </Callout> </Step> <Step> ### Generate Schema 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> The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details. </Step> </Steps> ## Usage ### OAuth Discovery Metadata Better Auth already handles the `/api/auth/.well-known/oauth-authorization-server` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-authorization-server` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients: ```ts title=".well-known/oauth-authorization-server/route.ts" import { oAuthDiscoveryMetadata } from "better-auth/plugins"; import { auth } from "../../../lib/auth"; export const GET = oAuthDiscoveryMetadata(auth); ``` ### OAuth Protected Resource Metadata Better Auth already handles the `/api/auth/.well-known/oauth-protected-resource` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-protected-resource` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients: ```ts title="/.well-known/oauth-protected-resource/route.ts" import { oAuthProtectedResourceMetadata } from "better-auth/plugins"; import { auth } from "@/lib/auth"; export const GET = oAuthProtectedResourceMetadata(auth); ``` ### MCP Session Handling You can use the helper function `withMcpAuth` to get the session and handle unauthenticated calls automatically. ```ts title="api/[transport]/route.ts" import { auth } from "@/lib/auth"; import { createMcpHandler } from "@vercel/mcp-adapter"; import { withMcpAuth } from "better-auth/plugins"; import { z } from "zod"; const handler = withMcpAuth(auth, (req, session) => { // session contains the access token record with scopes and user ID return createMcpHandler( (server) => { server.tool( "echo", "Echo a message", { message: z.string() }, async ({ message }) => { return { content: [{ type: "text", text: `Tool echo: ${message}` }], }; }, ); }, { capabilities: { tools: { echo: { description: "Echo a message", }, }, }, }, { redisUrl: process.env.REDIS_URL, basePath: "/api", verboseLogs: true, maxDuration: 60, }, )(req); }); export { handler as GET, handler as POST, handler as DELETE }; ``` You can also use `auth.api.getMcpSession` to get the session using the access token sent from the MCP client: ```ts title="api/[transport]/route.ts" import { auth } from "@/lib/auth"; import { createMcpHandler } from "@vercel/mcp-adapter"; import { z } from "zod"; const handler = async (req: Request) => { // session contains the access token record with scopes and user ID const session = await auth.api.getMcpSession({ headers: req.headers }) if(!session){ //this is important and you must return 401 return new Response(null, { status: 401 }) } return createMcpHandler( (server) => { server.tool( "echo", "Echo a message", { message: z.string() }, async ({ message }) => { return { content: [{ type: "text", text: `Tool echo: ${message}` }], }; }, ); }, { capabilities: { tools: { echo: { description: "Echo a message", }, }, }, }, { redisUrl: process.env.REDIS_URL, basePath: "/api", verboseLogs: true, maxDuration: 60, }, )(req); } export { handler as GET, handler as POST, handler as DELETE }; ``` ## Configuration The MCP plugin accepts the following configuration options: <TypeTable type={{ loginPage: { description: "Path to the login page where users will be redirected for authentication", type: "string", required: true }, resource: { description: "The resource that should be returned by the protected resource metadata endpoint", type: "string", required: false }, oidcConfig: { description: "Optional OIDC configuration options", type: "object", required: false } }} /> ### OIDC Configuration The plugin supports additional OIDC configuration options through the `oidcConfig` parameter: <TypeTable type={{ codeExpiresIn: { description: "Expiration time for authorization codes in seconds", type: "number", default: 600 }, accessTokenExpiresIn: { description: "Expiration time for access tokens in seconds", type: "number", default: 3600 }, refreshTokenExpiresIn: { description: "Expiration time for refresh tokens in seconds", type: "number", default: 604800 }, defaultScope: { description: "Default scope for OAuth requests", type: "string", default: "openid" }, scopes: { description: "Additional scopes to support", type: "string[]", default: '["openid", "profile", "email", "offline_access"]' } }} /> ## Schema The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details. ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/menubar.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { CheckIcon, ChevronRightIcon, DotFilledIcon, } from "@radix-ui/react-icons"; import * as MenubarPrimitive from "@radix-ui/react-menubar"; import { cn } from "@/lib/utils"; const MenubarMenu = MenubarPrimitive.Menu; const MenubarGroup = MenubarPrimitive.Group; const MenubarPortal = MenubarPrimitive.Portal; const MenubarSub = MenubarPrimitive.Sub; const MenubarRadioGroup = MenubarPrimitive.RadioGroup; const Menubar = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Root>>; }) => ( <MenubarPrimitive.Root ref={ref} className={cn( "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm", className, )} {...props} /> ); Menubar.displayName = MenubarPrimitive.Root.displayName; const MenubarTrigger = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Trigger>>; }) => ( <MenubarPrimitive.Trigger ref={ref} className={cn( "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", className, )} {...props} /> ); MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; const MenubarSubTrigger = ({ ref, className, inset, children, ...props }) => ( <MenubarPrimitive.SubTrigger ref={ref} className={cn( "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", inset && "pl-8", className, )} {...props} > {children} <ChevronRightIcon className="ml-auto h-4 w-4" /> </MenubarPrimitive.SubTrigger> ); MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; const MenubarSubContent = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.SubContent>>; }) => ( <MenubarPrimitive.SubContent ref={ref} className={cn( "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} /> ); MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; const MenubarContent = ({ ref, className, align = "start", alignOffset = -4, sideOffset = 8, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Content>>; }) => ( <MenubarPrimitive.Portal> <MenubarPrimitive.Content ref={ref} align={align} alignOffset={alignOffset} sideOffset={sideOffset} className={cn( "z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} /> </MenubarPrimitive.Portal> ); MenubarContent.displayName = MenubarPrimitive.Content.displayName; const MenubarItem = ({ ref, className, inset, ...props }) => ( <MenubarPrimitive.Item ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", inset && "pl-8", className, )} {...props} /> ); MenubarItem.displayName = MenubarPrimitive.Item.displayName; const MenubarCheckboxItem = ({ ref, className, children, checked, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.CheckboxItem>>; }) => ( <MenubarPrimitive.CheckboxItem ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", className, )} checked={checked} {...props} > <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <MenubarPrimitive.ItemIndicator> <CheckIcon className="h-4 w-4" /> </MenubarPrimitive.ItemIndicator> </span> {children} </MenubarPrimitive.CheckboxItem> ); MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; const MenubarRadioItem = ({ ref, className, children, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.RadioItem>>; }) => ( <MenubarPrimitive.RadioItem ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", className, )} {...props} > <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <MenubarPrimitive.ItemIndicator> <DotFilledIcon className="h-4 w-4 fill-current" /> </MenubarPrimitive.ItemIndicator> </span> {children} </MenubarPrimitive.RadioItem> ); MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; const MenubarLabel = ({ ref, className, inset, ...props }) => ( <MenubarPrimitive.Label ref={ref} className={cn( "px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className, )} {...props} /> ); MenubarLabel.displayName = MenubarPrimitive.Label.displayName; const MenubarSeparator = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> & { ref: React.RefObject<React.ElementRef<typeof MenubarPrimitive.Separator>>; }) => ( <MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> ); MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", className, )} {...props} /> ); }; MenubarShortcut.displayname = "MenubarShortcut"; export { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut, }; ``` -------------------------------------------------------------------------------- /docs/app/docs/lib/get-llm-text.ts: -------------------------------------------------------------------------------- ```typescript import { remark } from "remark"; import remarkGfm from "remark-gfm"; import { fileGenerator, remarkDocGen } from "fumadocs-docgen"; import { remarkNpm } from "fumadocs-core/mdx-plugins"; import remarkStringify from "remark-stringify"; import remarkMdx from "remark-mdx"; import { remarkAutoTypeTable } from "fumadocs-typescript"; import { remarkInclude } from "fumadocs-mdx/config"; import { readFile } from "fs/promises"; function extractAPIMethods(rawContent: string): string { const apiMethodRegex = /<APIMethod\s+([^>]+)>([\s\S]*?)<\/APIMethod>/g; return rawContent.replace(apiMethodRegex, (match, attributes, content) => { // Parse attributes by matching const pathMatch = attributes.match(/path="([^"]+)"/); const methodMatch = attributes.match(/method="([^"]+)"/); const requireSessionMatch = attributes.match(/requireSession/); const isServerOnlyMatch = attributes.match(/isServerOnly/); const isClientOnlyMatch = attributes.match(/isClientOnly/); const noResultMatch = attributes.match(/noResult/); const resultVariableMatch = attributes.match(/resultVariable="([^"]+)"/); const forceAsBodyMatch = attributes.match(/forceAsBody/); const forceAsQueryMatch = attributes.match(/forceAsQuery/); const path = pathMatch ? pathMatch[1] : ""; const method = methodMatch ? methodMatch[1] : "GET"; const requireSession = !!requireSessionMatch; const isServerOnly = !!isServerOnlyMatch; const isClientOnly = !!isClientOnlyMatch; const noResult = !!noResultMatch; const resultVariable = resultVariableMatch ? resultVariableMatch[1] : "data"; const forceAsBody = !!forceAsBodyMatch; const forceAsQuery = !!forceAsQueryMatch; const typeMatch = content.match(/type\s+(\w+)\s*=\s*\{([\s\S]*?)\}/); if (!typeMatch) { return match; // Return original if no type found } const functionName = typeMatch[1]; const typeBody = typeMatch[2]; const properties = parseTypeBody(typeBody); const clientCode = generateClientCode(functionName, properties, path); const serverCode = generateServerCode( functionName, properties, method, requireSession, forceAsBody, forceAsQuery, noResult, resultVariable, ); return ` ### Client Side \`\`\`ts ${clientCode} \`\`\` ### Server Side \`\`\`ts ${serverCode} \`\`\` ### Type Definition \`\`\`ts type ${functionName} = {${typeBody} } \`\`\` `; }); } function parseTypeBody(typeBody: string) { const properties: Array<{ name: string; type: string; required: boolean; description: string; exampleValue: string; isServerOnly: boolean; isClientOnly: boolean; }> = []; const lines = typeBody.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*")) continue; const propMatch = trimmed.match( /^(\w+)(\?)?:\s*(.+?)(\s*=\s*["']([^"']+)["'])?(\s*\/\/\s*(.+))?$/, ); if (propMatch) { const [, name, optional, type, , exampleValue, , description] = propMatch; let cleanType = type.trim(); let cleanExampleValue = exampleValue || ""; cleanType = cleanType.replace(/,$/, ""); properties.push({ name, type: cleanType, required: !optional, description: description || "", exampleValue: cleanExampleValue, isServerOnly: false, isClientOnly: false, }); } } return properties; } // Generate client code example function generateClientCode( functionName: string, properties: any[], path: string, ) { if (!functionName || !path) { return "// Unable to generate client code - missing function name or path"; } const clientMethodPath = pathToDotNotation(path); const body = createClientBody(properties); return `const { data, error } = await authClient.${clientMethodPath}(${body});`; } // Generate server code example function generateServerCode( functionName: string, properties: any[], method: string, requireSession: boolean, forceAsBody: boolean, forceAsQuery: boolean, noResult: boolean, resultVariable: string, ) { if (!functionName) { return "// Unable to generate server code - missing function name"; } const body = createServerBody( properties, method, requireSession, forceAsBody, forceAsQuery, ); return `${noResult ? "" : `const ${resultVariable} = `}await auth.api.${functionName}(${body});`; } function pathToDotNotation(input: string): string { return input .split("/") .filter(Boolean) .map((segment) => segment .split("-") .map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1), ) .join(""), ) .join("."); } // Helper function to create client body (simplified version) function createClientBody(props: any[]) { if (props.length === 0) return "{}"; let body = "{\n"; for (const prop of props) { if (prop.isServerOnly) continue; let comment = ""; if (!prop.required || prop.description) { const comments = []; if (!prop.required) comments.push("required"); if (prop.description) comments.push(prop.description); comment = ` // ${comments.join(", ")}`; } body += ` ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`; } body += "}"; return body; } function createServerBody( props: any[], method: string, requireSession: boolean, forceAsBody: boolean, forceAsQuery: boolean, ) { const relevantProps = props.filter((x) => !x.isClientOnly); if (relevantProps.length === 0 && !requireSession) { return "{}"; } let serverBody = "{\n"; if (relevantProps.length > 0) { const bodyKey = (method === "POST" || forceAsBody) && !forceAsQuery ? "body" : "query"; serverBody += ` ${bodyKey}: {\n`; for (const prop of relevantProps) { let comment = ""; if (!prop.required || prop.description) { const comments = []; if (!prop.required) comments.push("required"); if (prop.description) comments.push(prop.description); comment = ` // ${comments.join(", ")}`; } serverBody += ` ${prop.name}${prop.exampleValue ? `: ${prop.exampleValue}` : ""}${prop.type === "Object" ? ": {}" : ""},${comment}\n`; } serverBody += " }"; } if (requireSession) { if (relevantProps.length > 0) serverBody += ","; serverBody += "\n // This endpoint requires session cookies.\n headers: await headers()"; } serverBody += "\n}"; return serverBody; } const processor = remark() .use(remarkMdx) .use(remarkInclude) .use(remarkGfm) .use(remarkAutoTypeTable) .use(remarkDocGen, { generators: [fileGenerator()] }) .use(remarkNpm) .use(remarkStringify); export async function getLLMText(docPage: any) { const category = [docPage.slugs[0]]; // Read the raw file content const rawContent = await readFile(docPage.data._file.absolutePath, "utf-8"); // Extract APIMethod components & other nested wrapper before processing const processedContent = extractAPIMethods(rawContent); const processed = await processor.process({ path: docPage.data._file.absolutePath, value: processedContent, }); return `# ${category}: ${docPage.data.title} URL: ${docPage.url} Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/${ docPage.file.path } ${docPage.data.description} ${processed.toString()} `; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/magic-link/magic-link.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, vi } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { magicLink } from "."; import { createAuthClient } from "../../client"; import { magicLinkClient } from "./client"; import { defaultKeyHasher } from "./utils"; type VerificationEmail = { email: string; token: string; url: string; }; describe("magic link", async () => { let verificationEmail: VerificationEmail = { email: "", token: "", url: "", }; const { customFetchImpl, testUser, sessionSetter } = await getTestInstance({ plugins: [ magicLink({ async sendMagicLink(data) { verificationEmail = data; }, }), ], }); const client = createAuthClient({ plugins: [magicLinkClient()], fetchOptions: { customFetchImpl, }, baseURL: "http://localhost:3000", basePath: "/api/auth", }); it("should send magic link", async () => { await client.signIn.magicLink({ email: testUser.email, }); expect(verificationEmail).toMatchObject({ email: testUser.email, url: expect.stringContaining( "http://localhost:3000/api/auth/magic-link/verify", ), }); }); it("should verify magic link", async () => { const headers = new Headers(); const response = await client.magicLink.verify({ query: { token: new URL(verificationEmail.url).searchParams.get("token") || "", }, fetchOptions: { onSuccess: sessionSetter(headers), }, }); expect(response.data?.token).toBeDefined(); const betterAuthCookie = headers.get("set-cookie"); expect(betterAuthCookie).toBeDefined(); }); it("shouldn't verify magic link with the same token", async () => { await client.magicLink.verify( { query: { token: new URL(verificationEmail.url).searchParams.get("token") || "", }, }, { onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toContain("?error=INVALID_TOKEN"); }, }, ); }); it("shouldn't verify magic link with an expired token", async () => { await client.signIn.magicLink({ email: testUser.email, }); const token = verificationEmail.token; vi.useFakeTimers(); await vi.advanceTimersByTimeAsync(1000 * 60 * 5 + 1); await client.magicLink.verify( { query: { token, callbackURL: "/callback", }, }, { onError(context) { expect(context.response.status).toBe(302); const location = context.response.headers.get("location"); expect(location).toContain("?error=EXPIRED_TOKEN"); }, }, ); }); it("should sign up with magic link", async () => { const email = "[email protected]"; await client.signIn.magicLink({ email, name: "test", }); expect(verificationEmail).toMatchObject({ email, url: expect.stringContaining( "http://localhost:3000/api/auth/magic-link/verify", ), }); const headers = new Headers(); const response = await client.magicLink.verify({ query: { token: new URL(verificationEmail.url).searchParams.get("token") || "", }, fetchOptions: { onSuccess: sessionSetter(headers), }, }); const session = await client.getSession({ fetchOptions: { headers, }, }); expect(session.data?.user).toMatchObject({ name: "test", email: "[email protected]", emailVerified: true, }); }); it("should use custom generateToken function", async () => { const customGenerateToken = vi.fn(() => "custom_token"); const { customFetchImpl } = await getTestInstance({ plugins: [ magicLink({ async sendMagicLink(data) { verificationEmail = data; }, generateToken: customGenerateToken, }), ], }); const customClient = createAuthClient({ plugins: [magicLinkClient()], fetchOptions: { customFetchImpl, }, baseURL: "http://localhost:3000/api/auth", }); await customClient.signIn.magicLink({ email: testUser.email, }); expect(customGenerateToken).toHaveBeenCalled(); expect(verificationEmail.token).toBe("custom_token"); }); }); describe("magic link verify", async () => { const verificationEmail: VerificationEmail[] = [ { email: "", token: "", url: "", }, ]; const { customFetchImpl, testUser, sessionSetter } = await getTestInstance({ plugins: [ magicLink({ async sendMagicLink(data) { verificationEmail.push(data); }, }), ], }); const client = createAuthClient({ plugins: [magicLinkClient()], fetchOptions: { customFetchImpl, }, baseURL: "http://localhost:3000/api/auth", }); it("should verify last magic link", async () => { await client.signIn.magicLink({ email: testUser.email, }); await client.signIn.magicLink({ email: testUser.email, }); await client.signIn.magicLink({ email: testUser.email, }); const headers = new Headers(); const lastEmail = verificationEmail.pop() as VerificationEmail; const response = await client.magicLink.verify({ query: { token: new URL(lastEmail.url).searchParams.get("token") || "", }, fetchOptions: { onSuccess: sessionSetter(headers), }, }); expect(response.data?.token).toBeDefined(); const betterAuthCookie = headers.get("set-cookie"); expect(betterAuthCookie).toBeDefined(); }); }); describe("magic link storeToken", async () => { it("should store token in hashed", async () => { let verificationEmail: VerificationEmail = { email: "", token: "", url: "", }; const { auth, signInWithTestUser, client, testUser } = await getTestInstance({ plugins: [ magicLink({ storeToken: "hashed", sendMagicLink(data, request) { verificationEmail = data; }, }), ], }); const internalAdapter = (await auth.$context).internalAdapter; const { headers } = await signInWithTestUser(); const response = await auth.api.signInMagicLink({ body: { email: testUser.email, }, headers, }); const hashedToken = await defaultKeyHasher(verificationEmail.token); const storedToken = await internalAdapter.findVerificationValue(hashedToken); expect(storedToken).toBeDefined(); const response2 = await auth.api.signInMagicLink({ body: { email: testUser.email, }, headers, }); expect(response2.status).toBe(true); }); it("should store token with custom hasher", async () => { let verificationEmail: VerificationEmail = { email: "", token: "", url: "", }; const { auth, signInWithTestUser, client, testUser } = await getTestInstance({ plugins: [ magicLink({ storeToken: { type: "custom-hasher", async hash(token) { return token + "hashed"; }, }, sendMagicLink(data, request) { verificationEmail = data; }, }), ], }); const internalAdapter = (await auth.$context).internalAdapter; const { headers } = await signInWithTestUser(); await auth.api.signInMagicLink({ body: { email: testUser.email, }, headers, }); const hashedToken = `${verificationEmail.token}hashed`; const storedToken = await internalAdapter.findVerificationValue(hashedToken); expect(storedToken).toBeDefined(); const response2 = await auth.api.signInMagicLink({ body: { email: testUser.email, }, headers, }); expect(response2.status).toBe(true); }); }); ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/microsoft-entra-id.ts: -------------------------------------------------------------------------------- ```typescript import { validateAuthorizationCode, createAuthorizationURL, refreshAccessToken, } from "../oauth2"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { betterFetch } from "@better-fetch/fetch"; import { logger } from "../env"; import { decodeJwt } from "jose"; import { base64 } from "@better-auth/utils/base64"; /** * @see [Microsoft Identity Platform - Optional claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference) */ export interface MicrosoftEntraIDProfile extends Record<string, any> { /** Identifies the intended recipient of the token */ aud: string; /** Identifies the issuer, or "authorization server" that constructs and returns the token */ iss: string; /** Indicates when the authentication for the token occurred */ iat: Date; /** Records the identity provider that authenticated the subject of the token */ idp: string; /** Identifies the time before which the JWT can't be accepted for processing */ nbf: Date; /** Identifies the expiration time on or after which the JWT can't be accepted for processing */ exp: Date; /** Code hash included in ID tokens when issued with an OAuth 2.0 authorization code */ c_hash: string; /** Access token hash included in ID tokens when issued with an OAuth 2.0 access token */ at_hash: string; /** Internal claim used to record data for token reuse */ aio: string; /** The primary username that represents the user */ preferred_username: string; /** User's email address */ email: string; /** Human-readable value that identifies the subject of the token */ name: string; /** Matches the parameter included in the original authorize request */ nonce: string; /** User's profile picture */ picture: string; /** Immutable identifier for the user account */ oid: string; /** Set of roles assigned to the user */ roles: string[]; /** Internal claim used to revalidate tokens */ rh: string; /** Subject identifier - unique to application ID */ sub: string; /** Tenant ID the user is signing in to */ tid: string; /** Unique identifier for a session */ sid: string; /** Token identifier claim */ uti: string; /** Indicates if user is in at least one group */ hasgroups: boolean; /** User account status in tenant (0 = member, 1 = guest) */ acct: 0 | 1; /** Auth Context IDs */ acrs: string; /** Time when the user last authenticated */ auth_time: Date; /** User's country/region */ ctry: string; /** IP address of requesting client when inside VNET */ fwd: string; /** Group claims */ groups: string; /** Login hint for SSO */ login_hint: string; /** Resource tenant's country/region */ tenant_ctry: string; /** Region of the resource tenant */ tenant_region_scope: string; /** UserPrincipalName */ upn: string; /** User's verified primary email addresses */ verified_primary_email: string[]; /** User's verified secondary email addresses */ verified_secondary_email: string[]; /** VNET specifier information */ vnet: string; /** Client Capabilities */ xms_cc: string; /** Whether user's email domain is verified */ xms_edov: boolean; /** Preferred data location for Multi-Geo tenants */ xms_pdl: string; /** User preferred language */ xms_pl: string; /** Tenant preferred language */ xms_tpl: string; /** Zero-touch Deployment ID */ ztdid: string; /** IP Address */ ipaddr: string; /** On-premises Security Identifier */ onprem_sid: string; /** Password Expiration Time */ pwd_exp: number; /** Change Password URL */ pwd_url: string; /** Inside Corporate Network flag */ in_corp: string; /** User's family name/surname */ family_name: string; /** User's given/first name */ given_name: string; } export interface MicrosoftOptions extends ProviderOptions<MicrosoftEntraIDProfile> { clientId: string; /** * The tenant ID of the Microsoft account * @default "common" */ tenantId?: string; /** * The authentication authority URL. Use the default "https://login.microsoftonline.com" for standard Entra ID or "https://<tenant-id>.ciamlogin.com" for CIAM scenarios. * @default "https://login.microsoftonline.com" */ authority?: string; /** * The size of the profile photo * @default 48 */ profilePhotoSize?: 48 | 64 | 96 | 120 | 240 | 360 | 432 | 504 | 648; /** * Disable profile photo */ disableProfilePhoto?: boolean; } export const microsoft = (options: MicrosoftOptions) => { const tenant = options.tenantId || "common"; const authority = options.authority || "https://login.microsoftonline.com"; const authorizationEndpoint = `${authority}/${tenant}/oauth2/v2.0/authorize`; const tokenEndpoint = `${authority}/${tenant}/oauth2/v2.0/token`; return { id: "microsoft", name: "Microsoft EntraID", createAuthorizationURL(data) { const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"]; options.scope && scopes.push(...options.scope); data.scopes && scopes.push(...data.scopes); return createAuthorizationURL({ id: "microsoft", options, authorizationEndpoint, state: data.state, codeVerifier: data.codeVerifier, scopes, redirectURI: data.redirectURI, prompt: options.prompt, loginHint: data.loginHint, }); }, validateAuthorizationCode({ code, codeVerifier, redirectURI }) { return validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (!token.idToken) { return null; } const user = decodeJwt(token.idToken) as MicrosoftEntraIDProfile; const profilePhotoSize = options.profilePhotoSize || 48; await betterFetch<ArrayBuffer>( `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`, { headers: { Authorization: `Bearer ${token.accessToken}`, }, async onResponse(context) { if (options.disableProfilePhoto || !context.response.ok) { return; } try { const response = context.response.clone(); const pictureBuffer = await response.arrayBuffer(); const pictureBase64 = base64.encode(pictureBuffer); user.picture = `data:image/jpeg;base64, ${pictureBase64}`; } catch (e) { logger.error( e && typeof e === "object" && "name" in e ? (e.name as string) : "", e, ); } }, }, ); const userMap = await options.mapProfileToUser?.(user); return { user: { id: user.sub, name: user.name, email: user.email, image: user.picture, emailVerified: true, ...userMap, }, data: user, }; }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"]; options.scope && scopes.push(...options.scope); return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientSecret: options.clientSecret, }, extraParams: { scope: scopes.join(" "), // Include the scopes in request to microsoft }, tokenEndpoint, }); }, options, } satisfies OAuthProvider; }; ``` -------------------------------------------------------------------------------- /packages/telemetry/src/detectors/detect-auth-config.ts: -------------------------------------------------------------------------------- ```typescript import type { TelemetryContext } from "../types"; import type { BetterAuthOptions } from "@better-auth/core"; export function getTelemetryAuthConfig( options: BetterAuthOptions, context?: TelemetryContext, ) { return { database: context?.database, adapter: context?.adapter, emailVerification: { sendVerificationEmail: !!options.emailVerification?.sendVerificationEmail, sendOnSignUp: !!options.emailVerification?.sendOnSignUp, sendOnSignIn: !!options.emailVerification?.sendOnSignIn, autoSignInAfterVerification: !!options.emailVerification?.autoSignInAfterVerification, expiresIn: options.emailVerification?.expiresIn, onEmailVerification: !!options.emailVerification?.onEmailVerification, afterEmailVerification: !!options.emailVerification?.afterEmailVerification, }, emailAndPassword: { enabled: !!options.emailAndPassword?.enabled, disableSignUp: !!options.emailAndPassword?.disableSignUp, requireEmailVerification: !!options.emailAndPassword?.requireEmailVerification, maxPasswordLength: options.emailAndPassword?.maxPasswordLength, minPasswordLength: options.emailAndPassword?.minPasswordLength, sendResetPassword: !!options.emailAndPassword?.sendResetPassword, resetPasswordTokenExpiresIn: options.emailAndPassword?.resetPasswordTokenExpiresIn, onPasswordReset: !!options.emailAndPassword?.onPasswordReset, password: { hash: !!options.emailAndPassword?.password?.hash, verify: !!options.emailAndPassword?.password?.verify, }, autoSignIn: !!options.emailAndPassword?.autoSignIn, revokeSessionsOnPasswordReset: !!options.emailAndPassword?.revokeSessionsOnPasswordReset, }, socialProviders: Object.keys(options.socialProviders || {}).map((p) => { const provider = options.socialProviders?.[p as keyof typeof options.socialProviders]; if (!provider) return {}; return { id: p, mapProfileToUser: !!provider.mapProfileToUser, disableDefaultScope: !!provider.disableDefaultScope, disableIdTokenSignIn: !!provider.disableIdTokenSignIn, disableImplicitSignUp: provider.disableImplicitSignUp, disableSignUp: provider.disableSignUp, getUserInfo: !!provider.getUserInfo, overrideUserInfoOnSignIn: !!provider.overrideUserInfoOnSignIn, prompt: provider.prompt, verifyIdToken: !!provider.verifyIdToken, scope: provider.scope, refreshAccessToken: !!provider.refreshAccessToken, }; }), plugins: options.plugins?.map((p) => p.id.toString()), user: { modelName: options.user?.modelName, fields: options.user?.fields, additionalFields: options.user?.additionalFields, changeEmail: { enabled: options.user?.changeEmail?.enabled, sendChangeEmailVerification: !!options.user?.changeEmail?.sendChangeEmailVerification, }, }, verification: { modelName: options.verification?.modelName, disableCleanup: options.verification?.disableCleanup, fields: options.verification?.fields, }, session: { modelName: options.session?.modelName, additionalFields: options.session?.additionalFields, cookieCache: { enabled: options.session?.cookieCache?.enabled, maxAge: options.session?.cookieCache?.maxAge, }, disableSessionRefresh: options.session?.disableSessionRefresh, expiresIn: options.session?.expiresIn, fields: options.session?.fields, freshAge: options.session?.freshAge, preserveSessionInDatabase: options.session?.preserveSessionInDatabase, storeSessionInDatabase: options.session?.storeSessionInDatabase, updateAge: options.session?.updateAge, }, account: { modelName: options.account?.modelName, fields: options.account?.fields, encryptOAuthTokens: options.account?.encryptOAuthTokens, updateAccountOnSignIn: options.account?.updateAccountOnSignIn, accountLinking: { enabled: options.account?.accountLinking?.enabled, trustedProviders: options.account?.accountLinking?.trustedProviders, updateUserInfoOnLink: options.account?.accountLinking?.updateUserInfoOnLink, allowUnlinkingAll: options.account?.accountLinking?.allowUnlinkingAll, }, }, hooks: { after: !!options.hooks?.after, before: !!options.hooks?.before, }, secondaryStorage: !!options.secondaryStorage, advanced: { cookiePrefix: !!options.advanced?.cookiePrefix, //this shouldn't be tracked cookies: !!options.advanced?.cookies, crossSubDomainCookies: { domain: !!options.advanced?.crossSubDomainCookies?.domain, enabled: options.advanced?.crossSubDomainCookies?.enabled, additionalCookies: options.advanced?.crossSubDomainCookies?.additionalCookies, }, database: { useNumberId: !!options.advanced?.database?.useNumberId, generateId: options.advanced?.database?.generateId, defaultFindManyLimit: options.advanced?.database?.defaultFindManyLimit, }, useSecureCookies: options.advanced?.useSecureCookies, ipAddress: { disableIpTracking: options.advanced?.ipAddress?.disableIpTracking, ipAddressHeaders: options.advanced?.ipAddress?.ipAddressHeaders, }, disableCSRFCheck: options.advanced?.disableCSRFCheck, cookieAttributes: { expires: options.advanced?.defaultCookieAttributes?.expires, secure: options.advanced?.defaultCookieAttributes?.secure, sameSite: options.advanced?.defaultCookieAttributes?.sameSite, domain: !!options.advanced?.defaultCookieAttributes?.domain, path: options.advanced?.defaultCookieAttributes?.path, httpOnly: options.advanced?.defaultCookieAttributes?.httpOnly, }, }, trustedOrigins: options.trustedOrigins?.length, rateLimit: { storage: options.rateLimit?.storage, modelName: options.rateLimit?.modelName, window: options.rateLimit?.window, customStorage: !!options.rateLimit?.customStorage, enabled: options.rateLimit?.enabled, max: options.rateLimit?.max, }, onAPIError: { errorURL: options.onAPIError?.errorURL, onError: !!options.onAPIError?.onError, throw: options.onAPIError?.throw, }, logger: { disabled: options.logger?.disabled, level: options.logger?.level, log: !!options.logger?.log, }, databaseHooks: { user: { create: { after: !!options.databaseHooks?.user?.create?.after, before: !!options.databaseHooks?.user?.create?.before, }, update: { after: !!options.databaseHooks?.user?.update?.after, before: !!options.databaseHooks?.user?.update?.before, }, }, session: { create: { after: !!options.databaseHooks?.session?.create?.after, before: !!options.databaseHooks?.session?.create?.before, }, update: { after: !!options.databaseHooks?.session?.update?.after, before: !!options.databaseHooks?.session?.update?.before, }, }, account: { create: { after: !!options.databaseHooks?.account?.create?.after, before: !!options.databaseHooks?.account?.create?.before, }, update: { after: !!options.databaseHooks?.account?.update?.after, before: !!options.databaseHooks?.account?.update?.before, }, }, verification: { create: { after: !!options.databaseHooks?.verification?.create?.after, before: !!options.databaseHooks?.verification?.create?.before, }, update: { after: !!options.databaseHooks?.verification?.update?.after, before: !!options.databaseHooks?.verification?.update?.before, }, }, }, }; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/anonymous/index.ts: -------------------------------------------------------------------------------- ```typescript import { APIError, getSessionFromCtx } from "../../api"; import { createAuthEndpoint, createAuthMiddleware, } from "@better-auth/core/api"; import type { BetterAuthPlugin, GenericEndpointContext, } from "@better-auth/core"; import type { InferOptionSchema, Session, User } from "../../types"; import { parseSetCookieHeader, setSessionCookie } from "../../cookies"; import { getOrigin } from "../../utils/url"; import { mergeSchema } from "../../db/schema"; import type { EndpointContext } from "better-call"; import { generateId } from "../../utils/id"; import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; import type { AuthContext } from "@better-auth/core"; import { defineErrorCodes } from "@better-auth/core/utils"; export interface UserWithAnonymous extends User { isAnonymous: boolean; } export interface AnonymousOptions { /** * Configure the domain name of the temporary email * address for anonymous users in the database. * @default "baseURL" */ emailDomainName?: string; /** * A useful hook to run after an anonymous user * is about to link their account. */ onLinkAccount?: (data: { anonymousUser: { user: UserWithAnonymous & Record<string, any>; session: Session & Record<string, any>; }; newUser: { user: User & Record<string, any>; session: Session & Record<string, any>; }; ctx: GenericEndpointContext; }) => Promise<void> | void; /** * Disable deleting the anonymous user after linking */ disableDeleteAnonymousUser?: boolean; /** * A hook to generate a name for the anonymous user. * Useful if you want to have random names for anonymous users, or if `name` is unique in your database. * @returns The name for the anonymous user. */ generateName?: ( ctx: EndpointContext< "/sign-in/anonymous", { method: "POST"; }, AuthContext >, ) => Promise<string> | string; /** * Custom schema for the anonymous plugin */ schema?: InferOptionSchema<typeof schema>; } const schema = { user: { fields: { isAnonymous: { type: "boolean", required: false, }, }, }, } satisfies BetterAuthPluginDBSchema; const ERROR_CODES = defineErrorCodes({ FAILED_TO_CREATE_USER: "Failed to create user", COULD_NOT_CREATE_SESSION: "Could not create session", ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: "Anonymous users cannot sign in again anonymously", }); export const anonymous = (options?: AnonymousOptions) => { return { id: "anonymous", endpoints: { signInAnonymous: createAuthEndpoint( "/sign-in/anonymous", { method: "POST", metadata: { openapi: { description: "Sign in anonymously", responses: { 200: { description: "Sign in anonymously", content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/User", }, session: { $ref: "#/components/schemas/Session", }, }, }, }, }, }, }, }, }, }, async (ctx) => { // If the current request already has a valid anonymous session, we should // reject any further attempts to create another anonymous user. This // prevents an anonymous user from signing in anonymously again while they // are already authenticated. const existingSession = await getSessionFromCtx<{ isAnonymous: boolean; }>(ctx, { disableRefresh: true }); if (existingSession?.user.isAnonymous) { throw new APIError("BAD_REQUEST", { message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY, }); } const { emailDomainName = getOrigin(ctx.context.baseURL) } = options || {}; const id = generateId(); const email = `temp-${id}@${emailDomainName}`; const name = (await options?.generateName?.(ctx)) || "Anonymous"; const newUser = await ctx.context.internalAdapter.createUser({ email, emailVerified: false, isAnonymous: true, name, createdAt: new Date(), updatedAt: new Date(), }); if (!newUser) { throw ctx.error("INTERNAL_SERVER_ERROR", { message: ERROR_CODES.FAILED_TO_CREATE_USER, }); } const session = await ctx.context.internalAdapter.createSession( newUser.id, ); if (!session) { return ctx.json(null, { status: 400, body: { message: ERROR_CODES.COULD_NOT_CREATE_SESSION, }, }); } await setSessionCookie(ctx, { session, user: newUser, }); return ctx.json({ token: session.token, user: { id: newUser.id, email: newUser.email, emailVerified: newUser.emailVerified, name: newUser.name, createdAt: newUser.createdAt, updatedAt: newUser.updatedAt, }, }); }, ), }, hooks: { after: [ { matcher(ctx) { return ( ctx.path.startsWith("/sign-in") || ctx.path.startsWith("/sign-up") || ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback") || ctx.path.startsWith("/magic-link/verify") || ctx.path.startsWith("/email-otp/verify-email") || ctx.path.startsWith("/one-tap/callback") || ctx.path.startsWith("/passkey/verify-authentication") || ctx.path.startsWith("/phone-number/verify") ); }, handler: createAuthMiddleware(async (ctx) => { const setCookie = ctx.context.responseHeaders?.get("set-cookie"); /** * We can consider the user is about to sign in or sign up * if the response contains a session token. */ const sessionTokenName = ctx.context.authCookies.sessionToken.name; /** * The user is about to link their account. */ const sessionCookie = parseSetCookieHeader(setCookie || "") .get(sessionTokenName) ?.value.split(".")[0]!; if (!sessionCookie) { return; } /** * Make sure the user had an anonymous session. */ const session = await getSessionFromCtx<{ isAnonymous: boolean }>( ctx, { disableRefresh: true, }, ); if (!session || !session.user.isAnonymous) { return; } if (ctx.path === "/sign-in/anonymous" && !ctx.context.newSession) { throw new APIError("BAD_REQUEST", { message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY, }); } const newSession = ctx.context.newSession; if (!newSession) { return; } // At this point the user is linking their previous anonymous account with a // new credential (email / social). Invoke the provided callback so that the // integrator can perform any additional logic such as transferring data // from the anonymous user to the new user. if (options?.onLinkAccount) { await options?.onLinkAccount?.({ anonymousUser: session, newUser: newSession, ctx, }); } if (!options?.disableDeleteAnonymousUser) { await ctx.context.internalAdapter.deleteUser(session.user.id); } }), }, ], }, schema: mergeSchema(schema, options?.schema), $ERROR_CODES: ERROR_CODES, } satisfies BetterAuthPlugin; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/two-factor/totp/index.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import * as z from "zod"; import { createAuthEndpoint } from "@better-auth/core/api"; import { sessionMiddleware } from "../../../api"; import { symmetricDecrypt } from "../../../crypto"; import type { BackupCodeOptions } from "../backup-codes"; import { verifyTwoFactor } from "../verify-two-factor"; import type { TwoFactorProvider, TwoFactorTable, UserWithTwoFactor, } from "../types"; import { setSessionCookie } from "../../../cookies"; import { TWO_FACTOR_ERROR_CODES } from "../error-code"; import { createOTP } from "@better-auth/utils/otp"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; export type TOTPOptions = { /** * Issuer */ issuer?: string; /** * How many digits the otp to be * * @default 6 */ digits?: 6 | 8; /** * Period for otp in seconds. * @default 30 */ period?: number; /** * Backup codes configuration */ backupCodes?: BackupCodeOptions; /** * Disable totp */ disable?: boolean; }; export const totp2fa = (options?: TOTPOptions) => { const opts = { ...options, digits: options?.digits || 6, period: options?.period || 30, }; const twoFactorTable = "twoFactor"; const generateTOTP = createAuthEndpoint( "/totp/generate", { method: "POST", body: z.object({ secret: z.string().meta({ description: "The secret to generate the TOTP code", }), }), metadata: { openapi: { summary: "Generate TOTP code", description: "Use this endpoint to generate a TOTP code", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { code: { type: "string", }, }, }, }, }, }, }, }, SERVER_ONLY: true, }, }, async (ctx) => { if (options?.disable) { ctx.context.logger.error( "totp isn't configured. please pass totp option on two factor plugin to enable totp", ); throw new APIError("BAD_REQUEST", { message: "totp isn't configured", }); } const code = await createOTP(ctx.body.secret, { period: opts.period, digits: opts.digits, }).totp(); return { code }; }, ); const getTOTPURI = createAuthEndpoint( "/two-factor/get-totp-uri", { method: "POST", use: [sessionMiddleware], body: z.object({ password: z.string().meta({ description: "User password", }), }), metadata: { openapi: { summary: "Get TOTP URI", description: "Use this endpoint to get the TOTP URI", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { totpURI: { type: "string", }, }, }, }, }, }, }, }, }, }, async (ctx) => { if (options?.disable) { ctx.context.logger.error( "totp isn't configured. please pass totp option on two factor plugin to enable totp", ); throw new APIError("BAD_REQUEST", { message: "totp isn't configured", }); } const user = ctx.context.session.user as UserWithTwoFactor; const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({ model: twoFactorTable, where: [ { field: "userId", value: user.id, }, ], }); if (!twoFactor) { throw new APIError("BAD_REQUEST", { message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED, }); } const secret = await symmetricDecrypt({ key: ctx.context.secret, data: twoFactor.secret, }); await ctx.context.password.checkPassword(user.id, ctx); const totpURI = createOTP(secret, { digits: opts.digits, period: opts.period, }).url(options?.issuer || ctx.context.appName, user.email); return { totpURI, }; }, ); const verifyTOTP = createAuthEndpoint( "/two-factor/verify-totp", { method: "POST", body: z.object({ code: z.string().meta({ description: 'The otp code to verify. Eg: "012345"', }), /** * if true, the device will be trusted * for 30 days. It'll be refreshed on * every sign in request within this time. */ trustDevice: z .boolean() .meta({ description: "If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. Eg: true", }) .optional(), }), metadata: { openapi: { summary: "Verify two factor TOTP", description: "Verify two factor TOTP", responses: { 200: { description: "Successful response", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", }, }, }, }, }, }, }, }, }, }, async (ctx) => { if (options?.disable) { ctx.context.logger.error( "totp isn't configured. please pass totp option on two factor plugin to enable totp", ); throw new APIError("BAD_REQUEST", { message: "totp isn't configured", }); } const { session, valid, invalid } = await verifyTwoFactor(ctx); const user = session.user as UserWithTwoFactor; const twoFactor = await ctx.context.adapter.findOne<TwoFactorTable>({ model: twoFactorTable, where: [ { field: "userId", value: user.id, }, ], }); if (!twoFactor) { throw new APIError("BAD_REQUEST", { message: TWO_FACTOR_ERROR_CODES.TOTP_NOT_ENABLED, }); } const decrypted = await symmetricDecrypt({ key: ctx.context.secret, data: twoFactor.secret, }); const status = await createOTP(decrypted, { period: opts.period, digits: opts.digits, }).verify(ctx.body.code); if (!status) { return invalid("INVALID_CODE"); } if (!user.twoFactorEnabled) { if (!session.session) { throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION, }); } const updatedUser = await ctx.context.internalAdapter.updateUser( user.id, { twoFactorEnabled: true, }, ); const newSession = await ctx.context.internalAdapter .createSession(user.id, false, session.session) .catch((e) => { throw e; }); await ctx.context.internalAdapter.deleteSession(session.session.token); await setSessionCookie(ctx, { session: newSession, user: updatedUser, }); } return valid(ctx); }, ); return { id: "totp", endpoints: { /** * ### Endpoint * * POST `/totp/generate` * * ### API Methods * * **server:** * `auth.api.generateTOTP` * * **client:** * `authClient.totp.generate` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/totp#api-method-totp-generate) */ generateTOTP: generateTOTP, /** * ### Endpoint * * POST `/two-factor/get-totp-uri` * * ### API Methods * * **server:** * `auth.api.getTOTPURI` * * **client:** * `authClient.twoFactor.getTotpUri` * * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/two-factor#api-method-two-factor-get-totp-uri) */ getTOTPURI: getTOTPURI, verifyTOTP, }, } satisfies TwoFactorProvider; }; ``` -------------------------------------------------------------------------------- /docs/app/v1/page.tsx: -------------------------------------------------------------------------------- ```typescript import { ArrowRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { BackgroundLines } from "./bg-line"; import Link from "next/link"; import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; import { Metadata } from "next"; export const metadata: Metadata = { title: "V1.0 Release", description: "Better Auth V1.0 release notes", openGraph: { images: "https://better-auth.com/v1-og.png", title: "V1.0 Release", description: "Better Auth V1.0 release notes", url: "https://better-auth.com/v1", type: "article", siteName: "Better Auth", }, twitter: { images: "https://better-auth.com/v1-og.png", card: "summary_large_image", site: "@better_auth", creator: "@better_auth", title: "V1.0 Release", description: "Better Auth V1.0 release notes", }, }; export default function V1Ship() { return ( <div className="min-h-screen bg-transparent overflow-hidden"> <div className="h-[50vh] bg-transparent/10 relative"> <BackgroundLines> <div className="absolute bottom-1/3 left-1/2 transform -translate-x-1/2 text-center"> <h1 className="text-5xl mb-4">V1.0 - nov.22</h1> <p className="text-lg text-gray-400 max-w-xl mx-auto"> We are excited to announce the Better Auth V1.0 release. </p> </div> </BackgroundLines> </div> <div className="relative py-24"> <div className="absolute inset-0 z-0"> <div className="grid grid-cols-12 h-full"> {Array(12) .fill(null) .map((_, i) => ( <div key={i} className="border-l border-dashed border-stone-100 dark:border-white/10 h-full" /> ))} </div> <div className="grid grid-rows-12 w-full absolute top-0"> {Array(12) .fill(null) .map((_, i) => ( <div key={i} className="border-t border-dashed border-stone-100 dark:border-stone-900/60 w-full" /> ))} </div> </div> <div className="max-w-6xl mx-auto px-6 relative z-10"> <h2 className="text-3xl font-bold mb-12 font-geist text-center"> What does V1 means? </h2> <p> Since introducing Better Auth, the community's excitement has been incredibly motivating—thank you! <br /> <br /> V1 is an important milestone, but it simply means we believe you can use it in production and that we'll strive to keep the APIs stable until the next major version. However, we'll continue improving, adding new features, and fixing bugs at the same pace as before. <br /> <br /> If you were using Better Auth for production, we recommend updating to V1 as soon as possible. There are some breaking changes, feel free to join us on{" "} <Link href="https://discord.gg/better-auth">Discord</Link>, and we'll gladly assist. </p> </div> </div> <ReleaseRelated /> <div className="border-t border-white/10"> <div className="max-w-4xl mx-auto px-6 py-24"> <h2 className="text-3xl font-bold mb-12 font-geist">Changelog</h2> <div className="space-y-8"> <ChangelogItem version="1.0.0" date="2024" changes={[ "feat: Open API Docs", "docs: Sign In Box Builder", "feat: default memory adapter. If no database is provided, it will use memory adapter", "feat: New server only endpoints for Organization and Two Factor plugins", "refactor: all core tables now have `createdAt` and `updatedAt` fields", "refactor: accounts now store `expiresAt` for both refresh and access tokens", "feat: Email OTP forget password flow", "docs: NextAuth.js migration guide", "feat: sensitive endpoints now check for fresh tokens", "feat: two-factor now have different interface for redirect and callback", "and a lot more bug fixes and improvements...", ]} /> </div> </div> </div> </div> ); } function ReleaseRelated() { return ( <div className="relative dark:bg-transparent/10 bg-zinc-100 border-b-2 border-white/10 rounded-none py-24"> <div className="absolute inset-0 z-0"> <div className="grid grid-rows-12 w-full absolute top-0"> {Array(12) .fill(null) .map((_, i) => ( <div key={i} className="border-t border-dashed border-white/10 w-full" /> ))} </div> </div> <div className="max-w-6xl mx-auto px-6 relative z-10"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div> <h3 className="text-xl font-semibold mb-4">Install Latest</h3> <div className="dark:bg-white/5 bg-black/10 rounded-lg p-4 mb-2"> <code className="text-sm font-mono"> npm i better-auth@latest </code> </div> <p className="text-sm text-gray-400"> Get the latest{" "} <a href="#" className="underline"> Node.js and npm </a> . </p> </div> <div> <h3 className="text-xl font-semibold mb-4">Adopt the new Schema</h3> <div className="dark:bg-white/5 bg-black/10 rounded-lg p-4 mb-2"> <code className="text-sm font-mono "> pnpx @better-auth/cli migrate <br /> </code> </div> <p className="text-sm text-gray-400"> Ensure you have the latest{" "} <code className="text-xs dark:bg-white/5 bg-black/10 px-1 py-0.5 rounded"> schema required </code>{" "} by Better Auth. <code className="text-xs dark:bg-white/5 bg-black/10 px-1 py-0.5 rounded"> You can also </code>{" "} add them manually. Read the{" "} <a href="/docs/concepts/database#core-schema" className="underline" > Core Schema </a>{" "} for full instructions. </p> </div> <div> <h3 className="text-xl font-semibold mb-4"> Check out the change log, the new UI Builder, OpenAPI Docs, and more </h3> <p className="text-sm text-gray-400 mb-4"> We have some exciting new features and updates that you should check out. </p> <Link className="w-full" href="https://github.com/better-auth/better-auth" > <Button variant="outline" className="w-full justify-between"> <div className="flex items-center gap-2"> <GitHubLogoIcon fontSize={10} /> Star on GitHub </div> <ArrowRight className="w-4 h-4" /> </Button> </Link> <Link className="w-full" href="https://discord.gg/better-auth"> <Button variant="outline" className="w-full justify-between border-t-0" > <div className="flex items-center gap-2"> <DiscordLogoIcon /> Join Discord </div> <ArrowRight className="w-4 h-4" /> </Button> </Link> </div> </div> </div> </div> ); } function ChangelogItem({ version, date, changes, }: { version: string; date: string; changes: string[]; }) { return ( <div className="border-l-2 border-white/10 pl-6 relative"> <div className="absolute w-3 h-3 bg-white rounded-full -left-[7px] top-2" /> <div className="flex items-center gap-4 mb-4"> <h3 className="text-xl font-bold font-geist">{version}</h3> <span className="text-sm text-gray-400">{date}</span> </div> <ul className="space-y-3"> {changes.map((change, i) => ( <li key={i} className="text-gray-400"> {change} </li> ))} </ul> </div> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/oidc-provider/authorize.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import { getSessionFromCtx } from "../../api"; import type { AuthorizationQuery, OIDCOptions } from "./types"; import { generateRandomString } from "../../crypto"; import { getClient } from "./index"; import type { GenericEndpointContext } from "@better-auth/core"; function formatErrorURL(url: string, error: string, description: string) { return `${ url.includes("?") ? "&" : "?" }error=${error}&error_description=${description}`; } function getErrorURL( ctx: GenericEndpointContext, error: string, description: string, ) { const baseURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`; const formattedURL = formatErrorURL(baseURL, error, description); return formattedURL; } export async function authorize( ctx: GenericEndpointContext, options: OIDCOptions, ) { const handleRedirect = (url: string) => { const fromFetch = ctx.request?.headers.get("sec-fetch-mode") === "cors"; if (fromFetch) { return ctx.json({ redirect: true, url, }); } else { throw ctx.redirect(url); } }; const opts = { codeExpiresIn: 600, defaultScope: "openid", ...options, scopes: [ "openid", "profile", "email", "offline_access", ...(options?.scopes || []), ], }; if (!ctx.request) { throw new APIError("UNAUTHORIZED", { error_description: "request not found", error: "invalid_request", }); } const session = await getSessionFromCtx(ctx); if (!session) { /** * If the user is not logged in, we need to redirect them to the * login page. */ await ctx.setSignedCookie( "oidc_login_prompt", JSON.stringify(ctx.query), ctx.context.secret, { maxAge: 600, path: "/", sameSite: "lax", }, ); const queryFromURL = ctx.request.url?.split("?")[1]!; return handleRedirect(`${options.loginPage}?${queryFromURL}`); } const query = ctx.query as AuthorizationQuery; if (!query.client_id) { const errorURL = getErrorURL( ctx, "invalid_client", "client_id is required", ); throw ctx.redirect(errorURL); } if (!query.response_type) { const errorURL = getErrorURL( ctx, "invalid_request", "response_type is required", ); throw ctx.redirect( getErrorURL(ctx, "invalid_request", "response_type is required"), ); } const client = await getClient( ctx.query.client_id, ctx.context.adapter, options.trustedClients || [], ); if (!client) { const errorURL = getErrorURL( ctx, "invalid_client", "client_id is required", ); throw ctx.redirect(errorURL); } const redirectURI = client.redirectURLs.find( (url) => url === ctx.query.redirect_uri, ); if (!redirectURI || !query.redirect_uri) { /** * show UI error here warning the user that the redirect URI is invalid */ throw new APIError("BAD_REQUEST", { message: "Invalid redirect URI", }); } if (client.disabled) { const errorURL = getErrorURL(ctx, "client_disabled", "client is disabled"); throw ctx.redirect(errorURL); } if (query.response_type !== "code") { const errorURL = getErrorURL( ctx, "unsupported_response_type", "unsupported response type", ); throw ctx.redirect(errorURL); } const requestScope = query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" "); const invalidScopes = requestScope.filter((scope) => { return !opts.scopes.includes(scope); }); if (invalidScopes.length) { return handleRedirect( formatErrorURL( query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, ), ); } if ( (!query.code_challenge || !query.code_challenge_method) && options.requirePKCE ) { return handleRedirect( formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required"), ); } if (!query.code_challenge_method) { query.code_challenge_method = "plain"; } if ( ![ "s256", options.allowPlainCodeChallengeMethod ? "plain" : "s256", ].includes(query.code_challenge_method?.toLowerCase() || "") ) { return handleRedirect( formatErrorURL( query.redirect_uri, "invalid_request", "invalid code_challenge method", ), ); } const code = generateRandomString(32, "a-z", "A-Z", "0-9"); const codeExpiresInMs = opts.codeExpiresIn * 1000; const expiresAt = new Date(Date.now() + codeExpiresInMs); // Determine if consent is required // Consent is ALWAYS required unless: // 1. The client is trusted (skipConsent = true) // 2. The user has already consented and prompt is not "consent" const skipConsentForTrustedClient = client.skipConsent; const hasAlreadyConsented = await ctx.context.adapter .findOne<{ consentGiven: boolean; }>({ model: "oauthConsent", where: [ { field: "clientId", value: client.clientId, }, { field: "userId", value: session.user.id, }, ], }) .then((res) => !!res?.consentGiven); const requireConsent = !skipConsentForTrustedClient && (!hasAlreadyConsented || query.prompt === "consent"); try { /** * Save the code in the database */ await ctx.context.internalAdapter.createVerificationValue({ value: JSON.stringify({ clientId: client.clientId, redirectURI: query.redirect_uri, scope: requestScope, userId: session.user.id, authTime: new Date(session.session.createdAt).getTime(), /** * Consent is required per OIDC spec unless: * 1. Client is trusted (skipConsent = true) * 2. User has already consented (and prompt is not "consent") * * When consent is required, the code needs to be treated as a * consent request. Once the user consents, the code will be * updated with the actual authorization code. */ requireConsent, state: requireConsent ? query.state : null, codeChallenge: query.code_challenge, codeChallengeMethod: query.code_challenge_method, nonce: query.nonce, }), identifier: code, expiresAt, }); } catch (e) { return handleRedirect( formatErrorURL( query.redirect_uri, "server_error", "An error occurred while processing the request", ), ); } // If consent is not required, redirect with the code immediately if (!requireConsent) { const redirectURIWithCode = new URL(redirectURI); redirectURIWithCode.searchParams.set("code", code); redirectURIWithCode.searchParams.set("state", ctx.query.state); return handleRedirect(redirectURIWithCode.toString()); } // Consent is required - redirect to consent page or show consent HTML if (options?.consentPage) { // Set cookie to support cookie-based consent flows await ctx.setSignedCookie("oidc_consent_prompt", code, ctx.context.secret, { maxAge: 600, path: "/", sameSite: "lax", }); // Pass the consent code as a URL parameter to support URL-based consent flows const urlParams = new URLSearchParams(); urlParams.set("consent_code", code); urlParams.set("client_id", client.clientId); urlParams.set("scope", requestScope.join(" ")); const consentURI = `${options.consentPage}?${urlParams.toString()}`; return handleRedirect(consentURI); } const htmlFn = options?.getConsentHTML; if (!htmlFn) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "No consent page provided", }); } return new Response( htmlFn({ scopes: requestScope, clientMetadata: client.metadata, clientIcon: client?.icon, clientId: client.clientId, clientName: client.name, code, }), { headers: { "content-type": "text/html", }, }, ); } ``` -------------------------------------------------------------------------------- /docs/app/blog/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- ```typescript import { blogs } from "@/lib/source"; import { notFound } from "next/navigation"; import { absoluteUrl, formatDate } from "@/lib/utils"; import DatabaseTable from "@/components/mdx/database-tables"; import { cn } from "@/lib/utils"; import { Step, Steps } from "fumadocs-ui/components/steps"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { GenerateSecret } from "@/components/generate-secret"; import { AnimatePresence } from "@/components/ui/fade-in"; import { TypeTable } from "fumadocs-ui/components/type-table"; import { Features } from "@/components/blocks/features"; import { ForkButton } from "@/components/fork-button"; import Link from "next/link"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { File, Folder, Files } from "fumadocs-ui/components/files"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Pre } from "fumadocs-ui/components/codeblock"; import { Glow } from "../_components/default-changelog"; import { XIcon } from "../_components/icons"; import { StarField } from "../_components/stat-field"; import Image from "next/image"; import { BlogPage } from "../_components/blog-list"; import { Callout } from "@/components/ui/callout"; import { ArrowLeftIcon, ExternalLink } from "lucide-react"; import { Support } from "../_components/support"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; const metaTitle = "Blogs"; const metaDescription = "Latest changes , fixes and updates."; const ogImage = "https://better-auth.com/release-og/changelog-og.png"; export default async function Page({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; if (!slug) { return <BlogPage />; } const page = blogs.getPage(slug); if (!page) { notFound(); } const MDX = page.data?.body; const { title, description, date } = page.data; return ( <div className="relative min-h-screen"> <div className="pointer-events-none absolute inset-0 -z-10"> <StarField className="top-1/3 left-1/2 -translate-x-1/2" /> <Glow /> </div> <div className="relative mx-auto max-w-3xl px-4 md:px-0 pb-24 pt-12"> <h1 className="text-center text-3xl md:text-5xl font-semibold tracking-tighter"> {title} </h1> {description && ( <p className="mt-3 text-center text-muted-foreground"> {description} </p> )} <div className="my-2 flex items-center justify-center gap-3"> <div> <Avatar> <AvatarImage src={page.data?.author?.avatar} alt={page.data?.author?.name ?? "Author"} /> <AvatarFallback> {page.data?.author?.name?.charAt(0)?.toUpperCase() ?? ""} </AvatarFallback> </Avatar> </div> <div className="flex items-center gap-2 text-sm text-muted-foreground"> {page.data?.author?.name && ( <span className="font-medium text-foreground"> {page.data.author.name} </span> )} {page.data?.author?.twitter && ( <> <span>·</span> <a href={`https://x.com/${page.data.author.twitter}`} target="_blank" rel="noreferrer noopener" className="inline-flex items-center gap-1 underline decoration-dashed" > <XIcon className="size-3" />@{page.data.author.twitter} </a> </> )} {date && ( <> <span>·</span> <time dateTime={String(date)}>{formatDate(date)}</time> </> )} </div> </div> <div className="w-full flex items-center gap-2 my-4 mb-8"> <div className="flex items-center gap-2 opacity-80"> <ArrowLeftIcon className="size-4" /> <Link href="/blog" className=""> Blogs </Link> </div> <hr className="h-1 w-full opacity-80" /> </div> <article className="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-4 md:px-0"> <MDX components={{ ...defaultMdxComponents, a: ({ className, href, children, ...props }: any) => { const isExternal = typeof href === "string" && /^(https?:)?\/\//.test(href); const classes = cn( "inline-flex items-center gap-1 font-medium underline decoration-dashed", className, ); if (isExternal) { return ( <a className={classes} href={href} target="_blank" rel="noreferrer noopener" {...props} > {children} <ExternalLink className="ms-0.5 inline size-[0.9em] text-fd-muted-foreground" /> </a> ); } return ( <Link className={classes} href={href} {...(props as any)}> {children} </Link> ); }, Link: ({ className, href, children, ...props }: any) => { const isExternal = typeof href === "string" && /^(https?:)?\/\//.test(href); const classes = cn( "inline-flex items-center gap-1 font-medium underline decoration-dashed", className, ); if (isExternal) { return ( <a className={classes} href={href} target="_blank" rel="noreferrer noopener" {...props} > {children} <ExternalLink className="ms-0.5 inline size-[0.9em] text-fd-muted-foreground" /> </a> ); } return ( <Link className={classes} href={href} {...(props as any)}> {children} </Link> ); }, Step, Steps, File, Folder, Files, Tab, Tabs, Pre: Pre, GenerateSecret, AnimatePresence, TypeTable, Features, ForkButton, DatabaseTable, Accordion, Accordions, Callout: ({ children, type, ...props }: { children: React.ReactNode; type?: "info" | "warn" | "error" | "success" | "warning"; [key: string]: any; }) => ( <Callout type={type} {...props}> {children} </Callout> ), Support, }} /> </article> </div> </div> ); } export async function generateMetadata({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; if (!slug) { return { metadataBase: new URL("https://better-auth.com/blogs"), title: metaTitle, description: metaDescription, openGraph: { title: metaTitle, description: metaDescription, images: [ { url: ogImage, }, ], url: "https://better-auth.com/blogs", }, twitter: { card: "summary_large_image", title: metaTitle, description: metaDescription, images: [ogImage], }, }; } const page = blogs.getPage(slug); if (page == null) notFound(); const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL; const url = new URL( `${baseUrl?.startsWith("http") ? baseUrl : `https://${baseUrl}`}${ page.data?.image }`, ); const { title, description } = page.data; return { title, description, openGraph: { title, description, type: "website", url: absoluteUrl(`blog/${slug.join("/")}`), images: [ { url: url.toString(), width: 1200, height: 630, alt: title, }, ], }, twitter: { card: "summary_large_image", title, description, images: [url.toString()], }, }; } export function generateStaticParams() { return blogs.generateParams(); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/prisma-adapter/prisma-adapter.ts: -------------------------------------------------------------------------------- ```typescript import { BetterAuthError } from "@better-auth/core/error"; import type { BetterAuthOptions } from "@better-auth/core"; import { createAdapterFactory, type AdapterFactoryOptions, type AdapterFactoryCustomizeAdapterCreator, } from "../adapter-factory"; import type { DBAdapterDebugLogOption, DBAdapter, Where, } from "@better-auth/core/db/adapter"; export interface PrismaConfig { /** * Database provider. */ provider: | "sqlite" | "cockroachdb" | "mysql" | "postgresql" | "sqlserver" | "mongodb"; /** * Enable debug logs for the adapter * * @default false */ debugLogs?: DBAdapterDebugLogOption; /** * Use plural table names * * @default false */ usePlural?: boolean; /** * Whether to execute multiple operations in a transaction. * * If the database doesn't support transactions, * set this to `false` and operations will be executed sequentially. * @default false */ transaction?: boolean; } interface PrismaClient {} type PrismaClientInternal = { $transaction: ( callback: (db: PrismaClient) => Promise<any> | any, ) => Promise<any>; } & { [model: string]: { create: (data: any) => Promise<any>; findFirst: (data: any) => Promise<any>; findMany: (data: any) => Promise<any>; update: (data: any) => Promise<any>; updateMany: (data: any) => Promise<any>; delete: (data: any) => Promise<any>; [key: string]: any; }; }; export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => { let lazyOptions: BetterAuthOptions | null = null; const createCustomAdapter = (prisma: PrismaClient): AdapterFactoryCustomizeAdapterCreator => ({ getFieldName }) => { const db = prisma as PrismaClientInternal; const convertSelect = (select?: string[], model?: string) => { if (!select || !model) return undefined; return select.reduce((prev, cur) => { return { ...prev, [getFieldName({ model, field: cur })]: true, }; }, {}); }; function operatorToPrismaOperator(operator: string) { switch (operator) { case "starts_with": return "startsWith"; case "ends_with": return "endsWith"; case "ne": return "not"; case "not_in": return "notIn"; default: return operator; } } const convertWhereClause = (model: string, where?: Where[]) => { if (!where || !where.length) return {}; if (where.length === 1) { const w = where[0]!; if (!w) { return; } return { [getFieldName({ model, field: w.field })]: w.operator === "eq" || !w.operator ? w.value : { [operatorToPrismaOperator(w.operator)]: w.value, }, }; } const and = where.filter((w) => w.connector === "AND" || !w.connector); const or = where.filter((w) => w.connector === "OR"); const andClause = and.map((w) => { return { [getFieldName({ model, field: w.field })]: w.operator === "eq" || !w.operator ? w.value : { [operatorToPrismaOperator(w.operator)]: w.value, }, }; }); const orClause = or.map((w) => { return { [getFieldName({ model, field: w.field })]: w.operator === "eq" || !w.operator ? w.value : { [operatorToPrismaOperator(w.operator)]: w.value, }, }; }); return { ...(andClause.length ? { AND: andClause } : {}), ...(orClause.length ? { OR: orClause } : {}), }; }; return { async create({ model, data: values, select }) { if (!db[model]) { throw new BetterAuthError( `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`, ); } return await db[model]!.create({ data: values, select: convertSelect(select, model), }); }, async findOne({ model, where, select }) { const whereClause = convertWhereClause(model, where); if (!db[model]) { throw new BetterAuthError( `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`, ); } return await db[model]!.findFirst({ where: whereClause, select: convertSelect(select, model), }); }, async findMany({ model, where, limit, offset, sortBy }) { const whereClause = convertWhereClause(model, where); if (!db[model]) { throw new BetterAuthError( `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`, ); } return (await db[model]!.findMany({ where: whereClause, take: limit || 100, skip: offset || 0, ...(sortBy?.field ? { orderBy: { [getFieldName({ model, field: sortBy.field })]: sortBy.direction === "desc" ? "desc" : "asc", }, } : {}), })) as any[]; }, async count({ model, where }) { const whereClause = convertWhereClause(model, where); if (!db[model]) { throw new BetterAuthError( `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`, ); } return await db[model]!.count({ where: whereClause, }); }, async update({ model, where, update }) { if (!db[model]) { throw new BetterAuthError( `Model ${model} does not exist in the database. If you haven't generated the Prisma client, you need to run 'npx prisma generate'`, ); } const whereClause = convertWhereClause(model, where); return await db[model]!.update({ where: whereClause, data: update, }); }, async updateMany({ model, where, update }) { const whereClause = convertWhereClause(model, where); const result = await db[model]!.updateMany({ where: whereClause, data: update, }); return result ? (result.count as number) : 0; }, async delete({ model, where }) { const whereClause = convertWhereClause(model, where); try { await db[model]!.delete({ where: whereClause, }); } catch (e: any) { // If the record doesn't exist, we don't want to throw an error if (e?.meta?.cause === "Record to delete does not exist.") return; // otherwise if it's an unknown error, we want to just log it for debugging. console.log(e); } }, async deleteMany({ model, where }) { const whereClause = convertWhereClause(model, where); const result = await db[model]!.deleteMany({ where: whereClause, }); return result ? (result.count as number) : 0; }, options: config, }; }; let adapterOptions: AdapterFactoryOptions | null = null; adapterOptions = { config: { adapterId: "prisma", adapterName: "Prisma Adapter", usePlural: config.usePlural ?? false, debugLogs: config.debugLogs ?? false, transaction: (config.transaction ?? false) ? (cb) => (prisma as PrismaClientInternal).$transaction((tx) => { const adapter = createAdapterFactory({ config: adapterOptions!.config, adapter: createCustomAdapter(tx), })(lazyOptions!); return cb(adapter); }) : false, }, adapter: createCustomAdapter(prisma), }; const adapter = createAdapterFactory(adapterOptions); return (options: BetterAuthOptions): DBAdapter<BetterAuthOptions> => { lazyOptions = options; return adapter(options); }; }; ``` -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- ```css @import "tailwindcss"; @import "fumadocs-ui/css/black.css"; @import "fumadocs-ui/css/preset.css"; @config "../tailwind.config.js"; @plugin 'tailwindcss-animate'; @custom-variant dark (&:is(.dark *)); :root { --fd-nav-height: 56px; --fd-banner-height: 0px; --fd-tocnav-height: 0px; --background: oklch(1 0 0); --foreground: oklch(0.147 0.004 49.25); --card: oklch(1 0 0); --card-foreground: oklch(0.147 0.004 49.25); --popover: oklch(1 0 0); --popover-foreground: oklch(0.147 0.004 49.25); --primary: oklch(0.216 0.006 56.043); --primary-foreground: oklch(0.985 0.001 106.423); --secondary: oklch(0.97 0.001 106.424); --secondary-foreground: oklch(0.216 0.006 56.043); --muted: oklch(0.97 0.001 106.424); --muted-foreground: oklch(0.553 0.013 58.071); --accent: oklch(0.97 0.001 106.424); --accent-foreground: oklch(0.216 0.006 56.043); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325); --border: oklch(0.923 0.003 48.717); --input: oklch(0.923 0.003 48.717); --ring: oklch(0.709 0.01 56.259); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --radius: 0.2rem; --sidebar: oklch(0.985 0.001 106.423); --sidebar-foreground: oklch(0.147 0.004 49.25); --sidebar-primary: oklch(0.216 0.006 56.043); --sidebar-primary-foreground: oklch(0.985 0.001 106.423); --sidebar-accent: oklch(0.97 0.001 106.424); --sidebar-accent-foreground: oklch(0.216 0.006 56.043); --sidebar-border: oklch(0.923 0.003 48.717); --sidebar-ring: oklch(0.709 0.01 56.259); /* Scrollbar theme (light) */ --scrollbar-thumb: var(--border); --scrollbar-thumb-hover: var(--ring); --scrollbar-track: transparent; } .dark { --background: hsl(0 0% 0%); --foreground: oklch(0.985 0.001 106.423); --card: oklch(0.147 0.004 49.25); --card-foreground: oklch(0.985 0.001 106.423); --popover: oklch(0.147 0.004 49.25); --popover-foreground: oklch(0.985 0.001 106.423); --primary: oklch(0.985 0.001 106.423); --primary-foreground: oklch(0.216 0.006 56.043); --secondary: oklch(0.268 0.007 34.298); --secondary-foreground: oklch(0.985 0.001 106.423); --muted: oklch(0.268 0.007 34.298); --muted-foreground: oklch(0.709 0.01 56.259); --accent: oklch(0.268 0.007 34.298); --accent-foreground: oklch(0.985 0.001 106.423); --destructive: oklch(0.396 0.141 25.723); --destructive-foreground: oklch(0.637 0.237 25.331); --border: oklch(0.268 0.007 34.298); --input: oklch(0.268 0.007 34.298); --ring: oklch(0.553 0.013 58.071); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.216 0.006 56.043); --sidebar-foreground: oklch(0.985 0.001 106.423); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0.001 106.423); --sidebar-accent: oklch(0.268 0.007 34.298); --sidebar-accent-foreground: oklch(0.985 0.001 106.423); --sidebar-border: oklch(0.268 0.007 34.298); --sidebar-ring: oklch(0.553 0.013 58.071); /* Scrollbar theme (dark) */ --scrollbar-thumb: var(--border); --scrollbar-thumb-hover: var(--ring); --scrollbar-track: transparent; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; @keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } } } @layer base { * { @apply border-border outline-ring/50; } body { @apply overscroll-none bg-background text-foreground selection:bg-foreground selection:text-background; } } html { scroll-behavior: auto; scroll-padding-top: calc( var(--fd-nav-height, 56px) + var(--fd-banner-height, 0px) + var(--fd-tocnav-height, 0px) + 24px ); } html:not([data-anchor-scrolling]) { scroll-behavior: smooth; } /* Global, accessible custom scrollbars */ * { scrollbar-width: thin; /* Firefox */ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); } /* WebKit-based browsers */ ::-webkit-scrollbar { width: 12px; height: 12px; } ::-webkit-scrollbar-track { background: var(--scrollbar-track); } ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 9999px; border: 3px solid transparent; /* creates a gap between thumb and track */ background-clip: content-box; } ::-webkit-scrollbar-thumb:hover { background-color: var(--scrollbar-thumb-hover); } ::-webkit-scrollbar-corner { background: transparent; } @layer utilities { .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } } .markdown-content { @apply text-sm leading-relaxed; } .markdown-content pre { @apply max-w-full overflow-x-auto; } .markdown-content pre code { @apply whitespace-pre-wrap break-words; } .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6 { @apply font-semibold text-foreground; } .markdown-content p { @apply mb-2 last:mb-0; } .markdown-content ul, .markdown-content ol { @apply space-y-2 list-disc; } .markdown-content li { @apply text-sm; } .markdown-content code { @apply bg-muted px-1.5 py-0.5 rounded text-xs font-mono; } .markdown-content pre { @apply overflow-x-auto; } .markdown-content blockquote { @apply border-l-4 border-muted-foreground/20 pl-4 my-2 italic; } .markdown-content table { @apply w-full border-collapse; } .markdown-content th, .markdown-content td { @apply border border-border px-2 py-1 text-xs; } .markdown-content th { @apply bg-muted font-medium; } @keyframes stream-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .streaming-cursor { animation: stream-pulse 1s ease-in-out infinite; } ``` -------------------------------------------------------------------------------- /docs/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; } function ContextMenuTrigger({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { return ( <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> ); } function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { return ( <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> ); } function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { return ( <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> ); } function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />; } function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { return ( <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} /> ); } function ContextMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean; }) { return ( <ContextMenuPrimitive.SubTrigger data-slot="context-menu-sub-trigger" data-inset={inset} className={cn( "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > {children} <ChevronRightIcon className="ml-auto" /> </ContextMenuPrimitive.SubTrigger> ); } function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { return ( <ContextMenuPrimitive.SubContent data-slot="context-menu-sub-content" className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg", className, )} {...props} /> ); } function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { return ( <ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Content data-slot="context-menu-content" className={cn( "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md", className, )} {...props} /> </ContextMenuPrimitive.Portal> ); } function ContextMenuItem({ className, inset, variant = "default", ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { inset?: boolean; variant?: "default" | "destructive"; }) { return ( <ContextMenuPrimitive.Item data-slot="context-menu-item" data-inset={inset} data-variant={variant} className={cn( "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} /> ); } function ContextMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { return ( <ContextMenuPrimitive.CheckboxItem data-slot="context-menu-checkbox-item" className={cn( "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} checked={checked} {...props} > <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CheckIcon className="size-4" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.CheckboxItem> ); } function ContextMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { return ( <ContextMenuPrimitive.RadioItem data-slot="context-menu-radio-item" className={cn( "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <ContextMenuPrimitive.ItemIndicator> <CircleIcon className="size-2 fill-current" /> </ContextMenuPrimitive.ItemIndicator> </span> {children} </ContextMenuPrimitive.RadioItem> ); } function ContextMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { inset?: boolean; }) { return ( <ContextMenuPrimitive.Label data-slot="context-menu-label" data-inset={inset} className={cn( "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className, )} {...props} /> ); } function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { return ( <ContextMenuPrimitive.Separator data-slot="context-menu-separator" className={cn("bg-border -mx-1 my-1 h-px", className)} {...props} /> ); } function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { return ( <span data-slot="context-menu-shortcut" className={cn( "text-muted-foreground ml-auto text-xs tracking-widest", className, )} {...props} /> ); } export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, }; ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/your-first-plugin.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Create your first plugin description: A step-by-step guide to creating your first Better Auth plugin. --- In this guide, we’ll walk you through the steps of creating your first Better Auth plugin. <Callout type="warn"> This guide assumes you have <Link href="/docs/installation">setup the basics</Link> of Better Auth and are ready to create your first plugin. </Callout> <Steps> <Step> ## Plan your idea Before beginning, you must know what plugin you intend to create. In this guide, we’ll create a **birthday plugin** to keep track of user birth dates. </Step> <Step> ## Server plugin first Better Auth plugins operate as a pair: a <Link href="/docs/concepts/plugins#create-a-server-plugin">server plugin</Link> and a <Link href="/docs/concepts/plugins#create-a-client-plugin">client plugin</Link>. The server plugin forms the foundation of your authentication system, while the client plugin provides convenient frontend APIs to interact with your server implementation. <Callout> You can read more about server/client plugins in our <Link href="/docs/concepts/plugins#creating-a-plugin">documentation</Link>. </Callout> ### Creating the server plugin Go ahead and find a suitable location to create your birthday plugin folder, with an `index.ts` file within. <Files> <Folder name="birthday-plugin" defaultOpen> <File name="index.ts" /> </Folder> </Files> In the `index.ts` file, we’ll export a function that represents our server plugin. This will be what we will later add to our plugin list in the `auth.ts` file. ```ts title="index.ts" import { createAuthClient } from "better-auth/client"; import type { BetterAuthPlugin } from "better-auth"; export const birthdayPlugin = () => ({ id: "birthdayPlugin", } satisfies BetterAuthPlugin); ``` Although this does nothing, you have technically just made yourself your first plugin, congratulations! 🎉 </Step> <Step> ### Defining a schema In order to save each user’s birthday data, we must create a schema on top of the `user` model. By creating a schema here, this also allows <Link href="/docs/concepts/cli">Better Auth’s CLI</Link> to generate the schemas required to update your database. <Callout type="info"> You can learn more about <Link href="/docs/concepts/plugins#schema">plugin schemas here</Link>. </Callout> ```ts title="index.ts" //... export const birthdayPlugin = () => ({ id: "birthdayPlugin", schema: {// [!code highlight] user: {// [!code highlight] fields: {// [!code highlight] birthday: {// [!code highlight] type: "date", // string, number, boolean, date // [!code highlight] required: true, // if the field should be required on a new record. (default: false) // [!code highlight] unique: false, // if the field should be unique. (default: false) // [!code highlight] references: null // if the field is a reference to another table. (default: null) // [!code highlight] },// [!code highlight] },// [!code highlight] },// [!code highlight] }, } satisfies BetterAuthPlugin); ``` </Step> <Step> ### Authorization logic For this example guide, we’ll set up authentication logic to check and ensure that the user who signs-up is older than 5. But the same concept could be applied for something like verifying users agreeing to the TOS or anything alike. To do this, we’ll utilize <Link href="/docs/concepts/plugins#hooks">Hooks</Link>, which allows us to run code `before` or `after` an action is performed. ```ts title="index.ts" export const birthdayPlugin = () => ({ //... // In our case, we want to write authorization logic, // meaning we want to intercept it `before` hand. hooks: { before: [ { matcher: (context) => /* ... */, handler: createAuthMiddleware(async (ctx) => { //... }), }, ], }, } satisfies BetterAuthPlugin) ``` In our case we want to match any requests going to the signup path: ```ts title="Before hook" { matcher: (context) => context.path.startsWith("/sign-up/email"), //... } ``` And for our logic, we’ll write the following code to check the if user’s birthday makes them above 5 years old. ```ts title="Imports" import { APIError } from "better-auth/api"; import { createAuthMiddleware } from "better-auth/plugins"; ``` ```ts title="Before hook" { //... handler: createAuthMiddleware(async (ctx) => { const { birthday } = ctx.body; if(!(birthday instanceof Date)) { throw new APIError("BAD_REQUEST", { message: "Birthday must be of type Date." }); } const today = new Date(); const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5)); if(birthday >= fiveYearsAgo) { throw new APIError("BAD_REQUEST", { message: "User must be above 5 years old." }); } return { context: ctx }; }), } ``` **Authorized!** 🔒 We’ve now successfully written code to ensure authorization for users above 5! </Step> <Step> ## Client Plugin We’re close to the finish line! 🏁 Now that we have created our server plugin, the next step is to develop our client plugin. Since there isn’t much frontend APIs going on for this plugin, there isn’t much to do! First, let’s create our `client.ts` file first: <Files> <Folder name="birthday-plugin" defaultOpen> <File name="index.ts" /> <File name="client.ts" /> </Folder> </Files> Then, add the following code: ```ts title="client.ts" import { BetterAuthClientPlugin } from "better-auth"; import type { birthdayPlugin } from "./index"; // make sure to import the server plugin as a type // [!code highlight] type BirthdayPlugin = typeof birthdayPlugin; export const birthdayClientPlugin = () => { return { id: "birthdayPlugin", $InferServerPlugin: {} as ReturnType<BirthdayPlugin>, } satisfies BetterAuthClientPlugin; }; ``` What we’ve done is allow the client plugin to infer the types defined by our schema from the server plugin. And that’s it! This is all it takes for the birthday client plugin. 🎂 </Step> <Step> ## Initiate your plugin! Both the `client` and `server` plugins are now ready, the last step is to import them to both your `auth-client.ts` and your `server.ts` files respectively to initiate the plugin. ### Server initiation ```ts title="server.ts" import { betterAuth } from "better-auth"; import { birthdayPlugin } from "./birthday-plugin";// [!code highlight] export const auth = betterAuth({ plugins: [ birthdayPlugin(),// [!code highlight] ] }); ``` ### Client initiation ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; import { birthdayClientPlugin } from "./birthday-plugin/client";// [!code highlight] const authClient = createAuthClient({ plugins: [ birthdayClientPlugin()// [!code highlight] ] }); ``` ### Oh yeah, the schemas! Don’t forget to add your `birthday` field to your `user` table model! Or, use the `generate` <Link href="/docs/concepts/cli#generate">CLI command</Link>: ```bash npx @better-auth/cli@latest generate ``` </Step> </Steps> ## Wrapping Up Congratulations! You’ve successfully created your first ever Better Auth plugin. We highly recommend you visit our <Link href="/docs/concepts/plugins">plugins documentation</Link> to learn more information. If you have a plugin you’d like to share with the community, feel free to let us know through our <Link href="https://discord.gg/better-auth">Discord server</Link>, or through a <Link href="https://github.com/better-auth/better-auth/pulls">pull-request</Link> and we may add it to the <Link href="/docs/plugins/community-plugins">community-plugins</Link> list! ``` -------------------------------------------------------------------------------- /packages/cli/test/generate-all-db.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { generateDrizzleSchema } from "../src/generators/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { twoFactor, username } from "better-auth/plugins"; import { passkey } from "better-auth/plugins/passkey"; import type { BetterAuthOptions } from "better-auth"; describe("generate drizzle schema for all databases", async () => { it("should generate drizzle schema for MySQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, ), plugins: [twoFactor(), username()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-mysql.txt", ); }); it("should generate drizzle schema for SQLite", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [twoFactor(), username()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-sqlite.txt", ); }); it("should generate drizzle schema for MySQL with number id", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, ), plugins: [twoFactor(), username()], advanced: { database: { useNumberId: true, }, }, user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-mysql-number-id.txt", ); }); it("should generate drizzle schema for SQLite with number id", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [twoFactor(), username()], advanced: { database: { useNumberId: true, }, }, user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-sqlite-number-id.txt", ); }); }); describe("generate drizzle schema for all databases with passkey plugin", async () => { it("should generate drizzle schema for MySQL with passkey plugin", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, ), plugins: [passkey()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-mysql-passkey.txt", ); }); it("should generate drizzle schema for SQLite with passkey plugin", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [passkey()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-sqlite-passkey.txt", ); }); it("should generate drizzle schema for PostgreSQL with passkey plugin", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, }, ), plugins: [passkey()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-pg-passkey.txt", ); }); it("should generate drizzle schema for MySQL with passkey plugin and number id", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, ), plugins: [passkey()], advanced: { database: { useNumberId: true, }, }, user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-mysql-passkey-number-id.txt", ); }); it("should generate drizzle schema for SQLite with passkey plugin and number id", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [passkey()], advanced: { database: { useNumberId: true, }, }, user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-sqlite-passkey-number-id.txt", ); }); }); ```