This is page 7 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── 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-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 │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/components/ui/input-otp.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { OTPInput, OTPInputContext } from "input-otp"; 5 | import { MinusIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function InputOTP({ 10 | className, 11 | containerClassName, 12 | ...props 13 | }: React.ComponentProps<typeof OTPInput> & { 14 | containerClassName?: string; 15 | }) { 16 | return ( 17 | <OTPInput 18 | data-slot="input-otp" 19 | containerClassName={cn( 20 | "flex items-center gap-2 has-disabled:opacity-50", 21 | containerClassName, 22 | )} 23 | className={cn("disabled:cursor-not-allowed", className)} 24 | {...props} 25 | /> 26 | ); 27 | } 28 | 29 | function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { 30 | return ( 31 | <div 32 | data-slot="input-otp-group" 33 | className={cn("flex items-center", className)} 34 | {...props} 35 | /> 36 | ); 37 | } 38 | 39 | function InputOTPSlot({ 40 | index, 41 | className, 42 | ...props 43 | }: React.ComponentProps<"div"> & { 44 | index: number; 45 | }) { 46 | const inputOTPContext = React.useContext(OTPInputContext); 47 | const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}; 48 | 49 | return ( 50 | <div 51 | data-slot="input-otp-slot" 52 | data-active={isActive} 53 | className={cn( 54 | "border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", 55 | className, 56 | )} 57 | {...props} 58 | > 59 | {char} 60 | {hasFakeCaret && ( 61 | <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> 62 | <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" /> 63 | </div> 64 | )} 65 | </div> 66 | ); 67 | } 68 | 69 | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { 70 | return ( 71 | <div data-slot="input-otp-separator" role="separator" {...props}> 72 | <MinusIcon /> 73 | </div> 74 | ); 75 | } 76 | 77 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; 78 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/has-permission.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultRoles } from "./access"; 2 | import type { Role } from "../access"; 3 | import * as z from "zod"; 4 | import { APIError } from "../../api"; 5 | import type { OrganizationRole } from "./schema"; 6 | import { 7 | cacheAllRoles, 8 | hasPermissionFn, 9 | type HasPermissionBaseInput, 10 | } from "./permission"; 11 | import type { GenericEndpointContext } from "@better-auth/core"; 12 | 13 | export const hasPermission = async ( 14 | input: { 15 | organizationId: string; 16 | /** 17 | * If true, will use the in-memory cache of the roles. 18 | * Keep in mind to use this in a stateless mindset, the purpose of this is to avoid unnecessary database calls when running multiple 19 | * hasPermission calls in a row. 20 | * 21 | * @default false 22 | */ 23 | useMemoryCache?: boolean; 24 | } & HasPermissionBaseInput, 25 | ctx: GenericEndpointContext, 26 | ) => { 27 | let acRoles: { 28 | [x: string]: Role<any> | undefined; 29 | } = { ...(input.options.roles || defaultRoles) }; 30 | 31 | if ( 32 | ctx && 33 | input.organizationId && 34 | input.options.dynamicAccessControl?.enabled && 35 | input.options.ac && 36 | !input.useMemoryCache 37 | ) { 38 | // Load roles from database 39 | const roles = await ctx.context.adapter.findMany< 40 | OrganizationRole & { permission: string } 41 | >({ 42 | model: "organizationRole", 43 | where: [ 44 | { 45 | field: "organizationId", 46 | value: input.organizationId, 47 | }, 48 | ], 49 | }); 50 | 51 | for (const { role, permission: permissionsString } of roles) { 52 | // If it's for an existing role, skip as we shouldn't override hard-coded roles. 53 | if (role in acRoles) continue; 54 | 55 | const result = z 56 | .record(z.string(), z.array(z.string())) 57 | .safeParse(JSON.parse(permissionsString)); 58 | 59 | if (!result.success) { 60 | ctx.context.logger.error( 61 | "[hasPermission] Invalid permissions for role " + role, 62 | { 63 | permissions: JSON.parse(permissionsString), 64 | }, 65 | ); 66 | throw new APIError("INTERNAL_SERVER_ERROR", { 67 | message: "Invalid permissions for role " + role, 68 | }); 69 | } 70 | 71 | acRoles[role] = input.options.ac.newRole(result.data); 72 | } 73 | } 74 | 75 | if (input.useMemoryCache) { 76 | acRoles = cacheAllRoles.get(input.organizationId) || acRoles; 77 | } 78 | cacheAllRoles.set(input.organizationId, acRoles); 79 | 80 | return hasPermissionFn(input, acRoles); 81 | }; 82 | ``` -------------------------------------------------------------------------------- /docs/app/api/ai-chat/route.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { NextResponse } from "next/server"; 2 | export const maxDuration = 300; 3 | export async function POST(request: Request) { 4 | try { 5 | const body = await request.json(); 6 | const gurubasePayload = { 7 | question: body.question, 8 | stream: body.stream, 9 | external_user_id: body.external_user_id, 10 | session_id: body.session_id, 11 | fetch_existing: body.fetch_existing || false, 12 | }; 13 | const response = await fetch( 14 | `https://api.gurubase.io/api/v1/${process.env.GURUBASE_SLUG}/answer/`, 15 | { 16 | method: "POST", 17 | headers: { 18 | "Content-Type": "application/json", 19 | "x-api-key": `${process.env.GURUBASE_API_KEY}`, 20 | }, 21 | body: JSON.stringify(gurubasePayload), 22 | }, 23 | ); 24 | if (!response.ok) { 25 | const errorText = await response.text(); 26 | console.error("Gurubase API error:", response.status, errorText); 27 | if (response.status === 400) { 28 | return NextResponse.json( 29 | { 30 | error: 31 | "I'm sorry, I couldn't process that question. Please try asking something else about Better-Auth.", 32 | }, 33 | { status: 200 }, 34 | ); 35 | } 36 | 37 | return NextResponse.json( 38 | { error: `External API error: ${response.status} ${errorText}` }, 39 | { status: response.status }, 40 | ); 41 | } 42 | const isStreaming = gurubasePayload.stream === true; 43 | if (isStreaming) { 44 | const stream = new ReadableStream({ 45 | start(controller) { 46 | const reader = response.body?.getReader(); 47 | if (!reader) { 48 | controller.close(); 49 | return; 50 | } 51 | 52 | function pump(): Promise<void> { 53 | return reader!.read().then(({ done, value }) => { 54 | if (done) { 55 | controller.close(); 56 | return; 57 | } 58 | controller.enqueue(value); 59 | return pump(); 60 | }); 61 | } 62 | 63 | return pump(); 64 | }, 65 | }); 66 | 67 | return new NextResponse(stream, { 68 | headers: { 69 | "Content-Type": "text/plain; charset=utf-8", 70 | "Cache-Control": "no-cache", 71 | Connection: "keep-alive", 72 | }, 73 | }); 74 | } else { 75 | const data = await response.json(); 76 | return NextResponse.json(data); 77 | } 78 | } catch (error) { 79 | return NextResponse.json( 80 | { 81 | error: `Proxy error: ${error instanceof Error ? error.message : "Unknown error"}`, 82 | }, 83 | { status: 500 }, 84 | ); 85 | } 86 | } 87 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/mysql.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: MySQL 3 | description: Integrate Better Auth with MySQL. 4 | --- 5 | 6 | MySQL is a popular open-source relational database management system (RDBMS) that is widely used for building web applications and other types of software. It provides a flexible and scalable database solution that allows for efficient storage and retrieval of data. 7 | Read more here: [MySQL](https://www.mysql.com/). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have MySQL installed and configured. 12 | Then, you can connect it straight into Better Auth. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { createPool } from "mysql2/promise"; 17 | 18 | export const auth = betterAuth({ 19 | database: createPool({ 20 | host: "localhost", 21 | user: "root", 22 | password: "password", 23 | database: "database", 24 | timezone: "Z", // Important to ensure consistent timezone values 25 | }), 26 | }); 27 | ``` 28 | 29 | <Callout> 30 | For more information, read Kysely's documentation to the 31 | [MySQLDialect](https://kysely-org.github.io/kysely-apidoc/classes/MysqlDialect.html). 32 | </Callout> 33 | 34 | ## Schema generation & migration 35 | 36 | The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate 37 | your database schema based on your Better Auth configuration and plugins. 38 | 39 | <table> 40 | <thead> 41 | <tr className="border-b"> 42 | <th> 43 | <p className="font-bold text-[16px] mb-1">MySQL Schema Generation</p> 44 | </th> 45 | <th> 46 | <p className="font-bold text-[16px] mb-1">MySQL Schema Migration</p> 47 | </th> 48 | </tr> 49 | </thead> 50 | <tbody> 51 | <tr className="h-10"> 52 | <td>✅ Supported</td> 53 | <td>✅ Supported</td> 54 | </tr> 55 | </tbody> 56 | </table> 57 | 58 | ```bash title="Schema Generation" 59 | npx @better-auth/cli@latest generate 60 | ``` 61 | 62 | ```bash title="Schema Migration" 63 | npx @better-auth/cli@latest migrate 64 | ``` 65 | 66 | ## Additional Information 67 | 68 | MySQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (<Link href="/docs/adapters/other-relational-databases">Read more here</Link>) 69 | 70 | If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. 71 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-pg-enum.txt: -------------------------------------------------------------------------------- ``` 1 | import { pgTable, text, timestamp, boolean, pgEnum } from "drizzle-orm/pg-core"; 2 | 3 | export const user = pgTable("user", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | email: text("email").notNull().unique(), 7 | emailVerified: boolean("email_verified").default(false).notNull(), 8 | image: text("image"), 9 | createdAt: timestamp("created_at").defaultNow().notNull(), 10 | updatedAt: timestamp("updated_at") 11 | .defaultNow() 12 | .$onUpdate(() => /* @__PURE__ */ new Date()) 13 | .notNull(), 14 | role: pgEnum("role", ["admin", "user", "guest"]).notNull(), 15 | }); 16 | 17 | export const session = pgTable("session", { 18 | id: text("id").primaryKey(), 19 | expiresAt: timestamp("expires_at").notNull(), 20 | token: text("token").notNull().unique(), 21 | createdAt: timestamp("created_at").defaultNow().notNull(), 22 | updatedAt: timestamp("updated_at") 23 | .$onUpdate(() => /* @__PURE__ */ new Date()) 24 | .notNull(), 25 | ipAddress: text("ip_address"), 26 | userAgent: text("user_agent"), 27 | userId: text("user_id") 28 | .notNull() 29 | .references(() => user.id, { onDelete: "cascade" }), 30 | }); 31 | 32 | export const account = pgTable("account", { 33 | id: text("id").primaryKey(), 34 | accountId: text("account_id").notNull(), 35 | providerId: text("provider_id").notNull(), 36 | userId: text("user_id") 37 | .notNull() 38 | .references(() => user.id, { onDelete: "cascade" }), 39 | accessToken: text("access_token"), 40 | refreshToken: text("refresh_token"), 41 | idToken: text("id_token"), 42 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 43 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 44 | scope: text("scope"), 45 | password: text("password"), 46 | createdAt: timestamp("created_at").defaultNow().notNull(), 47 | updatedAt: timestamp("updated_at") 48 | .$onUpdate(() => /* @__PURE__ */ new Date()) 49 | .notNull(), 50 | }); 51 | 52 | export const verification = pgTable("verification", { 53 | id: text("id").primaryKey(), 54 | identifier: text("identifier").notNull(), 55 | value: text("value").notNull(), 56 | expiresAt: timestamp("expires_at").notNull(), 57 | createdAt: timestamp("created_at").defaultNow().notNull(), 58 | updatedAt: timestamp("updated_at") 59 | .defaultNow() 60 | .$onUpdate(() => /* @__PURE__ */ new Date()) 61 | .notNull(), 62 | }); 63 | ``` -------------------------------------------------------------------------------- /demo/nextjs/lib/email/reset-password.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Heading, 7 | Hr, 8 | Html, 9 | Link, 10 | Preview, 11 | Text, 12 | Tailwind, 13 | Section, 14 | } from "@react-email/components"; 15 | 16 | interface BetterAuthResetPasswordEmailProps { 17 | username?: string; 18 | resetLink?: string; 19 | } 20 | 21 | export const ResetPasswordEmail = ({ 22 | username, 23 | resetLink, 24 | }: BetterAuthResetPasswordEmailProps) => { 25 | const previewText = `Reset your Better Auth password`; 26 | return ( 27 | <Html> 28 | <Head /> 29 | <Preview>{previewText}</Preview> 30 | <Tailwind> 31 | <Body className="bg-white my-auto mx-auto font-sans px-2"> 32 | <Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]"> 33 | <Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0"> 34 | Reset your <strong>Better Auth</strong> password 35 | </Heading> 36 | <Text className="text-black text-[14px] leading-[24px]"> 37 | Hello {username}, 38 | </Text> 39 | <Text className="text-black text-[14px] leading-[24px]"> 40 | We received a request to reset your password for your Better Auth 41 | account. If you didn't make this request, you can safely ignore 42 | this email. 43 | </Text> 44 | <Section className="text-center mt-[32px] mb-[32px]"> 45 | <Button 46 | className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3" 47 | href={resetLink} 48 | > 49 | Reset Password 50 | </Button> 51 | </Section> 52 | <Text className="text-black text-[14px] leading-[24px]"> 53 | Or copy and paste this URL into your browser:{" "} 54 | <Link href={resetLink} className="text-blue-600 no-underline"> 55 | {resetLink} 56 | </Link> 57 | </Text> 58 | <Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" /> 59 | <Text className="text-[#666666] text-[12px] leading-[24px]"> 60 | If you didn't request a password reset, please ignore this email 61 | or contact support if you have concerns. 62 | </Text> 63 | </Container> 64 | </Body> 65 | </Tailwind> 66 | </Html> 67 | ); 68 | }; 69 | 70 | export function reactResetPasswordEmail( 71 | props: BetterAuthResetPasswordEmailProps, 72 | ) { 73 | console.log(props); 74 | return <ResetPasswordEmail {...props} />; 75 | } 76 | ``` -------------------------------------------------------------------------------- /docs/content/changelogs/1.0.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: v1.0 3 | description: Built-in CLI for managing your project. 4 | date: 2021-10-01 5 | --- 6 | 7 | Version update 8 | 9 | Better Auth comes with a built-in CLI to help you manage the database schema needed for both core functionality and plugins. 10 | 11 | ## Generate 12 | 13 | The `generate` command creates the schema required by Better Auth. If you're using a database adapter like Prisma or Drizzle, this command will generate the right schema for your ORM. If you're using the built-in Kysely adapter, it will generate an SQL file you can run directly on your database. 14 | 15 | ```bash title="Terminal" 16 | npx @better-auth/cli@latest generate 17 | ``` 18 | 19 | ### Options 20 | 21 | - `--output` - Where to save the generated schema. For Prisma, it will be saved in prisma/schema.prisma. For Drizzle, it goes to schema.ts in your project root. For Kysely, it’s an SQL file saved as schema.sql in your project root. 22 | - `--config` - The path to your Better Auth config file. By default, the CLI will search for a better-auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under `src` directory. 23 | - `--y` - Skip the confirmation prompt and generate the schema directly. 24 | 25 | ## Migrate 26 | 27 | The migrate command applies the Better Auth schema directly to your database. This is available if you’re using the built-in Kysely adapter. 28 | 29 | ```bash title="Terminal" 30 | npx @better-auth/cli@latest migrate 31 | ``` 32 | 33 | ### Options 34 | 35 | - `--config` - The path to your Better Auth config file. By default, the CLI will search for a better-auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under `src` directory. 36 | - `--y` - Skip the confirmation prompt and apply the schema directly. 37 | 38 | ## Common Issues 39 | 40 | **Error: Cannot find module X** 41 | 42 | If you see this error, it means the CLI can’t resolve imported modules in your Better Auth config file. We're working on a fix for many of these issues, but in the meantime, you can try the following: 43 | 44 | - Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases. 45 | 46 | ## Secret 47 | 48 | The CLI also provides a way to generate a secret key for your Better Auth instance. 49 | 50 | ```bash title="Terminal" 51 | npx @better-auth/cli@latest secret 52 | ``` 53 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/types.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expectTypeOf } from "vitest"; 2 | import { getTestInstance } from "../test-utils/test-instance"; 3 | import { organization, twoFactor } from "../plugins"; 4 | 5 | describe("general types", async (it) => { 6 | it("should infer base session", async () => { 7 | const { auth } = await getTestInstance(); 8 | type Session = typeof auth.$Infer.Session; 9 | expectTypeOf<Session>().toEqualTypeOf<{ 10 | session: { 11 | id: string; 12 | createdAt: Date; 13 | updatedAt: Date; 14 | userId: string; 15 | expiresAt: Date; 16 | token: string; 17 | ipAddress?: string | null | undefined; 18 | userAgent?: string | null | undefined; 19 | }; 20 | user: { 21 | id: string; 22 | createdAt: Date; 23 | updatedAt: Date; 24 | email: string; 25 | emailVerified: boolean; 26 | name: string; 27 | image?: string | null | undefined; 28 | }; 29 | }>(); 30 | }); 31 | 32 | it("should infer additional fields from plugins", async () => { 33 | const { auth } = await getTestInstance({ 34 | plugins: [twoFactor(), organization()], 35 | }); 36 | expectTypeOf<typeof auth.$Infer.Session.user>().toEqualTypeOf<{ 37 | id: string; 38 | email: string; 39 | emailVerified: boolean; 40 | name: string; 41 | image?: string | undefined | null; 42 | createdAt: Date; 43 | updatedAt: Date; 44 | twoFactorEnabled: boolean | undefined | null; 45 | }>(); 46 | 47 | expectTypeOf<typeof auth.$Infer.Session.session>().toEqualTypeOf<{ 48 | id: string; 49 | userId: string; 50 | expiresAt: Date; 51 | createdAt: Date; 52 | updatedAt: Date; 53 | token: string; 54 | ipAddress?: string | undefined | null; 55 | userAgent?: string | undefined | null; 56 | activeOrganizationId?: string | undefined | null; 57 | }>(); 58 | }); 59 | 60 | it("should infer the same types for empty plugins and no plugins", async () => { 61 | const { auth: authWithEmptyPlugins } = await getTestInstance({ 62 | plugins: [], 63 | secret: "test-secret", 64 | emailAndPassword: { 65 | enabled: true, 66 | }, 67 | }); 68 | 69 | const { auth: authWithoutPlugins } = await getTestInstance({ 70 | secret: "test-secret", 71 | emailAndPassword: { 72 | enabled: true, 73 | }, 74 | }); 75 | 76 | type SessionWithEmptyPlugins = typeof authWithEmptyPlugins.$Infer; 77 | type SessionWithoutPlugins = typeof authWithoutPlugins.$Infer; 78 | 79 | expectTypeOf<SessionWithEmptyPlugins>().toEqualTypeOf<SessionWithoutPlugins>(); 80 | }); 81 | }); 82 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/tanstack.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: TanStack Start Integration 3 | description: Integrate Better Auth with TanStack Start. 4 | --- 5 | 6 | This integration guide is assuming you are using TanStack Start. 7 | 8 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). 9 | 10 | ### Mount the handler 11 | 12 | We need to mount the handler to a TanStack API endpoint/Server Route. 13 | Create a new file: `/src/routes/api/auth/$.ts` 14 | 15 | ```ts title="src/routes/api/auth/$.ts" 16 | import { auth } from '@/lib/auth' 17 | import { createFileRoute } from '@tanstack/react-router' 18 | 19 | export const Route = createFileRoute('/api/auth/$')({ 20 | server: { 21 | handlers: { 22 | GET: ({ request }) => { 23 | return auth.handler(request) 24 | }, 25 | POST: ({ request }) => { 26 | return auth.handler(request) 27 | }, 28 | }, 29 | }, 30 | }) 31 | ``` 32 | 33 | If you haven't created your server route handler yet, you can do so by creating a file: `/src/server.ts` 34 | 35 | ```ts title="src/server.ts" 36 | import { 37 | createStartHandler, 38 | defaultStreamHandler, 39 | } from '@tanstack/react-start/server' 40 | import { createRouter } from './router' 41 | 42 | export default createStartHandler({ 43 | createRouter, 44 | })(defaultStreamHandler) 45 | ``` 46 | 47 | ### Usage tips 48 | 49 | - We recommend using the client SDK or `authClient` to handle authentication, rather than server actions with `auth.api`. 50 | - When you call functions that need to set cookies (like `signInEmail` or `signUpEmail`), you'll need to handle cookie setting for TanStack Start. Better Auth provides a `reactStartCookies` plugin to automatically handle this for you. 51 | 52 | ```ts title="src/lib/auth.ts" 53 | import { betterAuth } from "better-auth"; 54 | import { reactStartCookies } from "better-auth/react-start"; 55 | 56 | export const auth = betterAuth({ 57 | //...your config 58 | plugins: [reactStartCookies()] // make sure this is the last plugin in the array 59 | }) 60 | ``` 61 | 62 | Now, when you call functions that set cookies, they will be automatically set using TanStack Start's cookie handling system. 63 | 64 | ```ts 65 | import { auth } from "@/lib/auth" 66 | 67 | const signIn = async () => { 68 | await auth.api.signInEmail({ 69 | body: { 70 | email: "[email protected]", 71 | password: "password", 72 | } 73 | }) 74 | } 75 | ``` 76 | ``` -------------------------------------------------------------------------------- /docs/components/builder/code-tabs/code-editor.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Highlight } from "prism-react-renderer"; 5 | import { Check, Copy } from "lucide-react"; 6 | import { Button } from "@/components/ui/button"; 7 | import theme from "./theme"; 8 | 9 | interface CodeEditorProps { 10 | code: string; 11 | language: string; 12 | } 13 | 14 | export function CodeEditor({ code, language }: CodeEditorProps) { 15 | const [isCopied, setIsCopied] = useState(false); 16 | 17 | const copyToClipboard = async () => { 18 | try { 19 | await navigator.clipboard.writeText(code); 20 | setIsCopied(true); 21 | setTimeout(() => setIsCopied(false), 2000); 22 | } catch (err) { 23 | console.error("Failed to copy text: ", err); 24 | } 25 | }; 26 | 27 | return ( 28 | <div className="relative"> 29 | <div className="dark:bg-bg-white rounded-md overflow-hidden"> 30 | <Highlight theme={theme} code={code} language={language}> 31 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 32 | <div className="overflow-auto max-h-[400px]"> 33 | <pre 34 | className={`${className} relative text-sm p-4 w-full min-w-fit rounded-md`} 35 | style={style} 36 | > 37 | {tokens.map((line, i) => { 38 | const lineProps = getLineProps({ line, key: i }); 39 | return ( 40 | <div 41 | key={i} 42 | className={lineProps.className} 43 | style={lineProps.style} 44 | > 45 | <span className="inline-block w-8 pr-2 text-right mr-4 text-gray-500 select-none sticky left-0 bg-black"> 46 | {i + 1} 47 | </span> 48 | {line.map((token, key) => { 49 | const tokenProps = getTokenProps({ token, key }); 50 | return ( 51 | <span 52 | key={key} 53 | className={tokenProps.className} 54 | style={tokenProps.style} 55 | > 56 | {tokenProps.children} 57 | </span> 58 | ); 59 | })} 60 | </div> 61 | ); 62 | })} 63 | </pre> 64 | </div> 65 | )} 66 | </Highlight> 67 | <Button 68 | variant="outline" 69 | size="icon" 70 | className="absolute top-2 right-2" 71 | onClick={copyToClipboard} 72 | aria-label="Copy code" 73 | > 74 | {isCopied ? ( 75 | <Check className="h-4 w-4" /> 76 | ) : ( 77 | <Copy className="h-4 w-4" /> 78 | )} 79 | </Button> 80 | </div> 81 | </div> 82 | ); 83 | } 84 | ``` -------------------------------------------------------------------------------- /docs/app/api/chat/route.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ProvideLinksToolSchema } from "@/lib/chat/inkeep-qa-schema"; 2 | import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; 3 | import { convertToModelMessages, streamText } from "ai"; 4 | import { 5 | logConversationToAnalytics, 6 | type InkeepMessage, 7 | } from "@/lib/inkeep-analytics"; 8 | 9 | export const runtime = "edge"; 10 | 11 | const openai = createOpenAICompatible({ 12 | name: "inkeep", 13 | apiKey: process.env.INKEEP_API_KEY, 14 | baseURL: "https://api.inkeep.com/v1", 15 | }); 16 | 17 | export async function POST(req: Request) { 18 | const reqJson = await req.json(); 19 | 20 | const result = streamText({ 21 | model: openai("inkeep-qa-sonnet-4"), 22 | tools: { 23 | provideLinks: { 24 | inputSchema: ProvideLinksToolSchema, 25 | }, 26 | }, 27 | messages: convertToModelMessages(reqJson.messages, { 28 | ignoreIncompleteToolCalls: true, 29 | }), 30 | toolChoice: "auto", 31 | onFinish: async (event) => { 32 | try { 33 | const extractMessageContent = (msg: any): string => { 34 | if (typeof msg.content === "string") { 35 | return msg.content; 36 | } 37 | 38 | if (msg.parts && Array.isArray(msg.parts)) { 39 | return msg.parts 40 | .filter((part: any) => part.type === "text") 41 | .map((part: any) => part.text) 42 | .join(""); 43 | } 44 | 45 | if (msg.text) { 46 | return msg.text; 47 | } 48 | 49 | return ""; 50 | }; 51 | 52 | const assistantMessageId = 53 | event.response.id || 54 | `assistant_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 55 | 56 | const inkeepMessages: InkeepMessage[] = [ 57 | ...reqJson.messages 58 | .map((msg: any) => ({ 59 | id: 60 | msg.id || 61 | `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, 62 | role: msg.role, 63 | content: extractMessageContent(msg), 64 | })) 65 | .filter((msg: any) => msg.content.trim() !== ""), 66 | { 67 | id: assistantMessageId, 68 | role: "assistant" as const, 69 | content: event.text, 70 | }, 71 | ]; 72 | 73 | await logConversationToAnalytics({ 74 | type: "openai", 75 | messages: inkeepMessages, 76 | properties: { 77 | source: "better-auth-docs", 78 | timestamp: new Date().toISOString(), 79 | model: "inkeep-qa-sonnet-4", 80 | }, 81 | }); 82 | } catch (error) { 83 | // Don't fail the request if analytics logging fails 84 | } 85 | }, 86 | }); 87 | 88 | return result.toUIMessageStreamResponse(); 89 | } 90 | ``` -------------------------------------------------------------------------------- /docs/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { ChevronRight, MoreHorizontal } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />; 9 | } 10 | 11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { 12 | return ( 13 | <ol 14 | data-slot="breadcrumb-list" 15 | className={cn( 16 | "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", 17 | className, 18 | )} 19 | {...props} 20 | /> 21 | ); 22 | } 23 | 24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { 25 | return ( 26 | <li 27 | data-slot="breadcrumb-item" 28 | className={cn("inline-flex items-center gap-1.5", className)} 29 | {...props} 30 | /> 31 | ); 32 | } 33 | 34 | function BreadcrumbLink({ 35 | asChild, 36 | className, 37 | ...props 38 | }: React.ComponentProps<"a"> & { 39 | asChild?: boolean; 40 | }) { 41 | const Comp = asChild ? Slot : "a"; 42 | 43 | return ( 44 | <Comp 45 | data-slot="breadcrumb-link" 46 | className={cn("hover:text-foreground transition-colors", className)} 47 | {...props} 48 | /> 49 | ); 50 | } 51 | 52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { 53 | return ( 54 | <span 55 | data-slot="breadcrumb-page" 56 | role="link" 57 | aria-disabled="true" 58 | aria-current="page" 59 | className={cn("text-foreground font-normal", className)} 60 | {...props} 61 | /> 62 | ); 63 | } 64 | 65 | function BreadcrumbSeparator({ 66 | children, 67 | className, 68 | ...props 69 | }: React.ComponentProps<"li">) { 70 | return ( 71 | <li 72 | data-slot="breadcrumb-separator" 73 | role="presentation" 74 | aria-hidden="true" 75 | className={cn("[&>svg]:size-3.5", className)} 76 | {...props} 77 | > 78 | {children ?? <ChevronRight />} 79 | </li> 80 | ); 81 | } 82 | 83 | function BreadcrumbEllipsis({ 84 | className, 85 | ...props 86 | }: React.ComponentProps<"span">) { 87 | return ( 88 | <span 89 | data-slot="breadcrumb-ellipsis" 90 | role="presentation" 91 | aria-hidden="true" 92 | className={cn("flex size-9 items-center justify-center", className)} 93 | {...props} 94 | > 95 | <MoreHorizontal className="size-4" /> 96 | <span className="sr-only">More</span> 97 | </span> 98 | ); 99 | } 100 | 101 | export { 102 | Breadcrumb, 103 | BreadcrumbList, 104 | BreadcrumbItem, 105 | BreadcrumbLink, 106 | BreadcrumbPage, 107 | BreadcrumbSeparator, 108 | BreadcrumbEllipsis, 109 | }; 110 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/elysia.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Elysia Integration 3 | description: Integrate Better Auth with Elysia. 4 | --- 5 | 6 | This integration guide is assuming you are using Elysia with bun server. 7 | 8 | Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). 9 | 10 | ### Mount the handler 11 | 12 | We need to mount the handler to Elysia endpoint. 13 | 14 | ```ts 15 | import { Elysia } from "elysia"; 16 | import { auth } from "./auth"; 17 | 18 | const app = new Elysia().mount(auth.handler).listen(3000); 19 | 20 | console.log( 21 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`, 22 | ); 23 | ``` 24 | 25 | ### CORS 26 | 27 | To configure cors, you can use the `cors` plugin from `@elysiajs/cors`. 28 | 29 | ```ts 30 | import { Elysia } from "elysia"; 31 | import { cors } from "@elysiajs/cors"; 32 | 33 | import { auth } from "./auth"; 34 | 35 | const app = new Elysia() 36 | .use( 37 | cors({ 38 | origin: "http://localhost:3001", 39 | methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 40 | credentials: true, 41 | allowedHeaders: ["Content-Type", "Authorization"], 42 | }), 43 | ) 44 | .mount(auth.handler) 45 | .listen(3000); 46 | 47 | console.log( 48 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`, 49 | ); 50 | ``` 51 | 52 | ### Macro 53 | 54 | You can use [macro](https://elysiajs.com/patterns/macro.html#macro) with [resolve](https://elysiajs.com/essential/handler.html#resolve) to provide session and user information before pass to view. 55 | 56 | ```ts 57 | import { Elysia } from "elysia"; 58 | import { auth } from "./auth"; 59 | 60 | // user middleware (compute user and session and pass to routes) 61 | const betterAuth = new Elysia({ name: "better-auth" }) 62 | .mount(auth.handler) 63 | .macro({ 64 | auth: { 65 | async resolve({ status, request: { headers } }) { 66 | const session = await auth.api.getSession({ 67 | headers, 68 | }); 69 | 70 | if (!session) return status(401); 71 | 72 | return { 73 | user: session.user, 74 | session: session.session, 75 | }; 76 | }, 77 | }, 78 | }); 79 | 80 | const app = new Elysia() 81 | .use(betterAuth) 82 | .get("/user", ({ user }) => user, { 83 | auth: true, 84 | }) 85 | .listen(3000); 86 | 87 | console.log( 88 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`, 89 | ); 90 | ``` 91 | 92 | This will allow you to access the `user` and `session` object in all of your routes. 93 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/features.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import React from "react"; 3 | 4 | import { AnimatePresence, motion } from "framer-motion"; 5 | import { Logo } from "@/components/logo"; 6 | 7 | export function Features() { 8 | return ( 9 | <> 10 | <div className="flex flex-col lg:flex-row bg-white dark:bg-black w-full gap-4 mx-auto px-8"> 11 | <Card title="Better Auth" icon={<Logo className=" w-44" />}></Card> 12 | </div> 13 | </> 14 | ); 15 | } 16 | 17 | const Card = ({ 18 | title, 19 | icon, 20 | children, 21 | }: { 22 | title: string; 23 | icon: React.ReactNode; 24 | children?: React.ReactNode; 25 | }) => { 26 | const [hovered, setHovered] = React.useState(false); 27 | return ( 28 | <div 29 | onMouseEnter={() => setHovered(true)} 30 | onMouseLeave={() => setHovered(false)} 31 | className="border border-black/20 group/canvas-card flex items-center justify-center dark:border-white/20 max-w-sm w-full mx-auto p-4 relative h-72" 32 | > 33 | <Icon className="absolute h-6 w-6 -top-3 -left-3 dark:text-white text-black" /> 34 | <Icon className="absolute h-6 w-6 -bottom-3 -left-3 dark:text-white text-black" /> 35 | <Icon className="absolute h-6 w-6 -top-3 -right-3 dark:text-white text-black" /> 36 | <Icon className="absolute h-6 w-6 -bottom-3 -right-3 dark:text-white text-black" /> 37 | 38 | <AnimatePresence> 39 | {hovered && ( 40 | <motion.div 41 | initial={{ opacity: 0 }} 42 | animate={{ opacity: 1 }} 43 | className="h-full w-full absolute inset-0" 44 | > 45 | {children} 46 | </motion.div> 47 | )} 48 | </AnimatePresence> 49 | 50 | <div className="relative z-20"> 51 | <div className="text-center group-hover/canvas-card:-translate-y-4 group-hover/canvas-card:opacity-0 transition duration-200 w-full mx-auto flex items-center justify-center"> 52 | {icon} 53 | </div> 54 | <h2 className="dark:text-white text-xl opacity-0 group-hover/canvas-card:opacity-100 relative z-10 text-black mt-4 font-bold group-hover/canvas-card:text-white group-hover/canvas-card:-translate-y-2 transition duration-200"> 55 | {title} 56 | </h2> 57 | </div> 58 | </div> 59 | ); 60 | }; 61 | 62 | export const Icon = ({ className, ...rest }: any) => { 63 | return ( 64 | <svg 65 | xmlns="http://www.w3.org/2000/svg" 66 | fill="none" 67 | viewBox="0 0 24 24" 68 | strokeWidth="1.5" 69 | stroke="currentColor" 70 | className={className} 71 | {...rest} 72 | > 73 | <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" /> 74 | </svg> 75 | ); 76 | }; 77 | ``` -------------------------------------------------------------------------------- /docs/app/community/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import CommunityHeader from "./_components/header"; 2 | import Stats from "./_components/stats"; 3 | import Section from "@/components/landing/section"; 4 | type NpmPackageResp = { 5 | downloads: number; 6 | start: string; 7 | end: string; 8 | package: string; 9 | }; 10 | async function getNPMPackageDownloads() { 11 | const res = await fetch( 12 | `https://api.npmjs.org/downloads/point/last-year/better-auth`, 13 | { 14 | next: { revalidate: 60 }, 15 | }, 16 | ); 17 | 18 | const npmStat: NpmPackageResp = await res.json(); 19 | return npmStat; 20 | } 21 | async function getGitHubStars() { 22 | try { 23 | const response = await fetch( 24 | "https://api.github.com/repos/better-auth/better-auth", 25 | { 26 | next: { 27 | revalidate: 60, 28 | }, 29 | }, 30 | ); 31 | const json = await response.json(); 32 | const stars = Number(json.stargazers_count); 33 | return stars; 34 | } catch { 35 | return 0; 36 | } 37 | } 38 | export default async function CommunityPage() { 39 | const npmDownloads = await getNPMPackageDownloads(); 40 | const githubStars = await getGitHubStars(); 41 | 42 | return ( 43 | <Section 44 | id="hero" 45 | className="relative md:px-[3.4rem] md:pl-[3.9rem] md:max-w-7xl md:mx-auto overflow-hidden" 46 | crosses={false} 47 | crossesOffset="" 48 | customPaddings 49 | > 50 | <div className="min-h-screen w-full bg-transparent"> 51 | <div className="overflow-hidden flex flex-col w-full bg-transparent/10 relative"> 52 | <div className="h-[38vh]"> 53 | <CommunityHeader /> 54 | </div> 55 | <div className="relative py-0"> 56 | <div className="absolute inset-0 z-0"> 57 | <div className="grid grid-cols-12 h-full"> 58 | {Array(12) 59 | .fill(null) 60 | .map((_, i) => ( 61 | <div 62 | key={i} 63 | className="border-l border-dashed border-stone-100 dark:border-white/10 h-full" 64 | /> 65 | ))} 66 | </div> 67 | <div className="grid grid-rows-12 w-full absolute top-0"> 68 | {Array(12) 69 | .fill(null) 70 | .map((_, i) => ( 71 | <div 72 | key={i} 73 | className="border-t border-dashed border-stone-100 dark:border-stone-900/60 w-full" 74 | /> 75 | ))} 76 | </div> 77 | </div> 78 | </div> 79 | <div className="w-full md:mx-auto overflow-hidden"> 80 | <Stats 81 | npmDownloads={npmDownloads.downloads} 82 | githubStars={githubStars} 83 | /> 84 | </div> 85 | </div> 86 | </div> 87 | </Section> 88 | ); 89 | } 90 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 20 | with: 21 | node-version: 22.x 22 | 23 | - run: npx changelogithub 24 | env: 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | 27 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 28 | 29 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 30 | with: 31 | node-version: 22.x 32 | registry-url: 'https://registry.npmjs.org' 33 | 34 | - run: pnpm install 35 | 36 | - name: Build 37 | run: pnpm build 38 | 39 | - name: Determine npm tag 40 | id: determine_npm_tag 41 | shell: bash 42 | run: | 43 | TAG="${GITHUB_REF#refs/tags/}" 44 | if [[ "$TAG" =~ -(next|canary|beta|rc) ]]; then 45 | # Extract pre-release tag (e.g., beta, rc) 46 | NPM_TAG=${BASH_REMATCH[1]} 47 | else 48 | # Check if the commit is on the main branch or a version branch 49 | git fetch origin main 50 | CURRENT_BRANCH=$(git branch -r --contains "$GITHUB_SHA" | grep -E 'origin/(main|v[0-9]+\.[0-9]+\.x-latest)' | head -1 | sed 's/.*origin\///') 51 | 52 | if [[ "$CURRENT_BRANCH" == "main" ]]; then 53 | NPM_TAG="latest" 54 | elif [[ "$CURRENT_BRANCH" =~ ^v[0-9]+\.[0-9]+\.x-latest$ ]]; then 55 | # For version branches like v1.3.x-latest, v1.4.x-latest, use "latest" tag 56 | NPM_TAG="latest" 57 | else 58 | echo "The tagged commit is not on the main branch or a version branch (v*.*.x-latest)." 59 | echo "::error ::Releases with the 'latest' npm tag must be on the main branch or a version branch." 60 | exit 1 61 | fi 62 | fi 63 | echo "npm_tag=$NPM_TAG" >> $GITHUB_OUTPUT 64 | echo "Using npm tag: $NPM_TAG" 65 | 66 | - name: Publish to npm 67 | run: pnpm -r publish --access public --no-git-checks --tag ${{ steps.determine_npm_tag.outputs.npm_tag }} 68 | env: 69 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` -------------------------------------------------------------------------------- /demo/nextjs/components/sign-in-btn.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | import { Button } from "./ui/button"; 3 | import { auth } from "@/lib/auth"; 4 | import { headers } from "next/headers"; 5 | 6 | export async function SignInButton() { 7 | const session = await auth.api.getSession({ 8 | headers: await headers(), 9 | }); 10 | 11 | return ( 12 | <Link 13 | href={session?.session ? "/dashboard" : "/sign-in"} 14 | className="flex justify-center" 15 | > 16 | <Button className="gap-2 justify-between" variant="default"> 17 | {!session?.session ? ( 18 | <svg 19 | xmlns="http://www.w3.org/2000/svg" 20 | width="1.2em" 21 | height="1.2em" 22 | viewBox="0 0 24 24" 23 | > 24 | <path 25 | fill="currentColor" 26 | d="M5 3H3v4h2V5h14v14H5v-2H3v4h18V3zm12 8h-2V9h-2V7h-2v2h2v2H3v2h10v2h-2v2h2v-2h2v-2h2z" 27 | ></path> 28 | </svg> 29 | ) : ( 30 | <svg 31 | xmlns="http://www.w3.org/2000/svg" 32 | width="1.2em" 33 | height="1.2em" 34 | viewBox="0 0 24 24" 35 | > 36 | <path fill="currentColor" d="M2 3h20v18H2zm18 16V7H4v12z"></path> 37 | </svg> 38 | )} 39 | <span>{session?.session ? "Dashboard" : "Sign In"}</span> 40 | </Button> 41 | </Link> 42 | ); 43 | } 44 | 45 | function checkOptimisticSession(headers: Headers) { 46 | const guessIsSignIn = 47 | headers.get("cookie")?.includes("better-auth.session") || 48 | headers.get("cookie")?.includes("__Secure-better-auth.session-token"); 49 | return !!guessIsSignIn; 50 | } 51 | 52 | export async function SignInFallback() { 53 | //to avoid flash of unauthenticated state 54 | const guessIsSignIn = checkOptimisticSession(await headers()); 55 | return ( 56 | <Link 57 | href={guessIsSignIn ? "/dashboard" : "/sign-in"} 58 | className="flex justify-center" 59 | > 60 | <Button className="gap-2 justify-between" variant="default"> 61 | {!guessIsSignIn ? ( 62 | <svg 63 | xmlns="http://www.w3.org/2000/svg" 64 | width="1.2em" 65 | height="1.2em" 66 | viewBox="0 0 24 24" 67 | > 68 | <path 69 | fill="currentColor" 70 | d="M5 3H3v4h2V5h14v14H5v-2H3v4h18V3zm12 8h-2V9h-2V7h-2v2h2v2H3v2h10v2h-2v2h2v-2h2v-2h2z" 71 | ></path> 72 | </svg> 73 | ) : ( 74 | <svg 75 | xmlns="http://www.w3.org/2000/svg" 76 | width="1.2em" 77 | height="1.2em" 78 | viewBox="0 0 24 24" 79 | > 80 | <path fill="currentColor" d="M2 3h20v18H2zm18 16V7H4v12z"></path> 81 | </svg> 82 | )} 83 | <span>{guessIsSignIn ? "Dashboard" : "Sign In"}</span> 84 | </Button> 85 | </Link> 86 | ); 87 | } 88 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/zoom.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Zoom 3 | description: Zoom provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Create a Zoom App from Marketplace 9 | 1. Visit [Zoom Marketplace](https://marketplace.zoom.us). 10 | 11 | 1. Hover on the `Develop` button and select `Build App` 12 | 13 | 1. Select `General App` and click `Create` 14 | 15 | </Step> 16 | 17 | <Step> 18 | ### Configure your Zoom App 19 | 20 | Ensure that you are in the `Basic Information` of your app settings. 21 | 22 | 1. Under `Select how the app is managed`, choose `User-managed` 23 | 24 | 1. Under `App Credentials`, copy your `Client ID` and `Client Secret` and store them in a safe location 25 | 26 | 1. Under `OAuth Information` -> `OAuth Redirect URL`, add your Callback URL. For example, 27 | 28 | ``` 29 | http://localhost:3000/api/auth/callback/zoom 30 | ``` 31 | 32 | <Callout> 33 | For production, you should set it to the URL of your application. If you change the base 34 | path of the auth routes, you should update the redirect URL accordingly. 35 | </Callout> 36 | 37 | Skip to the `Scopes` section, then 38 | 1. Click the `Add Scopes` button 39 | 1. Search for `user:read:user` (View a user) and select it 40 | 1. Add any other scopes your applications needs and click `Done` 41 | 42 | </Step> 43 | 44 | <Step> 45 | ### Configure the provider 46 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 47 | 48 | ```ts title="auth.ts" 49 | import { betterAuth } from "better-auth" 50 | 51 | export const auth = betterAuth({ 52 | socialProviders: { 53 | zoom: { // [!code highlight] 54 | clientId: process.env.ZOOM_CLIENT_ID as string, // [!code highlight] 55 | clientSecret: process.env.ZOOM_CLIENT_SECRET as string, // [!code highlight] 56 | }, // [!code highlight] 57 | }, 58 | }) 59 | ``` 60 | 61 | </Step> 62 | 63 | <Step> 64 | ### Sign In with Zoom 65 | To sign in with Zoom, you can use the `signIn.social` function provided by the client. 66 | You will need to specify `zoom` as the provider. 67 | 68 | ```ts title="auth-client.ts" 69 | import { createAuthClient } from "better-auth/client" 70 | const authClient = createAuthClient() 71 | 72 | const signIn = async () => { 73 | const data = await authClient.signIn.social({ 74 | provider: "zoom" 75 | }) 76 | } 77 | ``` 78 | 79 | </Step> 80 | </Steps> 81 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/rate-limit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ERROR_CODES } from "."; 2 | import type { PredefinedApiKeyOptions } from "./routes"; 3 | import type { ApiKey } from "./types"; 4 | 5 | interface RateLimitResult { 6 | success: boolean; 7 | message: string | null; 8 | tryAgainIn: number | null; 9 | update: Partial<ApiKey> | null; 10 | } 11 | 12 | /** 13 | * Determines if a request is allowed based on rate limiting parameters. 14 | * 15 | * @returns An object indicating whether the request is allowed and, if not, 16 | * a message and updated ApiKey data. 17 | */ 18 | export function isRateLimited( 19 | /** 20 | * The ApiKey object containing rate limiting information 21 | */ 22 | apiKey: ApiKey, 23 | opts: PredefinedApiKeyOptions, 24 | ): RateLimitResult { 25 | const now = new Date(); 26 | const lastRequest = apiKey.lastRequest; 27 | const rateLimitTimeWindow = apiKey.rateLimitTimeWindow; 28 | const rateLimitMax = apiKey.rateLimitMax; 29 | let requestCount = apiKey.requestCount; 30 | 31 | if (opts.rateLimit.enabled === false) 32 | return { 33 | success: true, 34 | message: null, 35 | update: { lastRequest: now }, 36 | tryAgainIn: null, 37 | }; 38 | 39 | if (apiKey.rateLimitEnabled === false) 40 | return { 41 | success: true, 42 | message: null, 43 | update: { lastRequest: now }, 44 | tryAgainIn: null, 45 | }; 46 | 47 | if (rateLimitTimeWindow === null || rateLimitMax === null) { 48 | // Rate limiting is disabled. 49 | return { 50 | success: true, 51 | message: null, 52 | update: null, 53 | tryAgainIn: null, 54 | }; 55 | } 56 | 57 | if (lastRequest === null) { 58 | // No previous requests, so allow the first one. 59 | return { 60 | success: true, 61 | message: null, 62 | update: { lastRequest: now, requestCount: 1 }, 63 | tryAgainIn: null, 64 | }; 65 | } 66 | 67 | const timeSinceLastRequest = now.getTime() - new Date(lastRequest).getTime(); 68 | 69 | if (timeSinceLastRequest > rateLimitTimeWindow) { 70 | // Time window has passed, reset the request count. 71 | return { 72 | success: true, 73 | message: null, 74 | update: { lastRequest: now, requestCount: 1 }, 75 | tryAgainIn: null, 76 | }; 77 | } 78 | 79 | if (requestCount >= rateLimitMax) { 80 | // Rate limit exceeded. 81 | return { 82 | success: false, 83 | message: ERROR_CODES.RATE_LIMIT_EXCEEDED, 84 | update: null, 85 | tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest), 86 | }; 87 | } 88 | 89 | // Request is allowed. 90 | requestCount++; 91 | return { 92 | success: true, 93 | message: null, 94 | tryAgainIn: null, 95 | update: { lastRequest: now, requestCount: requestCount }, 96 | }; 97 | } 98 | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/create-authorization-url.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ProviderOptions } from "./index"; 2 | import { generateCodeChallenge } from "./utils"; 3 | 4 | export async function createAuthorizationURL({ 5 | id, 6 | options, 7 | authorizationEndpoint, 8 | state, 9 | codeVerifier, 10 | scopes, 11 | claims, 12 | redirectURI, 13 | duration, 14 | prompt, 15 | accessType, 16 | responseType, 17 | display, 18 | loginHint, 19 | hd, 20 | responseMode, 21 | additionalParams, 22 | scopeJoiner, 23 | }: { 24 | id: string; 25 | options: ProviderOptions; 26 | redirectURI: string; 27 | authorizationEndpoint: string; 28 | state: string; 29 | codeVerifier?: string; 30 | scopes: string[]; 31 | claims?: string[]; 32 | duration?: string; 33 | prompt?: string; 34 | accessType?: string; 35 | responseType?: string; 36 | display?: string; 37 | loginHint?: string; 38 | hd?: string; 39 | responseMode?: string; 40 | additionalParams?: Record<string, string>; 41 | scopeJoiner?: string; 42 | }) { 43 | const url = new URL(authorizationEndpoint); 44 | url.searchParams.set("response_type", responseType || "code"); 45 | const primaryClientId = Array.isArray(options.clientId) 46 | ? options.clientId[0] 47 | : options.clientId; 48 | url.searchParams.set("client_id", primaryClientId); 49 | url.searchParams.set("state", state); 50 | url.searchParams.set("scope", scopes.join(scopeJoiner || " ")); 51 | url.searchParams.set("redirect_uri", options.redirectURI || redirectURI); 52 | duration && url.searchParams.set("duration", duration); 53 | display && url.searchParams.set("display", display); 54 | loginHint && url.searchParams.set("login_hint", loginHint); 55 | prompt && url.searchParams.set("prompt", prompt); 56 | hd && url.searchParams.set("hd", hd); 57 | accessType && url.searchParams.set("access_type", accessType); 58 | responseMode && url.searchParams.set("response_mode", responseMode); 59 | if (codeVerifier) { 60 | const codeChallenge = await generateCodeChallenge(codeVerifier); 61 | url.searchParams.set("code_challenge_method", "S256"); 62 | url.searchParams.set("code_challenge", codeChallenge); 63 | } 64 | if (claims) { 65 | const claimsObj = claims.reduce( 66 | (acc, claim) => { 67 | acc[claim] = null; 68 | return acc; 69 | }, 70 | {} as Record<string, null>, 71 | ); 72 | url.searchParams.set( 73 | "claims", 74 | JSON.stringify({ 75 | id_token: { email: null, email_verified: null, ...claimsObj }, 76 | }), 77 | ); 78 | } 79 | if (additionalParams) { 80 | Object.entries(additionalParams).forEach(([key, value]) => { 81 | url.searchParams.set(key, value); 82 | }); 83 | } 84 | return url; 85 | } 86 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/table.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Table({ className, ...props }: React.ComponentProps<"table">) { 8 | return ( 9 | <div 10 | data-slot="table-container" 11 | className="relative w-full overflow-x-auto" 12 | > 13 | <table 14 | data-slot="table" 15 | className={cn("w-full caption-bottom text-sm", className)} 16 | {...props} 17 | /> 18 | </div> 19 | ); 20 | } 21 | 22 | function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { 23 | return ( 24 | <thead 25 | data-slot="table-header" 26 | className={cn("[&_tr]:border-b", className)} 27 | {...props} 28 | /> 29 | ); 30 | } 31 | 32 | function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { 33 | return ( 34 | <tbody 35 | data-slot="table-body" 36 | className={cn("[&_tr:last-child]:border-0", className)} 37 | {...props} 38 | /> 39 | ); 40 | } 41 | 42 | function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { 43 | return ( 44 | <tfoot 45 | data-slot="table-footer" 46 | className={cn( 47 | "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", 48 | className, 49 | )} 50 | {...props} 51 | /> 52 | ); 53 | } 54 | 55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) { 56 | return ( 57 | <tr 58 | data-slot="table-row" 59 | className={cn( 60 | "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", 61 | className, 62 | )} 63 | {...props} 64 | /> 65 | ); 66 | } 67 | 68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) { 69 | return ( 70 | <th 71 | data-slot="table-head" 72 | className={cn( 73 | "text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 74 | className, 75 | )} 76 | {...props} 77 | /> 78 | ); 79 | } 80 | 81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) { 82 | return ( 83 | <td 84 | data-slot="table-cell" 85 | className={cn( 86 | "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 87 | className, 88 | )} 89 | {...props} 90 | /> 91 | ); 92 | } 93 | 94 | function TableCaption({ 95 | className, 96 | ...props 97 | }: React.ComponentProps<"caption">) { 98 | return ( 99 | <caption 100 | data-slot="table-caption" 101 | className={cn("text-muted-foreground mt-4 text-sm", className)} 102 | {...props} 103 | /> 104 | ); 105 | } 106 | 107 | export { 108 | Table, 109 | TableHeader, 110 | TableBody, 111 | TableFooter, 112 | TableHead, 113 | TableRow, 114 | TableCell, 115 | TableCaption, 116 | }; 117 | ``` -------------------------------------------------------------------------------- /docs/components/ui/table.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Table({ className, ...props }: React.ComponentProps<"table">) { 8 | return ( 9 | <div 10 | data-slot="table-container" 11 | className="relative w-full overflow-x-auto" 12 | > 13 | <table 14 | data-slot="table" 15 | className={cn("w-full caption-bottom text-sm", className)} 16 | {...props} 17 | /> 18 | </div> 19 | ); 20 | } 21 | 22 | function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { 23 | return ( 24 | <thead 25 | data-slot="table-header" 26 | className={cn("[&_tr]:border-b", className)} 27 | {...props} 28 | /> 29 | ); 30 | } 31 | 32 | function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { 33 | return ( 34 | <tbody 35 | data-slot="table-body" 36 | className={cn("[&_tr:last-child]:border-0", className)} 37 | {...props} 38 | /> 39 | ); 40 | } 41 | 42 | function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { 43 | return ( 44 | <tfoot 45 | data-slot="table-footer" 46 | className={cn( 47 | "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", 48 | className, 49 | )} 50 | {...props} 51 | /> 52 | ); 53 | } 54 | 55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) { 56 | return ( 57 | <tr 58 | data-slot="table-row" 59 | className={cn( 60 | "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", 61 | className, 62 | )} 63 | {...props} 64 | /> 65 | ); 66 | } 67 | 68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) { 69 | return ( 70 | <th 71 | data-slot="table-head" 72 | className={cn( 73 | "text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 74 | className, 75 | )} 76 | {...props} 77 | /> 78 | ); 79 | } 80 | 81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) { 82 | return ( 83 | <td 84 | data-slot="table-cell" 85 | className={cn( 86 | "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 87 | className, 88 | )} 89 | {...props} 90 | /> 91 | ); 92 | } 93 | 94 | function TableCaption({ 95 | className, 96 | ...props 97 | }: React.ComponentProps<"caption">) { 98 | return ( 99 | <caption 100 | data-slot="table-caption" 101 | className={cn("text-muted-foreground mt-4 text-sm", className)} 102 | {...props} 103 | /> 104 | ); 105 | } 106 | 107 | export { 108 | Table, 109 | TableHeader, 110 | TableBody, 111 | TableFooter, 112 | TableHead, 113 | TableRow, 114 | TableCell, 115 | TableCaption, 116 | }; 117 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/access/access.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BetterAuthError } from "@better-auth/core/error"; 2 | import type { Statements, Subset } from "./types"; 3 | 4 | export type AuthorizeResponse = 5 | | { success: false; error: string } 6 | | { success: true; error?: never }; 7 | 8 | export function role<TStatements extends Statements>(statements: TStatements) { 9 | return { 10 | authorize<K extends keyof TStatements>( 11 | request: { 12 | [key in K]?: 13 | | TStatements[key] 14 | | { 15 | actions: TStatements[key]; 16 | connector: "OR" | "AND"; 17 | }; 18 | }, 19 | connector: "OR" | "AND" = "AND", 20 | ): AuthorizeResponse { 21 | let success = false; 22 | for (const [requestedResource, requestedActions] of Object.entries( 23 | request, 24 | )) { 25 | const allowedActions = statements[requestedResource]; 26 | if (!allowedActions) { 27 | return { 28 | success: false, 29 | error: `You are not allowed to access resource: ${requestedResource}`, 30 | }; 31 | } 32 | if (Array.isArray(requestedActions)) { 33 | success = (requestedActions as string[]).every((requestedAction) => 34 | allowedActions.includes(requestedAction), 35 | ); 36 | } else { 37 | if (typeof requestedActions === "object") { 38 | const actions = requestedActions as { 39 | actions: string[]; 40 | connector: "OR" | "AND"; 41 | }; 42 | if (actions.connector === "OR") { 43 | success = actions.actions.some((requestedAction) => 44 | allowedActions.includes(requestedAction), 45 | ); 46 | } else { 47 | success = actions.actions.every((requestedAction) => 48 | allowedActions.includes(requestedAction), 49 | ); 50 | } 51 | } else { 52 | throw new BetterAuthError("Invalid access control request"); 53 | } 54 | } 55 | if (success && connector === "OR") { 56 | return { success }; 57 | } 58 | if (!success && connector === "AND") { 59 | return { 60 | success: false, 61 | error: `unauthorized to access resource "${requestedResource}"`, 62 | }; 63 | } 64 | } 65 | if (success) { 66 | return { 67 | success, 68 | }; 69 | } 70 | return { 71 | success: false, 72 | error: "Not authorized", 73 | }; 74 | }, 75 | statements, 76 | }; 77 | } 78 | 79 | export function createAccessControl<const TStatements extends Statements>( 80 | s: TStatements, 81 | ) { 82 | return { 83 | newRole<K extends keyof TStatements>(statements: Subset<K, TStatements>) { 84 | return role<Subset<K, TStatements>>(statements); 85 | }, 86 | statements: s, 87 | }; 88 | } 89 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/drizzle-adapter/test/adapter.drizzle.mysql.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { drizzleAdapter } from "../drizzle-adapter"; 2 | import { testAdapter } from "../../test-adapter"; 3 | import { 4 | authFlowTestSuite, 5 | normalTestSuite, 6 | numberIdTestSuite, 7 | performanceTestSuite, 8 | transactionsTestSuite, 9 | } from "../../tests"; 10 | import { drizzle } from "drizzle-orm/mysql2"; 11 | import { generateDrizzleSchema, resetGenerationCount } from "./generate-schema"; 12 | import { createPool } from "mysql2/promise"; 13 | import { assert } from "vitest"; 14 | import { execSync } from "child_process"; 15 | 16 | const mysqlDB = createPool({ 17 | uri: "mysql://user:password@localhost:3306", 18 | timezone: "Z", 19 | }); 20 | 21 | const { execute } = await testAdapter({ 22 | adapter: async (options) => { 23 | const { schema } = await generateDrizzleSchema(mysqlDB, options, "mysql"); 24 | return drizzleAdapter(drizzle(mysqlDB), { 25 | debugLogs: { isRunningAdapterTests: true }, 26 | schema, 27 | provider: "mysql", 28 | }); 29 | }, 30 | async runMigrations(betterAuthOptions) { 31 | await mysqlDB.query("DROP DATABASE IF EXISTS better_auth"); 32 | await mysqlDB.query("CREATE DATABASE better_auth"); 33 | await mysqlDB.query("USE better_auth"); 34 | 35 | const { fileName } = await generateDrizzleSchema( 36 | mysqlDB, 37 | betterAuthOptions, 38 | "mysql", 39 | ); 40 | 41 | const command = `npx drizzle-kit push --dialect=mysql --schema=${fileName}.ts --url=mysql://user:password@localhost:3306/better_auth`; 42 | console.log(`Running: ${command}`); 43 | console.log(`Options:`, betterAuthOptions); 44 | try { 45 | // wait for the above console.log to be printed 46 | await new Promise((resolve) => setTimeout(resolve, 10)); 47 | execSync(command, { 48 | cwd: import.meta.dirname, 49 | stdio: "inherit", 50 | }); 51 | } catch (error) { 52 | console.error("Failed to push drizzle schema (mysql):", error); 53 | throw error; 54 | } 55 | 56 | // ensure migrations were run successfully 57 | const [tables_result] = (await mysqlDB.query("SHOW TABLES")) as unknown as [ 58 | { Tables_in_better_auth: string }[], 59 | ]; 60 | const tables = tables_result.map((table) => table.Tables_in_better_auth); 61 | assert(tables.length > 0, "No tables found"); 62 | }, 63 | prefixTests: "mysql", 64 | tests: [ 65 | normalTestSuite(), 66 | transactionsTestSuite({ disableTests: { ALL: true } }), 67 | authFlowTestSuite(), 68 | numberIdTestSuite(), 69 | performanceTestSuite({ dialect: "mysql" }), 70 | ], 71 | async onFinish() { 72 | await mysqlDB.end(); 73 | resetGenerationCount(); 74 | }, 75 | }); 76 | 77 | execute(); 78 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/spotify.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | validateAuthorizationCode, 6 | refreshAccessToken, 7 | } from "../oauth2"; 8 | 9 | export interface SpotifyProfile { 10 | id: string; 11 | display_name: string; 12 | email: string; 13 | images: { 14 | url: string; 15 | }[]; 16 | } 17 | 18 | export interface SpotifyOptions extends ProviderOptions<SpotifyProfile> { 19 | clientId: string; 20 | } 21 | 22 | export const spotify = (options: SpotifyOptions) => { 23 | return { 24 | id: "spotify", 25 | name: "Spotify", 26 | createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { 27 | const _scopes = options.disableDefaultScope ? [] : ["user-read-email"]; 28 | options.scope && _scopes.push(...options.scope); 29 | scopes && _scopes.push(...scopes); 30 | return createAuthorizationURL({ 31 | id: "spotify", 32 | options, 33 | authorizationEndpoint: "https://accounts.spotify.com/authorize", 34 | scopes: _scopes, 35 | state, 36 | codeVerifier, 37 | redirectURI, 38 | }); 39 | }, 40 | validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { 41 | return validateAuthorizationCode({ 42 | code, 43 | codeVerifier, 44 | redirectURI, 45 | options, 46 | tokenEndpoint: "https://accounts.spotify.com/api/token", 47 | }); 48 | }, 49 | refreshAccessToken: options.refreshAccessToken 50 | ? options.refreshAccessToken 51 | : async (refreshToken) => { 52 | return refreshAccessToken({ 53 | refreshToken, 54 | options: { 55 | clientId: options.clientId, 56 | clientKey: options.clientKey, 57 | clientSecret: options.clientSecret, 58 | }, 59 | tokenEndpoint: "https://accounts.spotify.com/api/token", 60 | }); 61 | }, 62 | async getUserInfo(token) { 63 | if (options.getUserInfo) { 64 | return options.getUserInfo(token); 65 | } 66 | const { data: profile, error } = await betterFetch<SpotifyProfile>( 67 | "https://api.spotify.com/v1/me", 68 | { 69 | method: "GET", 70 | headers: { 71 | Authorization: `Bearer ${token.accessToken}`, 72 | }, 73 | }, 74 | ); 75 | if (error) { 76 | return null; 77 | } 78 | const userMap = await options.mapProfileToUser?.(profile); 79 | return { 80 | user: { 81 | id: profile.id, 82 | name: profile.display_name, 83 | email: profile.email, 84 | image: profile.images[0]?.url, 85 | emailVerified: false, 86 | ...userMap, 87 | }, 88 | data: profile, 89 | }; 90 | }, 91 | options, 92 | } satisfies OAuthProvider<SpotifyProfile>; 93 | }; 94 | ``` -------------------------------------------------------------------------------- /docs/app/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Section from "@/components/landing/section"; 2 | import Hero from "@/components/landing/hero"; 3 | import Features from "@/components/features"; 4 | import Link from "next/link"; 5 | 6 | async function getGitHubStars() { 7 | try { 8 | const response = await fetch( 9 | "https://api.github.com/repos/better-auth/better-auth", 10 | { 11 | next: { 12 | revalidate: 60, 13 | }, 14 | }, 15 | ); 16 | if (!response?.ok) { 17 | return null; 18 | } 19 | const json = await response.json(); 20 | const stars = parseInt(json.stargazers_count).toLocaleString(); 21 | return stars; 22 | } catch { 23 | return null; 24 | } 25 | } 26 | 27 | export default async function HomePage() { 28 | const stars = await getGitHubStars(); 29 | return ( 30 | <main className="h-min mx-auto overflow-x-hidden"> 31 | <div className="w-full bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-950 dark:via-black dark:to-zinc-950 border-b border-dashed border-zinc-200 dark:border-zinc-800"> 32 | <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 33 | <div className="w-full h-full"> 34 | <div className="flex flex-col md:flex-row items-center justify-center h-12"> 35 | <span className="font-medium flex gap-2 text-sm text-zinc-700 dark:text-zinc-300"> 36 | <span className=" text-zinc-900 dark:text-white/90 hover:text-zinc-950 text-xs md:text-sm dark:hover:text-zinc-100 transition-colors"> 37 | Introducing{" "} 38 | <span className="font-semibold"> 39 | Better Auth Infrastructure 40 | </span> 41 | </span> 42 | <span className=" text-zinc-400 hidden md:block">|</span> 43 | <Link 44 | href="https://better-auth.build" 45 | className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 hidden dark:hover:text-blue-300 transition-colors md:block" 46 | > 47 | Join the waitlist → 48 | </Link> 49 | </span> 50 | <Link 51 | href="https://better-auth.build" 52 | className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 text-xs dark:hover:text-blue-300 transition-colors md:hidden" 53 | > 54 | Join the waitlist → 55 | </Link> 56 | </div> 57 | </div> 58 | </div> 59 | </div> 60 | <Section 61 | className="mb-1 overflow-y-clip" 62 | crosses 63 | crossesOffset="lg:translate-y-[5.25rem]" 64 | customPaddings 65 | id="hero" 66 | > 67 | <Hero /> 68 | <Features stars={stars} /> 69 | <hr className="h-px bg-gray-200" /> 70 | </Section> 71 | </main> 72 | ); 73 | } 74 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:latest 6 | container_name: mongodb 7 | ports: 8 | - "27017:27017" 9 | volumes: 10 | - mongodb_data:/data/db 11 | 12 | # drizzle 13 | postgres: 14 | image: postgres:latest 15 | container_name: postgres 16 | environment: 17 | POSTGRES_USER: user 18 | POSTGRES_PASSWORD: password 19 | POSTGRES_DB: better_auth 20 | ports: 21 | - "5432:5432" 22 | volumes: 23 | - postgres_data:/var/lib/postgresql/data 24 | 25 | postgres-kysely: 26 | image: postgres:latest 27 | container_name: postgres-kysely 28 | environment: 29 | POSTGRES_USER: user 30 | POSTGRES_PASSWORD: password 31 | POSTGRES_DB: better_auth 32 | ports: 33 | - "5433:5432" 34 | volumes: 35 | - postgres-kysely_data:/var/lib/postgresql/data 36 | 37 | postgres-prisma: 38 | image: postgres:latest 39 | container_name: postgres-prisma 40 | environment: 41 | POSTGRES_USER: user 42 | POSTGRES_PASSWORD: password 43 | POSTGRES_DB: better_auth 44 | ports: 45 | - "5434:5432" 46 | volumes: 47 | - postgres-prisma_data:/var/lib/postgresql/data 48 | 49 | # Drizzle tests 50 | mysql: 51 | image: mysql:latest 52 | container_name: mysql 53 | environment: 54 | MYSQL_ROOT_PASSWORD: root_password 55 | MYSQL_DATABASE: better_auth 56 | MYSQL_USER: user 57 | MYSQL_PASSWORD: password 58 | ports: 59 | - "3306:3306" 60 | volumes: 61 | - mysql_data:/var/lib/mysql 62 | 63 | 64 | mysql-kysely: 65 | image: mysql:latest 66 | container_name: mysql-kysely 67 | environment: 68 | MYSQL_ROOT_PASSWORD: root_password 69 | MYSQL_DATABASE: better_auth 70 | MYSQL_USER: user 71 | MYSQL_PASSWORD: password 72 | ports: 73 | - "3307:3306" 74 | volumes: 75 | - mysql-kysely_data:/var/lib/mysql 76 | 77 | mysql-prisma: 78 | image: mysql:latest 79 | container_name: mysql-prisma 80 | environment: 81 | MYSQL_ROOT_PASSWORD: root_password 82 | MYSQL_DATABASE: better_auth 83 | MYSQL_USER: user 84 | MYSQL_PASSWORD: password 85 | ports: 86 | - "3308:3306" 87 | volumes: 88 | - mysql-prisma_data:/var/lib/mysql 89 | 90 | 91 | mssql: 92 | image: mcr.microsoft.com/mssql/server:latest 93 | container_name: mssql 94 | environment: 95 | SA_PASSWORD: "Password123!" 96 | ACCEPT_EULA: "Y" 97 | ports: 98 | - "1433:1433" 99 | volumes: 100 | - mssql_data:/var/opt/mssql 101 | 102 | volumes: 103 | mongodb_data: 104 | postgres_data: 105 | postgres-kysely_data: 106 | postgres-prisma_data: 107 | mysql_data: 108 | mssql_data: 109 | mysql-kysely_data: 110 | mysql-prisma_data: ``` -------------------------------------------------------------------------------- /packages/telemetry/src/utils/package-json.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { PackageJson } from "type-fest"; 2 | let packageJSONCache: PackageJson | undefined; 3 | 4 | async function readRootPackageJson() { 5 | if (packageJSONCache) return packageJSONCache; 6 | try { 7 | const cwd = 8 | typeof process !== "undefined" && typeof process.cwd === "function" 9 | ? process.cwd() 10 | : ""; 11 | if (!cwd) return undefined; 12 | // Lazily import Node built-ins only when available (Node/Bun/Deno) and 13 | // avoid static analyzer/bundler resolution by obfuscating module names 14 | const importRuntime = (m: string) => 15 | (Function("mm", "return import(mm)") as any)(m); 16 | const [{ default: fs }, { default: path }] = await Promise.all([ 17 | importRuntime("fs/promises"), 18 | importRuntime("path"), 19 | ]); 20 | const raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8"); 21 | packageJSONCache = JSON.parse(raw); 22 | return packageJSONCache as PackageJson; 23 | } catch {} 24 | return undefined; 25 | } 26 | 27 | export async function getPackageVersion(pkg: string) { 28 | if (packageJSONCache) { 29 | return (packageJSONCache.dependencies?.[pkg] || 30 | packageJSONCache.devDependencies?.[pkg] || 31 | packageJSONCache.peerDependencies?.[pkg]) as string | undefined; 32 | } 33 | 34 | try { 35 | const cwd = 36 | typeof process !== "undefined" && typeof process.cwd === "function" 37 | ? process.cwd() 38 | : ""; 39 | if (!cwd) throw new Error("no-cwd"); 40 | const importRuntime = (m: string) => 41 | (Function("mm", "return import(mm)") as any)(m); 42 | const [{ default: fs }, { default: path }] = await Promise.all([ 43 | importRuntime("fs/promises"), 44 | importRuntime("path"), 45 | ]); 46 | const pkgJsonPath = path.join(cwd, "node_modules", pkg, "package.json"); 47 | const raw = await fs.readFile(pkgJsonPath, "utf-8"); 48 | const json = JSON.parse(raw); 49 | const resolved = 50 | (json.version as string) || 51 | (await getVersionFromLocalPackageJson(pkg)) || 52 | undefined; 53 | return resolved; 54 | } catch {} 55 | 56 | const fromRoot = await getVersionFromLocalPackageJson(pkg); 57 | return fromRoot; 58 | } 59 | 60 | async function getVersionFromLocalPackageJson(pkg: string) { 61 | const json = await readRootPackageJson(); 62 | if (!json) return undefined; 63 | const allDeps = { 64 | ...json.dependencies, 65 | ...json.devDependencies, 66 | ...json.peerDependencies, 67 | } as Record<string, string | undefined>; 68 | return allDeps[pkg]; 69 | } 70 | 71 | export async function getNameFromLocalPackageJson() { 72 | const json = await readRootPackageJson(); 73 | return json?.name as string | undefined; 74 | } 75 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/vanilla.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getClientConfig } from "./config"; 2 | import { capitalizeFirstLetter } from "../utils/misc"; 3 | import type { 4 | InferActions, 5 | InferClientAPI, 6 | InferErrorCodes, 7 | IsSignal, 8 | } from "./types"; 9 | import type { 10 | BetterAuthClientPlugin, 11 | BetterAuthClientOptions, 12 | } from "@better-auth/core"; 13 | import { createDynamicPathProxy } from "./proxy"; 14 | import type { PrettifyDeep, UnionToIntersection } from "../types/helper"; 15 | import type { Atom } from "nanostores"; 16 | import type { 17 | BetterFetchError, 18 | BetterFetchResponse, 19 | } from "@better-fetch/fetch"; 20 | import type { BASE_ERROR_CODES } from "@better-auth/core/error"; 21 | 22 | type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { 23 | plugins: Array<infer Plugin>; 24 | } 25 | ? UnionToIntersection< 26 | Plugin extends BetterAuthClientPlugin 27 | ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms 28 | ? Atoms extends Record<string, any> 29 | ? { 30 | [key in keyof Atoms as IsSignal<key> extends true 31 | ? never 32 | : key extends string 33 | ? `use${Capitalize<key>}` 34 | : never]: Atoms[key]; 35 | } 36 | : {} 37 | : {} 38 | : {} 39 | > 40 | : {}; 41 | 42 | export function createAuthClient<Option extends BetterAuthClientOptions>( 43 | options?: Option, 44 | ) { 45 | const { 46 | pluginPathMethods, 47 | pluginsActions, 48 | pluginsAtoms, 49 | $fetch, 50 | atomListeners, 51 | $store, 52 | } = getClientConfig(options); 53 | let resolvedHooks: Record<string, any> = {}; 54 | for (const [key, value] of Object.entries(pluginsAtoms)) { 55 | resolvedHooks[`use${capitalizeFirstLetter(key)}`] = value; 56 | } 57 | const routes = { 58 | ...pluginsActions, 59 | ...resolvedHooks, 60 | $fetch, 61 | $store, 62 | }; 63 | const proxy = createDynamicPathProxy( 64 | routes, 65 | $fetch, 66 | pluginPathMethods, 67 | pluginsAtoms, 68 | atomListeners, 69 | ); 70 | type ClientAPI = InferClientAPI<Option>; 71 | type Session = ClientAPI extends { 72 | getSession: () => Promise<infer Res>; 73 | } 74 | ? Res extends BetterFetchResponse<infer S> 75 | ? S 76 | : Res extends Record<string, any> 77 | ? Res 78 | : never 79 | : never; 80 | return proxy as UnionToIntersection<InferResolvedHooks<Option>> & 81 | ClientAPI & 82 | InferActions<Option> & { 83 | useSession: Atom<{ 84 | data: Session; 85 | error: BetterFetchError | null; 86 | isPending: boolean; 87 | }>; 88 | $fetch: typeof $fetch; 89 | $store: typeof $store; 90 | $Infer: { 91 | Session: NonNullable<Session>; 92 | }; 93 | $ERROR_CODES: PrettifyDeep< 94 | InferErrorCodes<Option> & typeof BASE_ERROR_CODES 95 | >; 96 | }; 97 | } 98 | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/client-credentials-token.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { base64Url } from "@better-auth/utils/base64"; 3 | import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider"; 4 | 5 | export function createClientCredentialsTokenRequest({ 6 | options, 7 | scope, 8 | authentication, 9 | resource, 10 | }: { 11 | options: ProviderOptions & { clientSecret: string }; 12 | scope?: string; 13 | authentication?: "basic" | "post"; 14 | resource?: string | string[]; 15 | }) { 16 | const body = new URLSearchParams(); 17 | const headers: Record<string, any> = { 18 | "content-type": "application/x-www-form-urlencoded", 19 | accept: "application/json", 20 | }; 21 | 22 | body.set("grant_type", "client_credentials"); 23 | scope && body.set("scope", scope); 24 | if (resource) { 25 | if (typeof resource === "string") { 26 | body.append("resource", resource); 27 | } else { 28 | for (const _resource of resource) { 29 | body.append("resource", _resource); 30 | } 31 | } 32 | } 33 | if (authentication === "basic") { 34 | const primaryClientId = Array.isArray(options.clientId) 35 | ? options.clientId[0] 36 | : options.clientId; 37 | const encodedCredentials = base64Url.encode( 38 | `${primaryClientId}:${options.clientSecret}`, 39 | ); 40 | headers["authorization"] = `Basic ${encodedCredentials}`; 41 | } else { 42 | const primaryClientId = Array.isArray(options.clientId) 43 | ? options.clientId[0] 44 | : options.clientId; 45 | body.set("client_id", primaryClientId); 46 | body.set("client_secret", options.clientSecret); 47 | } 48 | 49 | return { 50 | body, 51 | headers, 52 | }; 53 | } 54 | 55 | export async function clientCredentialsToken({ 56 | options, 57 | tokenEndpoint, 58 | scope, 59 | authentication, 60 | resource, 61 | }: { 62 | options: ProviderOptions & { clientSecret: string }; 63 | tokenEndpoint: string; 64 | scope: string; 65 | authentication?: "basic" | "post"; 66 | resource?: string | string[]; 67 | }): Promise<OAuth2Tokens> { 68 | const { body, headers } = createClientCredentialsTokenRequest({ 69 | options, 70 | scope, 71 | authentication, 72 | resource, 73 | }); 74 | 75 | const { data, error } = await betterFetch<{ 76 | access_token: string; 77 | expires_in?: number; 78 | token_type?: string; 79 | scope?: string; 80 | }>(tokenEndpoint, { 81 | method: "POST", 82 | body, 83 | headers, 84 | }); 85 | if (error) { 86 | throw error; 87 | } 88 | const tokens: OAuth2Tokens = { 89 | accessToken: data.access_token, 90 | tokenType: data.token_type, 91 | scopes: data.scope?.split(" "), 92 | }; 93 | 94 | if (data.expires_in) { 95 | const now = new Date(); 96 | tokens.accessTokenExpiresAt = new Date( 97 | now.getTime() + data.expires_in * 1000, 98 | ); 99 | } 100 | 101 | return tokens; 102 | } 103 | ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/icons.tsx: -------------------------------------------------------------------------------- ```typescript 1 | export function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { 2 | return ( 3 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 4 | <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> 5 | </svg> 6 | ); 7 | } 8 | 9 | export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { 10 | return ( 11 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 12 | <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> 13 | </svg> 14 | ); 15 | } 16 | 17 | export function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { 18 | return ( 19 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 20 | <path 21 | fillRule="evenodd" 22 | clipRule="evenodd" 23 | d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" 24 | /> 25 | </svg> 26 | ); 27 | } 28 | 29 | export function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { 30 | return ( 31 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 32 | <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> 33 | </svg> 34 | ); 35 | } 36 | ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/icons.tsx: -------------------------------------------------------------------------------- ```typescript 1 | export function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { 2 | return ( 3 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 4 | <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> 5 | </svg> 6 | ); 7 | } 8 | 9 | export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { 10 | return ( 11 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 12 | <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> 13 | </svg> 14 | ); 15 | } 16 | 17 | export function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { 18 | return ( 19 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 20 | <path 21 | fillRule="evenodd" 22 | clipRule="evenodd" 23 | d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" 24 | /> 25 | </svg> 26 | ); 27 | } 28 | 29 | export function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { 30 | return ( 31 | <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> 32 | <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> 33 | </svg> 34 | ); 35 | } 36 | ``` -------------------------------------------------------------------------------- /docs/components/ui/calendar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { DayPicker } from "react-day-picker"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | function Calendar({ 10 | className, 11 | classNames, 12 | showOutsideDays = true, 13 | ...props 14 | }: React.ComponentProps<typeof DayPicker>) { 15 | return ( 16 | <DayPicker 17 | showOutsideDays={showOutsideDays} 18 | className={cn("p-3", className)} 19 | classNames={{ 20 | months: "flex flex-col sm:flex-row gap-2", 21 | month: "flex flex-col gap-4", 22 | caption: "flex justify-center pt-1 relative items-center w-full", 23 | caption_label: "text-sm font-medium", 24 | nav: "flex items-center gap-1", 25 | nav_button: cn( 26 | buttonVariants({ variant: "outline" }), 27 | "size-7 bg-transparent p-0 opacity-50 hover:opacity-100", 28 | ), 29 | nav_button_previous: "absolute left-1", 30 | nav_button_next: "absolute right-1", 31 | table: "w-full border-collapse space-x-1", 32 | head_row: "flex", 33 | head_cell: 34 | "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 35 | row: "flex w-full mt-2", 36 | cell: cn( 37 | "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md", 38 | props.mode === "range" 39 | ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 40 | : "[&:has([aria-selected])]:rounded-md", 41 | ), 42 | day: cn( 43 | buttonVariants({ variant: "ghost" }), 44 | "size-8 p-0 font-normal aria-selected:opacity-100", 45 | ), 46 | day_range_start: 47 | "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground", 48 | day_range_end: 49 | "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground", 50 | day_selected: 51 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 52 | day_today: "bg-accent text-accent-foreground", 53 | day_outside: 54 | "day-outside text-muted-foreground aria-selected:text-muted-foreground", 55 | day_disabled: "text-muted-foreground opacity-50", 56 | day_range_middle: 57 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 58 | day_hidden: "invisible", 59 | ...classNames, 60 | }} 61 | {...props} 62 | /> 63 | ); 64 | } 65 | 66 | export { Calendar }; 67 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPlugin } from "@better-auth/core"; 2 | import type { CaptchaOptions } from "./types"; 3 | import { defaultEndpoints, Providers, siteVerifyMap } from "./constants"; 4 | import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "./error-codes"; 5 | import { middlewareResponse } from "../../utils/middleware-response"; 6 | import * as verifyHandlers from "./verify-handlers"; 7 | 8 | export const captcha = (options: CaptchaOptions) => 9 | ({ 10 | id: "captcha", 11 | onRequest: async (request, ctx) => { 12 | try { 13 | const endpoints = options.endpoints?.length 14 | ? options.endpoints 15 | : defaultEndpoints; 16 | 17 | if (!endpoints.some((endpoint) => request.url.includes(endpoint))) 18 | return undefined; 19 | 20 | if (!options.secretKey) { 21 | throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY); 22 | } 23 | 24 | const captchaResponse = request.headers.get("x-captcha-response"); 25 | const remoteUserIP = 26 | request.headers.get("x-captcha-user-remote-ip") ?? undefined; 27 | 28 | if (!captchaResponse) { 29 | return middlewareResponse({ 30 | message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE, 31 | status: 400, 32 | }); 33 | } 34 | 35 | const siteVerifyURL = 36 | options.siteVerifyURLOverride || siteVerifyMap[options.provider]; 37 | 38 | const handlerParams = { 39 | siteVerifyURL, 40 | captchaResponse, 41 | secretKey: options.secretKey, 42 | remoteIP: remoteUserIP, 43 | }; 44 | 45 | if (options.provider === Providers.CLOUDFLARE_TURNSTILE) { 46 | return await verifyHandlers.cloudflareTurnstile(handlerParams); 47 | } 48 | 49 | if (options.provider === Providers.GOOGLE_RECAPTCHA) { 50 | return await verifyHandlers.googleRecaptcha({ 51 | ...handlerParams, 52 | minScore: options.minScore, 53 | }); 54 | } 55 | 56 | if (options.provider === Providers.HCAPTCHA) { 57 | return await verifyHandlers.hCaptcha({ 58 | ...handlerParams, 59 | siteKey: options.siteKey, 60 | }); 61 | } 62 | 63 | if (options.provider === Providers.CAPTCHAFOX) { 64 | return await verifyHandlers.captchaFox({ 65 | ...handlerParams, 66 | siteKey: options.siteKey, 67 | }); 68 | } 69 | } catch (_error) { 70 | const errorMessage = 71 | _error instanceof Error ? _error.message : undefined; 72 | 73 | ctx.logger.error(errorMessage ?? "Unknown error", { 74 | endpoint: request.url, 75 | message: _error, 76 | }); 77 | 78 | return middlewareResponse({ 79 | message: EXTERNAL_ERROR_CODES.UNKNOWN_ERROR, 80 | status: 500, 81 | }); 82 | } 83 | }, 84 | }) satisfies BetterAuthPlugin; 85 | ``` -------------------------------------------------------------------------------- /docs/components/api-method-tabs.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const provider = React.createContext<{ 8 | current: string | null; 9 | setCurrent: (value: string | null) => void; 10 | }>({ 11 | current: null, 12 | setCurrent: () => {}, 13 | }); 14 | 15 | function ApiMethodTabs({ 16 | className, 17 | ...props 18 | }: React.ComponentProps<"div"> & { defaultValue: string | null }) { 19 | const [current, setCurrent] = React.useState<string | null>( 20 | props.defaultValue || null, 21 | ); 22 | return ( 23 | <provider.Provider value={{ current, setCurrent }}> 24 | <div 25 | data-slot="tabs" 26 | className={cn("flex flex-col gap-2", className)} 27 | {...props} 28 | /> 29 | </provider.Provider> 30 | ); 31 | } 32 | 33 | const useApiMethodTabs = () => { 34 | return React.useContext(provider); 35 | }; 36 | 37 | function ApiMethodTabsList({ 38 | className, 39 | ...props 40 | }: React.ComponentProps<"div">) { 41 | return ( 42 | <div 43 | data-slot="tabs-list" 44 | className={cn( 45 | "inline-flex justify-center items-center p-1 h-9 rounded-lg bg-muted text-muted-foreground w-fit", 46 | className, 47 | )} 48 | {...props} 49 | /> 50 | ); 51 | } 52 | 53 | function ApiMethodTabsTrigger({ 54 | className, 55 | ...props 56 | }: React.ComponentProps<"button"> & { value: string }) { 57 | const { setCurrent, current } = useApiMethodTabs(); 58 | return ( 59 | <button 60 | data-slot="tabs-trigger" 61 | className={cn( 62 | "data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 63 | className, 64 | )} 65 | data-state={props.value === current ? "active" : "inactive"} 66 | onClick={() => { 67 | setCurrent(props.value); 68 | }} 69 | {...props} 70 | /> 71 | ); 72 | } 73 | 74 | function ApiMethodTabsContent({ 75 | className, 76 | ...props 77 | }: React.ComponentProps<"div"> & { value: string }) { 78 | const { current } = useApiMethodTabs(); 79 | return ( 80 | <div 81 | data-slot="tabs-content" 82 | className={cn( 83 | "flex-1 outline-none", 84 | className, 85 | props.value === current && "block", 86 | props.value !== current && "hidden", 87 | )} 88 | {...props} 89 | /> 90 | ); 91 | } 92 | 93 | export { 94 | ApiMethodTabs, 95 | ApiMethodTabsList, 96 | ApiMethodTabsTrigger, 97 | ApiMethodTabsContent, 98 | }; 99 | ``` -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { Navbar } from "@/components/nav-bar"; 2 | import "./global.css"; 3 | import { RootProvider } from "fumadocs-ui/provider"; 4 | import type { ReactNode } from "react"; 5 | import { NavbarProvider } from "@/components/nav-mobile"; 6 | import { GeistMono } from "geist/font/mono"; 7 | import { GeistSans } from "geist/font/sans"; 8 | import { baseUrl, createMetadata } from "@/lib/metadata"; 9 | import { Analytics } from "@vercel/analytics/react"; 10 | import { ThemeProvider } from "@/components/theme-provider"; 11 | import { Toaster } from "@/components/ui/sonner"; 12 | import { CustomSearchDialog } from "@/components/search-dialog"; 13 | import { AnchorScroll } from "@/components/anchor-scroll-fix"; 14 | 15 | export const metadata = createMetadata({ 16 | title: { 17 | template: "%s | Better Auth", 18 | default: "Better Auth", 19 | }, 20 | description: "The most comprehensive authentication library for TypeScript.", 21 | metadataBase: baseUrl, 22 | }); 23 | 24 | export default function Layout({ children }: { children: ReactNode }) { 25 | return ( 26 | <html lang="en" suppressHydrationWarning> 27 | <head> 28 | <link rel="icon" href="/favicon/favicon.ico" sizes="any" /> 29 | <script 30 | dangerouslySetInnerHTML={{ 31 | __html: ` 32 | try { 33 | if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 34 | document.querySelector('meta[name="theme-color"]').setAttribute('content') 35 | } 36 | } catch (_) {} 37 | `, 38 | }} 39 | /> 40 | </head> 41 | <body 42 | className={`${GeistSans.variable} ${GeistMono.variable} bg-background font-sans relative `} 43 | > 44 | <ThemeProvider 45 | attribute="class" 46 | defaultTheme="dark" 47 | enableSystem 48 | disableTransitionOnChange 49 | > 50 | <RootProvider 51 | theme={{ 52 | enableSystem: true, 53 | defaultTheme: "dark", 54 | }} 55 | search={{ 56 | enabled: true, 57 | SearchDialog: process.env.ORAMA_PRIVATE_API_KEY 58 | ? CustomSearchDialog 59 | : undefined, 60 | }} 61 | > 62 | <AnchorScroll /> 63 | <NavbarProvider> 64 | <Navbar /> 65 | {children} 66 | <Toaster 67 | toastOptions={{ 68 | style: { 69 | borderRadius: "0px", 70 | fontSize: "11px", 71 | }, 72 | }} 73 | /> 74 | </NavbarProvider> 75 | </RootProvider> 76 | <Analytics /> 77 | </ThemeProvider> 78 | </body> 79 | </html> 80 | ); 81 | } 82 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/next-js.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPlugin } from "@better-auth/core"; 2 | import { parseSetCookieHeader } from "../cookies"; 3 | import { createAuthMiddleware } from "@better-auth/core/api"; 4 | 5 | export function toNextJsHandler( 6 | auth: 7 | | { 8 | handler: (request: Request) => Promise<Response>; 9 | } 10 | | ((request: Request) => Promise<Response>), 11 | ) { 12 | const handler = async (request: Request) => { 13 | return "handler" in auth ? auth.handler(request) : auth(request); 14 | }; 15 | return { 16 | GET: handler, 17 | POST: handler, 18 | }; 19 | } 20 | 21 | export const nextCookies = () => { 22 | return { 23 | id: "next-cookies", 24 | hooks: { 25 | after: [ 26 | { 27 | matcher(ctx) { 28 | return true; 29 | }, 30 | handler: createAuthMiddleware(async (ctx) => { 31 | const returned = ctx.context.responseHeaders; 32 | if ("_flag" in ctx && ctx._flag === "router") { 33 | return; 34 | } 35 | if (returned instanceof Headers) { 36 | const setCookies = returned?.get("set-cookie"); 37 | if (!setCookies) return; 38 | const parsed = parseSetCookieHeader(setCookies); 39 | const { cookies } = await import("next/headers"); 40 | let cookieHelper: Awaited<ReturnType<typeof cookies>>; 41 | try { 42 | cookieHelper = await cookies(); 43 | } catch (error) { 44 | if ( 45 | error instanceof Error && 46 | error.message.startsWith( 47 | "`cookies` was called outside a request scope.", 48 | ) 49 | ) { 50 | // If error it means the `cookies` was called outside request scope. 51 | // NextJS docs on this: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context 52 | // This often gets called in a monorepo workspace (outside of NextJS), 53 | // so we will try to catch this suppress it, and ignore using next-cookies. 54 | return; 55 | } 56 | // If it's an unexpected error, throw it. 57 | throw error; 58 | } 59 | parsed.forEach((value, key) => { 60 | if (!key) return; 61 | const opts = { 62 | sameSite: value.samesite, 63 | secure: value.secure, 64 | maxAge: value["max-age"], 65 | httpOnly: value.httponly, 66 | domain: value.domain, 67 | path: value.path, 68 | } as const; 69 | try { 70 | cookieHelper.set(key, decodeURIComponent(value.value), opts); 71 | } catch (e) { 72 | // this will fail if the cookie is being set on server component 73 | } 74 | }); 75 | return; 76 | } 77 | }), 78 | }, 79 | ], 80 | }, 81 | } satisfies BetterAuthPlugin; 82 | }; 83 | ```