This is page 10 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/content/docs/plugins/bearer.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Bearer Token Authentication description: Authenticate API requests using Bearer tokens instead of browser cookies --- The Bearer plugin enables authentication using Bearer tokens as an alternative to browser cookies. It intercepts requests, adding the Bearer token to the Authorization header before forwarding them to your API. <Callout type="warn"> Use this cautiously; it is intended only for APIs that don't support cookies or require Bearer tokens for authentication. Improper implementation could easily lead to security vulnerabilities. </Callout> ## Installing the Bearer Plugin Add the Bearer plugin to your authentication setup: ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { bearer } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [bearer()] }); ``` ## How to Use Bearer Tokens ### 1. Obtain the Bearer Token After a successful sign-in, you'll receive a session token in the response headers. Store this token securely (e.g., in `localStorage`): ```ts title="auth-client.ts" const { data } = await authClient.signIn.email({ email: "[email protected]", password: "securepassword" }, { onSuccess: (ctx)=>{ const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers // Store the token securely (e.g., in localStorage) localStorage.setItem("bearer_token", authToken); } }); ``` You can also set this up globally in your auth client: ```ts title="auth-client.ts" export const authClient = createAuthClient({ fetchOptions: { onSuccess: (ctx) => { const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers // Store the token securely (e.g., in localStorage) if(authToken){ localStorage.setItem("bearer_token", authToken); } } } }); ``` You may want to clear the token based on the response status code or other conditions: ### 2. Configure the Auth Client Set up your auth client to include the Bearer token in all requests: ```ts title="auth-client.ts" export const authClient = createAuthClient({ fetchOptions: { auth: { type:"Bearer", token: () => localStorage.getItem("bearer_token") || "" // get the token from localStorage } } }); ``` ### 3. Make Authenticated Requests Now you can make authenticated API calls: ```ts title="auth-client.ts" // This request is automatically authenticated const { data } = await authClient.listSessions(); ``` ### 4. Per-Request Token (Optional) You can also provide the token for individual requests: ```ts title="auth-client.ts" const { data } = await authClient.listSessions({ fetchOptions: { headers: { Authorization: `Bearer ${token}` } } }); ``` ### 5. Using Bearer Tokens Outside the Auth Client The Bearer token can be used to authenticate any request to your API, even when not using the auth client: ```ts title="api-call.ts" const token = localStorage.getItem("bearer_token"); const response = await fetch("https://api.example.com/data", { headers: { Authorization: `Bearer ${token}` } }); const data = await response.json(); ``` And in the server, you can use the `auth.api.getSession` function to authenticate requests: ```ts title="server.ts" import { auth } from "@/auth"; export async function handler(req, res) { const session = await auth.api.getSession({ headers: req.headers }); if (!session) { return res.status(401).json({ error: "Unauthorized" }); } // Process authenticated request // ... } ``` ## Options **requireSignature** (boolean): Require the token to be signed. Default: `false`. ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/gitlab.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, validateAuthorizationCode, refreshAccessToken, } from "../oauth2"; export interface GitlabProfile extends Record<string, any> { id: number; username: string; email: string; name: string; state: string; avatar_url: string; web_url: string; created_at: string; bio: string; location?: string; public_email: string; skype: string; linkedin: string; twitter: string; website_url: string; organization: string; job_title: string; pronouns: string; bot: boolean; work_information?: string; followers: number; following: number; local_time: string; last_sign_in_at: string; confirmed_at: string; theme_id: number; last_activity_on: string; color_scheme_id: number; projects_limit: number; current_sign_in_at: string; identities: Array<{ provider: string; extern_uid: string; }>; can_create_group: boolean; can_create_project: boolean; two_factor_enabled: boolean; external: boolean; private_profile: boolean; commit_email: string; shared_runners_minutes_limit: number; extra_shared_runners_minutes_limit: number; } export interface GitlabOptions extends ProviderOptions<GitlabProfile> { clientId: string; issuer?: string; } const cleanDoubleSlashes = (input: string = "") => { return input .split("://") .map((str) => str.replace(/\/{2,}/g, "/")) .join("://"); }; const issuerToEndpoints = (issuer?: string) => { let baseUrl = issuer || "https://gitlab.com"; return { authorizationEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/authorize`), tokenEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/token`), userinfoEndpoint: cleanDoubleSlashes(`${baseUrl}/api/v4/user`), }; }; export const gitlab = (options: GitlabOptions) => { const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } = issuerToEndpoints(options.issuer); const issuerId = "gitlab"; const issuerName = "Gitlab"; return { id: issuerId, name: issuerName, createAuthorizationURL: async ({ state, scopes, codeVerifier, loginHint, redirectURI, }) => { const _scopes = options.disableDefaultScope ? [] : ["read_user"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await createAuthorizationURL({ id: issuerId, options, authorizationEndpoint, scopes: _scopes, state, redirectURI, codeVerifier, loginHint, }); }, validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => { return validateAuthorizationCode({ code, redirectURI, options, codeVerifier, tokenEndpoint, }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint: tokenEndpoint, }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<GitlabProfile>( userinfoEndpoint, { headers: { authorization: `Bearer ${token.accessToken}` } }, ); if (error || profile.state !== "active" || profile.locked) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.name ?? profile.username, email: profile.email, image: profile.avatar_url, emailVerified: true, ...userMap, }, data: profile, }; }, options, } satisfies OAuthProvider<GitlabProfile>; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/drizzle.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Drizzle ORM Adapter description: Integrate Better Auth with Drizzle ORM. --- Drizzle ORM is a powerful and flexible ORM for Node.js and TypeScript. It provides a simple and intuitive API for working with databases, and supports a wide range of databases including MySQL, PostgreSQL, SQLite, and more. Read more here: [Drizzle ORM](https://orm.drizzle.team/). ## Example Usage Make sure you have Drizzle installed and configured. Then, you can use the Drizzle adapter to connect to your database. ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "./database.ts"; export const auth = betterAuth({ database: drizzleAdapter(db, { // [!code highlight] provider: "sqlite", // or "pg" or "mysql" // [!code highlight] }), // [!code highlight] //... the rest of your config }); ``` ## Schema generation & migration The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate your database schema based on your Better Auth configuration and plugins. To generate the schema required by Better Auth, run the following command: ```bash title="Schema Generation" npx @better-auth/cli@latest generate ``` To generate and apply the migration, run the following commands: ```bash title="Schema Migration" npx drizzle-kit generate # generate the migration file npx drizzle-kit migrate # apply the migration ``` ## Modifying Table Names The Drizzle adapter expects the schema you define to match the table names. For example, if your Drizzle schema maps the `user` table to `users`, you need to manually pass the schema and map it to the user table. ```ts import { betterAuth } from "better-auth"; import { db } from "./drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { schema } from "./schema"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", // or "pg" or "mysql" schema: { ...schema, user: schema.users, }, }), }); ``` You can either modify the provided schema values like the example above, or you can mutate the auth config's `modelName` property directly. For example: ```ts export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", // or "pg" or "mysql" schema, }), user: { modelName: "users", // [!code highlight] } }); ``` ## Modifying Field Names We map field names based on property you passed to your Drizzle schema. For example, if you want to modify the `email` field to `email_address`, you simply need to change the Drizzle schema to: ```ts export const user = mysqlTable("user", { // Changed field name without changing the schema property name // This allows drizzle & better-auth to still use the original field name, // while your DB uses the modified field name email: varchar("email_address", { length: 255 }).notNull().unique(), // [!code highlight] // ... others }); ``` You can either modify the Drizzle schema like the example above, or you can mutate the auth config's `fields` property directly. For example: ```ts export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite", // or "pg" or "mysql" schema, }), user: { fields: { email: "email_address", // [!code highlight] } } }); ``` ## Using Plural Table Names If all your tables are using plural form, you can just pass the `usePlural` option: ```ts export const auth = betterAuth({ database: drizzleAdapter(db, { ... usePlural: true, }), }); ``` ## Performance Tips 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>. ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/multi-session/multi-session.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { multiSession } from "."; import { multiSessionClient } from "./client"; import { parseSetCookieHeader } from "../../cookies"; describe("multi-session", async () => { const { client, testUser, cookieSetter } = await getTestInstance( { plugins: [ multiSession({ maximumSessions: 2, }), ], }, { clientOptions: { plugins: [multiSessionClient()], }, }, ); let headers = new Headers(); const testUser2 = { email: "[email protected]", password: "password", name: "Name", }; it("should set multi session when there is set-cookie header", async () => { await client.signIn.email( { email: testUser.email, password: testUser.password, }, { onResponse(context) { const setCookieString = context.response.headers.get("set-cookie"); const setCookies = parseSetCookieHeader(setCookieString || ""); const sessionToken = setCookies .get("better-auth.session_token") ?.value.split(".")[0]; const multiSession = setCookies.get( `better-auth.session_token_multi-${sessionToken?.toLowerCase()}`, )?.value; expect(sessionToken).not.toBe(null); expect(multiSession).not.toBe(null); expect(multiSession).toContain(sessionToken); expect(setCookieString).toContain("better-auth.session_token_multi-"); }, onSuccess: cookieSetter(headers), }, ); await client.signUp.email(testUser2, { onSuccess: cookieSetter(headers), }); }); it("should get active session", async () => { const session = await client.getSession({ fetchOptions: { headers, }, }); expect(session.data?.user.email).toBe(testUser2.email); }); let sessionToken = ""; it("should list all device sessions", async () => { const res = await client.multiSession.listDeviceSessions({ fetchOptions: { headers, }, }); if (res.data) { sessionToken = res.data.find((s) => s.user.email === testUser.email)?.session.token || ""; } expect(res.data).toHaveLength(2); }); it("should set active session", async () => { const res = await client.multiSession.setActive({ sessionToken, fetchOptions: { headers, }, }); expect(res.data?.user.email).toBe(testUser.email); }); it("should revoke a session and set the next active", async () => { const testUser3 = { email: "[email protected]", password: "password", name: "Name", }; let token = ""; const signUpRes = await client.signUp.email(testUser3, { onSuccess: (ctx) => { const header = ctx.response.headers.get("set-cookie"); expect(header).toContain("better-auth.session_token"); const cookies = parseSetCookieHeader(header || ""); token = cookies.get("better-auth.session_token")?.value.split(".")[0] || ""; }, }); await client.multiSession.revoke( { sessionToken: token, }, { onSuccess(context) { expect(context.response.headers.get("set-cookie")).toContain( `better-auth.session_token=`, ); }, headers, }, ); const res = await client.multiSession.listDeviceSessions({ fetchOptions: { headers, }, }); expect(res.data).toHaveLength(2); }); it("should sign-out all sessions", async () => { const newHeaders = new Headers(); await client.signOut({ fetchOptions: { headers, onSuccess: cookieSetter(newHeaders), }, }); const res = await client.multiSession.listDeviceSessions({ fetchOptions: { headers, }, }); expect(res.data).toHaveLength(0); const res2 = await client.multiSession.listDeviceSessions({ fetchOptions: { headers: newHeaders, }, }); expect(res2.data).toHaveLength(0); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/passkey/passkey.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { type Passkey, passkey } from "."; import { createAuthClient } from "../../client"; import { passkeyClient } from "./client"; describe("passkey", async () => { const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({ plugins: [passkey()], }); it("should generate register options", async () => { const { headers } = await signInWithTestUser(); const options = await auth.api.generatePasskeyRegistrationOptions({ headers: headers, }); expect(options).toBeDefined(); expect(options).toHaveProperty("challenge"); expect(options).toHaveProperty("rp"); expect(options).toHaveProperty("user"); expect(options).toHaveProperty("pubKeyCredParams"); const client = createAuthClient({ plugins: [passkeyClient()], baseURL: "http://localhost:3000/api/auth", fetchOptions: { headers: headers, customFetchImpl, }, }); await client.$fetch("/passkey/generate-register-options", { headers: headers, method: "GET", onResponse(context) { const setCookie = context.response.headers.get("Set-Cookie"); expect(setCookie).toBeDefined(); expect(setCookie).toContain("better-auth-passkey"); }, }); }); it("should generate authenticate options", async () => { const { headers } = await signInWithTestUser(); const options = await auth.api.generatePasskeyAuthenticationOptions({ headers: headers, }); expect(options).toBeDefined(); expect(options).toHaveProperty("challenge"); expect(options).toHaveProperty("rpId"); expect(options).toHaveProperty("allowCredentials"); expect(options).toHaveProperty("userVerification"); }); it("should generate authenticate options without session (discoverable credentials)", async () => { // Test without any session/auth headers - simulating a new sign-in with discoverable credentials const options = await auth.api.generatePasskeyAuthenticationOptions({}); expect(options).toBeDefined(); expect(options).toHaveProperty("challenge"); expect(options).toHaveProperty("rpId"); expect(options).toHaveProperty("userVerification"); }); it("should list user passkeys", async () => { const { headers, user } = await signInWithTestUser(); const context = await auth.$context; await context.adapter.create<Omit<Passkey, "id">, Passkey>({ model: "passkey", data: { userId: user.id, publicKey: "mockPublicKey", name: "mockName", counter: 0, deviceType: "singleDevice", credentialID: "mockCredentialID", createdAt: new Date(), backedUp: false, transports: "mockTransports", aaguid: "mockAAGUID", } satisfies Omit<Passkey, "id">, }); const passkeys = await auth.api.listPasskeys({ headers: headers, }); expect(Array.isArray(passkeys)).toBe(true); expect(passkeys[0]).toHaveProperty("id"); expect(passkeys[0]).toHaveProperty("userId"); expect(passkeys[0]).toHaveProperty("publicKey"); expect(passkeys[0]).toHaveProperty("credentialID"); expect(passkeys[0]).toHaveProperty("aaguid"); }); it("should update a passkey", async () => { const { headers } = await signInWithTestUser(); const passkeys = await auth.api.listPasskeys({ headers: headers, }); const passkey = passkeys[0]!; const updateResult = await auth.api.updatePasskey({ headers: headers, body: { id: passkey.id, name: "newName", }, }); expect(updateResult.passkey.name).toBe("newName"); }); it("should delete a passkey", async () => { const { headers } = await signInWithTestUser(); const deleteResult = await auth.api.deletePasskey({ headers: headers, body: { id: "mockPasskeyId", }, }); expect(deleteResult).toBe(null); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/community-adapters.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Community Adapters description: Integrate Better Auth with community made database adapters. --- This page showcases a list of recommended community made database adapters. We encourage you to create any missing database adapters and maybe get added to the list! | Adapter | Database Dialect | Author | | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [convex-better-auth](https://github.com/get-convex/better-auth) | [Convex Database](https://www.convex.dev/) | <img src="https://github.com/erquhart.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [erquhart](https://github.com/erquhart) | | [surreal-better-auth](https://github.com/oskar-gmerek/surreal-better-auth) | [SurrealDB](https://surrealdb.com/) | <img src="https://github.com/oskar-gmerek.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> <a href="https://oskargmerek.com" alt="Web Developer UK">Oskar Gmerek</a> | | [surrealdb-better-auth](https://github.com/Necmttn/surrealdb-better-auth) | [Surreal Database](https://surrealdb.com/) | <img src="https://github.com/Necmttn.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [Necmttn](https://github.com/Necmttn) | | [better-auth-surrealdb](https://github.com/msanchezdev/better-auth-surrealdb) | [Surreal Database](https://surrealdb.com/) | <img src="https://github.com/msanchezdev.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [msanchezdev](https://github.com/msanchezdev) | | [payload-better-auth](https://github.com/ForrestDevs/payload-better-auth/tree/main/packages/db-adapter) | [Payload CMS](https://payloadcms.com/) | <img src="https://github.com/forrestdevs.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [forrestdevs](https://github.com/forrestdevs) | | [@ronin/better-auth](https://github.com/ronin-co/better-auth) | [RONIN](https://ronin.co) | <img src="https://github.com/ronin-co.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [ronin-co](https://github.com/ronin-co) | | [better-auth-instantdb](https://github.com/daveyplate/better-auth-instantdb) | [InstantDB](https://www.instantdb.com/) | <img src="https://github.com/daveycodez.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [daveycodez](https://github.com/daveycodez) | | [@nerdfolio/remult-better-auth](https://github.com/nerdfolio/remult-better-auth) | [Remult](https://remult.dev/) | <img src="https://github.com/taivo.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [Tai Vo](https://github.com/taivo) | | [pocketbase-better-auth](https://github.com/LightInn/pocketbase-better-auth) | [PocketBase](https://pocketbase.io/) | <img src="https://github.com/LightInn.png" className="rounded-full w-6 h-6 border opacity-70 m-0 inline mr-1" /> [LightInn](https://github.com/LightInn) | ``` -------------------------------------------------------------------------------- /packages/core/src/oauth2/validate-authorization-code.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { jwtVerify } from "jose"; import type { ProviderOptions } from "./index"; import { getOAuth2Tokens } from "./index"; import { base64 } from "@better-auth/utils/base64"; export function createAuthorizationCodeRequest({ code, codeVerifier, redirectURI, options, authentication, deviceId, headers, additionalParams = {}, resource, }: { code: string; redirectURI: string; options: Partial<ProviderOptions>; codeVerifier?: string; deviceId?: string; authentication?: "basic" | "post"; headers?: Record<string, string>; additionalParams?: Record<string, string>; resource?: string | string[]; }) { const body = new URLSearchParams(); const requestHeaders: Record<string, any> = { "content-type": "application/x-www-form-urlencoded", accept: "application/json", "user-agent": "better-auth", ...headers, }; body.set("grant_type", "authorization_code"); body.set("code", code); codeVerifier && body.set("code_verifier", codeVerifier); options.clientKey && body.set("client_key", options.clientKey); deviceId && body.set("device_id", deviceId); body.set("redirect_uri", options.redirectURI || redirectURI); if (resource) { if (typeof resource === "string") { body.append("resource", resource); } else { for (const _resource of resource) { body.append("resource", _resource); } } } // Use standard Base64 encoding for HTTP Basic Auth (OAuth2 spec, RFC 7617) // Fixes compatibility with providers like Notion, Twitter, etc. if (authentication === "basic") { const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId; const encodedCredentials = base64.encode( `${primaryClientId}:${options.clientSecret ?? ""}`, ); requestHeaders["authorization"] = `Basic ${encodedCredentials}`; } else { const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId; body.set("client_id", primaryClientId); if (options.clientSecret) { body.set("client_secret", options.clientSecret); } } for (const [key, value] of Object.entries(additionalParams)) { if (!body.has(key)) body.append(key, value); } return { body, headers: requestHeaders, }; } export async function validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint, authentication, deviceId, headers, additionalParams = {}, resource, }: { code: string; redirectURI: string; options: Partial<ProviderOptions>; codeVerifier?: string; deviceId?: string; tokenEndpoint: string; authentication?: "basic" | "post"; headers?: Record<string, string>; additionalParams?: Record<string, string>; resource?: string | string[]; }) { const { body, headers: requestHeaders } = createAuthorizationCodeRequest({ code, codeVerifier, redirectURI, options, authentication, deviceId, headers, additionalParams, resource, }); const { data, error } = await betterFetch<object>(tokenEndpoint, { method: "POST", body: body, headers: requestHeaders, }); if (error) { throw error; } const tokens = getOAuth2Tokens(data); return tokens; } export async function validateToken(token: string, jwksEndpoint: string) { const { data, error } = await betterFetch<{ keys: { kid: string; kty: string; use: string; n: string; e: string; x5c: string[]; }[]; }>(jwksEndpoint, { method: "GET", headers: { accept: "application/json", "user-agent": "better-auth", }, }); if (error) { throw error; } const keys = data["keys"]; const header = JSON.parse(atob(token.split(".")[0]!)); const key = keys.find((key) => key.kid === header.kid); if (!key) { throw new Error("Key not found"); } const verified = await jwtVerify(token, key); return verified; } ``` -------------------------------------------------------------------------------- /docs/components/ui/sheet.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SheetPrimitive from "@radix-ui/react-dialog"; import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { return <SheetPrimitive.Root data-slot="sheet" {...props} />; } function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; } function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) { return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; } function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) { return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; } function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { return ( <SheetPrimitive.Overlay data-slot="sheet-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", className, )} {...props} /> ); } function SheetContent({ className, children, side = "right", ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: "top" | "right" | "bottom" | "left"; }) { return ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Content data-slot="sheet-content" className={cn( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", side === "right" && "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", side === "left" && "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", side === "top" && "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", side === "bottom" && "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", className, )} {...props} > {children} <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <XIcon className="size-4" /> <span className="sr-only">Close</span> </SheetPrimitive.Close> </SheetPrimitive.Content> </SheetPortal> ); } function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} /> ); } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> ); } function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) { return ( <SheetPrimitive.Title data-slot="sheet-title" className={cn("text-foreground font-semibold", className)} {...props} /> ); } function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) { return ( <SheetPrimitive.Description data-slot="sheet-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/other-social-providers.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Other Social Providers description: Other social providers setup and usage. --- Better Auth provides out of the box support for a [Generic OAuth Plugin](/docs/plugins/generic-oauth) which allows you to use any social provider that implements the OAuth2 protocol or OpenID Connect (OIDC) flows. To use a provider that is not supported out of the box, you can use the [Generic OAuth Plugin](/docs/plugins/generic-oauth). ## Installation <Steps> <Step> ### Add the plugin to your auth config To use the Generic OAuth plugin, add it to your auth config. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { genericOAuth } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ // ... other config options plugins: [ genericOAuth({ // [!code highlight] config: [ // [!code highlight] { // [!code highlight] providerId: "provider-id", // [!code highlight] clientId: "test-client-id", // [!code highlight] clientSecret: "test-client-secret", // [!code highlight] discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight] // ... other config options // [!code highlight] }, // [!code highlight] // Add more providers as needed // [!code highlight] ] // [!code highlight] }) // [!code highlight] ] }) ``` </Step> <Step> ### Add the client plugin Include the Generic OAuth client plugin in your authentication client instance. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { genericOAuthClient } from "better-auth/client/plugins" const authClient = createAuthClient({ plugins: [ genericOAuthClient() ] }) ``` </Step> </Steps> <Callout> Read more about installation and usage of the Generic Oauth plugin [here](/docs/plugins/generic-oauth#usage). </Callout> ## Example usage ### Instagram Example ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; export const auth = betterAuth({ // ... other config options plugins: [ genericOAuth({ config: [ { providerId: "instagram", clientId: process.env.INSTAGRAM_CLIENT_ID as string, clientSecret: process.env.INSTAGRAM_CLIENT_SECRET as string, authorizationUrl: "https://api.instagram.com/oauth/authorize", tokenUrl: "https://api.instagram.com/oauth/access_token", scopes: ["user_profile", "user_media"], }, ], }), ], }); ``` ```ts title="sign-in.ts" const response = await authClient.signIn.oauth2({ providerId: "instagram", callbackURL: "/dashboard", // the path to redirect to after the user is authenticated }); ``` ### Coinbase Example ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; export const auth = betterAuth({ // ... other config options plugins: [ genericOAuth({ config: [ { providerId: "coinbase", clientId: process.env.COINBASE_CLIENT_ID as string, clientSecret: process.env.COINBASE_CLIENT_SECRET as string, authorizationUrl: "https://www.coinbase.com/oauth/authorize", tokenUrl: "https://api.coinbase.com/oauth/token", scopes: ["wallet:user:read"], // and more... }, ], }), ], }); ``` ```ts title="sign-in.ts" const response = await authClient.signIn.oauth2({ providerId: "coinbase", callbackURL: "/dashboard", // the path to redirect to after the user is authenticated }); ``` ``` -------------------------------------------------------------------------------- /docs/components/landing/grid-pattern.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { motion } from "framer-motion"; import { useEffect, useId, useRef, useState } from "react"; const Block = ({ x, y, ...props }: Omit<React.ComponentPropsWithoutRef<typeof motion.path>, "x" | "y"> & { x: number; y: number; }) => { return ( <motion.path transform={`translate(${-32 * y + 96 * x} ${160 * y})`} d="M45.119 4.5a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C6.82 148.861 12.262 155.5 19.52 155.5h63.366a11.5 11.5 0 0 0 11.277-9.245l25.6-128c1.423-7.116-4.02-13.755-11.277-13.755H45.119Z" {...props} /> ); }; export const GridPattern = ({ yOffset = 0, interactive = false, ...props }) => { const id = useId(); const ref = useRef<React.ElementRef<"svg">>(null); const currentBlock = useRef<[x: number, y: number]>(); const counter = useRef(0); const [hoveredBlocks, setHoveredBlocks] = useState< Array<[x: number, y: number, key: number]> >([]); const staticBlocks = [ [1, 1], [2, 2], [4, 3], [6, 2], [7, 4], [5, 5], ]; useEffect(() => { if (!interactive) { return; } function onMouseMove(event: MouseEvent) { if (!ref.current) { return; } const rect = ref.current.getBoundingClientRect(); let x = event.clientX - rect.left; let y = event.clientY - rect.top; if (x < 0 || y < 0 || x > rect.width || y > rect.height) { return; } x = x - rect.width / 2 - 32; y = y - yOffset; x += Math.tan(32 / 160) * y; x = Math.floor(x / 96); y = Math.floor(y / 160); if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) { return; } currentBlock.current = [x, y]; setHoveredBlocks((blocks) => { const key = counter.current++; const block = [x, y, key] as (typeof hoveredBlocks)[number]; return [...blocks, block].filter( (block) => !(block[0] === x && block[1] === y && block[2] !== key), ); }); } window.addEventListener("mousemove", onMouseMove); return () => { window.removeEventListener("mousemove", onMouseMove); }; }, [yOffset, interactive]); return ( <motion.svg ref={ref} aria-hidden="true" {...props} exit={{ opacity: 0 }} animate={{ opacity: 1 }} initial={{ opacity: 0 }} > <rect width="100%" height="100%" fill={`url(#${id})`} strokeWidth="0" /> <svg x="50%" y={yOffset} strokeWidth="0" className="overflow-visible"> {staticBlocks.map((block) => ( <Block key={`${block}`} x={block[0]} y={block[1]} /> ))} {hoveredBlocks.map((block) => ( <Block key={block[2]} x={block[0]} y={block[1]} animate={{ opacity: [0, 1, 0] }} transition={{ duration: 1, times: [0, 0, 1] }} onAnimationComplete={() => { setHoveredBlocks((blocks) => blocks.filter((b) => b[2] !== block[2]), ); }} /> ))} </svg> <defs> <pattern id={id} width="96" height="480" x="50%" patternUnits="userSpaceOnUse" patternTransform={`translate(0 ${yOffset})`} fill="none" > <path d="M128 0 98.572 147.138A16 16 0 0 1 82.883 160H13.117a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-45.117 320H-116M64-160 34.572-12.862A16 16 0 0 1 18.883 0h-69.766a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-109.117 160H-180M192 160l-29.428 147.138A15.999 15.999 0 0 1 146.883 320H77.117a16 16 0 0 0-15.69 12.862L34.573 467.138A16 16 0 0 1 18.883 480H-52M-136 480h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1-18.883 320h69.766a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 109.117 160H192M-72 640h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 45.117 480h69.766a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A15.999 15.999 0 0 1 173.117 320H256M-200 320h58.883a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A16 16 0 0 1-82.883 160h69.766a16 16 0 0 0 15.69-12.862L29.427 12.862A16 16 0 0 1 45.117 0H128" /> </pattern> </defs> </motion.svg> ); }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/two-factor/verify-two-factor.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import { TRUST_DEVICE_COOKIE_NAME, TWO_FACTOR_COOKIE_NAME } from "./constant"; import { setSessionCookie } from "../../cookies"; import { getSessionFromCtx } from "../../api"; import type { UserWithTwoFactor } from "./types"; import { createHMAC } from "@better-auth/utils/hmac"; import { TWO_FACTOR_ERROR_CODES } from "./error-code"; import type { GenericEndpointContext } from "@better-auth/core"; export async function verifyTwoFactor(ctx: GenericEndpointContext) { const session = await getSessionFromCtx(ctx); if (!session) { const cookieName = ctx.context.createAuthCookie(TWO_FACTOR_COOKIE_NAME); const twoFactorCookie = await ctx.getSignedCookie( cookieName.name, ctx.context.secret, ); if (!twoFactorCookie) { throw new APIError("UNAUTHORIZED", { message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, }); } const verificationToken = await ctx.context.internalAdapter.findVerificationValue(twoFactorCookie); if (!verificationToken) { throw new APIError("UNAUTHORIZED", { message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, }); } const user = (await ctx.context.internalAdapter.findUserById( verificationToken.value, )) as UserWithTwoFactor; if (!user) { throw new APIError("UNAUTHORIZED", { message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, }); } const dontRememberMe = await ctx.getSignedCookie( ctx.context.authCookies.dontRememberToken.name, ctx.context.secret, ); return { valid: async (ctx: GenericEndpointContext) => { const session = await ctx.context.internalAdapter.createSession( verificationToken.value, !!dontRememberMe, ); if (!session) { throw new APIError("INTERNAL_SERVER_ERROR", { message: "failed to create session", }); } await setSessionCookie(ctx, { session, user, }); if (ctx.body.trustDevice) { const trustDeviceCookie = ctx.context.createAuthCookie( TRUST_DEVICE_COOKIE_NAME, { maxAge: 30 * 24 * 60 * 60, // 30 days, it'll be refreshed on sign in requests }, ); /** * create a token that will be used to * verify the device */ const token = await createHMAC("SHA-256", "base64urlnopad").sign( ctx.context.secret, `${user.id}!${session.token}`, ); await ctx.setSignedCookie( trustDeviceCookie.name, `${token}!${session.token}`, ctx.context.secret, trustDeviceCookie.attributes, ); // delete the dont remember me cookie ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", { maxAge: 0, }); // delete the two factor cookie ctx.setCookie(cookieName.name, "", { maxAge: 0, }); } return ctx.json({ token: session.token, user: { id: user.id, email: user.email, emailVerified: user.emailVerified, name: user.name, image: user.image, createdAt: user.createdAt, updatedAt: user.updatedAt, }, }); }, invalid: async (errorKey: keyof typeof TWO_FACTOR_ERROR_CODES) => { throw new APIError("UNAUTHORIZED", { message: TWO_FACTOR_ERROR_CODES[errorKey], }); }, session: { session: null, user, }, key: twoFactorCookie, }; } return { valid: async (ctx: GenericEndpointContext) => { return ctx.json({ token: session.session.token, user: { id: session.user.id, email: session.user.email, emailVerified: session.user.emailVerified, name: session.user.name, image: session.user.image, createdAt: session.user.createdAt, updatedAt: session.user.updatedAt, }, }); }, invalid: async () => { throw new APIError("UNAUTHORIZED", { message: TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE, }); }, session, key: `${session.user.id}!${session.session.id}`, }; } ``` -------------------------------------------------------------------------------- /packages/core/src/env/color-depth.ts: -------------------------------------------------------------------------------- ```typescript // Source code copied & modified from node internals: https://github.com/nodejs/node/blob/5b32bb1573dace2dd058c05ac4fab1e4e446c775/lib/internal/tty.js#L123 import { env, getEnvVar } from "./env-impl"; const COLORS_2 = 1; const COLORS_16 = 4; const COLORS_256 = 8; const COLORS_16m = 24; const TERM_ENVS: Record<string, number> = { eterm: COLORS_16, cons25: COLORS_16, console: COLORS_16, cygwin: COLORS_16, dtterm: COLORS_16, gnome: COLORS_16, hurd: COLORS_16, jfbterm: COLORS_16, konsole: COLORS_16, kterm: COLORS_16, mlterm: COLORS_16, mosh: COLORS_16m, putty: COLORS_16, st: COLORS_16, // http://lists.schmorp.de/pipermail/rxvt-unicode/2016q2/002261.html "rxvt-unicode-24bit": COLORS_16m, // https://bugs.launchpad.net/terminator/+bug/1030562 terminator: COLORS_16m, "xterm-kitty": COLORS_16m, }; const CI_ENVS_MAP = new Map( Object.entries({ APPVEYOR: COLORS_256, BUILDKITE: COLORS_256, CIRCLECI: COLORS_16m, DRONE: COLORS_256, GITEA_ACTIONS: COLORS_16m, GITHUB_ACTIONS: COLORS_16m, GITLAB_CI: COLORS_256, TRAVIS: COLORS_256, }), ); const TERM_ENVS_REG_EXP = [ /ansi/, /color/, /linux/, /direct/, /^con[0-9]*x[0-9]/, /^rxvt/, /^screen/, /^xterm/, /^vt100/, /^vt220/, ]; // The `getColorDepth` API got inspired by multiple sources such as // https://github.com/chalk/supports-color, // https://github.com/isaacs/color-support. export function getColorDepth(): number { // Use level 0-3 to support the same levels as `chalk` does. This is done for // consistency throughout the ecosystem. if (getEnvVar("FORCE_COLOR") !== undefined) { switch (getEnvVar("FORCE_COLOR")) { case "": case "1": case "true": return COLORS_16; case "2": return COLORS_256; case "3": return COLORS_16m; default: return COLORS_2; } } if ( (getEnvVar("NODE_DISABLE_COLORS") !== undefined && getEnvVar("NODE_DISABLE_COLORS") !== "") || // See https://no-color.org/ (getEnvVar("NO_COLOR") !== undefined && getEnvVar("NO_COLOR") !== "") || // The "dumb" special terminal, as defined by terminfo, doesn't support // ANSI color control codes. // See https://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials getEnvVar("TERM") === "dumb" ) { return COLORS_2; } // Edge runtime doesn't support `process?.platform` syntax // if (typeof process !== "undefined" && process?.platform === "win32") { // // Windows 10 build 14931 (from 2016) has true color support // return COLORS_16m; // } if (getEnvVar("TMUX")) { return COLORS_16m; } // Azure DevOps if ("TF_BUILD" in env && "AGENT_NAME" in env) { return COLORS_16; } if ("CI" in env) { for (const { 0: envName, 1: colors } of CI_ENVS_MAP) { if (envName in env) { return colors; } } if (getEnvVar("CI_NAME") === "codeship") { return COLORS_256; } return COLORS_2; } if ("TEAMCITY_VERSION" in env) { return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.exec( getEnvVar("TEAMCITY_VERSION"), ) !== null ? COLORS_16 : COLORS_2; } switch (getEnvVar("TERM_PROGRAM")) { case "iTerm.app": if ( !getEnvVar("TERM_PROGRAM_VERSION") || /^[0-2]\./.exec(getEnvVar("TERM_PROGRAM_VERSION")) !== null ) { return COLORS_256; } return COLORS_16m; case "HyperTerm": case "MacTerm": return COLORS_16m; case "Apple_Terminal": return COLORS_256; } if ( getEnvVar("COLORTERM") === "truecolor" || getEnvVar("COLORTERM") === "24bit" ) { return COLORS_16m; } if (getEnvVar("TERM")) { if (/truecolor/.exec(getEnvVar("TERM")) !== null) { return COLORS_16m; } if (/^xterm-256/.exec(getEnvVar("TERM")) !== null) { return COLORS_256; } const termEnv = getEnvVar("TERM").toLowerCase(); if (TERM_ENVS[termEnv]) { return TERM_ENVS[termEnv]; } if (TERM_ENVS_REG_EXP.some((term) => term.exec(termEnv) !== null)) { return COLORS_16; } } // Move 16 color COLORTERM below 16m and 256 if (getEnvVar("COLORTERM")) { return COLORS_16; } return COLORS_2; } ``` -------------------------------------------------------------------------------- /docs/content/blogs/authjs-joins-better-auth.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Auth.js is now part of Better Auth description: "Auth.js, formerly known as NextAuth.js, is now being maintained and overseen by Better Auth team" date: 2025-09-22 author: name: "Bereket Engida" avatar: "/avatars/beka.jpg" twitter: "imbereket" image: "/blogs/authjs-joins.png" tags: ["seed round", "authentication", "funding"] --- We’re excited to announce that [Auth.js](https://authjs.dev), formerly known as NextAuth.js, is now being maintained and overseen by Better Auth team. If you haven't heard of Auth.js, it has long been one of the most widely used open source authentication libraries in the JavaScript ecosystem. Chances are, if you’ve used [ChatGPT](https://chatgpt.com), [Google Labs](https://labs.google), [Cal.com](https://cal.com) or a million other websites, you’ve already interacted with Auth.js. ## Back Story about Better Auth and Auth.js Before Better Auth, Auth.js gave developers like us the ability to own our auth without spending months wrestling with OAuth integrations or session management. But as applications became more complex and authentication needs evolved, some of its limitations became harder to ignore. We found ourselves rebuilding the same primitives over and over. The Auth.js team recognized these challenges and had big ideas for the future, but for various reasons couldn’t execute them as fully as they hoped. That shared frustration and the vision of empowering everyone to truly own their auth started the creation of Better Auth. Since our goals aligned with the Auth.js team, we were excited to help maintain Auth.js and make auth better across the web. As we talked more, we realized that Better Auth was the best home for Auth.js. ## What does this mean for existing users? We recognize how important this project is for countless applications, companies, and developers. If you’re using Auth.js/NextAuth.js today, you can continue doing so without disruption—we’ll keep addressing security patches and urgent issues as they come up. But we strongly recommend new projects to start with Better Auth unless there are some very specific feature gaps (most notably stateless session management without a database). Our roadmap includes bringing those capabilities into Better Auth, so the ecosystem can converge rather than fragment. <Callout> For teams considering migration, we’ve prepared a [guide](/docs/guides/next-auth-migration-guide) and we’ll be adding more guides and documentation soon. </Callout> ## Final Thoughts We are deeply grateful to the Auth.js community who have carried the project to this point. In particular, the core maintainers-[Balázs](https://x.com/balazsorban44), who served as lead maintainer, [Thang Vu](https://x.com/thanghvu),[Nico Domino](https://ndo.dev), Lluis Agusti and [Falco Winkler](https://github.com/falcowinkler)-pushed through difficult phases, brought in new primitives, and kept the project alive long enough for this transition to even be possible. Better Auth beginning was inspired by Auth.js, and now, together, the two projects can carry the ecosystem further. The end goal remains unchanged: you should own your auth! <Callout type="none"> For the Auth.js team's announcement, see [GitHub discussion](https://github.com/nextauthjs/next-auth/discussions/13252). </Callout> ### Learn More <Cards> <Card href="https://www.better-auth.com/docs/introduction" title="Better Auth Setup" > Get started with installing Better Auth </Card> <Card href="https://www.better-auth.com/docs/comparison" title="Comparison" > Comparison between Better Auth and other options </Card> <Card href="https://www.better-auth.com/docs/guides/next-auth-migration-guide" title="NextAuth Migration Guide" > Migrate from NextAuth to Better Auth </Card> <Card href="https://www.better-auth.com/docs/guides/clerk-migration-guide" title="Clerk Migration Guide" > Migrate from Clerk to Better Auth </Card> </Cards> ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/form.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext, } from "react-hook-form"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > = { name: TName; }; const FormFieldContext = React.createContext<FormFieldContextValue>( {} as FormFieldContextValue, ); const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, >({ ...props }: ControllerProps<TFieldValues, TName>) => { return ( <FormFieldContext.Provider value={{ name: props.name }}> <Controller {...props} /> </FormFieldContext.Provider> ); }; const useFormField = () => { const fieldContext = React.useContext(FormFieldContext); const itemContext = React.useContext(FormItemContext); const { getFieldState, formState } = useFormContext(); const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { throw new Error("useFormField should be used within <FormField>"); } const { id } = itemContext; return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, }; }; type FormItemContextValue = { id: string; }; const FormItemContext = React.createContext<FormItemContextValue>( {} as FormItemContextValue, ); const FormItem = ({ ref, className, ...props }: React.HTMLAttributes<HTMLDivElement> & { ref: React.RefObject<HTMLDivElement>; }) => { const id = React.useId(); return ( <FormItemContext.Provider value={{ id }}> <div ref={ref} className={cn("space-y-2", className)} {...props} /> </FormItemContext.Provider> ); }; FormItem.displayName = "FormItem"; const FormLabel = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { ref: React.RefObject<React.ElementRef<typeof LabelPrimitive.Root>>; }) => { const { error, formItemId } = useFormField(); return ( <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} /> ); }; FormLabel.displayName = "FormLabel"; const FormControl = ({ ref, ...props }: React.ComponentPropsWithoutRef<typeof Slot> & { ref: React.RefObject<React.ElementRef<typeof Slot>>; }) => { const { error, formItemId, formDescriptionId, formMessageId } = useFormField(); return ( <Slot ref={ref} id={formItemId} aria-describedby={ !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}` } aria-invalid={!!error} {...props} /> ); }; FormControl.displayName = "FormControl"; const FormDescription = ({ ref, className, ...props }: React.HTMLAttributes<HTMLParagraphElement> & { ref: React.RefObject<HTMLParagraphElement>; }) => { const { formDescriptionId } = useFormField(); return ( <p ref={ref} id={formDescriptionId} className={cn("text-[0.8rem] text-muted-foreground", className)} {...props} /> ); }; FormDescription.displayName = "FormDescription"; const FormMessage = ({ ref, className, children, ...props }: React.HTMLAttributes<HTMLParagraphElement> & { ref: React.RefObject<HTMLParagraphElement>; }) => { const { error, formMessageId } = useFormField(); const body = error ? String(error?.message) : children; if (!body) { return null; } return ( <p ref={ref} id={formMessageId} className={cn("text-[0.8rem] font-medium text-destructive", className)} {...props} > {body} </p> ); }; FormMessage.displayName = "FormMessage"; export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, }; ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/blog-list.tsx: -------------------------------------------------------------------------------- ```typescript import { formatBlogDate } from "@/lib/blog"; import Link from "next/link"; import { blogs } from "@/lib/source"; import { IconLink } from "./changelog-layout"; import { GitHubIcon, BookIcon, XIcon } from "./icons"; import { Glow } from "./default-changelog"; import { StarField } from "./stat-field"; import { DiscordLogoIcon } from "@radix-ui/react-icons"; import Image from "next/image"; export async function BlogPage() { const posts = blogs.getPages().sort((a, b) => { return new Date(b.data.date).getTime() - new Date(a.data.date).getTime(); }); return ( <div className="md:grid md:grid-cols-2 items-start"> <div className="bg-gradient-to-tr hidden md:block overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> <StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" /> <Glow /> <div className="flex flex-col md:justify-center max-w-xl mx-auto h-full"> <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> Blogs </h1> <p className="text-sm text-gray-600 dark:text-gray-300"> Latest updates, articles, and insights about Better Auth </p> <hr className="h-px bg-gray-300 mt-5" /> <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 gap-x-1 gap-y-3 sm:gap-x-2"> <IconLink href="/docs" icon={BookIcon} className="flex-none text-gray-600 dark:text-gray-300" > Documentation </IconLink> <IconLink href="https://github.com/better-auth/better-auth" icon={GitHubIcon} className="flex-none text-gray-600 dark:text-gray-300" > GitHub </IconLink> <IconLink href="https://discord.gg/better-auth" icon={DiscordLogoIcon} className="flex-none text-gray-600 dark:text-gray-300" > Community </IconLink> </div> <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> <IconLink href="https://x.com/better_auth" icon={XIcon} compact> BETTER-AUTH. </IconLink> </p> </div> </div> <div className="py-6 lg:py-10 px-3"> <div className="flex flex-col gap-2"> {posts.map((post) => ( <div className="group/blog flex flex-col gap-3 transition-colors p-4" key={post.slugs.join("/")} > <article className="group relative flex flex-col space-y-2 flex-3/4 py-1"> <div className="flex gap-2"> <div className="flex flex-col gap-2 border-b border-dashed pb-2"> <p className="text-xs opacity-50"> {formatBlogDate(post.data.date)} </p> <h2 className="text-2xl font-bold">{post.data?.title}</h2> </div> </div> {post.data?.image && ( <Image src={post.data.image} alt={post.data.title} width={1206} height={756} className="rounded-md w-full bg-muted transition-colors" /> )} <div className="flex gap-2"> <div className="flex flex-col gap-2 border-b border-dashed pb-2"> <p className="text-muted-foreground"> {post.data?.description.substring(0, 100)}... </p> </div> </div> <p className="text-xs opacity-50"> {post.data.structuredData.contents[0].content.substring( 0, 250, )} ... </p> <Link href={`/blog/${post.slugs.join("/")}`}> <p className="text-xs group-hover/blog:underline underline-offset-4 transition-all"> Read More </p> </Link> <Link href={`/blog/${post.slugs.join("/")}`} className="absolute inset-0" > <span className="sr-only">View Article</span> </Link> </article> </div> ))} </div> </div> </div> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/anonymous.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Anonymous description: Anonymous plugin for Better Auth. --- The Anonymous plugin allows users to have an authenticated experience without requiring them to provide an email address, password, OAuth provider, or any other Personally Identifiable Information (PII). Users can later link an authentication method to their account when ready. ## Installation <Steps> <Step> ### Add the plugin to your auth config To enable anonymous authentication, add the anonymous plugin to your authentication configuration. ```ts title="auth.ts" import { betterAuth } from "better-auth" import { anonymous } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ // ... other config options plugins: [ anonymous() // [!code highlight] ] }) ``` </Step> <Step> ### Migrate the database Run the migration or generate the schema to add the necessary fields and tables to the database. <Tabs items={["migrate", "generate"]}> <Tab value="migrate"> ```bash npx @better-auth/cli migrate ``` </Tab> <Tab value="generate"> ```bash npx @better-auth/cli generate ``` </Tab> </Tabs> See the [Schema](#schema) section to add the fields manually. </Step> <Step> ### Add the client plugin Next, include the anonymous client plugin in your authentication client instance. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" import { anonymousClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [ anonymousClient() ] }) ``` </Step> </Steps> ## Usage ### Sign In To sign in a user anonymously, use the `signIn.anonymous()` method. ```ts title="example.ts" const user = await authClient.signIn.anonymous() ``` ### Link Account If a user is already signed in anonymously and tries to `signIn` or `signUp` with another method, their anonymous activities can be linked to the new account. To do that you first need to provide `onLinkAccount` callback to the plugin. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ anonymous({ onLinkAccount: async ({ anonymousUser, newUser }) => { // perform actions like moving the cart items from anonymous user to the new user } }) ] ``` Then when you call `signIn` or `signUp` with another method, the `onLinkAccount` callback will be called. And the `anonymousUser` will be deleted by default. ```ts title="example.ts" const user = await authClient.signIn.email({ email, }) ``` ## Options - `emailDomainName`: The domain name to use when generating an email address for anonymous users. Defaults to the domain name of the current site. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ plugins: [ anonymous({ emailDomainName: "example.com" }) ] }) ``` - `onLinkAccount`: A callback function that is called when an anonymous user links their account to a new authentication method. The callback receives an object with the `anonymousUser` and the `newUser`. - `disableDeleteAnonymousUser`: By default, the anonymous user is deleted when the account is linked to a new authentication method. Set this option to `true` to disable this behavior. - `generateName`: A callback function that is called to generate a name for the anonymous user. Useful if you want to have random names for anonymous users, or if `name` is unique in your database. ## Schema The anonymous plugin requires an additional field in the user table: <DatabaseTable fields={[ { name: "isAnonymous", type: "boolean", description: "Indicates whether the user is anonymous.", isOptional: true }, ]} /> ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: pull_request: types: - opened - synchronize push: branches: - main - canary merge_group: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Cache turbo build setup uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} restore-keys: | ${{ runner.os }}-turbo- - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x registry-url: 'https://registry.npmjs.org' cache: pnpm - name: Install run: pnpm install - name: Build env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm build - name: Lint env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm lint typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Cache turbo build setup uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} restore-keys: | ${{ runner.os }}-turbo- - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 22.x registry-url: 'https://registry.npmjs.org' cache: pnpm - name: Install run: pnpm install - name: Build env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm build - name: Typecheck env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm typecheck test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: [ 22.x, 24.x, 25.x ] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Cache turbo build setup uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} restore-keys: | ${{ runner.os }}-turbo- - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' cache: pnpm - name: Install run: pnpm install - name: Build env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm build - name: Start Docker Containers run: | docker compose up -d # Wait for services to be ready (optional) sleep 10 - name: Test env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} run: pnpm test - name: Stop Docker Containers run: docker compose down ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/schema.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; import parseJSON from "../../client/parser"; export const apiKeySchema = ({ timeWindow, rateLimitMax, }: { timeWindow: number; rateLimitMax: number; }) => ({ apikey: { fields: { /** * The name of the key. */ name: { type: "string", required: false, input: false, }, /** * Shows the first few characters of the API key * This allows you to show those few characters in the UI to make it easier for users to identify the API key. */ start: { type: "string", required: false, input: false, }, /** * The prefix of the key. */ prefix: { type: "string", required: false, input: false, }, /** * The hashed key value. */ key: { type: "string", required: true, input: false, }, /** * The user id of the user who created the key. */ userId: { type: "string", references: { model: "user", field: "id", onDelete: "cascade" }, required: true, input: false, }, /** * The interval to refill the key in milliseconds. */ refillInterval: { type: "number", required: false, input: false, }, /** * The amount to refill the remaining count of the key. */ refillAmount: { type: "number", required: false, input: false, }, /** * The date and time when the key was last refilled. */ lastRefillAt: { type: "date", required: false, input: false, }, /** * Whether the key is enabled. */ enabled: { type: "boolean", required: false, input: false, defaultValue: true, }, /** * Whether the key has rate limiting enabled. */ rateLimitEnabled: { type: "boolean", required: false, input: false, defaultValue: true, }, /** * The time window in milliseconds for the rate limit. */ rateLimitTimeWindow: { type: "number", required: false, input: false, defaultValue: timeWindow, }, /** * The maximum number of requests allowed within the `rateLimitTimeWindow`. */ rateLimitMax: { type: "number", required: false, input: false, defaultValue: rateLimitMax, }, /** * The number of requests made within the rate limit time window */ requestCount: { type: "number", required: false, input: false, defaultValue: 0, }, /** * The remaining number of requests before the key is revoked. * * If this is null, then the key is not revoked. * * If `refillInterval` & `refillAmount` are provided, than this will refill accordingly. */ remaining: { type: "number", required: false, input: false, }, /** * The date and time of the last request made to the key. */ lastRequest: { type: "date", required: false, input: false, }, /** * The date and time when the key will expire. */ expiresAt: { type: "date", required: false, input: false, }, /** * The date and time when the key was created. */ createdAt: { type: "date", required: true, input: false, }, /** * The date and time when the key was last updated. */ updatedAt: { type: "date", required: true, input: false, }, /** * The permissions of the key. */ permissions: { type: "string", required: false, input: false, }, /** * Any additional metadata you want to store with the key. */ metadata: { type: "string", required: false, input: true, transform: { input(value) { return JSON.stringify(value); }, output(value) { if (!value) return null; return parseJSON<any>(value as string); }, }, }, }, }, }) satisfies BetterAuthPluginDBSchema; ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/stat-field.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useEffect, useId, useRef } from "react"; import clsx from "clsx"; import { animate, Segment } from "motion/react"; type Star = [x: number, y: number, dim?: boolean, blur?: boolean]; const stars: Array<Star> = [ [4, 4, true, true], [4, 44, true], [36, 22], [50, 146, true, true], [64, 43, true, true], [76, 30, true], [101, 116], [140, 36, true], [149, 134], [162, 74, true], [171, 96, true, true], [210, 56, true, true], [235, 90], [275, 82, true, true], [306, 6], [307, 64, true, true], [380, 68, true], [380, 108, true, true], [391, 148, true, true], [405, 18, true], [412, 86, true, true], [426, 210, true, true], [427, 56, true, true], [538, 138], [563, 88, true, true], [611, 154, true, true], [637, 150], [651, 146, true], [682, 70, true, true], [683, 128], [781, 82, true, true], [785, 158, true], [832, 146, true, true], [852, 89], ]; const constellations: Array<Array<Star>> = [ [ [247, 103], [261, 86], [307, 104], [357, 36], ], [ [586, 120], [516, 100], [491, 62], [440, 107], [477, 180], [516, 100], ], [ [733, 100], [803, 120], [879, 113], [823, 164], [803, 120], ], ]; function Star({ blurId, point: [cx, cy, dim, blur], }: { blurId: string; point: Star; }) { let groupRef = useRef<React.ElementRef<"g">>(null); let ref = useRef<React.ElementRef<"circle">>(null); useEffect(() => { if (!groupRef.current || !ref.current) { return; } let delay = Math.random() * 2; let animations = [ animate(groupRef.current, { opacity: 1 }, { duration: 4, delay }), animate( ref.current, { opacity: dim ? [0.2, 0.5] : [1, 0.6], scale: dim ? [1, 1.2] : [1.2, 1], }, { duration: 10, delay, }, ), ]; return () => { for (let animation of animations) { animation.cancel(); } }; }, [dim]); return ( <g ref={groupRef} className="opacity-0"> <circle ref={ref} cx={cx} cy={cy} r={1} style={{ transformOrigin: `${cx / 16}rem ${cy / 16}rem`, opacity: dim ? 0.2 : 1, transform: `scale(${dim ? 1 : 1.2})`, }} filter={blur ? `url(#${blurId})` : undefined} /> </g> ); } function Constellation({ points, blurId, }: { points: Array<Star>; blurId: string; }) { let ref = useRef<React.ElementRef<"path">>(null); let uniquePoints = points.filter( (point, pointIndex) => points.findIndex((p) => String(p) === String(point)) === pointIndex, ); let isFilled = uniquePoints.length !== points.length; useEffect(() => { if (!ref.current) { return; } let sequence: Array<Segment> = [ [ ref.current, { strokeDashoffset: 0, visibility: "visible" }, { duration: 5, delay: Math.random() * 3 + 2 }, ], ]; if (isFilled) { sequence.push([ ref.current, { fill: "rgb(255 255 255 / 0.02)" }, { duration: 1 }, ]); } let animation = animate(sequence); return () => { animation.cancel(); }; }, [isFilled]); return ( <> <path ref={ref} stroke="white" strokeOpacity="0.2" strokeDasharray={1} strokeDashoffset={1} pathLength={1} fill="transparent" d={`M ${points.join("L")}`} className="invisible" /> {uniquePoints.map((point, pointIndex) => ( <Star key={pointIndex} point={point} blurId={blurId} /> ))} </> ); } export function StarField({ className }: { className?: string }) { let blurId = useId(); return ( <svg viewBox="0 0 881 211" fill="white" aria-hidden="true" className={clsx( "pointer-events-none absolute w-[55.0625rem] origin-top-right rotate-[30deg] overflow-visible opacity-70", className, )} > <defs> <filter id={blurId}> <feGaussianBlur in="SourceGraphic" stdDeviation=".5" /> </filter> </defs> {constellations.map((points, constellationIndex) => ( <Constellation key={constellationIndex} points={points} blurId={blurId} /> ))} {stars.map((point, pointIndex) => ( <Star key={pointIndex} point={point} blurId={blurId} /> ))} </svg> ); } ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/stat-field.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useEffect, useId, useRef } from "react"; import clsx from "clsx"; import { animate, Segment } from "motion/react"; type Star = [x: number, y: number, dim?: boolean, blur?: boolean]; const stars: Array<Star> = [ [4, 4, true, true], [4, 44, true], [36, 22], [50, 146, true, true], [64, 43, true, true], [76, 30, true], [101, 116], [140, 36, true], [149, 134], [162, 74, true], [171, 96, true, true], [210, 56, true, true], [235, 90], [275, 82, true, true], [306, 6], [307, 64, true, true], [380, 68, true], [380, 108, true, true], [391, 148, true, true], [405, 18, true], [412, 86, true, true], [426, 210, true, true], [427, 56, true, true], [538, 138], [563, 88, true, true], [611, 154, true, true], [637, 150], [651, 146, true], [682, 70, true, true], [683, 128], [781, 82, true, true], [785, 158, true], [832, 146, true, true], [852, 89], ]; const constellations: Array<Array<Star>> = [ [ [247, 103], [261, 86], [307, 104], [357, 36], ], [ [586, 120], [516, 100], [491, 62], [440, 107], [477, 180], [516, 100], ], [ [733, 100], [803, 120], [879, 113], [823, 164], [803, 120], ], ]; function Star({ blurId, point: [cx, cy, dim, blur], }: { blurId: string; point: Star; }) { let groupRef = useRef<React.ElementRef<"g">>(null); let ref = useRef<React.ElementRef<"circle">>(null); useEffect(() => { if (!groupRef.current || !ref.current) { return; } let delay = Math.random() * 2; let animations = [ animate(groupRef.current, { opacity: 1 }, { duration: 4, delay }), animate( ref.current, { opacity: dim ? [0.2, 0.5] : [1, 0.6], scale: dim ? [1, 1.2] : [1.2, 1], }, { duration: 10, delay, }, ), ]; return () => { for (let animation of animations) { animation.cancel(); } }; }, [dim]); return ( <g ref={groupRef} className="opacity-0"> <circle ref={ref} cx={cx} cy={cy} r={1} style={{ transformOrigin: `${cx / 16}rem ${cy / 16}rem`, opacity: dim ? 0.2 : 1, transform: `scale(${dim ? 1 : 1.2})`, }} filter={blur ? `url(#${blurId})` : undefined} /> </g> ); } function Constellation({ points, blurId, }: { points: Array<Star>; blurId: string; }) { let ref = useRef<React.ElementRef<"path">>(null); let uniquePoints = points.filter( (point, pointIndex) => points.findIndex((p) => String(p) === String(point)) === pointIndex, ); let isFilled = uniquePoints.length !== points.length; useEffect(() => { if (!ref.current) { return; } let sequence: Array<Segment> = [ [ ref.current, { strokeDashoffset: 0, visibility: "visible" }, { duration: 5, delay: Math.random() * 3 + 2 }, ], ]; if (isFilled) { sequence.push([ ref.current, { fill: "rgb(255 255 255 / 0.02)" }, { duration: 1 }, ]); } let animation = animate(sequence); return () => { animation.cancel(); }; }, [isFilled]); return ( <> <path ref={ref} stroke="white" strokeOpacity="0.2" strokeDasharray={1} strokeDashoffset={1} pathLength={1} fill="transparent" d={`M ${points.join("L")}`} className="invisible" /> {uniquePoints.map((point, pointIndex) => ( <Star key={pointIndex} point={point} blurId={blurId} /> ))} </> ); } export function StarField({ className }: { className?: string }) { let blurId = useId(); return ( <svg viewBox="0 0 881 211" fill="white" aria-hidden="true" className={clsx( "pointer-events-none absolute w-[55.0625rem] origin-top-right rotate-[30deg] overflow-visible opacity-70", className, )} > <defs> <filter id={blurId}> <feGaussianBlur in="SourceGraphic" stdDeviation=".5" /> </filter> </defs> {constellations.map((points, constellationIndex) => ( <Constellation key={constellationIndex} points={points} blurId={blurId} /> ))} {stars.map((point, pointIndex) => ( <Star key={pointIndex} point={point} blurId={blurId} /> ))} </svg> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/parser.ts: -------------------------------------------------------------------------------- ```typescript const PROTO_POLLUTION_PATTERNS = { proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/, constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/, protoShort: /"__proto__"\s*:/, constructorShort: /"constructor"\s*:/, } as const; const JSON_SIGNATURE = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/; const SPECIAL_VALUES = { true: true, false: false, null: null, undefined: undefined, nan: Number.NaN, infinity: Number.POSITIVE_INFINITY, "-infinity": Number.NEGATIVE_INFINITY, } as const; const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(?:Z|([+-])(\d{2}):(\d{2}))$/; type ParseOptions = { /** Throw errors instead of returning the original value */ strict?: boolean; /** Log warnings when suspicious patterns are detected */ warnings?: boolean; /** Custom reviver function */ reviver?: (key: string, value: any) => any; /** Automatically convert ISO date strings to Date objects */ parseDates?: boolean; }; function isValidDate(date: Date): boolean { return date instanceof Date && !isNaN(date.getTime()); } function parseISODate(value: string): Date | null { const match = ISO_DATE_REGEX.exec(value); if (!match) return null; const [ , year, month, day, hour, minute, second, ms, offsetSign, offsetHour, offsetMinute, ] = match; let date = new Date( Date.UTC( parseInt(year!, 10), parseInt(month!, 10) - 1, parseInt(day!, 10), parseInt(hour!, 10), parseInt(minute!, 10), parseInt(second!, 10), ms ? parseInt(ms.padEnd(3, "0"), 10) : 0, ), ); if (offsetSign) { const offset = (parseInt(offsetHour!, 10) * 60 + parseInt(offsetMinute!, 10)) * (offsetSign === "+" ? -1 : 1); date.setUTCMinutes(date.getUTCMinutes() + offset); } return isValidDate(date) ? date : null; } function betterJSONParse<T = unknown>( value: unknown, options: ParseOptions = {}, ): T { const { strict = false, warnings = false, reviver, parseDates = true, } = options; if (typeof value !== "string") { return value as T; } const trimmed = value.trim(); if ( trimmed.length > 0 && trimmed[0] === '"' && trimmed.endsWith('"') && !trimmed.slice(1, -1).includes('"') ) { return trimmed.slice(1, -1) as T; } const lowerValue = trimmed.toLowerCase(); if (lowerValue.length <= 9 && lowerValue in SPECIAL_VALUES) { return SPECIAL_VALUES[lowerValue as keyof typeof SPECIAL_VALUES] as T; } if (!JSON_SIGNATURE.test(trimmed)) { if (strict) { throw new SyntaxError("[better-json] Invalid JSON"); } return value as T; } const hasProtoPattern = Object.entries(PROTO_POLLUTION_PATTERNS).some( ([key, pattern]) => { const matches = pattern.test(trimmed); if (matches && warnings) { console.warn( `[better-json] Detected potential prototype pollution attempt using ${key} pattern`, ); } return matches; }, ); if (hasProtoPattern && strict) { throw new Error( "[better-json] Potential prototype pollution attempt detected", ); } try { const secureReviver = (key: string, value: any) => { if ( key === "__proto__" || (key === "constructor" && value && typeof value === "object" && "prototype" in value) ) { if (warnings) { console.warn( `[better-json] Dropping "${key}" key to prevent prototype pollution`, ); } return undefined; } if (parseDates && typeof value === "string") { const date = parseISODate(value); if (date) { return date; } } return reviver ? reviver(key, value) : value; }; return JSON.parse(trimmed, secureReviver); } catch (error) { if (strict) { throw error; } return value as T; } } export function parseJSON<T = unknown>( value: unknown, options: ParseOptions = { strict: true }, ): T { return betterJSONParse<T>(value, options); } export default parseJSON; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/tests/performance.ts: -------------------------------------------------------------------------------- ```typescript import { assert, expect } from "vitest"; import { createTestSuite } from "../create-test-suite"; /** * This test suite tests the performance of the adapter and logs the results. */ export const performanceTestSuite = createTestSuite( "performance", {}, ( { adapter, generate, cleanup }, config?: { iterations?: number; userSeedCount?: number; dialect?: string }, ) => { const tests = { create: [] as number[], update: [] as number[], delete: [] as number[], count: [] as number[], findOne: [] as number[], findMany: [] as number[], }; const iterations = config?.iterations ?? 10; const userSeedCount = config?.userSeedCount ?? 15; assert( userSeedCount >= iterations, "userSeedCount must be greater than iterations", ); const seedUser = async () => { const user = await generate("user"); return await adapter.create({ model: "user", data: user, forceAllowId: true, }); }; const seedManyUsers = async () => { const users = []; for (let i = 0; i < userSeedCount; i++) { users.push(await seedUser()); } return users; }; const performanceTests = { create: async () => { for (let i = 0; i < iterations; i++) { const start = performance.now(); await seedUser(); const end = performance.now(); tests.create.push(end - start); } }, update: async () => { const users = await seedManyUsers(); for (let i = 0; i < iterations; i++) { const start = performance.now(); await adapter.update({ model: "user", where: [{ field: "id", value: users[i]!.id }], update: { name: `user-${i}`, }, }); const end = performance.now(); tests.update.push(end - start); } }, delete: async () => { const users = await seedManyUsers(); for (let i = 0; i < iterations; i++) { const start = performance.now(); await adapter.delete({ model: "user", where: [{ field: "id", value: users[i]!.id }], }); const end = performance.now(); tests.delete.push(end - start); } }, count: async () => { const users = await seedManyUsers(); for (let i = 0; i < iterations; i++) { const start = performance.now(); const c = await adapter.count({ model: "user", }); const end = performance.now(); tests.count.push(end - start); expect(c).toEqual(users.length); } }, findOne: async () => { const users = await seedManyUsers(); for (let i = 0; i < iterations; i++) { const start = performance.now(); await adapter.findOne({ model: "user", where: [{ field: "id", value: users[i]!.id }], }); const end = performance.now(); tests.findOne.push(end - start); } }, findMany: async () => { const users = await seedManyUsers(); for (let i = 0; i < iterations; i++) { const start = performance.now(); const result = await adapter.findMany({ model: "user", where: [{ field: "name", value: "user", operator: "starts_with" }], limit: users.length, }); const end = performance.now(); tests.findMany.push(end - start); expect(result.length).toBe(users.length); } }, }; return { "run performance test": async () => { for (const test of Object.keys(performanceTests)) { await performanceTests[test as keyof typeof performanceTests](); await cleanup(); } // Calculate averages for each test const averages = Object.entries(tests).reduce( (acc, [key, values]) => { const average = values.length > 0 ? values.reduce((sum, val) => sum + val, 0) / values.length : 0; acc[key] = `${average.toFixed(3)}ms`; return acc; }, {} as Record<string, string>, ); console.log(`Performance tests results, counting averages:`); console.table(averages); console.log({ iterations, userSeedCount, adapter: adapter.options?.adapterConfig.adapterId, ...(config?.dialect ? { dialect: config.dialect } : {}), }); expect(1).toBe(1); }, }; }, ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/one-time-token/one-time-token.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, vi } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; import { oneTimeToken } from "."; import { APIError } from "better-call"; import { oneTimeTokenClient } from "./client"; import { defaultKeyHasher } from "./utils"; describe("One-time token", async () => { const { auth, signInWithTestUser, client } = await getTestInstance( { plugins: [oneTimeToken()], }, { clientOptions: { plugins: [oneTimeTokenClient()], }, }, ); it("should work", async () => { const { headers } = await signInWithTestUser(); const response = await auth.api.generateOneTimeToken({ headers, }); expect(response.token).toBeDefined(); const session = await auth.api.verifyOneTimeToken({ body: { token: response.token, }, }); expect(session).toBeDefined(); const shouldFail = await auth.api .verifyOneTimeToken({ body: { token: response.token, }, }) .catch((e) => e); expect(shouldFail).toBeInstanceOf(APIError); }); it("should expire", async () => { const { headers } = await signInWithTestUser(); const response = await auth.api.generateOneTimeToken({ headers, }); vi.useFakeTimers(); await vi.advanceTimersByTimeAsync(5 * 60 * 1000); const shouldFail = await auth.api .verifyOneTimeToken({ body: { token: response.token, }, }) .catch((e) => e); expect(shouldFail).toBeInstanceOf(APIError); vi.useRealTimers(); }); it("should work with client", async () => { const { headers } = await signInWithTestUser(); const response = await client.oneTimeToken.generate({ fetchOptions: { headers, throw: true, }, }); expect(response.token).toBeDefined(); const session = await client.oneTimeToken.verify({ token: response.token, }); expect(session.data?.session).toBeDefined(); }); describe("should work with different storeToken options", () => { describe("hashed", async () => { const { auth, signInWithTestUser, client } = await getTestInstance( { plugins: [ oneTimeToken({ storeToken: "hashed", async generateToken(session, ctx) { return "123456"; }, }), ], }, { clientOptions: { plugins: [oneTimeTokenClient()], }, }, ); const { internalAdapter } = await auth.$context; it("should work with hashed", async () => { const { headers } = await signInWithTestUser(); const response = await auth.api.generateOneTimeToken({ headers, }); expect(response.token).toBeDefined(); expect(response.token).toBe("123456"); const hashedToken = await defaultKeyHasher(response.token); const storedToken = await internalAdapter.findVerificationValue( `one-time-token:${hashedToken}`, ); expect(storedToken).toBeDefined(); const session = await auth.api.verifyOneTimeToken({ body: { token: response.token, }, }); expect(session).toBeDefined(); expect(session.user.email).toBeDefined(); }); }); describe("custom hasher", async () => { const { auth, signInWithTestUser, client } = await getTestInstance({ plugins: [ oneTimeToken({ storeToken: { type: "custom-hasher", hash: async (token) => { return token + "hashed"; }, }, async generateToken(session, ctx) { return "123456"; }, }), ], }); const { internalAdapter } = await auth.$context; it("should work with custom hasher", async () => { const { headers } = await signInWithTestUser(); const response = await auth.api.generateOneTimeToken({ headers, }); expect(response.token).toBeDefined(); expect(response.token).toBe("123456"); const hashedToken = response.token + "hashed"; const storedToken = await internalAdapter.findVerificationValue( `one-time-token:${hashedToken}`, ); expect(storedToken).toBeDefined(); const session = await auth.api.verifyOneTimeToken({ body: { token: response.token, }, }); expect(session).toBeDefined(); }); }); }); }); ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/nuxt.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Nuxt Integration description: Integrate Better Auth with Nuxt. --- 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). ### Create API Route We need to mount the handler to an API route. Create a file inside `/server/api/auth` called `[...all].ts` and add the following code: ```ts title="server/api/auth/[...all].ts" import { auth } from "~/lib/auth"; // import your auth config export default defineEventHandler((event) => { return auth.handler(toWebRequest(event)); }); ``` <Callout type="info"> You can change the path on your better-auth configuration but it's recommended to keep it as `/api/auth/[...all]` </Callout> ### Migrate the database Run the following command to create the necessary tables in your database: ```bash npx @better-auth/cli migrate ``` ## Create a client Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/vue" // make sure to import from better-auth/vue export const authClient = createAuthClient({ //you can pass client configuration here }) ``` Once you have created the client, you can use it to sign up, sign in, and perform other actions. Some of the actions are reactive. ### Example usage ```vue title="index.vue" <script setup lang="ts"> import { authClient } from "~/lib/client" const session = authClient.useSession() </script> <template> <div> <button v-if="!session?.data" @click="() => authClient.signIn.social({ provider: 'github' })"> Continue with GitHub </button> <div> <pre>{{ session.data }}</pre> <button v-if="session.data" @click="authClient.signOut()"> Sign out </button> </div> </div> </template> ``` ### Server Usage The `api` object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints. **Example: Getting Session on a server API route** ```tsx title="server/api/example.ts" import { auth } from "~/lib/auth"; export default defineEventHandler((event) => { const session = await auth.api.getSession({ headers: event.headers }); if(session) { // access the session.session && session.user } }); ``` ### SSR Usage If you are using Nuxt with SSR, you can use the `useSession` function in the `setup` function of your page component and pass `useFetch` to make it work with SSR. ```vue title="index.vue" <script setup lang="ts"> import { authClient } from "~/lib/auth-client"; const { data: session } = await authClient.useSession(useFetch); </script> <template> <p> {{ session }} </p> </template> ``` ### Middleware To add middleware to your Nuxt project, you can use the `useSession` method from the client. ```ts title="middleware/auth.global.ts" import { authClient } from "~/lib/auth-client"; export default defineNuxtRouteMiddleware(async (to, from) => { const { data: session } = await authClient.useSession(useFetch); if (!session.value) { if (to.path === "/dashboard") { return navigateTo("/"); } } }); ``` ### Resources & Examples - [Nuxt and Nuxt Hub example](https://github.com/atinux/nuxthub-better-auth) on GitHub. - [NuxtZzle is Nuxt,Drizzle ORM example](https://github.com/leamsigc/nuxt-better-auth-drizzle) on GitHub [preview](https://nuxt-better-auth.giessen.dev/) - [Nuxt example](https://stackblitz.com/github/better-auth/examples/tree/main/nuxt-example) on StackBlitz. - [NuxSaaS (Github)](https://github.com/NuxSaaS/NuxSaaS) is a full-stack SaaS Starter Kit that leverages Better Auth for secure and efficient user authentication. [Demo](https://nuxsaas.com/) - [NuxtOne (Github)](https://github.com/nuxtone/nuxt-one) is a Nuxt-based starter template for building AIaaS (AI-as-a-Service) applications [preview](https://www.one.devv.zone) ``` -------------------------------------------------------------------------------- /docs/content/docs/concepts/cli.mdx: -------------------------------------------------------------------------------- ```markdown --- title: CLI description: Built-in CLI for managing your project. --- Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup. ## Generate 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. ```bash title="Terminal" npx @better-auth/cli@latest generate ``` ### Options - `--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. - `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory. - `--yes` - Skip the confirmation prompt and generate the schema directly. ## Migrate The migrate command applies the Better Auth schema directly to your database. This is available if you're using the built-in Kysely adapter. For other adapters, you'll need to apply the schema using your ORM's migration tool. ```bash title="Terminal" npx @better-auth/cli@latest migrate ``` ### Options - `--config` - The path to your Better Auth config file. By default, the CLI will search for an auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under the `src` directory. - `--yes` - Skip the confirmation prompt and apply the schema directly. ## Init The `init` command allows you to initialize Better Auth in your project. ```bash title="Terminal" npx @better-auth/cli@latest init ``` ### Options - `--name` - The name of your application. (defaults to the `name` property in your `package.json`). - `--framework` - The framework your codebase is using. Currently, the only supported framework is `Next.js`. - `--plugins` - The plugins you want to use. You can specify multiple plugins by separating them with a comma. - `--database` - The database you want to use. Currently, the only supported database is `SQLite`. - `--package-manager` - The package manager you want to use. Currently, the only supported package managers are `npm`, `pnpm`, `yarn`, `bun` (defaults to the manager you used to initialize the CLI). ## Info The `info` command provides diagnostic information about your Better Auth setup and environment. Useful for debugging and sharing when seeking support. ```bash title="Terminal" npx @better-auth/cli@latest info ``` ### Output The command displays: - **System**: OS, CPU, memory, Node.js version - **Package Manager**: Detected manager and version - **Better Auth**: Version and configuration (sensitive data auto-redacted) - **Frameworks**: Detected frameworks (Next.js, React, Vue, etc.) - **Databases**: Database clients and ORMs (Prisma, Drizzle, etc.) ### Options - `--config` - Path to your Better Auth config file - `--json` - Output as JSON for sharing or programmatic use ### Examples ```bash # Basic usage npx @better-auth/cli@latest info # Custom config path npx @better-auth/cli@latest info --config ./config/auth.ts # JSON output npx @better-auth/cli@latest info --json > auth-info.json ``` Sensitive data like secrets, API keys, and database URLs are automatically replaced with `[REDACTED]` for safe sharing. ## Secret The CLI also provides a way to generate a secret key for your Better Auth instance. ```bash title="Terminal" npx @better-auth/cli@latest secret ``` ## Common Issues **Error: Cannot find module X** If you see this error, it means the CLI can't resolve imported modules in your Better Auth config file. We are working on a fix for many of these issues, but in the meantime, you can try the following: - Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases. ``` -------------------------------------------------------------------------------- /docs/components/github-stat.tsx: -------------------------------------------------------------------------------- ```typescript import { kFormatter } from "@/lib/utils"; export function GithubStat({ stars }: { stars: string | null }) { let result = 0; if (stars) { result = parseInt(stars?.replace(/,/g, ""), 10); } else { return <></>; } return ( <a href="https://github.com/better-auth/better-auth" className="flex mt-4 border border-input shadow-sm hover:bg-accent hover:text-accent-foreground rounded-none h-10 p-5 ml-auto z-50 overflow-hidden items-center text-sm font-medium focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 bg-transparent dark:text-white text-black px-4 py-2 max-w-[14.8rem] whitespace-pre md:flex group relative w-full justify-center gap-2 transition-all duration-300 ease-out hover:ring-black" > <span className="absolute right-0 -mt-12 h-32 w-8 translate-x-12 rotate-12 dark:bg-white/60 bg-black/60 opacity-10 transition-all duration-1000 ease-out group-hover:-translate-x-40"></span> <div className="flex items-center ml-2"> <svg className="w-4 h-4 fill-current" viewBox="0 0 438.549 438.549"> <path d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"></path> </svg> <span className="ml-2 text-black dark:text-white">Star on GitHub</span> </div> <div className="ml-2 flex items-center gap-2 text-sm md:flex"> <svg className="w-4 h-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" data-slot="icon" aria-hidden="true" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" > <path clipRule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" fillRule="evenodd" ></path> </svg> <span className="inline-block tabular-nums tracking-wider font-mono font-medium text-black dark:text-white"> {kFormatter(result)} </span> </div> </a> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/jwt/utils.ts: -------------------------------------------------------------------------------- ```typescript import { getWebcryptoSubtle } from "@better-auth/utils"; import { base64 } from "@better-auth/utils/base64"; import { joseSecs } from "../../utils/time"; import type { JwtOptions, Jwk } from "./types"; import { generateKeyPair, exportJWK } from "jose"; import { symmetricEncrypt } from "../../crypto"; import { getJwksAdapter } from "./adapter"; import type { GenericEndpointContext } from "@better-auth/core"; /** * Converts an expirationTime to ISO seconds expiration time (the format of JWT exp) * * See https://github.com/panva/jose/blob/main/src/lib/jwt_claims_set.ts#L245 * * @param expirationTime - see options.jwt.expirationTime * @param iat - the iat time to consolidate on * @returns */ export function toExpJWT( expirationTime: number | Date | string, iat: number, ): number { if (typeof expirationTime === "number") { return expirationTime; } else if (expirationTime instanceof Date) { return Math.floor(expirationTime.getTime() / 1000); } else { return iat + joseSecs(expirationTime); } } async function deriveKey(secretKey: string): Promise<CryptoKey> { const enc = new TextEncoder(); const subtle = getWebcryptoSubtle(); const keyMaterial = await subtle.importKey( "raw", enc.encode(secretKey), { name: "PBKDF2" }, false, ["deriveKey"], ); return subtle.deriveKey( { name: "PBKDF2", salt: enc.encode("encryption_salt"), iterations: 100000, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], ); } export async function encryptPrivateKey( privateKey: string, secretKey: string, ): Promise<{ encryptedPrivateKey: string; iv: string; authTag: string }> { const key = await deriveKey(secretKey); // Derive a 32-byte key from the provided secret const iv = crypto.getRandomValues(new Uint8Array(12)); // 12-byte IV for AES-GCM const enc = new TextEncoder(); const ciphertext = await getWebcryptoSubtle().encrypt( { name: "AES-GCM", iv: iv, }, key, enc.encode(privateKey), ); const encryptedPrivateKey = base64.encode(ciphertext); const ivBase64 = base64.encode(iv); return { encryptedPrivateKey, iv: ivBase64, authTag: encryptedPrivateKey.slice(-16), }; } export async function decryptPrivateKey( encryptedPrivate: { encryptedPrivateKey: string; iv: string; authTag: string; }, secretKey: string, ): Promise<string> { const key = await deriveKey(secretKey); const { encryptedPrivateKey, iv } = encryptedPrivate; const ivBuffer = base64.decode(iv); const ciphertext = base64.decode(encryptedPrivateKey); const decrypted = await getWebcryptoSubtle().decrypt( { name: "AES-GCM", iv: ivBuffer as BufferSource, }, key, ciphertext as BufferSource, ); const dec = new TextDecoder(); return dec.decode(decrypted); } export async function generateExportedKeyPair(options?: JwtOptions) { const { alg, ...cfg } = options?.jwks?.keyPairConfig ?? { alg: "EdDSA", crv: "Ed25519", }; const { publicKey, privateKey } = await generateKeyPair(alg, { ...cfg, extractable: true, }); const publicWebKey = await exportJWK(publicKey); const privateWebKey = await exportJWK(privateKey); return { publicWebKey, privateWebKey, alg, cfg }; } /** * Creates a Jwk on the database * * @param ctx * @param options * @returns */ export async function createJwk( ctx: GenericEndpointContext, options?: JwtOptions, ) { const { publicWebKey, privateWebKey, alg, cfg } = await generateExportedKeyPair(options); const stringifiedPrivateWebKey = JSON.stringify(privateWebKey); const privateKeyEncryptionEnabled = !options?.jwks?.disablePrivateKeyEncryption; let jwk: Omit<Jwk, "id"> = { alg, ...(cfg && "crv" in cfg ? { crv: (cfg as { crv: (typeof jwk)["crv"] }).crv, } : {}), publicKey: JSON.stringify(publicWebKey), privateKey: privateKeyEncryptionEnabled ? JSON.stringify( await symmetricEncrypt({ key: ctx.context.secret, data: stringifiedPrivateWebKey, }), ) : stringifiedPrivateWebKey, createdAt: new Date(), }; const adapter = getJwksAdapter(ctx.context.adapter); const key = await adapter.createJwk(jwk as Jwk); return key; } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/sheet.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as SheetPrimitive from "@radix-ui/react-dialog"; import { Cross2Icon } from "@radix-ui/react-icons"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const Sheet = SheetPrimitive.Root; const SheetTrigger = SheetPrimitive.Trigger; const SheetClose = SheetPrimitive.Close; const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> & { ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Overlay>>; }) => ( <SheetPrimitive.Overlay className={cn( "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className, )} {...props} ref={ref} /> ); SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", { variants: { side: { top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", }, }, defaultVariants: { side: "right", }, }, ); interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {} const SheetContent = ({ ref, side = "right", className, children, ...props }: SheetContentProps & { ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Content>>; }) => ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props} > <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> <Cross2Icon className="h-4 w-4" /> <span className="sr-only">Close</span> </SheetPrimitive.Close> {children} </SheetPrimitive.Content> </SheetPortal> ); SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn( "flex flex-col space-y-2 text-center sm:text-left", className, )} {...props} /> ); SheetHeader.displayName = "SheetHeader"; const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( <div className={cn( "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className, )} {...props} /> ); SheetFooter.displayName = "SheetFooter"; const SheetTitle = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> & { ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Title>>; }) => ( <SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} /> ); SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> & { ref: React.RefObject<React.ElementRef<typeof SheetPrimitive.Description>>; }) => ( <SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> ); SheetDescription.displayName = SheetPrimitive.Description.displayName; export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, }; ```