This is page 16 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /demo/nextjs/components/one-tap.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { client, signIn } from "@/lib/auth-client"; 4 | import { useEffect, useState } from "react"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogDescription, 9 | DialogHeader, 10 | DialogTitle, 11 | } from "./ui/dialog"; 12 | import { Input } from "./ui/input"; 13 | import { useRouter } from "next/navigation"; 14 | import Link from "next/link"; 15 | import { PasswordInput } from "./ui/password-input"; 16 | import { Checkbox } from "./ui/checkbox"; 17 | import { Button } from "./ui/button"; 18 | import { Key, Loader2 } from "lucide-react"; 19 | import { toast } from "sonner"; 20 | import { Label } from "./ui/label"; 21 | 22 | export function OneTap() { 23 | const [isOpen, setIsOpen] = useState(false); 24 | useEffect(() => { 25 | client.oneTap({ 26 | onPromptNotification(notification) { 27 | setIsOpen(true); 28 | }, 29 | }); 30 | }, []); 31 | return ( 32 | <Dialog open={isOpen} onOpenChange={(change) => setIsOpen(change)}> 33 | <DialogContent> 34 | <DialogHeader> 35 | <DialogTitle className="text-lg md:text-xl">Sign In</DialogTitle> 36 | <DialogDescription className="text-xs md:text-sm"> 37 | Enter your email below to login to your account 38 | </DialogDescription> 39 | </DialogHeader> 40 | <SignInBox /> 41 | </DialogContent> 42 | </Dialog> 43 | ); 44 | } 45 | 46 | function SignInBox() { 47 | const [email, setEmail] = useState(""); 48 | const [password, setPassword] = useState(""); 49 | const [rememberMe, setRememberMe] = useState(false); 50 | const router = useRouter(); 51 | const [loading, setLoading] = useState(false); 52 | return ( 53 | <div className="grid gap-4"> 54 | <div className="grid gap-2"> 55 | <Label htmlFor="email">Email</Label> 56 | <Input 57 | id="email" 58 | type="email" 59 | placeholder="[email protected]" 60 | required 61 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 62 | setEmail(e.target.value); 63 | }} 64 | value={email} 65 | /> 66 | </div> 67 | <div className="grid gap-2"> 68 | <div className="flex items-center"> 69 | <Label htmlFor="password">Password</Label> 70 | <Link 71 | href="/forget-password" 72 | className="ml-auto inline-block text-sm underline" 73 | > 74 | Forgot your password? 75 | </Link> 76 | </div> 77 | <PasswordInput 78 | id="password" 79 | value={password} 80 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => 81 | setPassword(e.target.value) 82 | } 83 | autoComplete="password" 84 | placeholder="Password" 85 | /> 86 | </div> 87 | <div className="flex items-center gap-2"> 88 | <Checkbox 89 | onClick={() => { 90 | setRememberMe(!rememberMe); 91 | }} 92 | /> 93 | <Label>Remember me</Label> 94 | </div> 95 | 96 | <Button 97 | type="submit" 98 | className="w-full" 99 | disabled={loading} 100 | onClick={async () => { 101 | await signIn.email( 102 | { 103 | email: email, 104 | password: password, 105 | callbackURL: "/dashboard", 106 | rememberMe, 107 | }, 108 | { 109 | onRequest: () => { 110 | setLoading(true); 111 | }, 112 | onResponse: () => { 113 | setLoading(false); 114 | }, 115 | onError: (ctx) => { 116 | toast.error(ctx.error.message); 117 | }, 118 | }, 119 | ); 120 | }} 121 | > 122 | {loading ? <Loader2 size={16} className="animate-spin" /> : "Login"} 123 | </Button> 124 | <Button 125 | variant="outline" 126 | className=" gap-2" 127 | onClick={async () => { 128 | await signIn.social({ 129 | provider: "google", 130 | callbackURL: "/dashboard", 131 | }); 132 | }} 133 | > 134 | <svg 135 | xmlns="http://www.w3.org/2000/svg" 136 | width="0.98em" 137 | height="1em" 138 | viewBox="0 0 256 262" 139 | > 140 | <path 141 | fill="#4285F4" 142 | d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" 143 | /> 144 | <path 145 | fill="#34A853" 146 | d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" 147 | /> 148 | <path 149 | fill="#FBBC05" 150 | d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z" 151 | /> 152 | <path 153 | fill="#EB4335" 154 | d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" 155 | /> 156 | </svg> 157 | <p>Continue With Google</p> 158 | </Button> 159 | <Button 160 | variant="outline" 161 | className="gap-2" 162 | onClick={async () => { 163 | await signIn.passkey({ 164 | fetchOptions: { 165 | onSuccess(context) { 166 | router.push("/dashboard"); 167 | }, 168 | onError(context) { 169 | toast.error(context.error.message); 170 | }, 171 | }, 172 | }); 173 | }} 174 | > 175 | <Key size={16} /> 176 | Sign-in with Passkey 177 | </Button> 178 | </div> 179 | ); 180 | } 181 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/client-test/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState, useTransition } from "react"; 4 | import { signIn, client } from "@/lib/auth-client"; 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardHeader, 11 | CardTitle, 12 | CardFooter, 13 | } from "@/components/ui/card"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Label } from "@/components/ui/label"; 16 | import { toast } from "sonner"; 17 | import { Loader2 } from "lucide-react"; 18 | 19 | export default function ClientTest() { 20 | const [email, setEmail] = useState(""); 21 | const [password, setPassword] = useState(""); 22 | const [loading, startTransition] = useTransition(); 23 | 24 | // Get the session data using the useSession hook 25 | const { data: session, isPending, error } = client.useSession(); 26 | 27 | const handleLogin = async () => { 28 | startTransition(async () => { 29 | await signIn.email( 30 | { 31 | email, 32 | password, 33 | callbackURL: "/client-test", 34 | }, 35 | { 36 | onError: (ctx) => { 37 | toast.error(ctx.error.message); 38 | }, 39 | onSuccess: () => { 40 | toast.success("Successfully logged in!"); 41 | setEmail(""); 42 | setPassword(""); 43 | }, 44 | }, 45 | ); 46 | }); 47 | }; 48 | 49 | return ( 50 | <div className="container mx-auto py-10 space-y-8"> 51 | <h1 className="text-2xl font-bold text-center"> 52 | Client Authentication Test 53 | </h1> 54 | 55 | <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> 56 | {/* Login Form */} 57 | <Card> 58 | <CardHeader> 59 | <CardTitle>Sign In</CardTitle> 60 | <CardDescription> 61 | Enter your email and password to sign in 62 | </CardDescription> 63 | </CardHeader> 64 | <CardContent> 65 | <div className="grid gap-4"> 66 | <div className="grid gap-2"> 67 | <Label htmlFor="email">Email</Label> 68 | <Input 69 | id="email" 70 | type="email" 71 | placeholder="[email protected]" 72 | value={email} 73 | onChange={(e) => setEmail(e.target.value)} 74 | /> 75 | </div> 76 | <div className="grid gap-2"> 77 | <Label htmlFor="password">Password</Label> 78 | <Input 79 | id="password" 80 | type="password" 81 | placeholder="••••••••" 82 | value={password} 83 | onChange={(e) => setPassword(e.target.value)} 84 | /> 85 | </div> 86 | </div> 87 | </CardContent> 88 | <CardFooter> 89 | <Button className="w-full" onClick={handleLogin} disabled={loading}> 90 | {loading ? ( 91 | <> 92 | <Loader2 size={16} className="mr-2 animate-spin" /> 93 | Signing in... 94 | </> 95 | ) : ( 96 | "Sign In" 97 | )} 98 | </Button> 99 | </CardFooter> 100 | </Card> 101 | 102 | {/* Session Display */} 103 | <Card> 104 | <CardHeader> 105 | <CardTitle>Session Information</CardTitle> 106 | <CardDescription> 107 | {isPending 108 | ? "Loading session..." 109 | : session 110 | ? "You are currently logged in" 111 | : "You are not logged in"} 112 | </CardDescription> 113 | </CardHeader> 114 | <CardContent> 115 | {isPending ? ( 116 | <div className="flex justify-center py-4"> 117 | <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> 118 | </div> 119 | ) : error ? ( 120 | <div className="p-4 bg-destructive/10 text-destructive rounded-md"> 121 | Error: {error.message} 122 | </div> 123 | ) : session ? ( 124 | <div className="space-y-4"> 125 | <div className="flex items-center gap-4"> 126 | {session.user.image ? ( 127 | <img 128 | src={session.user.image} 129 | alt="Profile" 130 | className="h-12 w-12 rounded-full object-cover" 131 | /> 132 | ) : ( 133 | <div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center"> 134 | <span className="text-lg font-medium"> 135 | {session.user.name?.charAt(0) || 136 | session.user.email?.charAt(0)} 137 | </span> 138 | </div> 139 | )} 140 | <div> 141 | <p className="font-medium">{session.user.name}</p> 142 | <p className="text-sm text-muted-foreground"> 143 | {session.user.email} 144 | </p> 145 | </div> 146 | </div> 147 | 148 | <div className="rounded-md bg-muted p-4"> 149 | <p className="text-sm font-medium mb-2">Session Details:</p> 150 | <pre className="text-xs overflow-auto max-h-40"> 151 | {JSON.stringify(session, null, 2)} 152 | </pre> 153 | </div> 154 | </div> 155 | ) : ( 156 | <div className="py-8 text-center text-muted-foreground"> 157 | <p>Sign in to view your session information</p> 158 | </div> 159 | )} 160 | </CardContent> 161 | {session && ( 162 | <CardFooter> 163 | <Button 164 | variant="outline" 165 | className="w-full" 166 | onClick={() => 167 | client.signOut({ 168 | fetchOptions: { 169 | onSuccess: () => { 170 | toast.success("Successfully signed out!"); 171 | }, 172 | }, 173 | }) 174 | } 175 | > 176 | Sign Out 177 | </Button> 178 | </CardFooter> 179 | )} 180 | </Card> 181 | </div> 182 | </div> 183 | ); 184 | } 185 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/magic-link.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Magic link 3 | description: Magic link plugin 4 | --- 5 | 6 | Magic link or email link is a way to authenticate users without a password. When a user enters their email, a link is sent to their email. When the user clicks on the link, they are authenticated. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ### Add the server Plugin 13 | 14 | Add the magic link plugin to your server: 15 | 16 | ```ts title="server.ts" 17 | import { betterAuth } from "better-auth"; 18 | import { magicLink } from "better-auth/plugins"; 19 | 20 | export const auth = betterAuth({ 21 | plugins: [ 22 | magicLink({ 23 | sendMagicLink: async ({ email, token, url }, request) => { 24 | // send email to user 25 | } 26 | }) 27 | ] 28 | }) 29 | ``` 30 | </Step> 31 | 32 | <Step> 33 | ### Add the client Plugin 34 | 35 | Add the magic link plugin to your client: 36 | 37 | ```ts title="auth-client.ts" 38 | import { createAuthClient } from "better-auth/client"; 39 | import { magicLinkClient } from "better-auth/client/plugins"; 40 | export const authClient = createAuthClient({ 41 | plugins: [ 42 | magicLinkClient() 43 | ] 44 | }); 45 | ``` 46 | </Step> 47 | 48 | </Steps> 49 | 50 | ## Usage 51 | 52 | ### Sign In with Magic Link 53 | 54 | To sign in with a magic link, you need to call `signIn.magicLink` with the user's email address. The `sendMagicLink` function is called to send the magic link to the user's email. 55 | 56 | 57 | <APIMethod 58 | path="/sign-in/magic-link" 59 | method="POST" 60 | requireSession 61 | > 62 | 63 | ```ts 64 | type signInMagicLink = { 65 | /** 66 | * Email address to send the magic link. 67 | */ 68 | email: string = "[email protected]" 69 | /** 70 | * User display name. Only used if the user is registering for the first time. 71 | */ 72 | name?: string = "my-name" 73 | /** 74 | * URL to redirect after magic link verification. 75 | */ 76 | callbackURL?: string = "/dashboard" 77 | /** 78 | * URL to redirect after new user signup 79 | */ 80 | newUserCallbackURL?: string = "/welcome" 81 | /** 82 | * URL to redirect if an error happen on verification 83 | * If only callbackURL is provided but without an `errorCallbackURL` then they will be 84 | * redirected to the callbackURL with an `error` query parameter. 85 | */ 86 | errorCallbackURL?: string = "/error" 87 | } 88 | ``` 89 | </APIMethod> 90 | 91 | <Callout> 92 | If the user has not signed up, unless `disableSignUp` is set to `true`, the user will be signed up automatically. 93 | </Callout> 94 | 95 | ### Verify Magic Link 96 | 97 | When you send the URL generated by the `sendMagicLink` function to a user, clicking the link will authenticate them and redirect them to the `callbackURL` specified in the `signIn.magicLink` function. If an error occurs, the user will be redirected to the `callbackURL` with an error query parameter. 98 | 99 | <Callout type="warn"> 100 | If no `callbackURL` is provided, the user will be redirected to the root URL. 101 | </Callout> 102 | 103 | If you want to handle the verification manually, (e.g, if you send the user a different URL), you can use the `verify` function. 104 | 105 | 106 | <APIMethod 107 | path="/magic-link/verify" 108 | method="GET" 109 | requireSession 110 | > 111 | ```ts 112 | type magicLinkVerify = { 113 | /** 114 | * Verification token. 115 | */ 116 | token: string = "123456" 117 | /** 118 | * URL to redirect after magic link verification, if not provided will return the session. 119 | */ 120 | callbackURL?: string = "/dashboard" 121 | } 122 | ``` 123 | </APIMethod> 124 | 125 | ## Configuration Options 126 | 127 | **sendMagicLink**: The `sendMagicLink` function is called when a user requests a magic link. It takes an object with the following properties: 128 | 129 | - `email`: The email address of the user. 130 | - `url`: The URL to be sent to the user. This URL contains the token. 131 | - `token`: The token if you want to send the token with custom URL. 132 | 133 | and a `request` object as the second parameter. 134 | 135 | **expiresIn**: specifies the time in seconds after which the magic link will expire. The default value is `300` seconds (5 minutes). 136 | 137 | **disableSignUp**: If set to `true`, the user will not be able to sign up using the magic link. The default value is `false`. 138 | 139 | **generateToken**: The `generateToken` function is called to generate a token which is used to uniquely identify the user. The default value is a random string. There is one parameter: 140 | 141 | - `email`: The email address of the user. 142 | 143 | <Callout type="warn"> 144 | When using `generateToken`, ensure that the returned string is hard to guess 145 | because it is used to verify who someone actually is in a confidential way. By 146 | default, we return a long and cryptographically secure string. 147 | </Callout> 148 | 149 | **storeToken**: The `storeToken` function is called to store the magic link token in the database. The default value is `"plain"`. 150 | 151 | The `storeToken` function can be one of the following: 152 | 153 | - `"plain"`: The token is stored in plain text. 154 | - `"hashed"`: The token is hashed using the default hasher. 155 | - `{ type: "custom-hasher", hash: (token: string) => Promise<string> }`: The token is hashed using a custom hasher. 156 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/tests/auth-flow.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect } from "vitest"; 2 | import { createTestSuite } from "../create-test-suite"; 3 | 4 | /** 5 | * This test suite tests basic authentication flow using the adapter. 6 | */ 7 | export const authFlowTestSuite = createTestSuite( 8 | "auth-flow", 9 | {}, 10 | ( 11 | { generate, getAuth, modifyBetterAuthOptions, tryCatch }, 12 | debug?: { showDB?: () => Promise<void> }, 13 | ) => ({ 14 | "should successfully sign up": async () => { 15 | await modifyBetterAuthOptions( 16 | { 17 | emailAndPassword: { 18 | enabled: true, 19 | password: { hash: async (password) => password }, 20 | }, 21 | }, 22 | false, 23 | ); 24 | const auth = await getAuth(); 25 | const user = await generate("user"); 26 | const start = Date.now(); 27 | const result = await auth.api.signUpEmail({ 28 | body: { 29 | email: user.email, 30 | password: crypto.randomUUID(), 31 | name: user.name, 32 | image: user.image || "", 33 | }, 34 | }); 35 | const end = Date.now(); 36 | console.log(`signUpEmail took ${end - start}ms (without hashing)`); 37 | expect(result.user).toBeDefined(); 38 | expect(result.user.email).toBe(user.email); 39 | expect(result.user.name).toBe(user.name); 40 | expect(result.user.image).toBe(user.image || ""); 41 | expect(result.user.emailVerified).toBe(false); 42 | expect(result.user.createdAt).toBeDefined(); 43 | expect(result.user.updatedAt).toBeDefined(); 44 | }, 45 | "should successfully sign in": async () => { 46 | await modifyBetterAuthOptions( 47 | { 48 | emailAndPassword: { 49 | enabled: true, 50 | password: { 51 | hash: async (password) => password, 52 | async verify(data) { 53 | return data.hash === data.password; 54 | }, 55 | }, 56 | }, 57 | }, 58 | false, 59 | ); 60 | const auth = await getAuth(); 61 | const user = await generate("user"); 62 | const password = crypto.randomUUID(); 63 | const signUpResult = await auth.api.signUpEmail({ 64 | body: { 65 | email: user.email, 66 | password: password, 67 | name: user.name, 68 | image: user.image || "", 69 | }, 70 | }); 71 | const start = Date.now(); 72 | const result = await auth.api.signInEmail({ 73 | body: { email: user.email, password: password }, 74 | }); 75 | const end = Date.now(); 76 | console.log(`signInEmail took ${end - start}ms (without hashing)`); 77 | expect(result.user).toBeDefined(); 78 | expect(result.user.id).toBe(signUpResult.user.id); 79 | }, 80 | "should successfully get session": async () => { 81 | await modifyBetterAuthOptions( 82 | { 83 | emailAndPassword: { 84 | enabled: true, 85 | password: { hash: async (password) => password }, 86 | }, 87 | }, 88 | false, 89 | ); 90 | const auth = await getAuth(); 91 | const user = await generate("user"); 92 | const password = crypto.randomUUID(); 93 | 94 | const { headers, response: signUpResult } = await auth.api.signUpEmail({ 95 | body: { 96 | email: user.email, 97 | password: password, 98 | name: user.name, 99 | image: user.image || "", 100 | }, 101 | returnHeaders: true, 102 | }); 103 | 104 | // Convert set-cookie header to cookie header for getSession call 105 | const modifiedHeaders = new Headers(headers); 106 | if (headers.has("set-cookie")) { 107 | modifiedHeaders.set("cookie", headers.getSetCookie().join("; ")); 108 | modifiedHeaders.delete("set-cookie"); 109 | } 110 | 111 | const start = Date.now(); 112 | const result = await auth.api.getSession({ 113 | headers: modifiedHeaders, 114 | }); 115 | const end = Date.now(); 116 | console.log(`getSession took ${end - start}ms`); 117 | expect(result?.user).toBeDefined(); 118 | expect(result?.user).toStrictEqual(signUpResult.user); 119 | expect(result?.session).toBeDefined(); 120 | }, 121 | "should not sign in with invalid email": async () => { 122 | await modifyBetterAuthOptions( 123 | { emailAndPassword: { enabled: true } }, 124 | false, 125 | ); 126 | const auth = await getAuth(); 127 | const user = await generate("user"); 128 | const { data, error } = await tryCatch( 129 | auth.api.signInEmail({ 130 | body: { email: user.email, password: crypto.randomUUID() }, 131 | }), 132 | ); 133 | expect(data).toBeNull(); 134 | expect(error).toBeDefined(); 135 | }, 136 | "should store and retrieve timestamps correctly across timezones": 137 | async () => { 138 | using _ = recoverProcessTZ(); 139 | await modifyBetterAuthOptions( 140 | { emailAndPassword: { enabled: true } }, 141 | false, 142 | ); 143 | const auth = await getAuth(); 144 | const user = await generate("user"); 145 | const password = crypto.randomUUID(); 146 | const userSignUp = await auth.api.signUpEmail({ 147 | body: { 148 | email: user.email, 149 | password: password, 150 | name: user.name, 151 | image: user.image || "", 152 | }, 153 | }); 154 | process.env.TZ = "Europe/London"; 155 | const userSignIn = await auth.api.signInEmail({ 156 | body: { email: user.email, password: password }, 157 | }); 158 | process.env.TZ = "America/Los_Angeles"; 159 | expect(userSignUp.user.createdAt.toISOString()).toStrictEqual( 160 | userSignIn.user.createdAt.toISOString(), 161 | ); 162 | }, 163 | }), 164 | ); 165 | 166 | function recoverProcessTZ() { 167 | const originalTZ = process.env.TZ; 168 | return { 169 | [Symbol.dispose]: () => { 170 | process.env.TZ = originalTZ; 171 | }, 172 | }; 173 | } 174 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/remix.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Remix Integration 3 | description: Integrate Better Auth with Remix. 4 | --- 5 | 6 | Better Auth can be easily integrated with Remix. This guide will show you how to integrate Better Auth with Remix. 7 | 8 | You can follow the steps from [installation](/docs/installation) to get started or you can follow this guide to make it the Remix-way. 9 | 10 | If you have followed the installation steps, you can skip the first step. 11 | 12 | ## Create auth instance 13 | 14 | 15 | Create a file named `auth.server.ts` in one of these locations: 16 | - Project root 17 | - `lib/` folder 18 | - `utils/` folder 19 | 20 | You can also nest any of these folders under `app/` folder. (e.g. `app/lib/auth.server.ts`) 21 | 22 | And in this file, import Better Auth and create your instance. 23 | 24 | <Callout type="warn"> 25 | Make sure to export the auth instance with the variable name `auth` or as a `default` export. 26 | </Callout> 27 | 28 | ```ts title="app/lib/auth.server.ts" 29 | import { betterAuth } from "better-auth" 30 | 31 | export const auth = betterAuth({ 32 | database: { 33 | provider: "postgres", //change this to your database provider 34 | url: process.env.DATABASE_URL, // path to your database or connection string 35 | } 36 | }) 37 | ``` 38 | 39 | ## Create API Route 40 | 41 | We need to mount the handler to a API route. Create a resource route file `api.auth.$.ts` inside `app/routes/` directory. And add the following code: 42 | 43 | ```ts title="app/routes/api.auth.$.ts" 44 | import { auth } from '~/lib/auth.server' // Adjust the path as necessary 45 | import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node" 46 | 47 | export async function loader({ request }: LoaderFunctionArgs) { 48 | return auth.handler(request) 49 | } 50 | 51 | export async function action({ request }: ActionFunctionArgs) { 52 | return auth.handler(request) 53 | } 54 | ``` 55 | 56 | <Callout type="info"> 57 | You can change the path on your better-auth configuration but it's recommended to keep it as `routes/api.auth.$.ts` 58 | </Callout> 59 | 60 | ## Create a client 61 | 62 | Create a client instance. Here we are creating `auth-client.ts` file inside the `lib/` directory. 63 | 64 | ```ts title="app/lib/auth-client.ts" 65 | import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react 66 | 67 | export const authClient = createAuthClient({ 68 | //you can pass client configuration here 69 | }) 70 | ``` 71 | 72 | Once you have created the client, you can use it to sign up, sign in, and perform other actions. 73 | 74 | ### Example usage 75 | 76 | #### Sign Up 77 | 78 | ```ts title="app/routes/signup.tsx" 79 | import { Form } from "@remix-run/react" 80 | import { useState } from "react" 81 | import { authClient } from "~/lib/auth-client" 82 | 83 | export default function SignUp() { 84 | const [email, setEmail] = useState("") 85 | const [name, setName] = useState("") 86 | const [password, setPassword] = useState("") 87 | 88 | const signUp = async () => { 89 | await authClient.signUp.email( 90 | { 91 | email, 92 | password, 93 | name, 94 | }, 95 | { 96 | onRequest: (ctx) => { 97 | // show loading state 98 | }, 99 | onSuccess: (ctx) => { 100 | // redirect to home 101 | }, 102 | onError: (ctx) => { 103 | alert(ctx.error) 104 | }, 105 | }, 106 | ) 107 | } 108 | 109 | return ( 110 | <div> 111 | <h2> 112 | Sign Up 113 | </h2> 114 | <Form 115 | onSubmit={signUp} 116 | > 117 | <input 118 | type="text" 119 | value={name} 120 | onChange={(e) => setName(e.target.value)} 121 | placeholder="Name" 122 | /> 123 | <input 124 | type="email" 125 | value={email} 126 | onChange={(e) => setEmail(e.target.value)} 127 | placeholder="Email" 128 | /> 129 | <input 130 | type="password" 131 | value={password} 132 | onChange={(e) => setPassword(e.target.value)} 133 | placeholder="Password" 134 | /> 135 | <button 136 | type="submit" 137 | > 138 | Sign Up 139 | </button> 140 | </Form> 141 | </div> 142 | ) 143 | } 144 | 145 | ``` 146 | 147 | #### Sign In 148 | 149 | ```ts title="app/routes/signin.tsx" 150 | import { Form } from "@remix-run/react" 151 | import { useState } from "react" 152 | import { authClient } from "~/services/auth-client" 153 | 154 | export default function SignIn() { 155 | const [email, setEmail] = useState("") 156 | const [password, setPassword] = useState("") 157 | 158 | const signIn = async () => { 159 | await authClient.signIn.email( 160 | { 161 | email, 162 | password, 163 | }, 164 | { 165 | onRequest: (ctx) => { 166 | // show loading state 167 | }, 168 | onSuccess: (ctx) => { 169 | // redirect to home 170 | }, 171 | onError: (ctx) => { 172 | alert(ctx.error) 173 | }, 174 | }, 175 | ) 176 | } 177 | 178 | return ( 179 | <div> 180 | <h2> 181 | Sign In 182 | </h2> 183 | <Form onSubmit={signIn}> 184 | <input 185 | type="email" 186 | value={email} 187 | onChange={(e) => setEmail(e.target.value)} 188 | /> 189 | <input 190 | type="password" 191 | value={password} 192 | onChange={(e) => setPassword(e.target.value)} 193 | /> 194 | <button 195 | type="submit" 196 | > 197 | Sign In 198 | </button> 199 | </Form> 200 | </div> 201 | ) 202 | } 203 | ``` 204 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/error-codes.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineErrorCodes } from "@better-auth/core/utils"; 2 | 3 | export const ORGANIZATION_ERROR_CODES = defineErrorCodes({ 4 | YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION: 5 | "You are not allowed to create a new organization", 6 | YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS: 7 | "You have reached the maximum number of organizations", 8 | ORGANIZATION_ALREADY_EXISTS: "Organization already exists", 9 | ORGANIZATION_SLUG_ALREADY_TAKEN: "Organization slug already taken", 10 | ORGANIZATION_NOT_FOUND: "Organization not found", 11 | USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION: 12 | "User is not a member of the organization", 13 | YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION: 14 | "You are not allowed to update this organization", 15 | YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION: 16 | "You are not allowed to delete this organization", 17 | NO_ACTIVE_ORGANIZATION: "No active organization", 18 | USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION: 19 | "User is already a member of this organization", 20 | MEMBER_NOT_FOUND: "Member not found", 21 | ROLE_NOT_FOUND: "Role not found", 22 | YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM: 23 | "You are not allowed to create a new team", 24 | TEAM_ALREADY_EXISTS: "Team already exists", 25 | TEAM_NOT_FOUND: "Team not found", 26 | YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER: 27 | "You cannot leave the organization as the only owner", 28 | YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER: 29 | "You cannot leave the organization without an owner", 30 | YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER: 31 | "You are not allowed to delete this member", 32 | YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION: 33 | "You are not allowed to invite users to this organization", 34 | USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION: 35 | "User is already invited to this organization", 36 | INVITATION_NOT_FOUND: "Invitation not found", 37 | YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION: 38 | "You are not the recipient of the invitation", 39 | EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION: 40 | "Email verification required before accepting or rejecting invitation", 41 | YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION: 42 | "You are not allowed to cancel this invitation", 43 | INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION: 44 | "Inviter is no longer a member of the organization", 45 | YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE: 46 | "You are not allowed to invite a user with this role", 47 | FAILED_TO_RETRIEVE_INVITATION: "Failed to retrieve invitation", 48 | YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS: 49 | "You have reached the maximum number of teams", 50 | UNABLE_TO_REMOVE_LAST_TEAM: "Unable to remove last team", 51 | YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER: 52 | "You are not allowed to update this member", 53 | ORGANIZATION_MEMBERSHIP_LIMIT_REACHED: 54 | "Organization membership limit reached", 55 | YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION: 56 | "You are not allowed to create teams in this organization", 57 | YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION: 58 | "You are not allowed to delete teams in this organization", 59 | YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM: 60 | "You are not allowed to update this team", 61 | YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM: 62 | "You are not allowed to delete this team", 63 | INVITATION_LIMIT_REACHED: "Invitation limit reached", 64 | TEAM_MEMBER_LIMIT_REACHED: "Team member limit reached", 65 | USER_IS_NOT_A_MEMBER_OF_THE_TEAM: "User is not a member of the team", 66 | YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM: 67 | "You are not allowed to list the members of this team", 68 | YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM: "You do not have an active team", 69 | YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER: 70 | "You are not allowed to create a new member", 71 | YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER: 72 | "You are not allowed to remove a team member", 73 | YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION: 74 | "You are not allowed to access this organization as an owner", 75 | YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION: 76 | "You are not a member of this organization", 77 | MISSING_AC_INSTANCE: 78 | "Dynamic Access Control requires a pre-defined ac instance on the server auth plugin. Read server logs for more information", 79 | YOU_MUST_BE_IN_AN_ORGANIZATION_TO_CREATE_A_ROLE: 80 | "You must be in an organization to create a role", 81 | YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE: "You are not allowed to create a role", 82 | YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE: "You are not allowed to update a role", 83 | YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE: "You are not allowed to delete a role", 84 | YOU_ARE_NOT_ALLOWED_TO_READ_A_ROLE: "You are not allowed to read a role", 85 | YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE: "You are not allowed to list a role", 86 | YOU_ARE_NOT_ALLOWED_TO_GET_A_ROLE: "You are not allowed to get a role", 87 | TOO_MANY_ROLES: "This organization has too many roles", 88 | INVALID_RESOURCE: "The provided permission includes an invalid resource", 89 | ROLE_NAME_IS_ALREADY_TAKEN: "That role name is already taken", 90 | CANNOT_DELETE_A_PRE_DEFINED_ROLE: "Cannot delete a pre-defined role", 91 | }); 92 | ``` -------------------------------------------------------------------------------- /packages/telemetry/src/detectors/detect-system-info.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { env } from "@better-auth/core/env"; 2 | import { importRuntime } from "../utils/import-util"; 3 | 4 | function getVendor() { 5 | const hasAny = (...keys: string[]) => 6 | keys.some((k) => Boolean((env as any)[k])); 7 | 8 | if ( 9 | hasAny("CF_PAGES", "CF_PAGES_URL", "CF_ACCOUNT_ID") || 10 | (typeof navigator !== "undefined" && 11 | navigator.userAgent === "Cloudflare-Workers") 12 | ) { 13 | return "cloudflare"; 14 | } 15 | 16 | if (hasAny("VERCEL", "VERCEL_URL", "VERCEL_ENV")) return "vercel"; 17 | 18 | if (hasAny("NETLIFY", "NETLIFY_URL")) return "netlify"; 19 | 20 | if ( 21 | hasAny( 22 | "RENDER", 23 | "RENDER_URL", 24 | "RENDER_INTERNAL_HOSTNAME", 25 | "RENDER_SERVICE_ID", 26 | ) 27 | ) { 28 | return "render"; 29 | } 30 | 31 | if ( 32 | hasAny("AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV", "LAMBDA_TASK_ROOT") 33 | ) { 34 | return "aws"; 35 | } 36 | 37 | if ( 38 | hasAny( 39 | "GOOGLE_CLOUD_FUNCTION_NAME", 40 | "GOOGLE_CLOUD_PROJECT", 41 | "GCP_PROJECT", 42 | "K_SERVICE", 43 | ) 44 | ) { 45 | return "gcp"; 46 | } 47 | 48 | if ( 49 | hasAny( 50 | "AZURE_FUNCTION_NAME", 51 | "FUNCTIONS_WORKER_RUNTIME", 52 | "WEBSITE_INSTANCE_ID", 53 | "WEBSITE_SITE_NAME", 54 | ) 55 | ) { 56 | return "azure"; 57 | } 58 | 59 | if (hasAny("DENO_DEPLOYMENT_ID", "DENO_REGION")) return "deno-deploy"; 60 | 61 | if (hasAny("FLY_APP_NAME", "FLY_REGION", "FLY_ALLOC_ID")) return "fly-io"; 62 | 63 | if (hasAny("RAILWAY_STATIC_URL", "RAILWAY_ENVIRONMENT_NAME")) 64 | return "railway"; 65 | 66 | if (hasAny("DYNO", "HEROKU_APP_NAME")) return "heroku"; 67 | 68 | if (hasAny("DO_DEPLOYMENT_ID", "DO_APP_NAME", "DIGITALOCEAN")) 69 | return "digitalocean"; 70 | 71 | if (hasAny("KOYEB", "KOYEB_DEPLOYMENT_ID", "KOYEB_APP_NAME")) return "koyeb"; 72 | 73 | return null; 74 | } 75 | 76 | export async function detectSystemInfo() { 77 | try { 78 | //check if it's cloudflare 79 | if (getVendor() === "cloudflare") return "cloudflare"; 80 | const os = await importRuntime<typeof import("os")>("os"); 81 | const cpus = os.cpus(); 82 | return { 83 | deploymentVendor: getVendor(), 84 | systemPlatform: os.platform(), 85 | systemRelease: os.release(), 86 | systemArchitecture: os.arch(), 87 | cpuCount: cpus.length, 88 | cpuModel: cpus.length ? cpus[0]!.model : null, 89 | cpuSpeed: cpus.length ? cpus[0]!.speed : null, 90 | memory: os.totalmem(), 91 | isWSL: await isWsl(), 92 | isDocker: await isDocker(), 93 | isTTY: 94 | typeof process !== "undefined" && (process as any).stdout 95 | ? (process as any).stdout.isTTY 96 | : null, 97 | }; 98 | } catch (e) { 99 | return { 100 | systemPlatform: null, 101 | systemRelease: null, 102 | systemArchitecture: null, 103 | cpuCount: null, 104 | cpuModel: null, 105 | cpuSpeed: null, 106 | memory: null, 107 | isWSL: null, 108 | isDocker: null, 109 | isTTY: null, 110 | }; 111 | } 112 | } 113 | 114 | let isDockerCached: boolean | undefined; 115 | 116 | async function hasDockerEnv() { 117 | if (getVendor() === "cloudflare") return false; 118 | 119 | try { 120 | const fs = await importRuntime<typeof import("fs")>("fs"); 121 | fs.statSync("/.dockerenv"); 122 | return true; 123 | } catch { 124 | return false; 125 | } 126 | } 127 | 128 | async function hasDockerCGroup() { 129 | if (getVendor() === "cloudflare") return false; 130 | try { 131 | const fs = await importRuntime<typeof import("fs")>("fs"); 132 | return fs.readFileSync("/proc/self/cgroup", "utf8").includes("docker"); 133 | } catch { 134 | return false; 135 | } 136 | } 137 | 138 | async function isDocker() { 139 | if (getVendor() === "cloudflare") return false; 140 | 141 | if (isDockerCached === undefined) { 142 | isDockerCached = (await hasDockerEnv()) || (await hasDockerCGroup()); 143 | } 144 | 145 | return isDockerCached; 146 | } 147 | 148 | async function isWsl() { 149 | try { 150 | if (getVendor() === "cloudflare") return false; 151 | if (typeof process === "undefined" || process?.platform !== "linux") { 152 | return false; 153 | } 154 | const fs = await importRuntime<typeof import("fs")>("fs"); 155 | const os = await importRuntime<typeof import("os")>("os"); 156 | if (os.release().toLowerCase().includes("microsoft")) { 157 | if (await isInsideContainer()) { 158 | return false; 159 | } 160 | 161 | return true; 162 | } 163 | 164 | return fs 165 | .readFileSync("/proc/version", "utf8") 166 | .toLowerCase() 167 | .includes("microsoft") 168 | ? !(await isInsideContainer()) 169 | : false; 170 | } catch { 171 | return false; 172 | } 173 | } 174 | 175 | let isInsideContainerCached: boolean | undefined; 176 | 177 | const hasContainerEnv = async () => { 178 | if (getVendor() === "cloudflare") return false; 179 | try { 180 | const fs = await importRuntime<typeof import("fs")>("fs"); 181 | fs.statSync("/run/.containerenv"); 182 | return true; 183 | } catch { 184 | return false; 185 | } 186 | }; 187 | 188 | async function isInsideContainer() { 189 | if (isInsideContainerCached === undefined) { 190 | isInsideContainerCached = (await hasContainerEnv()) || (await isDocker()); 191 | } 192 | 193 | return isInsideContainerCached; 194 | } 195 | 196 | export function isCI() { 197 | return ( 198 | env.CI !== "false" && 199 | ("BUILD_ID" in env || // Jenkins, Cloudbees 200 | "BUILD_NUMBER" in env || // Jenkins, TeamCity (fixed typo: extra space removed) 201 | "CI" in env || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari, Cloudflare 202 | "CI_APP_ID" in env || // Appflow 203 | "CI_BUILD_ID" in env || // Appflow 204 | "CI_BUILD_NUMBER" in env || // Appflow 205 | "CI_NAME" in env || // Codeship and others 206 | "CONTINUOUS_INTEGRATION" in env || // Travis CI, Cirrus CI 207 | "RUN_ID" in env) // TaskCluster, dsari 208 | ); 209 | } 210 | ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/optimizing-for-performance.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Optimizing for Performance 3 | description: A guide to optimizing your Better Auth application for performance. 4 | --- 5 | 6 | In this guide, we’ll go over some of the ways you can optimize your application for a more performant Better Auth app. 7 | 8 | ## Caching 9 | 10 | Caching is a powerful technique that can significantly improve the performance of your Better Auth application by reducing the number of database queries and speeding up response times. 11 | 12 | ### Cookie Cache 13 | 14 | Calling your database every time `useSession` or `getSession` is invoked isn’t ideal, especially if sessions don’t change frequently. Cookie caching handles this by storing session data in a short-lived, signed cookie similar to how JWT access tokens are used with refresh tokens. 15 | 16 | To turn on cookie caching, just set `session.cookieCache` in your auth config: 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth"; 20 | 21 | export const auth = betterAuth({ 22 | session: { 23 | cookieCache: { 24 | enabled: true, 25 | maxAge: 5 * 60, // Cache duration in seconds 26 | }, 27 | }, 28 | }); 29 | ``` 30 | 31 | Read more about [cookie caching](/docs/concepts/session-management#cookie-cache). 32 | 33 | ### Framework Caching 34 | 35 | Here are examples of how you can do caching in different frameworks and environments: 36 | 37 | <Tabs items={["Next", "Remix", "SolidStart", "React Query"]}> 38 | <Tab value="Next"> 39 | Since Next v15, we can use the `"use cache"` directive to cache the response of a server function. 40 | 41 | ```ts 42 | export async function getUsers() { 43 | 'use cache' // [!code highlight] 44 | const { users } = await auth.api.listUsers(); 45 | return users 46 | } 47 | ``` 48 | 49 | Learn more about NextJS use cache directive <Link href="https://nextjs.org/docs/app/api-reference/directives/use-cache">here</Link>. 50 | 51 | </Tab> 52 | <Tab value="Remix"> 53 | In Remix, you can use the `cache` option in the `loader` function to cache responses on the server. Here’s an example: 54 | 55 | ```ts 56 | import { json } from '@remix-run/node'; 57 | 58 | export const loader = async () => { 59 | const { users } = await auth.api.listUsers(); 60 | return json(users, { 61 | headers: { 62 | 'Cache-Control': 'max-age=3600', // Cache for 1 hour 63 | }, 64 | }); 65 | }; 66 | ``` 67 | 68 | 69 | You can read a nice guide on Loader vs Route Cache Headers in Remix <Link href="https://sergiodxa.com/articles/loader-vs-route-cache-headers-in-remix">here</Link>. 70 | 71 | </Tab> 72 | 73 | <Tab value="SolidStart"> 74 | In SolidStart, you can use the `query` function to cache data. Here’s an example: 75 | 76 | ```tsx 77 | const getUsers = query( 78 | async () => (await auth.api.listUsers()).users, 79 | "getUsers" 80 | ); 81 | ``` 82 | 83 | Learn more about SolidStart `query` function <Link href="https://docs.solidjs.com/solid-router/reference/data-apis/query">here</Link>. 84 | 85 | </Tab> 86 | <Tab value="React Query"> 87 | With React Query you can use the `useQuery` hook to cache data. Here’s an example: 88 | 89 | ```ts 90 | import { useQuery } from '@tanstack/react-query'; 91 | 92 | const fetchUsers = async () => { 93 | const { users } = await auth.api.listUsers(); 94 | return users; 95 | }; 96 | 97 | export default function Users() { 98 | const { data: users, isLoading } = useQuery('users', fetchUsers, { 99 | staleTime: 1000 * 60 * 15, // Cache for 15 minutes 100 | }); 101 | 102 | if (isLoading) return <div>Loading...</div>; 103 | 104 | return ( 105 | <ul> 106 | {users.map(user => ( 107 | <li key={user.id}>{user.name}</li> 108 | ))} 109 | </ul> 110 | ); 111 | } 112 | ``` 113 | 114 | Learn more about React Query use cache directive <Link href="https://react-query.tanstack.com/reference/useQuery#usecache">here</Link>. 115 | 116 | </Tab> 117 | </Tabs> 118 | 119 | ## SSR Optimizations 120 | 121 | If you're using a framework that supports server-side rendering, it's usually best to pre-fetch the user session on the server and use it as a fallback on the client. 122 | 123 | ```ts 124 | const session = await auth.api.getSession({ 125 | headers: await headers(), 126 | }); 127 | //then pass the session to the client 128 | ``` 129 | 130 | ## Database optimizations 131 | 132 | Optimizing database performance is essential to get the best out of Better Auth. 133 | 134 | #### Recommended fields to index 135 | 136 | | Table | Fields | Plugin | 137 | | ------------- | -------------------------- | ------------ | 138 | | users | `email` | | 139 | | accounts | `userId` | | 140 | | sessions | `userId`, `token` | | 141 | | verifications | `identifier` | | 142 | | invitations | `email`, `organizationId` | organization | 143 | | members | `userId`, `organizationId` | organization | 144 | | organizations | `slug` | organization | 145 | | passkey | `userId` | passkey | 146 | | twoFactor | `secret` | twoFactor | 147 | 148 | <Callout> 149 | We intend to add indexing support in our schema generation tool in the future. 150 | </Callout> 151 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | import type { BetterAuthOptions } from "@better-auth/core"; 3 | import { APIError } from "better-call"; 4 | import type { Account, Session, User } from "../types"; 5 | import type { DBFieldAttribute } from "@better-auth/core/db"; 6 | 7 | // Cache for parsed schemas to avoid reparsing on every request 8 | const cache = new WeakMap< 9 | BetterAuthOptions, 10 | Map<string, Record<string, DBFieldAttribute>> 11 | >(); 12 | 13 | function parseOutputData<T extends Record<string, any>>( 14 | data: T, 15 | schema: { 16 | fields: Record<string, DBFieldAttribute>; 17 | }, 18 | ) { 19 | const fields = schema.fields; 20 | const parsedData: Record<string, any> = {}; 21 | for (const key in data) { 22 | const field = fields[key]; 23 | if (!field) { 24 | parsedData[key] = data[key]; 25 | continue; 26 | } 27 | if (field.returned === false) { 28 | continue; 29 | } 30 | parsedData[key] = data[key]; 31 | } 32 | return parsedData as T; 33 | } 34 | 35 | function getAllFields(options: BetterAuthOptions, table: string) { 36 | if (!cache.has(options)) { 37 | cache.set(options, new Map()); 38 | } 39 | const tableCache = cache.get(options)!; 40 | if (tableCache.has(table)) { 41 | return tableCache.get(table)!; 42 | } 43 | let schema: Record<string, DBFieldAttribute> = { 44 | ...(table === "user" ? options.user?.additionalFields : {}), 45 | ...(table === "session" ? options.session?.additionalFields : {}), 46 | }; 47 | for (const plugin of options.plugins || []) { 48 | if (plugin.schema && plugin.schema[table]) { 49 | schema = { 50 | ...schema, 51 | ...plugin.schema[table].fields, 52 | }; 53 | } 54 | } 55 | cache.get(options)!.set(table, schema); 56 | return schema; 57 | } 58 | 59 | export function parseUserOutput(options: BetterAuthOptions, user: User) { 60 | const schema = getAllFields(options, "user"); 61 | return parseOutputData(user, { fields: schema }); 62 | } 63 | 64 | export function parseAccountOutput( 65 | options: BetterAuthOptions, 66 | account: Account, 67 | ) { 68 | const schema = getAllFields(options, "account"); 69 | return parseOutputData(account, { fields: schema }); 70 | } 71 | 72 | export function parseSessionOutput( 73 | options: BetterAuthOptions, 74 | session: Session, 75 | ) { 76 | const schema = getAllFields(options, "session"); 77 | return parseOutputData(session, { fields: schema }); 78 | } 79 | 80 | export function parseInputData<T extends Record<string, any>>( 81 | data: T, 82 | schema: { 83 | fields: Record<string, DBFieldAttribute>; 84 | action?: "create" | "update"; 85 | }, 86 | ) { 87 | const action = schema.action || "create"; 88 | const fields = schema.fields; 89 | const parsedData: Record<string, any> = Object.assign( 90 | Object.create(null), 91 | null, 92 | ); 93 | for (const key in fields) { 94 | if (key in data) { 95 | if (fields[key]!.input === false) { 96 | if (fields[key]!.defaultValue !== undefined) { 97 | parsedData[key] = fields[key]!.defaultValue; 98 | continue; 99 | } 100 | if (parsedData[key]) { 101 | throw new APIError("BAD_REQUEST", { 102 | message: `${key} is not allowed to be set`, 103 | }); 104 | } 105 | continue; 106 | } 107 | if (fields[key]!.validator?.input && data[key] !== undefined) { 108 | parsedData[key] = fields[key]!.validator.input.parse(data[key]); 109 | continue; 110 | } 111 | if (fields[key]!.transform?.input && data[key] !== undefined) { 112 | parsedData[key] = fields[key]!.transform?.input(data[key]); 113 | continue; 114 | } 115 | parsedData[key] = data[key]; 116 | continue; 117 | } 118 | 119 | if (fields[key]!.defaultValue !== undefined && action === "create") { 120 | parsedData[key] = fields[key]!.defaultValue; 121 | continue; 122 | } 123 | 124 | if (fields[key]!.required && action === "create") { 125 | throw new APIError("BAD_REQUEST", { 126 | message: `${key} is required`, 127 | }); 128 | } 129 | } 130 | return parsedData as Partial<T>; 131 | } 132 | 133 | export function parseUserInput( 134 | options: BetterAuthOptions, 135 | user: Record<string, any> = {}, 136 | action: "create" | "update", 137 | ) { 138 | const schema = getAllFields(options, "user"); 139 | return parseInputData(user, { fields: schema, action }); 140 | } 141 | 142 | export function parseAdditionalUserInput( 143 | options: BetterAuthOptions, 144 | user?: Record<string, any>, 145 | ) { 146 | const schema = getAllFields(options, "user"); 147 | return parseInputData(user || {}, { fields: schema }); 148 | } 149 | 150 | export function parseAccountInput( 151 | options: BetterAuthOptions, 152 | account: Partial<Account>, 153 | ) { 154 | const schema = getAllFields(options, "account"); 155 | return parseInputData(account, { fields: schema }); 156 | } 157 | 158 | export function parseSessionInput( 159 | options: BetterAuthOptions, 160 | session: Partial<Session>, 161 | ) { 162 | const schema = getAllFields(options, "session"); 163 | return parseInputData(session, { fields: schema }); 164 | } 165 | 166 | export function mergeSchema<S extends BetterAuthPluginDBSchema>( 167 | schema: S, 168 | newSchema?: { 169 | [K in keyof S]?: { 170 | modelName?: string; 171 | fields?: { 172 | [P: string]: string; 173 | }; 174 | }; 175 | }, 176 | ) { 177 | if (!newSchema) { 178 | return schema; 179 | } 180 | for (const table in newSchema) { 181 | const newModelName = newSchema[table]?.modelName; 182 | if (newModelName) { 183 | schema[table]!.modelName = newModelName; 184 | } 185 | for (const field in schema[table]!.fields) { 186 | const newField = newSchema[table]?.fields?.[field]; 187 | if (!newField) { 188 | continue; 189 | } 190 | schema[table]!.fields[field]!.fieldName = newField; 191 | } 192 | } 193 | return schema; 194 | } 195 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/one-tap/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { APIError } from "../../api"; 3 | import { createAuthEndpoint } from "@better-auth/core/api"; 4 | import { setSessionCookie } from "../../cookies"; 5 | import type { BetterAuthPlugin } from "@better-auth/core"; 6 | import { jwtVerify, createRemoteJWKSet } from "jose"; 7 | import { toBoolean } from "../../utils/boolean"; 8 | 9 | interface OneTapOptions { 10 | /** 11 | * Disable the signup flow 12 | * 13 | * @default false 14 | */ 15 | disableSignup?: boolean; 16 | /** 17 | * Google Client ID 18 | * 19 | * If a client ID is provided in the social provider configuration, 20 | * it will be used. 21 | */ 22 | clientId?: string; 23 | } 24 | 25 | export const oneTap = (options?: OneTapOptions) => 26 | ({ 27 | id: "one-tap", 28 | endpoints: { 29 | oneTapCallback: createAuthEndpoint( 30 | "/one-tap/callback", 31 | { 32 | method: "POST", 33 | body: z.object({ 34 | idToken: z.string().meta({ 35 | description: 36 | "Google ID token, which the client obtains from the One Tap API", 37 | }), 38 | }), 39 | metadata: { 40 | openapi: { 41 | summary: "One tap callback", 42 | description: 43 | "Use this endpoint to authenticate with Google One Tap", 44 | responses: { 45 | 200: { 46 | description: "Successful response", 47 | content: { 48 | "application/json": { 49 | schema: { 50 | type: "object", 51 | properties: { 52 | session: { 53 | $ref: "#/components/schemas/Session", 54 | }, 55 | user: { 56 | $ref: "#/components/schemas/User", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | 400: { 64 | description: "Invalid token", 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | async (ctx) => { 71 | const { idToken } = ctx.body; 72 | let payload: any; 73 | try { 74 | const JWKS = createRemoteJWKSet( 75 | new URL("https://www.googleapis.com/oauth2/v3/certs"), 76 | ); 77 | const { payload: verifiedPayload } = await jwtVerify( 78 | idToken, 79 | JWKS, 80 | { 81 | issuer: ["https://accounts.google.com", "accounts.google.com"], 82 | audience: 83 | options?.clientId || 84 | ctx.context.options.socialProviders?.google?.clientId, 85 | }, 86 | ); 87 | payload = verifiedPayload; 88 | } catch (error) { 89 | throw new APIError("BAD_REQUEST", { 90 | message: "invalid id token", 91 | }); 92 | } 93 | const { email, email_verified, name, picture, sub } = payload; 94 | if (!email) { 95 | return ctx.json({ error: "Email not available in token" }); 96 | } 97 | 98 | const user = await ctx.context.internalAdapter.findUserByEmail(email); 99 | if (!user) { 100 | if (options?.disableSignup) { 101 | throw new APIError("BAD_GATEWAY", { 102 | message: "User not found", 103 | }); 104 | } 105 | const newUser = await ctx.context.internalAdapter.createOAuthUser( 106 | { 107 | email, 108 | emailVerified: 109 | typeof email_verified === "boolean" 110 | ? email_verified 111 | : toBoolean(email_verified), 112 | name, 113 | image: picture, 114 | }, 115 | { 116 | providerId: "google", 117 | accountId: sub, 118 | }, 119 | ); 120 | if (!newUser) { 121 | throw new APIError("INTERNAL_SERVER_ERROR", { 122 | message: "Could not create user", 123 | }); 124 | } 125 | const session = await ctx.context.internalAdapter.createSession( 126 | newUser.user.id, 127 | ); 128 | await setSessionCookie(ctx, { 129 | user: newUser.user, 130 | session, 131 | }); 132 | return ctx.json({ 133 | token: session.token, 134 | user: { 135 | id: newUser.user.id, 136 | email: newUser.user.email, 137 | emailVerified: newUser.user.emailVerified, 138 | name: newUser.user.name, 139 | image: newUser.user.image, 140 | createdAt: newUser.user.createdAt, 141 | updatedAt: newUser.user.updatedAt, 142 | }, 143 | }); 144 | } 145 | const account = await ctx.context.internalAdapter.findAccount(sub); 146 | if (!account) { 147 | const accountLinking = ctx.context.options.account?.accountLinking; 148 | const shouldLinkAccount = 149 | accountLinking?.enabled && 150 | (accountLinking.trustedProviders?.includes("google") || 151 | email_verified); 152 | if (shouldLinkAccount) { 153 | await ctx.context.internalAdapter.linkAccount({ 154 | userId: user.user.id, 155 | providerId: "google", 156 | accountId: sub, 157 | scope: "openid,profile,email", 158 | idToken, 159 | }); 160 | } else { 161 | throw new APIError("UNAUTHORIZED", { 162 | message: "Google sub doesn't match", 163 | }); 164 | } 165 | } 166 | const session = await ctx.context.internalAdapter.createSession( 167 | user.user.id, 168 | ); 169 | 170 | await setSessionCookie(ctx, { 171 | user: user.user, 172 | session, 173 | }); 174 | return ctx.json({ 175 | token: session.token, 176 | user: { 177 | id: user.user.id, 178 | email: user.user.email, 179 | emailVerified: user.user.emailVerified, 180 | name: user.user.name, 181 | image: user.user.image, 182 | createdAt: user.user.createdAt, 183 | updatedAt: user.user.updatedAt, 184 | }, 185 | }); 186 | }, 187 | ), 188 | }, 189 | }) satisfies BetterAuthPlugin; 190 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/google.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Google 3 | description: Google provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Google credentials 9 | To use Google as a social provider, you need to get your Google credentials. You can get them by creating a new project in the [Google Cloud Console](https://console.cloud.google.com/apis/dashboard). 10 | 11 | In the Google Cloud Console > Credentials > Authorized redirect URIs, make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/google` for local development. For production, make sure to set the redirect URL as your application domain, e.g. `https://example.com/api/auth/callback/google`. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.google` in your auth configuration. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | 21 | export const auth = betterAuth({ 22 | socialProviders: { 23 | google: { // [!code highlight] 24 | clientId: process.env.GOOGLE_CLIENT_ID as string, // [!code highlight] 25 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, // [!code highlight] 26 | }, // [!code highlight] 27 | }, 28 | }) 29 | ``` 30 | </Step> 31 | 32 | </Steps> 33 | 34 | ## Usage 35 | 36 | ### Sign In with Google 37 | 38 | To sign in with Google, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 39 | 40 | - `provider`: The provider to use. It should be set to `google`. 41 | 42 | ```ts title="auth-client.ts" / 43 | import { createAuthClient } from "better-auth/client"; 44 | const authClient = createAuthClient(); 45 | 46 | const signIn = async () => { 47 | const data = await authClient.signIn.social({ 48 | provider: "google", 49 | }); 50 | }; 51 | ``` 52 | 53 | ### Sign In with Google With ID Token 54 | 55 | To sign in with Google using the ID Token, you can use the `signIn.social` function to pass the ID Token. 56 | 57 | This is useful when you have the ID Token from Google on the client-side and want to use it to sign in on the server. 58 | 59 | <Callout> 60 | If ID token is provided no redirection will happen, and the user will be 61 | signed in directly. 62 | </Callout> 63 | 64 | ```ts title="auth-client.ts" 65 | const data = await authClient.signIn.social({ 66 | provider: "google", 67 | idToken: { 68 | token: // Google ID Token, 69 | accessToken: // Google Access Token 70 | } 71 | }) 72 | ``` 73 | 74 | <Callout> 75 | If you want to use google one tap, you can use the [One Tap 76 | Plugin](/docs/plugins/one-tap) guide. 77 | </Callout> 78 | 79 | ### Always ask to select an account 80 | 81 | If you want to always ask the user to select an account, you pass the `prompt` parameter to the provider, setting it to `select_account`. 82 | 83 | ```ts 84 | socialProviders: { 85 | google: { 86 | prompt: "select_account", // [!code highlight] 87 | clientId: process.env.GOOGLE_CLIENT_ID as string, 88 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 89 | }, 90 | } 91 | ``` 92 | 93 | ### Requesting Additional Google Scopes 94 | 95 | If your application needs additional Google scopes after the user has already signed up (e.g., for Google Drive, Gmail, or other Google services), you can request them using the `linkSocial` method with the same Google provider. 96 | 97 | ```tsx title="auth-client.ts" 98 | const requestGoogleDriveAccess = async () => { 99 | await authClient.linkSocial({ 100 | provider: "google", 101 | scopes: ["https://www.googleapis.com/auth/drive.file"], 102 | }); 103 | }; 104 | 105 | // Example usage in a React component 106 | return ( 107 | <button onClick={requestGoogleDriveAccess}> 108 | Add Google Drive Permissions 109 | </button> 110 | ); 111 | ``` 112 | 113 | This will trigger a new OAuth flow that requests the additional scopes. After completion, your account will have the new scope in the database, and the access token will give you access to the requested Google APIs. 114 | 115 | <Callout> 116 | Ensure you're using Better Auth version 1.2.7 or later to avoid "Social 117 | account already linked" errors when requesting additional scopes from the same 118 | provider. 119 | </Callout> 120 | 121 | ### Always get refresh token 122 | 123 | Google only issues a refresh token the first time a user consents to your app. 124 | If the user has already authorized your app, subsequent OAuth flows will only return an access token, not a refresh token. 125 | 126 | To always get a refresh token, you can set the `accessType` to `offline`, and `prompt` to `select_account consent` in the provider options. 127 | 128 | ```ts 129 | socialProviders: { 130 | google: { 131 | clientId: process.env.GOOGLE_CLIENT_ID as string, 132 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 133 | accessType: "offline", // [!code highlight] 134 | prompt: "select_account consent", // [!code highlight] 135 | }, 136 | } 137 | ``` 138 | 139 | <Callout> 140 | **Revoking Access:** If you want to get a new refresh token for a user who has 141 | already authorized your app, you must have them revoke your app's access in 142 | their Google account settings, then re-authorize. 143 | </Callout> 144 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/navigation-menu.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 3 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const NavigationMenu = ({ 9 | ref, 10 | className, 11 | children, 12 | ...props 13 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> & { 14 | ref: React.RefObject<React.ElementRef<typeof NavigationMenuPrimitive.Root>>; 15 | }) => ( 16 | <NavigationMenuPrimitive.Root 17 | ref={ref} 18 | className={cn( 19 | "relative z-10 flex max-w-max flex-1 items-center justify-center", 20 | className, 21 | )} 22 | {...props} 23 | > 24 | {children} 25 | <NavigationMenuViewport /> 26 | </NavigationMenuPrimitive.Root> 27 | ); 28 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; 29 | 30 | const NavigationMenuList = ({ 31 | ref, 32 | className, 33 | ...props 34 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> & { 35 | ref: React.RefObject<React.ElementRef<typeof NavigationMenuPrimitive.List>>; 36 | }) => ( 37 | <NavigationMenuPrimitive.List 38 | ref={ref} 39 | className={cn( 40 | "group flex flex-1 list-none items-center justify-center space-x-1", 41 | className, 42 | )} 43 | {...props} 44 | /> 45 | ); 46 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; 47 | 48 | const NavigationMenuItem = NavigationMenuPrimitive.Item; 49 | 50 | const navigationMenuTriggerStyle = cva( 51 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50", 52 | ); 53 | 54 | const NavigationMenuTrigger = ({ 55 | ref, 56 | className, 57 | children, 58 | ...props 59 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> & { 60 | ref: React.RefObject< 61 | React.ElementRef<typeof NavigationMenuPrimitive.Trigger> 62 | >; 63 | }) => ( 64 | <NavigationMenuPrimitive.Trigger 65 | ref={ref} 66 | className={cn(navigationMenuTriggerStyle(), "group", className)} 67 | {...props} 68 | > 69 | {children}{" "} 70 | <ChevronDownIcon 71 | className="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180" 72 | aria-hidden="true" 73 | /> 74 | </NavigationMenuPrimitive.Trigger> 75 | ); 76 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; 77 | 78 | const NavigationMenuContent = ({ 79 | ref, 80 | className, 81 | ...props 82 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> & { 83 | ref: React.RefObject< 84 | React.ElementRef<typeof NavigationMenuPrimitive.Content> 85 | >; 86 | }) => ( 87 | <NavigationMenuPrimitive.Content 88 | ref={ref} 89 | className={cn( 90 | "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", 91 | className, 92 | )} 93 | {...props} 94 | /> 95 | ); 96 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; 97 | 98 | const NavigationMenuLink = NavigationMenuPrimitive.Link; 99 | 100 | const NavigationMenuViewport = ({ 101 | ref, 102 | className, 103 | ...props 104 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> & { 105 | ref: React.RefObject< 106 | React.ElementRef<typeof NavigationMenuPrimitive.Viewport> 107 | >; 108 | }) => ( 109 | <div className={cn("absolute left-0 top-full flex justify-center")}> 110 | <NavigationMenuPrimitive.Viewport 111 | className={cn( 112 | "origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--radix-navigation-menu-viewport-width)", 113 | className, 114 | )} 115 | ref={ref} 116 | {...props} 117 | /> 118 | </div> 119 | ); 120 | NavigationMenuViewport.displayName = 121 | NavigationMenuPrimitive.Viewport.displayName; 122 | 123 | const NavigationMenuIndicator = ({ 124 | ref, 125 | className, 126 | ...props 127 | }: React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> & { 128 | ref: React.RefObject< 129 | React.ElementRef<typeof NavigationMenuPrimitive.Indicator> 130 | >; 131 | }) => ( 132 | <NavigationMenuPrimitive.Indicator 133 | ref={ref} 134 | className={cn( 135 | "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", 136 | className, 137 | )} 138 | {...props} 139 | > 140 | <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> 141 | </NavigationMenuPrimitive.Indicator> 142 | ); 143 | NavigationMenuIndicator.displayName = 144 | NavigationMenuPrimitive.Indicator.displayName; 145 | 146 | export { 147 | navigationMenuTriggerStyle, 148 | NavigationMenu, 149 | NavigationMenuList, 150 | NavigationMenuItem, 151 | NavigationMenuContent, 152 | NavigationMenuTrigger, 153 | NavigationMenuLink, 154 | NavigationMenuIndicator, 155 | NavigationMenuViewport, 156 | }; 157 | ``` -------------------------------------------------------------------------------- /packages/cli/src/commands/migrate.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Command } from "commander"; 2 | import * as z from "zod/v4"; 3 | import { existsSync } from "fs"; 4 | import path from "path"; 5 | import yoctoSpinner from "yocto-spinner"; 6 | import chalk from "chalk"; 7 | import prompts from "prompts"; 8 | import { logger, createTelemetry, getTelemetryAuthConfig } from "better-auth"; 9 | import { getAdapter, getMigrations } from "better-auth/db"; 10 | import { getConfig } from "../utils/get-config"; 11 | 12 | export async function migrateAction(opts: any) { 13 | const options = z 14 | .object({ 15 | cwd: z.string(), 16 | config: z.string().optional(), 17 | y: z.boolean().optional(), 18 | yes: z.boolean().optional(), 19 | }) 20 | .parse(opts); 21 | 22 | const cwd = path.resolve(options.cwd); 23 | if (!existsSync(cwd)) { 24 | logger.error(`The directory "${cwd}" does not exist.`); 25 | process.exit(1); 26 | } 27 | 28 | const config = await getConfig({ 29 | cwd, 30 | configPath: options.config, 31 | }); 32 | if (!config) { 33 | logger.error( 34 | "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.", 35 | ); 36 | return; 37 | } 38 | 39 | const db = await getAdapter(config); 40 | 41 | if (!db) { 42 | logger.error( 43 | "Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter.", 44 | ); 45 | process.exit(1); 46 | } 47 | 48 | if (db.id !== "kysely") { 49 | if (db.id === "prisma") { 50 | logger.error( 51 | "The migrate command only works with the built-in Kysely adapter. For Prisma, run `npx @better-auth/cli generate` to create the schema, then use Prisma’s migrate or push to apply it.", 52 | ); 53 | try { 54 | const telemetry = await createTelemetry(config); 55 | await telemetry.publish({ 56 | type: "cli_migrate", 57 | payload: { 58 | outcome: "unsupported_adapter", 59 | adapter: "prisma", 60 | config: getTelemetryAuthConfig(config), 61 | }, 62 | }); 63 | } catch {} 64 | process.exit(0); 65 | } 66 | if (db.id === "drizzle") { 67 | logger.error( 68 | "The migrate command only works with the built-in Kysely adapter. For Drizzle, run `npx @better-auth/cli generate` to create the schema, then use Drizzle’s migrate or push to apply it.", 69 | ); 70 | try { 71 | const telemetry = await createTelemetry(config); 72 | await telemetry.publish({ 73 | type: "cli_migrate", 74 | payload: { 75 | outcome: "unsupported_adapter", 76 | adapter: "drizzle", 77 | config: getTelemetryAuthConfig(config), 78 | }, 79 | }); 80 | } catch {} 81 | process.exit(0); 82 | } 83 | logger.error("Migrate command isn't supported for this adapter."); 84 | try { 85 | const telemetry = await createTelemetry(config); 86 | await telemetry.publish({ 87 | type: "cli_migrate", 88 | payload: { 89 | outcome: "unsupported_adapter", 90 | adapter: db.id, 91 | config: getTelemetryAuthConfig(config), 92 | }, 93 | }); 94 | } catch {} 95 | process.exit(1); 96 | } 97 | 98 | const spinner = yoctoSpinner({ text: "preparing migration..." }).start(); 99 | 100 | const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config); 101 | 102 | if (!toBeAdded.length && !toBeCreated.length) { 103 | spinner.stop(); 104 | logger.info("🚀 No migrations needed."); 105 | try { 106 | const telemetry = await createTelemetry(config); 107 | await telemetry.publish({ 108 | type: "cli_migrate", 109 | payload: { 110 | outcome: "no_changes", 111 | config: getTelemetryAuthConfig(config), 112 | }, 113 | }); 114 | } catch {} 115 | process.exit(0); 116 | } 117 | 118 | spinner.stop(); 119 | logger.info(`🔑 The migration will affect the following:`); 120 | 121 | for (const table of [...toBeCreated, ...toBeAdded]) { 122 | console.log( 123 | "->", 124 | chalk.magenta(Object.keys(table.fields).join(", ")), 125 | chalk.white("fields on"), 126 | chalk.yellow(`${table.table}`), 127 | chalk.white("table."), 128 | ); 129 | } 130 | 131 | if (options.y) { 132 | console.warn("WARNING: --y is deprecated. Consider -y or --yes"); 133 | options.yes = true; 134 | } 135 | 136 | let migrate = options.yes; 137 | if (!migrate) { 138 | const response = await prompts({ 139 | type: "confirm", 140 | name: "migrate", 141 | message: "Are you sure you want to run these migrations?", 142 | initial: false, 143 | }); 144 | migrate = response.migrate; 145 | } 146 | 147 | if (!migrate) { 148 | logger.info("Migration cancelled."); 149 | try { 150 | const telemetry = await createTelemetry(config); 151 | await telemetry.publish({ 152 | type: "cli_migrate", 153 | payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) }, 154 | }); 155 | } catch {} 156 | process.exit(0); 157 | } 158 | 159 | spinner?.start("migrating..."); 160 | await runMigrations(); 161 | spinner.stop(); 162 | logger.info("🚀 migration was completed successfully!"); 163 | try { 164 | const telemetry = await createTelemetry(config); 165 | await telemetry.publish({ 166 | type: "cli_migrate", 167 | payload: { outcome: "migrated", config: getTelemetryAuthConfig(config) }, 168 | }); 169 | } catch {} 170 | process.exit(0); 171 | } 172 | 173 | export const migrate = new Command("migrate") 174 | .option( 175 | "-c, --cwd <cwd>", 176 | "the working directory. defaults to the current directory.", 177 | process.cwd(), 178 | ) 179 | .option( 180 | "--config <config>", 181 | "the path to the configuration file. defaults to the first configuration file found.", 182 | ) 183 | .option( 184 | "-y, --yes", 185 | "automatically accept and run migrations without prompting", 186 | false, 187 | ) 188 | .option("--y", "(deprecated) same as --yes", false) 189 | .action(migrateAction); 190 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/accept-invitation/[id]/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { CheckIcon, XIcon } from "lucide-react"; 13 | import { useEffect, useState } from "react"; 14 | import { useParams, useRouter } from "next/navigation"; 15 | import { Skeleton } from "@/components/ui/skeleton"; 16 | import { client, organization } from "@/lib/auth-client"; 17 | import { InvitationError } from "./invitation-error"; 18 | 19 | export default function InvitationPage() { 20 | const params = useParams<{ 21 | id: string; 22 | }>(); 23 | const router = useRouter(); 24 | const [invitationStatus, setInvitationStatus] = useState< 25 | "pending" | "accepted" | "rejected" 26 | >("pending"); 27 | 28 | const handleAccept = async () => { 29 | await organization 30 | .acceptInvitation({ 31 | invitationId: params.id, 32 | }) 33 | .then((res) => { 34 | if (res.error) { 35 | setError(res.error.message || "An error occurred"); 36 | } else { 37 | setInvitationStatus("accepted"); 38 | router.push(`/dashboard`); 39 | } 40 | }); 41 | }; 42 | 43 | const handleReject = async () => { 44 | await organization 45 | .rejectInvitation({ 46 | invitationId: params.id, 47 | }) 48 | .then((res) => { 49 | if (res.error) { 50 | setError(res.error.message || "An error occurred"); 51 | } else { 52 | setInvitationStatus("rejected"); 53 | } 54 | }); 55 | }; 56 | 57 | const [invitation, setInvitation] = useState<{ 58 | organizationName: string; 59 | organizationSlug: string; 60 | inviterEmail: string; 61 | id: string; 62 | status: "pending" | "accepted" | "rejected" | "canceled"; 63 | email: string; 64 | expiresAt: Date; 65 | organizationId: string; 66 | role: string; 67 | inviterId: string; 68 | } | null>(null); 69 | 70 | const [error, setError] = useState<string | null>(null); 71 | 72 | useEffect(() => { 73 | client.organization 74 | .getInvitation({ 75 | query: { 76 | id: params.id, 77 | }, 78 | }) 79 | .then((res) => { 80 | if (res.error) { 81 | setError(res.error.message || "An error occurred"); 82 | } else { 83 | setInvitation(res.data); 84 | } 85 | }); 86 | }, []); 87 | 88 | return ( 89 | <div className="min-h-[80vh] flex items-center justify-center"> 90 | <div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white mask-[radial-gradient(ellipse_at_center,transparent_20%,black)]"></div> 91 | {invitation ? ( 92 | <Card className="w-full max-w-md"> 93 | <CardHeader> 94 | <CardTitle>Organization Invitation</CardTitle> 95 | <CardDescription> 96 | You've been invited to join an organization 97 | </CardDescription> 98 | </CardHeader> 99 | <CardContent> 100 | {invitationStatus === "pending" && ( 101 | <div className="space-y-4"> 102 | <p> 103 | <strong>{invitation?.inviterEmail}</strong> has invited you to 104 | join <strong>{invitation?.organizationName}</strong>. 105 | </p> 106 | <p> 107 | This invitation was sent to{" "} 108 | <strong>{invitation?.email}</strong>. 109 | </p> 110 | </div> 111 | )} 112 | {invitationStatus === "accepted" && ( 113 | <div className="space-y-4"> 114 | <div className="flex items-center justify-center w-16 h-16 mx-auto bg-green-100 rounded-full"> 115 | <CheckIcon className="w-8 h-8 text-green-600" /> 116 | </div> 117 | <h2 className="text-2xl font-bold text-center"> 118 | Welcome to {invitation?.organizationName}! 119 | </h2> 120 | <p className="text-center"> 121 | You've successfully joined the organization. We're excited to 122 | have you on board! 123 | </p> 124 | </div> 125 | )} 126 | {invitationStatus === "rejected" && ( 127 | <div className="space-y-4"> 128 | <div className="flex items-center justify-center w-16 h-16 mx-auto bg-red-100 rounded-full"> 129 | <XIcon className="w-8 h-8 text-red-600" /> 130 | </div> 131 | <h2 className="text-2xl font-bold text-center"> 132 | Invitation Declined 133 | </h2> 134 | <p className="text-center"> 135 | You‘ve declined the invitation to join{" "} 136 | {invitation?.organizationName}. 137 | </p> 138 | </div> 139 | )} 140 | </CardContent> 141 | {invitationStatus === "pending" && ( 142 | <CardFooter className="flex justify-between"> 143 | <Button variant="outline" onClick={handleReject}> 144 | Decline 145 | </Button> 146 | <Button onClick={handleAccept}>Accept Invitation</Button> 147 | </CardFooter> 148 | )} 149 | </Card> 150 | ) : error ? ( 151 | <InvitationError /> 152 | ) : ( 153 | <InvitationSkeleton /> 154 | )} 155 | </div> 156 | ); 157 | } 158 | 159 | function InvitationSkeleton() { 160 | return ( 161 | <Card className="w-full max-w-md mx-auto"> 162 | <CardHeader> 163 | <div className="flex items-center space-x-2"> 164 | <Skeleton className="w-6 h-6 rounded-full" /> 165 | <Skeleton className="h-6 w-24" /> 166 | </div> 167 | <Skeleton className="h-4 w-full mt-2" /> 168 | <Skeleton className="h-4 w-2/3 mt-2" /> 169 | </CardHeader> 170 | <CardContent> 171 | <div className="space-y-2"> 172 | <Skeleton className="h-4 w-full" /> 173 | <Skeleton className="h-4 w-full" /> 174 | <Skeleton className="h-4 w-2/3" /> 175 | </div> 176 | </CardContent> 177 | <CardFooter className="flex justify-end"> 178 | <Skeleton className="h-10 w-24" /> 179 | </CardFooter> 180 | </Card> 181 | ); 182 | } 183 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/kakao.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | validateAuthorizationCode, 6 | refreshAccessToken, 7 | } from "../oauth2"; 8 | 9 | interface Partner { 10 | /** Partner-specific ID (consent required: kakaotalk_message) */ 11 | uuid?: string; 12 | } 13 | 14 | interface Profile { 15 | /** Nickname (consent required: profile/nickname) */ 16 | nickname?: string; 17 | /** Thumbnail image URL (consent required: profile/profile image) */ 18 | thumbnail_image_url?: string; 19 | /** Profile image URL (consent required: profile/profile image) */ 20 | profile_image_url?: string; 21 | /** Whether the profile image is the default */ 22 | is_default_image?: boolean; 23 | /** Whether the nickname is the default */ 24 | is_default_nickname?: boolean; 25 | } 26 | 27 | interface KakaoAccount { 28 | /** Consent required: profile info (nickname/profile image) */ 29 | profile_needs_agreement?: boolean; 30 | /** Consent required: nickname */ 31 | profile_nickname_needs_agreement?: boolean; 32 | /** Consent required: profile image */ 33 | profile_image_needs_agreement?: boolean; 34 | /** Profile info */ 35 | profile?: Profile; 36 | /** Consent required: name */ 37 | name_needs_agreement?: boolean; 38 | /** Name */ 39 | name?: string; 40 | /** Consent required: email */ 41 | email_needs_agreement?: boolean; 42 | /** Email valid */ 43 | is_email_valid?: boolean; 44 | /** Email verified */ 45 | is_email_verified?: boolean; 46 | /** Email */ 47 | email?: string; 48 | /** Consent required: age range */ 49 | age_range_needs_agreement?: boolean; 50 | /** Age range */ 51 | age_range?: string; 52 | /** Consent required: birth year */ 53 | birthyear_needs_agreement?: boolean; 54 | /** Birth year (YYYY) */ 55 | birthyear?: string; 56 | /** Consent required: birthday */ 57 | birthday_needs_agreement?: boolean; 58 | /** Birthday (MMDD) */ 59 | birthday?: string; 60 | /** Birthday type (SOLAR/LUNAR) */ 61 | birthday_type?: string; 62 | /** Whether birthday is in a leap month */ 63 | is_leap_month?: boolean; 64 | /** Consent required: gender */ 65 | gender_needs_agreement?: boolean; 66 | /** Gender (male/female) */ 67 | gender?: string; 68 | /** Consent required: phone number */ 69 | phone_number_needs_agreement?: boolean; 70 | /** Phone number */ 71 | phone_number?: string; 72 | /** Consent required: CI */ 73 | ci_needs_agreement?: boolean; 74 | /** CI (unique identifier) */ 75 | ci?: string; 76 | /** CI authentication time (UTC) */ 77 | ci_authenticated_at?: string; 78 | } 79 | 80 | export interface KakaoProfile { 81 | /** Kakao user ID */ 82 | id: number; 83 | /** 84 | * Whether the user has signed up (only present if auto-connection is disabled) 85 | * false: preregistered, true: registered 86 | */ 87 | has_signed_up?: boolean; 88 | /** UTC datetime when the user connected the service */ 89 | connected_at?: string; 90 | /** UTC datetime when the user signed up via Kakao Sync */ 91 | synched_at?: string; 92 | /** Custom user properties */ 93 | properties?: Record<string, any>; 94 | /** Kakao account info */ 95 | kakao_account: KakaoAccount; 96 | /** Partner info */ 97 | for_partner?: Partner; 98 | } 99 | 100 | export interface KakaoOptions extends ProviderOptions<KakaoProfile> { 101 | clientId: string; 102 | } 103 | 104 | export const kakao = (options: KakaoOptions) => { 105 | return { 106 | id: "kakao", 107 | name: "Kakao", 108 | createAuthorizationURL({ state, scopes, redirectURI }) { 109 | const _scopes = options.disableDefaultScope 110 | ? [] 111 | : ["account_email", "profile_image", "profile_nickname"]; 112 | options.scope && _scopes.push(...options.scope); 113 | scopes && _scopes.push(...scopes); 114 | return createAuthorizationURL({ 115 | id: "kakao", 116 | options, 117 | authorizationEndpoint: "https://kauth.kakao.com/oauth/authorize", 118 | scopes: _scopes, 119 | state, 120 | redirectURI, 121 | }); 122 | }, 123 | validateAuthorizationCode: async ({ code, redirectURI }) => { 124 | return validateAuthorizationCode({ 125 | code, 126 | redirectURI, 127 | options, 128 | tokenEndpoint: "https://kauth.kakao.com/oauth/token", 129 | }); 130 | }, 131 | refreshAccessToken: options.refreshAccessToken 132 | ? options.refreshAccessToken 133 | : async (refreshToken) => { 134 | return refreshAccessToken({ 135 | refreshToken, 136 | options: { 137 | clientId: options.clientId, 138 | clientKey: options.clientKey, 139 | clientSecret: options.clientSecret, 140 | }, 141 | tokenEndpoint: "https://kauth.kakao.com/oauth/token", 142 | }); 143 | }, 144 | async getUserInfo(token) { 145 | if (options.getUserInfo) { 146 | return options.getUserInfo(token); 147 | } 148 | const { data: profile, error } = await betterFetch<KakaoProfile>( 149 | "https://kapi.kakao.com/v2/user/me", 150 | { 151 | headers: { 152 | Authorization: `Bearer ${token.accessToken}`, 153 | }, 154 | }, 155 | ); 156 | if (error || !profile) { 157 | return null; 158 | } 159 | const userMap = await options.mapProfileToUser?.(profile); 160 | const account = profile.kakao_account || {}; 161 | const kakaoProfile = account.profile || {}; 162 | const user = { 163 | id: String(profile.id), 164 | name: kakaoProfile.nickname || account.name || undefined, 165 | email: account.email, 166 | image: 167 | kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url, 168 | emailVerified: !!account.is_email_valid && !!account.is_email_verified, 169 | ...userMap, 170 | }; 171 | return { 172 | user, 173 | data: profile, 174 | }; 175 | }, 176 | options, 177 | } satisfies OAuthProvider<KakaoProfile>; 178 | }; 179 | ``` -------------------------------------------------------------------------------- /docs/components/features.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { 4 | Globe2Icon, 5 | PlugIcon, 6 | PlugZap2Icon, 7 | Plus, 8 | RabbitIcon, 9 | ShieldCheckIcon, 10 | Webhook, 11 | } from "lucide-react"; 12 | import { LockClosedIcon } from "@radix-ui/react-icons"; 13 | 14 | import { TechStackDisplay } from "./display-techstack"; 15 | import { Ripple } from "./ripple"; 16 | import { GithubStat } from "./github-stat"; 17 | import { cn } from "@/lib/utils"; 18 | import { Testimonial } from "./landing/testimonials"; 19 | const features = [ 20 | { 21 | id: 1, 22 | label: "Framework Agnostic", 23 | title: "Support for popular <strong>frameworks</strong>.", 24 | description: 25 | "Supports popular frameworks, including React, Vue, Svelte, Astro, Solid, Next.js, Nuxt, Tanstack Start, Hono, and more.", 26 | icon: PlugZap2Icon, 27 | }, 28 | { 29 | id: 2, 30 | label: "Authentication", 31 | title: "Email & Password <strong>Authentication</strong>.", 32 | description: 33 | "Built-in support for email and password authentication, with session and account management features.", 34 | icon: LockClosedIcon, 35 | }, 36 | { 37 | id: 3, 38 | label: "Social Sign-on", 39 | title: "Support multiple <strong>OAuth providers</strong>.", 40 | description: 41 | "Allow users to sign in with their accounts, including GitHub, Google, Discord, Twitter, and more.", 42 | icon: Webhook, 43 | }, 44 | { 45 | id: 4, 46 | label: "Two Factor", 47 | title: "Multi Factor <strong>Authentication</strong>.", 48 | description: 49 | "Secure your users accounts with two factor authentication with a few lines of code.", 50 | icon: ShieldCheckIcon, 51 | }, 52 | { 53 | id: 5, 54 | label: "Multi Tenant", 55 | title: "<strong>Organization</strong> Members and Invitation.", 56 | description: 57 | "Multi tenant support with members, organization, teams and invitation with access control.", 58 | 59 | icon: RabbitIcon, 60 | }, 61 | 62 | { 63 | id: 6, 64 | label: "Plugin Ecosystem", 65 | title: "A lot more features with <strong>plugins</strong>.", 66 | description: 67 | "Improve your application experience with our official plugins and those created by the community.", 68 | icon: PlugIcon, 69 | }, 70 | ]; 71 | 72 | export default function Features({ stars }: { stars: string | null }) { 73 | return ( 74 | <div className="md:w-10/12 mt-10 mx-auto font-geist relative md:border-l-0 md:border-b-0 md:border-[1.2px] rounded-none -pr-2 dark:bg-black/[0.95] "> 75 | <div className="w-full md:mx-0"> 76 | <div className="grid grid-cols-1 relative md:grid-rows-2 md:grid-cols-3 border-b-[1.2px]"> 77 | <div className="hidden md:grid top-1/2 left-0 -translate-y-1/2 w-full grid-cols-3 z-10 pointer-events-none select-none absolute"> 78 | <Plus className="w-8 h-8 text-neutral-300 translate-x-[16.5px] translate-y-[.5px] ml-auto dark:text-neutral-600" /> 79 | <Plus className="w-8 h-8 text-neutral-300 ml-auto translate-x-[16.5px] translate-y-[.5px] dark:text-neutral-600" /> 80 | </div> 81 | {features.map((feature, index) => ( 82 | <div 83 | key={feature.id} 84 | className={cn( 85 | "justify-center border-l-[1.2px] md:min-h-[240px] border-t-[1.2px] md:border-t-0 transform-gpu flex flex-col p-10 2xl:p-12", 86 | index >= 3 && "md:border-t-[1.2px]", 87 | )} 88 | > 89 | <div className="flex items-center gap-2 my-1"> 90 | <feature.icon className="w-4 h-4" /> 91 | <p className="text-gray-600 dark:text-gray-400"> 92 | {feature.label} 93 | </p> 94 | </div> 95 | <div className="mt-2"> 96 | <div className="max-w-full"> 97 | <div className="flex gap-3 "> 98 | <p 99 | className="max-w-lg text-xl font-normal tracking-tighter md:text-2xl" 100 | dangerouslySetInnerHTML={{ 101 | __html: feature.title, 102 | }} 103 | /> 104 | </div> 105 | </div> 106 | <p className="mt-2 text-sm text-left text-muted-foreground"> 107 | {feature.description} 108 | <a className="ml-2 underline" href="/docs" target="_blank"> 109 | Learn more 110 | </a> 111 | </p> 112 | </div> 113 | </div> 114 | ))} 115 | </div> 116 | <div className="w-full border-l-2 hidden md:block"> 117 | <Testimonial /> 118 | </div> 119 | <div className="relative col-span-3 border-t-[1.2px] border-l-[1.2px] md:border-b-[1.2px] dark:border-b-0 h-full py-20"> 120 | <div className="w-full h-full p-16 pt-10 md:px-10 2xl:px-16"> 121 | <div className="flex flex-col items-center justify-center w-full h-full gap-3"> 122 | <div className="flex items-center gap-2"> 123 | <Globe2Icon className="w-4 h-4" /> 124 | <p className="text-gray-600 dark:text-gray-400"> 125 | Own your auth 126 | </p> 127 | </div> 128 | <p className="max-w-md mx-auto mt-4 text-4xl font-normal tracking-tighter text-center md:text-4xl"> 129 | <strong>Roll your own auth with confidence in minutes!</strong> 130 | </p> 131 | <div className="flex mt-[10px] z-20 justify-center items-start"> 132 | <TechStackDisplay 133 | skills={[ 134 | "nextJs", 135 | "nuxt", 136 | "svelteKit", 137 | "astro", 138 | "solidStart", 139 | // "react", 140 | // "hono", 141 | "expo", 142 | "tanstack", 143 | ]} 144 | /> 145 | </div> 146 | <div className="flex items-center gap-2"> 147 | <GithubStat stars={stars} /> 148 | </div> 149 | <Ripple /> 150 | </div> 151 | </div> 152 | </div> 153 | </div> 154 | </div> 155 | ); 156 | } 157 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/field.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { 2 | DBFieldAttribute, 3 | DBFieldAttributeConfig, 4 | DBFieldType, 5 | } from "@better-auth/core/db"; 6 | import type { BetterAuthOptions } from "@better-auth/core"; 7 | 8 | export const createFieldAttribute = < 9 | T extends DBFieldType, 10 | C extends DBFieldAttributeConfig, 11 | >( 12 | type: T, 13 | config?: C, 14 | ) => { 15 | return { 16 | type, 17 | ...config, 18 | } satisfies DBFieldAttribute<T>; 19 | }; 20 | 21 | export type InferValueType<T extends DBFieldType> = T extends "string" 22 | ? string 23 | : T extends "number" 24 | ? number 25 | : T extends "boolean" 26 | ? boolean 27 | : T extends "date" 28 | ? Date 29 | : T extends `${infer T}[]` 30 | ? T extends "string" 31 | ? string[] 32 | : number[] 33 | : T extends Array<any> 34 | ? T[number] 35 | : never; 36 | 37 | export type InferFieldsOutput<Field> = Field extends Record< 38 | infer Key, 39 | DBFieldAttribute 40 | > 41 | ? { 42 | [key in Key as Field[key]["required"] extends false 43 | ? Field[key]["defaultValue"] extends boolean | string | number | Date 44 | ? key 45 | : never 46 | : key]: InferFieldOutput<Field[key]>; 47 | } & { 48 | [key in Key as Field[key]["returned"] extends false 49 | ? never 50 | : key]?: InferFieldOutput<Field[key]> | null; 51 | } 52 | : {}; 53 | 54 | export type InferFieldsInput<Field> = Field extends Record< 55 | infer Key, 56 | DBFieldAttribute 57 | > 58 | ? { 59 | [key in Key as Field[key]["required"] extends false 60 | ? never 61 | : Field[key]["defaultValue"] extends string | number | boolean | Date 62 | ? never 63 | : Field[key]["input"] extends false 64 | ? never 65 | : key]: InferFieldInput<Field[key]>; 66 | } & { 67 | [key in Key as Field[key]["input"] extends false ? never : key]?: 68 | | InferFieldInput<Field[key]> 69 | | undefined 70 | | null; 71 | } 72 | : {}; 73 | 74 | /** 75 | * For client will add "?" on optional fields 76 | */ 77 | export type InferFieldsInputClient<Field> = Field extends Record< 78 | infer Key, 79 | DBFieldAttribute 80 | > 81 | ? { 82 | [key in Key as Field[key]["required"] extends false 83 | ? never 84 | : Field[key]["defaultValue"] extends string | number | boolean | Date 85 | ? never 86 | : Field[key]["input"] extends false 87 | ? never 88 | : key]: InferFieldInput<Field[key]>; 89 | } & { 90 | [key in Key as Field[key]["input"] extends false 91 | ? never 92 | : Field[key]["required"] extends false 93 | ? key 94 | : Field[key]["defaultValue"] extends string | number | boolean | Date 95 | ? key 96 | : never]?: InferFieldInput<Field[key]> | undefined | null; 97 | } 98 | : {}; 99 | 100 | type InferFieldOutput<T extends DBFieldAttribute> = T["returned"] extends false 101 | ? never 102 | : T["required"] extends false 103 | ? InferValueType<T["type"]> | undefined | null 104 | : InferValueType<T["type"]>; 105 | 106 | /** 107 | * Converts a Record<string, DBFieldAttribute> to an object type 108 | * with keys and value types inferred from DBFieldAttribute["type"]. 109 | */ 110 | export type FieldAttributeToObject< 111 | Fields extends Record<string, DBFieldAttribute>, 112 | > = AddOptionalFields< 113 | { 114 | [K in keyof Fields]: InferValueType<Fields[K]["type"]>; 115 | }, 116 | Fields 117 | >; 118 | 119 | type AddOptionalFields< 120 | T extends Record<string, any>, 121 | Fields extends Record<keyof T, DBFieldAttribute>, 122 | > = { 123 | // Required fields: required === true 124 | [K in keyof T as Fields[K] extends { required: true } ? K : never]: T[K]; 125 | } & { 126 | // Optional fields: required !== true 127 | [K in keyof T as Fields[K] extends { required: true } ? never : K]?: T[K]; 128 | }; 129 | 130 | /** 131 | * Infer the additional fields from the plugin options. 132 | * For example, you can infer the additional fields of the org plugin's organization schema like this: 133 | * ```ts 134 | * type AdditionalFields = InferAdditionalFieldsFromPluginOptions<"organization", OrganizationOptions> 135 | * ``` 136 | */ 137 | export type InferAdditionalFieldsFromPluginOptions< 138 | SchemaName extends string, 139 | Options extends { 140 | schema?: { 141 | [key in SchemaName]?: { 142 | additionalFields?: Record<string, DBFieldAttribute>; 143 | }; 144 | }; 145 | }, 146 | isClientSide extends boolean = true, 147 | > = Options["schema"] extends { 148 | [key in SchemaName]?: { 149 | additionalFields: infer Field extends Record<string, DBFieldAttribute>; 150 | }; 151 | } 152 | ? isClientSide extends true 153 | ? FieldAttributeToObject<RemoveFieldsWithInputFalse<Field>> 154 | : FieldAttributeToObject<Field> 155 | : {}; 156 | 157 | type RemoveFieldsWithInputFalse<T extends Record<string, DBFieldAttribute>> = { 158 | [K in keyof T as T[K]["input"] extends false ? never : K]: T[K]; 159 | }; 160 | 161 | type InferFieldInput<T extends DBFieldAttribute> = InferValueType<T["type"]>; 162 | 163 | export type PluginFieldAttribute = Omit< 164 | DBFieldAttribute, 165 | "transform" | "defaultValue" | "hashValue" 166 | >; 167 | 168 | export type InferFieldsFromPlugins< 169 | Options extends BetterAuthOptions, 170 | Key extends string, 171 | Format extends "output" | "input" = "output", 172 | > = Options["plugins"] extends [] 173 | ? {} 174 | : Options["plugins"] extends Array<infer T> 175 | ? T extends { 176 | schema: { 177 | [key in Key]: { 178 | fields: infer Field; 179 | }; 180 | }; 181 | } 182 | ? Format extends "output" 183 | ? InferFieldsOutput<Field> 184 | : InferFieldsInput<Field> 185 | : {} 186 | : {}; 187 | 188 | export type InferFieldsFromOptions< 189 | Options extends BetterAuthOptions, 190 | Key extends "session" | "user", 191 | Format extends "output" | "input" = "output", 192 | > = Options[Key] extends { 193 | additionalFields: infer Field; 194 | } 195 | ? Format extends "output" 196 | ? InferFieldsOutput<Field> 197 | : InferFieldsInput<Field> 198 | : {}; 199 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/hono.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Hono Integration 3 | description: Integrate Better Auth with Hono. 4 | --- 5 | 6 | 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). 7 | 8 | ### Mount the handler 9 | 10 | We need to mount the handler to Hono endpoint. 11 | 12 | ```ts 13 | import { Hono } from "hono"; 14 | import { auth } from "./auth"; 15 | import { serve } from "@hono/node-server"; 16 | 17 | const app = new Hono(); 18 | 19 | app.on(["POST", "GET"], "/api/auth/*", (c) => { 20 | return auth.handler(c.req.raw); 21 | }); 22 | 23 | serve(app); 24 | ``` 25 | 26 | ### Cors 27 | 28 | To configure cors, you need to use the `cors` plugin from `hono/cors`. 29 | 30 | ```ts 31 | import { Hono } from "hono"; 32 | import { auth } from "./auth"; 33 | import { serve } from "@hono/node-server"; 34 | import { cors } from "hono/cors"; 35 | 36 | const app = new Hono(); 37 | 38 | app.use( 39 | "/api/auth/*", // or replace with "*" to enable cors for all routes 40 | cors({ 41 | origin: "http://localhost:3001", // replace with your origin 42 | allowHeaders: ["Content-Type", "Authorization"], 43 | allowMethods: ["POST", "GET", "OPTIONS"], 44 | exposeHeaders: ["Content-Length"], 45 | maxAge: 600, 46 | credentials: true, 47 | }), 48 | ); 49 | 50 | app.on(["POST", "GET"], "/api/auth/*", (c) => { 51 | return auth.handler(c.req.raw); 52 | }); 53 | 54 | serve(app); 55 | ``` 56 | 57 | > **Important:** CORS middleware must be registered before your routes. This ensures that cross-origin requests are properly handled before they reach your authentication endpoints. 58 | 59 | ### Middleware 60 | 61 | You can add a middleware to save the `session` and `user` in a `context` and also add validations for every route. 62 | 63 | ```ts 64 | import { Hono } from "hono"; 65 | import { auth } from "./auth"; 66 | import { serve } from "@hono/node-server"; 67 | import { cors } from "hono/cors"; 68 | 69 | const app = new Hono<{ 70 | Variables: { 71 | user: typeof auth.$Infer.Session.user | null; 72 | session: typeof auth.$Infer.Session.session | null 73 | } 74 | }>(); 75 | 76 | app.use("*", async (c, next) => { 77 | const session = await auth.api.getSession({ headers: c.req.raw.headers }); 78 | 79 | if (!session) { 80 | c.set("user", null); 81 | c.set("session", null); 82 | return next(); 83 | } 84 | 85 | c.set("user", session.user); 86 | c.set("session", session.session); 87 | return next(); 88 | }); 89 | 90 | app.on(["POST", "GET"], "/api/auth/*", (c) => { 91 | return auth.handler(c.req.raw); 92 | }); 93 | 94 | 95 | serve(app); 96 | ``` 97 | 98 | This will allow you to access the `user` and `session` object in all of your routes. 99 | 100 | ```ts 101 | app.get("/session", (c) => { 102 | const session = c.get("session") 103 | const user = c.get("user") 104 | 105 | if(!user) return c.body(null, 401); 106 | 107 | return c.json({ 108 | session, 109 | user 110 | }); 111 | }); 112 | ``` 113 | 114 | ### Cross-Domain Cookies 115 | 116 | By default, all Better Auth cookies are set with `SameSite=Lax`. If you need to use cookies across different domains, you’ll need to set `SameSite=None` and `Secure=true`. However, we recommend using subdomains whenever possible, as this allows you to keep `SameSite=Lax`. To enable cross-subdomain cookies, simply turn on `crossSubDomainCookies` in your auth config. 117 | 118 | ```ts title="auth.ts" 119 | export const auth = createAuth({ 120 | advanced: { 121 | crossSubDomainCookies: { 122 | enabled: true 123 | } 124 | } 125 | }) 126 | ``` 127 | 128 | If you still need to set `SameSite=None` and `Secure=true`, you can adjust these attributes globally through `cookieOptions` in the `createAuth` configuration. 129 | 130 | ```ts title="auth.ts" 131 | export const auth = createAuth({ 132 | advanced: { 133 | defaultCookieAttributes: { 134 | sameSite: "none", 135 | secure: true, 136 | partitioned: true // New browser standards will mandate this for foreign cookies 137 | } 138 | } 139 | }) 140 | ``` 141 | 142 | You can also customize cookie attributes individually by setting them within `cookies` in your auth config. 143 | 144 | ```ts title="auth.ts" 145 | export const auth = createAuth({ 146 | advanced: { 147 | cookies: { 148 | sessionToken: { 149 | attributes: { 150 | sameSite: "none", 151 | secure: true, 152 | partitioned: true // New browser standards will mandate this for foreign cookies 153 | } 154 | } 155 | } 156 | } 157 | }) 158 | ``` 159 | 160 | ### Client-Side Configuration 161 | 162 | When using the Hono client (`@hono/client`) to make requests to your Better Auth-protected endpoints, you need to configure it to send credentials (cookies) with cross-origin requests. 163 | 164 | ```ts title="api.ts" 165 | import { hc } from "hono/client"; 166 | import type { AppType } from "./server"; // Your Hono app type 167 | 168 | const client = hc<AppType>("http://localhost:8787/", { 169 | init: { 170 | credentials: "include", // Required for sending cookies cross-origin 171 | }, 172 | }); 173 | 174 | // Now your client requests will include credentials 175 | const response = await client.someProtectedEndpoint.$get(); 176 | ``` 177 | 178 | This configuration is necessary when: 179 | - Your client and server are on different domains/ports during development 180 | - You're making cross-origin requests in production 181 | - You need to send authentication cookies with your requests 182 | 183 | The `credentials: "include"` option tells the fetch client to send cookies even for cross-origin requests. This works in conjunction with the CORS configuration on your server that has `credentials: true`. 184 | 185 | > **Note:** Make sure your CORS configuration on the server matches your client's domain, and that `credentials: true` is set in both the server's CORS config and the client's fetch config. 186 | ```