This is page 12 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/account-switch.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 6 | import { 7 | Popover, 8 | PopoverContent, 9 | PopoverTrigger, 10 | } from "@/components/ui/popover"; 11 | import { 12 | Command, 13 | CommandGroup, 14 | CommandItem, 15 | CommandList, 16 | CommandSeparator, 17 | } from "@/components/ui/command"; 18 | import { ChevronDown, PlusCircle } from "lucide-react"; 19 | import { Session } from "@/lib/auth-types"; 20 | import { client, useSession } from "@/lib/auth-client"; 21 | import { useRouter } from "next/navigation"; 22 | 23 | export default function AccountSwitcher({ sessions }: { sessions: Session[] }) { 24 | const { data: currentUser } = useSession(); 25 | const [open, setOpen] = useState(false); 26 | const router = useRouter(); 27 | return ( 28 | <Popover open={open} onOpenChange={setOpen}> 29 | <PopoverTrigger asChild> 30 | <Button 31 | variant="outline" 32 | role="combobox" 33 | aria-expanded={open} 34 | aria-label="Select a user" 35 | className="w-[250px] justify-between" 36 | > 37 | <Avatar className="mr-2 h-6 w-6"> 38 | <AvatarImage 39 | src={currentUser?.user.image || undefined} 40 | alt={currentUser?.user.name} 41 | /> 42 | <AvatarFallback>{currentUser?.user.name.charAt(0)}</AvatarFallback> 43 | </Avatar> 44 | {currentUser?.user.name} 45 | <ChevronDown className="ml-auto h-4 w-4 shrink-0 opacity-50" /> 46 | </Button> 47 | </PopoverTrigger> 48 | <PopoverContent className="w-[250px] p-0"> 49 | <Command> 50 | <CommandList> 51 | <CommandGroup heading="Current Account"> 52 | <CommandItem 53 | onSelect={() => {}} 54 | className="text-sm w-full justify-between" 55 | key={currentUser?.user.id} 56 | > 57 | <div className="flex items-center"> 58 | <Avatar className="mr-2 h-5 w-5"> 59 | <AvatarImage 60 | src={currentUser?.user.image || undefined} 61 | alt={currentUser?.user.name} 62 | /> 63 | <AvatarFallback> 64 | {currentUser?.user.name.charAt(0)} 65 | </AvatarFallback> 66 | </Avatar> 67 | {currentUser?.user.name} 68 | </div> 69 | </CommandItem> 70 | </CommandGroup> 71 | <CommandSeparator /> 72 | <CommandGroup heading="Switch Account"> 73 | {sessions 74 | .filter((s) => s.user.id !== currentUser?.user.id) 75 | .map((u, i) => ( 76 | <CommandItem 77 | key={i} 78 | onSelect={async () => { 79 | await client.multiSession.setActive({ 80 | sessionToken: u.session.token, 81 | }); 82 | setOpen(false); 83 | }} 84 | className="text-sm" 85 | > 86 | <Avatar className="mr-2 h-5 w-5"> 87 | <AvatarImage 88 | src={u.user.image || undefined} 89 | alt={u.user.name} 90 | /> 91 | <AvatarFallback>{u.user.name.charAt(0)}</AvatarFallback> 92 | </Avatar> 93 | <div className="flex items-center justify-between w-full"> 94 | <div> 95 | <p>{u.user.name}</p> 96 | <p className="text-xs">({u.user.email})</p> 97 | </div> 98 | </div> 99 | </CommandItem> 100 | ))} 101 | </CommandGroup> 102 | </CommandList> 103 | <CommandSeparator /> 104 | <CommandList> 105 | <CommandGroup> 106 | <CommandItem 107 | onSelect={() => { 108 | router.push("/sign-in"); 109 | setOpen(false); 110 | }} 111 | className="cursor-pointer text-sm" 112 | > 113 | <PlusCircle className="mr-2 h-5 w-5" /> 114 | Add Account 115 | </CommandItem> 116 | </CommandGroup> 117 | </CommandList> 118 | </Command> 119 | </PopoverContent> 120 | </Popover> 121 | ); 122 | } 123 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/oauth/authorize/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { Metadata } from "next"; 2 | import { auth } from "@/lib/auth"; 3 | import { headers } from "next/headers"; 4 | import { ArrowLeftRight, ArrowUpRight, Mail, Users } from "lucide-react"; 5 | import { Card, CardContent } from "@/components/ui/card"; 6 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 7 | import { Logo } from "@/components/logo"; 8 | import Image from "next/image"; 9 | import { ConsentBtns } from "./concet-buttons"; 10 | 11 | export const metadata: Metadata = { 12 | title: "Authorize Application", 13 | description: "Grant access to your account", 14 | }; 15 | 16 | interface AuthorizePageProps { 17 | searchParams: Promise<{ 18 | redirect_uri: string; 19 | scope: string; 20 | cancel_uri: string; 21 | client_id: string; 22 | }>; 23 | } 24 | 25 | export default async function AuthorizePage({ 26 | searchParams, 27 | }: AuthorizePageProps) { 28 | const { redirect_uri, scope, client_id, cancel_uri } = await searchParams; 29 | const session = await auth.api.getSession({ 30 | headers: await headers(), 31 | }); 32 | // @ts-expect-error 33 | const clientDetails = await auth.api.getOAuthClient({ 34 | params: { 35 | id: client_id, 36 | }, 37 | headers: await headers(), 38 | }); 39 | 40 | return ( 41 | <div className="container mx-auto py-10"> 42 | <h1 className="text-2xl font-bold mb-6 text-center"> 43 | Authorize Application 44 | </h1> 45 | <div className="min-h-screen bg-black text-white flex flex-col"> 46 | <div className="flex flex-col items-center justify-center max-w-2xl mx-auto px-4"> 47 | <div className="flex items-center gap-8 mb-8"> 48 | <div className="w-16 h-16 border rounded-full flex items-center justify-center"> 49 | {clientDetails.icon ? ( 50 | <Image 51 | src={clientDetails.icon} 52 | alt="App Logo" 53 | className="object-cover" 54 | width={64} 55 | height={64} 56 | /> 57 | ) : ( 58 | <Logo /> 59 | )} 60 | </div> 61 | <ArrowLeftRight className="h-6 w-6" /> 62 | <div className="w-16 h-16 rounded-full overflow-hidden"> 63 | <Avatar className="hidden h-16 w-16 sm:flex "> 64 | <AvatarImage 65 | src={session?.user.image || "#"} 66 | alt="Avatar" 67 | className="object-cover" 68 | /> 69 | <AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback> 70 | </Avatar> 71 | </div> 72 | </div> 73 | 74 | <h1 className="text-3xl font-semibold text-center mb-8"> 75 | {clientDetails.name} is requesting access to your Better Auth 76 | account 77 | </h1> 78 | 79 | <Card className="w-full bg-zinc-900 border-zinc-800 rounded-none"> 80 | <CardContent className="p-6"> 81 | <div className="flex items-center justify-between p-4 bg-zinc-800 rounded-lg mb-6"> 82 | <div> 83 | <div className="font-medium">{session?.user.name}</div> 84 | <div className="text-zinc-400">{session?.user.email}</div> 85 | </div> 86 | <ArrowUpRight className="h-5 w-5 text-zinc-400" /> 87 | </div> 88 | <div className="flex flex-col gap-1"> 89 | <div className="text-lg mb-4"> 90 | Continuing will allow Sign in with {clientDetails.name} to: 91 | </div> 92 | {scope.includes("profile") && ( 93 | <div className="flex items-center gap-3 text-zinc-300"> 94 | <Users className="h-5 w-5" /> 95 | <span>Read your Better Auth user data.</span> 96 | </div> 97 | )} 98 | 99 | {scope.includes("email") && ( 100 | <div className="flex items-center gap-3 text-zinc-300"> 101 | <Mail className="h-5 w-5" /> 102 | <span>Read your email address.</span> 103 | </div> 104 | )} 105 | </div> 106 | </CardContent> 107 | <ConsentBtns /> 108 | </Card> 109 | </div> 110 | </div> 111 | </div> 112 | ); 113 | } 114 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Ionicons from "@expo/vector-icons/AntDesign"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { Text } from "@/components/ui/text"; 5 | import { authClient } from "@/lib/auth-client"; 6 | import { Image, View } from "react-native"; 7 | import { Separator } from "@/components/ui/separator"; 8 | import { Input } from "@/components/ui/input"; 9 | import { useEffect, useState } from "react"; 10 | import { router, useNavigationContainerRef } from "expo-router"; 11 | import { useStore } from "@nanostores/react"; 12 | 13 | export default function Index() { 14 | const { data: isAuthenticated } = useStore(authClient.useSession); 15 | const navContainerRef = useNavigationContainerRef(); 16 | const [email, setEmail] = useState(""); 17 | const [password, setPassword] = useState(""); 18 | 19 | useEffect(() => { 20 | if (isAuthenticated) { 21 | if (navContainerRef.isReady()) { 22 | router.push("/dashboard"); 23 | } 24 | } 25 | }, [isAuthenticated, navContainerRef.isReady()]); 26 | return ( 27 | <Card className="z-50 mx-6 backdrop-blur-lg bg-gray-200/70"> 28 | <CardHeader className="flex items-center justify-center gap-8"> 29 | <Image 30 | source={require("../../assets/images/logo.png")} 31 | style={{ 32 | width: 40, 33 | height: 40, 34 | }} 35 | /> 36 | <CardTitle>Sign In to your account</CardTitle> 37 | </CardHeader> 38 | <View className="px-6 flex gap-2"> 39 | <Button 40 | onPress={() => { 41 | authClient.signIn.social({ 42 | provider: "google", 43 | callbackURL: "/dashboard", 44 | }); 45 | }} 46 | variant="secondary" 47 | className="flex flex-row gap-2 items-center bg-white/50" 48 | > 49 | <Ionicons name="google" size={16} /> 50 | <Text>Sign In with Google</Text> 51 | </Button> 52 | <Button 53 | variant="secondary" 54 | className="flex flex-row gap-2 items-center bg-white/50" 55 | onPress={() => { 56 | authClient.signIn.social({ 57 | provider: "github", 58 | callbackURL: "/dashboard", 59 | }); 60 | }} 61 | > 62 | <Ionicons name="github" size={16} /> 63 | <Text>Sign In with GitHub</Text> 64 | </Button> 65 | </View> 66 | <View className="flex-row gap-2 w-full items-center px-6 my-4"> 67 | <Separator className="flex-grow w-3/12" /> 68 | <Text>or continue with</Text> 69 | <Separator className="flex-grow w-3/12" /> 70 | </View> 71 | <View className="px-6"> 72 | <Input 73 | placeholder="Email Address" 74 | className="rounded-b-none border-b-0" 75 | value={email} 76 | onChangeText={(text) => { 77 | setEmail(text); 78 | }} 79 | /> 80 | <Input 81 | placeholder="Password" 82 | className="rounded-t-none" 83 | secureTextEntry 84 | value={password} 85 | onChangeText={(text) => { 86 | setPassword(text); 87 | }} 88 | /> 89 | </View> 90 | <CardFooter> 91 | <View className="w-full"> 92 | <Button 93 | variant="link" 94 | className="w-full" 95 | onPress={() => { 96 | router.push("/forget-password"); 97 | }} 98 | > 99 | <Text className="underline text-center">Forget Password?</Text> 100 | </Button> 101 | <Button 102 | onPress={() => { 103 | authClient.signIn.email( 104 | { 105 | email, 106 | password, 107 | }, 108 | { 109 | onError: (ctx) => { 110 | alert(ctx.error.message); 111 | }, 112 | }, 113 | ); 114 | }} 115 | > 116 | <Text>Continue</Text> 117 | </Button> 118 | <Text className="text-center mt-2"> 119 | Don't have an account?{" "} 120 | <Text 121 | className="underline" 122 | onPress={() => { 123 | router.push("/sign-up"); 124 | }} 125 | > 126 | Create Account 127 | </Text> 128 | </Text> 129 | </View> 130 | </CardFooter> 131 | </Card> 132 | ); 133 | } 134 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPlugin } from "@better-auth/core"; 2 | import type { StripEmptyObjects, UnionToIntersection } from "../types/helper"; 3 | import type { Auth } from "../auth"; 4 | import type { InferRoutes } from "./path-to-object"; 5 | import type { Session, User } from "../types"; 6 | import type { InferFieldsInputClient, InferFieldsOutput } from "../db"; 7 | import type { 8 | ClientStore, 9 | ClientAtomListener, 10 | BetterAuthClientOptions, 11 | BetterAuthClientPlugin, 12 | } from "@better-auth/core"; 13 | export type { 14 | ClientStore, 15 | ClientAtomListener, 16 | BetterAuthClientOptions, 17 | BetterAuthClientPlugin, 18 | }; 19 | 20 | /** 21 | * @deprecated use type `BetterAuthClientOptions` instead. 22 | */ 23 | export type Store = ClientStore; 24 | /** 25 | * @deprecated use type `ClientAtomListener` instead. 26 | */ 27 | export type AtomListener = ClientAtomListener; 28 | /** 29 | * @deprecated use type `BetterAuthClientPlugin` instead. 30 | */ 31 | export type ClientOptions = BetterAuthClientOptions; 32 | 33 | export type InferClientAPI<O extends BetterAuthClientOptions> = InferRoutes< 34 | O["plugins"] extends Array<any> 35 | ? Auth["api"] & 36 | (O["plugins"] extends Array<infer Pl> 37 | ? UnionToIntersection< 38 | Pl extends { 39 | $InferServerPlugin: infer Plug; 40 | } 41 | ? Plug extends { 42 | endpoints: infer Endpoints; 43 | } 44 | ? Endpoints 45 | : {} 46 | : {} 47 | > 48 | : {}) 49 | : Auth["api"], 50 | O 51 | >; 52 | 53 | export type InferActions<O extends BetterAuthClientOptions> = 54 | (O["plugins"] extends Array<infer Plugin> 55 | ? UnionToIntersection< 56 | Plugin extends BetterAuthClientPlugin 57 | ? Plugin["getActions"] extends (...args: any) => infer Actions 58 | ? Actions 59 | : {} 60 | : {} 61 | > 62 | : {}) & 63 | //infer routes from auth config 64 | InferRoutes< 65 | O["$InferAuth"] extends { 66 | plugins: infer Plugins; 67 | } 68 | ? Plugins extends Array<infer Plugin> 69 | ? Plugin extends { 70 | endpoints: infer Endpoints; 71 | } 72 | ? Endpoints 73 | : {} 74 | : {} 75 | : {}, 76 | O 77 | >; 78 | 79 | export type InferErrorCodes<O extends BetterAuthClientOptions> = 80 | O["plugins"] extends Array<infer Plugin> 81 | ? UnionToIntersection< 82 | Plugin extends BetterAuthClientPlugin 83 | ? Plugin["$InferServerPlugin"] extends BetterAuthPlugin 84 | ? Plugin["$InferServerPlugin"]["$ERROR_CODES"] 85 | : {} 86 | : {} 87 | > 88 | : {}; 89 | /** 90 | * signals are just used to recall a computed value. 91 | * as a convention they start with "$" 92 | */ 93 | export type IsSignal<T> = T extends `$${infer _}` ? true : false; 94 | 95 | export type InferPluginsFromClient<O extends BetterAuthClientOptions> = 96 | O["plugins"] extends Array<BetterAuthClientPlugin> 97 | ? Array<O["plugins"][number]["$InferServerPlugin"]> 98 | : undefined; 99 | 100 | export type InferSessionFromClient<O extends BetterAuthClientOptions> = 101 | StripEmptyObjects< 102 | Session & 103 | UnionToIntersection<InferAdditionalFromClient<O, "session", "output">> 104 | >; 105 | export type InferUserFromClient<O extends BetterAuthClientOptions> = 106 | StripEmptyObjects< 107 | User & UnionToIntersection<InferAdditionalFromClient<O, "user", "output">> 108 | >; 109 | 110 | export type InferAdditionalFromClient< 111 | Options extends BetterAuthClientOptions, 112 | Key extends string, 113 | Format extends "input" | "output" = "output", 114 | > = Options["plugins"] extends Array<infer T> 115 | ? T extends BetterAuthClientPlugin 116 | ? T["$InferServerPlugin"] extends { 117 | schema: { 118 | [key in Key]: { 119 | fields: infer Field; 120 | }; 121 | }; 122 | } 123 | ? Format extends "input" 124 | ? InferFieldsInputClient<Field> 125 | : InferFieldsOutput<Field> 126 | : {} 127 | : {} 128 | : {}; 129 | 130 | export type SessionQueryParams = { 131 | disableCookieCache?: boolean; 132 | disableRefresh?: boolean; 133 | }; 134 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-up.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { afterEach, describe, expect, vi } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | 4 | describe("sign-up with custom fields", async (it) => { 5 | const mockFn = vi.fn(); 6 | const { auth, db } = await getTestInstance( 7 | { 8 | account: { 9 | fields: { 10 | providerId: "provider_id", 11 | accountId: "account_id", 12 | }, 13 | }, 14 | user: { 15 | additionalFields: { 16 | newField: { 17 | type: "string", 18 | required: false, 19 | }, 20 | newField2: { 21 | type: "string", 22 | required: false, 23 | }, 24 | isAdmin: { 25 | type: "boolean", 26 | defaultValue: true, 27 | input: false, 28 | }, 29 | role: { 30 | input: false, 31 | type: "string", 32 | required: false, 33 | }, 34 | }, 35 | }, 36 | emailVerification: { 37 | sendOnSignUp: true, 38 | sendVerificationEmail: mockFn, 39 | }, 40 | }, 41 | { 42 | disableTestUser: true, 43 | }, 44 | ); 45 | 46 | afterEach(() => { 47 | mockFn.mockReset(); 48 | }); 49 | 50 | it("should work with custom fields on account table", async () => { 51 | const res = await auth.api.signUpEmail({ 52 | body: { 53 | email: "[email protected]", 54 | password: "password", 55 | name: "Test Name", 56 | image: "https://picsum.photos/200", 57 | }, 58 | }); 59 | expect(res.token).toBeDefined(); 60 | const users = await db.findMany({ 61 | model: "user", 62 | }); 63 | const accounts = await db.findMany({ 64 | model: "account", 65 | }); 66 | expect(accounts).toHaveLength(1); 67 | 68 | expect("isAdmin" in (users[0] as any)).toBe(true); 69 | expect((users[0] as any).isAdmin).toBe(true); 70 | 71 | expect(mockFn).toHaveBeenCalledTimes(1); 72 | expect(mockFn).toHaveBeenCalledWith( 73 | expect.objectContaining({ 74 | token: expect.any(String), 75 | url: expect.any(String), 76 | user: expect.any(Object), 77 | }), 78 | ); 79 | }); 80 | 81 | it("should get the ipAddress and userAgent from headers", async () => { 82 | const res = await auth.api.signUpEmail({ 83 | body: { 84 | email: "[email protected]", 85 | password: "password", 86 | name: "Test Name", 87 | }, 88 | headers: new Headers({ 89 | "x-forwarded-for": "127.0.0.1", 90 | "user-agent": "test-user-agent", 91 | }), 92 | }); 93 | const session = await auth.api.getSession({ 94 | headers: new Headers({ 95 | authorization: `Bearer ${res.token}`, 96 | }), 97 | }); 98 | expect(session).toBeDefined(); 99 | expect(session!.session).toMatchObject({ 100 | userAgent: "test-user-agent", 101 | ipAddress: "127.0.0.1", 102 | }); 103 | }); 104 | 105 | it("should rollback when session creation fails", async ({ skip }) => { 106 | const ctx = await auth.$context; 107 | if (!ctx.adapter.options?.adapterConfig.transaction) { 108 | skip(); 109 | } 110 | const originalCreateSession = ctx.internalAdapter.createSession; 111 | ctx.internalAdapter.createSession = vi 112 | .fn() 113 | .mockRejectedValue(new Error("Session creation failed")); 114 | 115 | await expect( 116 | auth.api.signUpEmail({ 117 | body: { 118 | email: "[email protected]", 119 | password: "password", 120 | name: "Rollback Test", 121 | }, 122 | }), 123 | ).rejects.toThrow(); 124 | 125 | const users = await db.findMany({ model: "user" }); 126 | const rollbackUser = users.find( 127 | (u: any) => u.email === "[email protected]", 128 | ); 129 | expect(rollbackUser).toBeUndefined(); 130 | 131 | ctx.internalAdapter.createSession = originalCreateSession; 132 | }); 133 | 134 | it("should not allow user to set the field that is set to input: false", async () => { 135 | const res = await auth.api.signUpEmail({ 136 | body: { 137 | email: "[email protected]", 138 | password: "password", 139 | name: "Input False Test", 140 | //@ts-expect-error 141 | role: "admin", 142 | }, 143 | }); 144 | const session = await auth.api.getSession({ 145 | headers: new Headers({ 146 | authorization: `Bearer ${res.token}`, 147 | }), 148 | }); 149 | expect(session?.user.role).toBeNull(); 150 | }); 151 | }); 152 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/error.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { HIDE_METADATA } from "../../utils/hide-metadata"; 2 | import { createAuthEndpoint } from "@better-auth/core/api"; 3 | 4 | function sanitize(input: string): string { 5 | return input 6 | .replace(/&/g, "&") 7 | .replace(/</g, "<") 8 | .replace(/>/g, ">") 9 | .replace(/"/g, """) 10 | .replace(/'/g, "'"); 11 | } 12 | 13 | const html = (errorCode: string = "Unknown") => `<!DOCTYPE html> 14 | <html lang="en"> 15 | <head> 16 | <meta charset="UTF-8"> 17 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 18 | <title>Authentication Error</title> 19 | <style> 20 | :root { 21 | --bg-color: #f8f9fa; 22 | --text-color: #212529; 23 | --accent-color: #000000; 24 | --error-color: #dc3545; 25 | --border-color: #e9ecef; 26 | } 27 | body { 28 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 29 | background-color: var(--bg-color); 30 | color: var(--text-color); 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | height: 100vh; 35 | margin: 0; 36 | line-height: 1.5; 37 | } 38 | .error-container { 39 | background-color: #ffffff; 40 | border-radius: 12px; 41 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 42 | padding: 2.5rem; 43 | text-align: center; 44 | max-width: 90%; 45 | width: 400px; 46 | } 47 | h1 { 48 | color: var(--error-color); 49 | font-size: 1.75rem; 50 | margin-bottom: 1rem; 51 | font-weight: 600; 52 | } 53 | p { 54 | margin-bottom: 1.5rem; 55 | color: #495057; 56 | } 57 | .btn { 58 | background-color: var(--accent-color); 59 | color: #ffffff; 60 | text-decoration: none; 61 | padding: 0.75rem 1.5rem; 62 | border-radius: 6px; 63 | transition: all 0.3s ease; 64 | display: inline-block; 65 | font-weight: 500; 66 | border: 2px solid var(--accent-color); 67 | } 68 | .btn:hover { 69 | background-color: #131721; 70 | } 71 | .error-code { 72 | font-size: 0.875rem; 73 | color: #6c757d; 74 | margin-top: 1.5rem; 75 | padding-top: 1.5rem; 76 | border-top: 1px solid var(--border-color); 77 | } 78 | .icon { 79 | font-size: 3rem; 80 | margin-bottom: 1rem; 81 | } 82 | </style> 83 | </head> 84 | <body> 85 | <div class="error-container"> 86 | <div class="icon">⚠️</div> 87 | <h1>Better Auth Error</h1> 88 | <p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p> 89 | <a href="/" id="returnLink" class="btn">Return to Application</a> 90 | <div class="error-code">Error Code: <span id="errorCode">${sanitize( 91 | errorCode, 92 | )}</span></div> 93 | </div> 94 | </body> 95 | </html>`; 96 | export const error = createAuthEndpoint( 97 | "/error", 98 | { 99 | method: "GET", 100 | metadata: { 101 | ...HIDE_METADATA, 102 | openapi: { 103 | description: "Displays an error page", 104 | responses: { 105 | "200": { 106 | description: "Success", 107 | content: { 108 | "text/html": { 109 | schema: { 110 | type: "string", 111 | description: "The HTML content of the error page", 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }, 118 | }, 119 | }, 120 | async (c) => { 121 | const query = 122 | new URL(c.request?.url || "").searchParams.get("error") || "Unknown"; 123 | return new Response(html(query), { 124 | headers: { 125 | "Content-Type": "text/html", 126 | }, 127 | }); 128 | }, 129 | ); 130 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createFetch } from "@better-fetch/fetch"; 2 | import { getBaseURL } from "../utils/url"; 3 | import { type WritableAtom } from "nanostores"; 4 | import type { 5 | BetterAuthClientOptions, 6 | ClientAtomListener, 7 | } from "@better-auth/core"; 8 | import { redirectPlugin } from "./fetch-plugins"; 9 | import { getSessionAtom } from "./session-atom"; 10 | import { parseJSON } from "./parser"; 11 | 12 | export const getClientConfig = ( 13 | options?: BetterAuthClientOptions, 14 | loadEnv?: boolean, 15 | ) => { 16 | /* check if the credentials property is supported. Useful for cf workers */ 17 | const isCredentialsSupported = "credentials" in Request.prototype; 18 | const baseURL = 19 | getBaseURL(options?.baseURL, options?.basePath, undefined, loadEnv) ?? 20 | "/api/auth"; 21 | const pluginsFetchPlugins = 22 | options?.plugins 23 | ?.flatMap((plugin) => plugin.fetchPlugins) 24 | .filter((pl) => pl !== undefined) || []; 25 | const lifeCyclePlugin = { 26 | id: "lifecycle-hooks", 27 | name: "lifecycle-hooks", 28 | hooks: { 29 | onSuccess: options?.fetchOptions?.onSuccess, 30 | onError: options?.fetchOptions?.onError, 31 | onRequest: options?.fetchOptions?.onRequest, 32 | onResponse: options?.fetchOptions?.onResponse, 33 | }, 34 | }; 35 | const { onSuccess, onError, onRequest, onResponse, ...restOfFetchOptions } = 36 | options?.fetchOptions || {}; 37 | const $fetch = createFetch({ 38 | baseURL, 39 | ...(isCredentialsSupported ? { credentials: "include" } : {}), 40 | method: "GET", 41 | jsonParser(text) { 42 | if (!text) { 43 | return null as any; 44 | } 45 | return parseJSON(text, { 46 | strict: false, 47 | }); 48 | }, 49 | customFetchImpl: fetch, 50 | ...restOfFetchOptions, 51 | plugins: [ 52 | lifeCyclePlugin, 53 | ...(restOfFetchOptions.plugins || []), 54 | ...(options?.disableDefaultFetchPlugins ? [] : [redirectPlugin]), 55 | ...pluginsFetchPlugins, 56 | ], 57 | }); 58 | const { $sessionSignal, session } = getSessionAtom($fetch); 59 | const plugins = options?.plugins || []; 60 | let pluginsActions = {} as Record<string, any>; 61 | let pluginsAtoms = { 62 | $sessionSignal, 63 | session, 64 | } as Record<string, WritableAtom<any>>; 65 | let pluginPathMethods: Record<string, "POST" | "GET"> = { 66 | "/sign-out": "POST", 67 | "/revoke-sessions": "POST", 68 | "/revoke-other-sessions": "POST", 69 | "/delete-user": "POST", 70 | }; 71 | const atomListeners: ClientAtomListener[] = [ 72 | { 73 | signal: "$sessionSignal", 74 | matcher(path) { 75 | return ( 76 | path === "/sign-out" || 77 | path === "/update-user" || 78 | path.startsWith("/sign-in") || 79 | path.startsWith("/sign-up") || 80 | path === "/delete-user" || 81 | path === "/verify-email" 82 | ); 83 | }, 84 | }, 85 | ]; 86 | 87 | for (const plugin of plugins) { 88 | if (plugin.getAtoms) { 89 | Object.assign(pluginsAtoms, plugin.getAtoms?.($fetch)); 90 | } 91 | if (plugin.pathMethods) { 92 | Object.assign(pluginPathMethods, plugin.pathMethods); 93 | } 94 | if (plugin.atomListeners) { 95 | atomListeners.push(...plugin.atomListeners); 96 | } 97 | } 98 | 99 | const $store = { 100 | notify: (signal?: Omit<string, "$sessionSignal"> | "$sessionSignal") => { 101 | pluginsAtoms[signal as keyof typeof pluginsAtoms]!.set( 102 | !pluginsAtoms[signal as keyof typeof pluginsAtoms]!.get(), 103 | ); 104 | }, 105 | listen: ( 106 | signal: Omit<string, "$sessionSignal"> | "$sessionSignal", 107 | listener: (value: boolean, oldValue?: boolean | undefined) => void, 108 | ) => { 109 | pluginsAtoms[signal as keyof typeof pluginsAtoms]!.subscribe(listener); 110 | }, 111 | atoms: pluginsAtoms, 112 | }; 113 | 114 | for (const plugin of plugins) { 115 | if (plugin.getActions) { 116 | Object.assign( 117 | pluginsActions, 118 | plugin.getActions?.($fetch, $store, options), 119 | ); 120 | } 121 | } 122 | return { 123 | get baseURL() { 124 | return baseURL; 125 | }, 126 | pluginsActions, 127 | pluginsAtoms, 128 | pluginPathMethods, 129 | atomListeners, 130 | $fetch, 131 | $store, 132 | }; 133 | }; 134 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/slack.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Slack 3 | description: Slack provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Slack credentials 9 | To use Slack as a social provider, you need to create a Slack app and get your credentials. 10 | 11 | 1. Go to [Your Apps on Slack API](https://api.slack.com/apps) and click "Create New App" 12 | 2. Choose "From scratch" and give your app a name and select a development workspace 13 | 3. In your app settings, navigate to "OAuth & Permissions" 14 | 4. Under "Redirect URLs", add your redirect URL: 15 | - For local development: `http://localhost:3000/api/auth/callback/slack` 16 | - For production: `https://yourdomain.com/api/auth/callback/slack` 17 | 5. Copy your Client ID and Client Secret from the "Basic Information" page 18 | 19 | <Callout> 20 | Slack requires HTTPS for redirect URLs in production. For local development, you can use tools like [ngrok](https://ngrok.com/) to create a secure tunnel. 21 | </Callout> 22 | </Step> 23 | 24 | <Step> 25 | ### Configure the provider 26 | To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.slack` in your auth configuration. 27 | 28 | ```ts title="auth.ts" 29 | import { betterAuth } from "better-auth" 30 | 31 | export const auth = betterAuth({ 32 | socialProviders: { 33 | slack: { // [!code highlight] 34 | clientId: process.env.SLACK_CLIENT_ID as string, // [!code highlight] 35 | clientSecret: process.env.SLACK_CLIENT_SECRET as string, // [!code highlight] 36 | }, // [!code highlight] 37 | }, 38 | }) 39 | ``` 40 | </Step> 41 | 42 | </Steps> 43 | 44 | ## Usage 45 | 46 | ### Sign In with Slack 47 | 48 | To sign in with Slack, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 49 | 50 | - `provider`: The provider to use. It should be set to `slack`. 51 | 52 | ```ts title="auth-client.ts" 53 | import { createAuthClient } from "better-auth/client"; 54 | const authClient = createAuthClient(); 55 | 56 | const signIn = async () => { 57 | const data = await authClient.signIn.social({ provider: "slack" }); 58 | }; 59 | ``` 60 | 61 | ### Requesting Additional Scopes 62 | 63 | By default, Slack uses OpenID Connect scopes: `openid`, `profile`, and `email`. You can request additional Slack scopes during sign-in: 64 | 65 | ```ts title="auth-client.ts" 66 | const signInWithSlack = async () => { 67 | await authClient.signIn.social({ 68 | provider: "slack", 69 | scopes: ["channels:read", "chat:write"], // Additional Slack API scopes 70 | }); 71 | }; 72 | ``` 73 | 74 | ### Workspace-Specific Sign In 75 | 76 | If you want to restrict sign-in to a specific Slack workspace, you can pass the `team` parameter: 77 | 78 | ```ts title="auth.ts" 79 | socialProviders: { 80 | slack: { 81 | clientId: process.env.SLACK_CLIENT_ID as string, 82 | clientSecret: process.env.SLACK_CLIENT_SECRET as string, 83 | team: "T1234567890", // Your Slack workspace ID 84 | }, 85 | } 86 | ``` 87 | 88 | ### Using Slack API After Sign In 89 | 90 | After successful authentication, you can access the user's Slack information through the session. The access token can be used to make requests to the Slack API: 91 | 92 | ```ts 93 | const session = await authClient.getSession(); 94 | if (session?.user) { 95 | // Access Slack-specific data 96 | const slackUserId = session.user.id; // This is the Slack user ID 97 | // The access token is stored securely on the server 98 | } 99 | ``` 100 | 101 | <Callout> 102 | The Slack provider uses OpenID Connect by default, which provides basic user 103 | information. If you need to access other Slack APIs, make sure to request the 104 | appropriate scopes during sign-in. 105 | </Callout> 106 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/forget-password/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Alert, AlertDescription } from "@/components/ui/alert"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardFooter, 10 | CardHeader, 11 | CardTitle, 12 | } from "@/components/ui/card"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Label } from "@/components/ui/label"; 15 | import { client } from "@/lib/auth-client"; 16 | import { AlertCircle, ArrowLeft, CheckCircle2 } from "lucide-react"; 17 | import Link from "next/link"; 18 | import { useState } from "react"; 19 | 20 | export default function Component() { 21 | const [email, setEmail] = useState(""); 22 | const [isSubmitting, setIsSubmitting] = useState(false); 23 | const [isSubmitted, setIsSubmitted] = useState(false); 24 | const [error, setError] = useState(""); 25 | 26 | const handleSubmit = async (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | setIsSubmitting(true); 29 | setError(""); 30 | 31 | try { 32 | await client.requestPasswordReset({ 33 | email, 34 | redirectTo: "/reset-password", 35 | }); 36 | setIsSubmitted(true); 37 | } catch (err) { 38 | setError("An error occurred. Please try again."); 39 | } finally { 40 | setIsSubmitting(false); 41 | } 42 | }; 43 | 44 | if (isSubmitted) { 45 | return ( 46 | <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> 47 | <Card className="w-[350px]"> 48 | <CardHeader> 49 | <CardTitle>Check your email</CardTitle> 50 | <CardDescription> 51 | We've sent a password reset link to your email. 52 | </CardDescription> 53 | </CardHeader> 54 | <CardContent> 55 | <Alert variant="default"> 56 | <CheckCircle2 className="h-4 w-4" /> 57 | <AlertDescription> 58 | If you don't see the email, check your spam folder. 59 | </AlertDescription> 60 | </Alert> 61 | </CardContent> 62 | <CardFooter> 63 | <Button 64 | variant="outline" 65 | className="w-full" 66 | onClick={() => setIsSubmitted(false)} 67 | > 68 | <ArrowLeft className="mr-2 h-4 w-4" /> Back to reset password 69 | </Button> 70 | </CardFooter> 71 | </Card> 72 | </main> 73 | ); 74 | } 75 | 76 | return ( 77 | <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> 78 | {/* Radial gradient for the container to give a faded look */} 79 | <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> 80 | <Card className="w-[350px]"> 81 | <CardHeader> 82 | <CardTitle>Forgot password</CardTitle> 83 | <CardDescription> 84 | Enter your email to reset your password 85 | </CardDescription> 86 | </CardHeader> 87 | <CardContent> 88 | <form onSubmit={handleSubmit}> 89 | <div className="grid w-full items-center gap-4"> 90 | <div className="flex flex-col space-y-1.5"> 91 | <Label htmlFor="email">Email</Label> 92 | <Input 93 | id="email" 94 | type="email" 95 | placeholder="Enter your email" 96 | value={email} 97 | onChange={(e) => setEmail(e.target.value)} 98 | required 99 | /> 100 | </div> 101 | </div> 102 | {error && ( 103 | <Alert variant="destructive" className="mt-4"> 104 | <AlertCircle className="h-4 w-4" /> 105 | <AlertDescription>{error}</AlertDescription> 106 | </Alert> 107 | )} 108 | <Button 109 | className="w-full mt-4" 110 | type="submit" 111 | disabled={isSubmitting} 112 | > 113 | {isSubmitting ? "Sending..." : "Send reset link"} 114 | </Button> 115 | </form> 116 | </CardContent> 117 | <CardFooter className="flex justify-center"> 118 | <Link href="/sign-in"> 119 | <Button variant="link" className="px-0"> 120 | Back to sign in 121 | </Button> 122 | </Link> 123 | </CardFooter> 124 | </Card> 125 | </main> 126 | ); 127 | } 128 | ``` -------------------------------------------------------------------------------- /docs/components/builder/code-tabs/index.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useState } from "react"; 2 | import { TabBar } from "./tab-bar"; 3 | import { CodeEditor } from "./code-editor"; 4 | import { useAtom } from "jotai"; 5 | import { optionsAtom } from "../store"; 6 | import { js_beautify } from "js-beautify"; 7 | import { signUpString } from "../sign-up"; 8 | import { signInString } from "../sign-in"; 9 | 10 | export default function CodeTabs() { 11 | const [options] = useAtom(optionsAtom); 12 | 13 | const initialFiles = [ 14 | { 15 | id: "1", 16 | name: "auth.ts", 17 | content: `import { betterAuth } from 'better-auth'; 18 | 19 | export const auth = betterAuth({ 20 | ${ 21 | options.email 22 | ? `emailAndPassword: { 23 | enabled: true, 24 | ${ 25 | options.requestPasswordReset 26 | ? `async sendResetPassword(data, request) { 27 | // Send an email to the user with a link to reset their password 28 | },` 29 | : `` 30 | } 31 | },` 32 | : "" 33 | }${ 34 | options.socialProviders.length 35 | ? `socialProviders: ${JSON.stringify( 36 | options.socialProviders.reduce((acc, provider) => { 37 | return { 38 | ...acc, 39 | [provider]: { 40 | clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`, 41 | clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`, 42 | }, 43 | }; 44 | }, {}), 45 | ).replace(/"/g, "")},` 46 | : "" 47 | } 48 | ${ 49 | options.magicLink || options.passkey 50 | ? `plugins: [ 51 | ${ 52 | options.magicLink 53 | ? `magicLink({ 54 | async sendMagicLink(data) { 55 | // Send an email to the user with a magic link 56 | }, 57 | }),` 58 | : `${options.passkey ? `passkey(),` : ""}` 59 | } 60 | ${options.passkey && options.magicLink ? `passkey(),` : ""} 61 | ]` 62 | : "" 63 | } 64 | /** if no database is provided, the user data will be stored in memory. 65 | * Make sure to provide a database to persist user data **/ 66 | }); 67 | `, 68 | }, 69 | { 70 | id: "2", 71 | name: "auth-client.ts", 72 | content: `import { createAuthClient } from "better-auth/react"; 73 | ${ 74 | options.magicLink || options.passkey 75 | ? `import { ${options.magicLink ? "magicLinkClient, " : ""}, ${ 76 | options.passkey ? "passkeyClient" : "" 77 | } } from "better-auth/client/plugins";` 78 | : "" 79 | } 80 | 81 | export const authClient = createAuthClient({ 82 | baseURL: process.env.NEXT_PUBLIC_APP_URL, 83 | ${ 84 | options.magicLink || options.passkey 85 | ? `plugins: [${options.magicLink ? `magicLinkClient(),` : ""}${ 86 | options.passkey ? `passkeyClient(),` : "" 87 | }],` 88 | : "" 89 | } 90 | }) 91 | 92 | export const { signIn, signOut, signUp, useSession } = authClient; 93 | `, 94 | }, 95 | { 96 | id: "3", 97 | name: "sign-in.tsx", 98 | content: signInString(options), 99 | }, 100 | ]; 101 | if (options.email) { 102 | initialFiles.push({ 103 | id: "4", 104 | name: "sign-up.tsx", 105 | content: signUpString, 106 | }); 107 | } 108 | 109 | const [files, setFiles] = useState(initialFiles); 110 | const [activeFileId, setActiveFileId] = useState(files[0].id); 111 | 112 | const handleTabClick = (fileId: string) => { 113 | setActiveFileId(fileId); 114 | }; 115 | 116 | const handleTabClose = (fileId: string) => { 117 | setFiles(files.filter((file) => file.id !== fileId)); 118 | if (activeFileId === fileId) { 119 | setActiveFileId(files[0].id); 120 | } 121 | }; 122 | 123 | const activeFile = files.find((file) => file.id === activeFileId); 124 | 125 | return ( 126 | <div className="w-full mr-auto max-w-[45rem] mt-8 border border-border rounded-md overflow-hidden"> 127 | <TabBar 128 | files={files} 129 | activeFileId={activeFileId} 130 | onTabClick={handleTabClick} 131 | onTabClose={handleTabClose} 132 | /> 133 | <div className=""> 134 | {activeFile && ( 135 | <CodeEditor 136 | language="typescript" 137 | code={ 138 | activeFile.name.endsWith(".ts") 139 | ? js_beautify(activeFile.content) 140 | : activeFile.content.replace(/\n{3,}/g, "\n\n") 141 | } 142 | /> 143 | )} 144 | </div> 145 | </div> 146 | ); 147 | } 148 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-sqlite-number-id.txt: -------------------------------------------------------------------------------- ``` 1 | import { sql } from "drizzle-orm"; 2 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 3 | 4 | export const custom_user = sqliteTable("custom_user", { 5 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 6 | name: text("name").notNull(), 7 | email: text("email").notNull().unique(), 8 | emailVerified: integer("email_verified", { mode: "boolean" }) 9 | .default(false) 10 | .notNull(), 11 | image: text("image"), 12 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 13 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 14 | .notNull(), 15 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 16 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 17 | .$onUpdate(() => /* @__PURE__ */ new Date()) 18 | .notNull(), 19 | twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).default( 20 | false, 21 | ), 22 | username: text("username").unique(), 23 | displayUsername: text("display_username"), 24 | }); 25 | 26 | export const custom_session = sqliteTable("custom_session", { 27 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 28 | expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), 29 | token: text("token").notNull().unique(), 30 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 31 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 32 | .notNull(), 33 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 34 | .$onUpdate(() => /* @__PURE__ */ new Date()) 35 | .notNull(), 36 | ipAddress: text("ip_address"), 37 | userAgent: text("user_agent"), 38 | userId: integer("user_id") 39 | .notNull() 40 | .references(() => custom_user.id, { onDelete: "cascade" }), 41 | }); 42 | 43 | export const custom_account = sqliteTable("custom_account", { 44 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 45 | accountId: text("account_id").notNull(), 46 | providerId: text("provider_id").notNull(), 47 | userId: integer("user_id") 48 | .notNull() 49 | .references(() => custom_user.id, { onDelete: "cascade" }), 50 | accessToken: text("access_token"), 51 | refreshToken: text("refresh_token"), 52 | idToken: text("id_token"), 53 | accessTokenExpiresAt: integer("access_token_expires_at", { 54 | mode: "timestamp_ms", 55 | }), 56 | refreshTokenExpiresAt: integer("refresh_token_expires_at", { 57 | mode: "timestamp_ms", 58 | }), 59 | scope: text("scope"), 60 | password: text("password"), 61 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 62 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 63 | .notNull(), 64 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 65 | .$onUpdate(() => /* @__PURE__ */ new Date()) 66 | .notNull(), 67 | }); 68 | 69 | export const custom_verification = sqliteTable("custom_verification", { 70 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 71 | identifier: text("identifier").notNull(), 72 | value: text("value").notNull(), 73 | expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), 74 | createdAt: integer("created_at", { mode: "timestamp_ms" }) 75 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 76 | .notNull(), 77 | updatedAt: integer("updated_at", { mode: "timestamp_ms" }) 78 | .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) 79 | .$onUpdate(() => /* @__PURE__ */ new Date()) 80 | .notNull(), 81 | }); 82 | 83 | export const twoFactor = sqliteTable("two_factor", { 84 | id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 85 | secret: text("secret").notNull(), 86 | backupCodes: text("backup_codes").notNull(), 87 | userId: integer("user_id") 88 | .notNull() 89 | .references(() => custom_user.id, { onDelete: "cascade" }), 90 | }); 91 | ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/faq.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: FAQ 3 | description: Frequently asked questions about Better Auth. 4 | --- 5 | 6 | This page contains frequently asked questions, common issues, and other helpful information about Better Auth. 7 | 8 | <Accordions> 9 | <Accordion title="Auth client not working"> 10 | When encountering `createAuthClient` related errors, make sure to have the correct import path as it varies based on environment. 11 | 12 | If you're using the auth client on react front-end, you'll need to import it from `/react`: 13 | 14 | ```ts title="component.ts" 15 | import { createAuthClient } from "better-auth/react"; 16 | ``` 17 | 18 | Where as if you're using the auth client in Next.js middleware, server-actions, server-components or anything server-related, you'll likely need to import it from `/client`: 19 | 20 | ```ts title="server.ts" 21 | import { createAuthClient } from "better-auth/client"; 22 | ``` 23 | 24 | </Accordion> 25 | 26 | <Accordion title="getSession not working"> 27 | If you try to call `authClient.getSession` on a server environment (e.g, a Next.js server component), it doesn't work since it can't access the cookies. You can use the `auth.api.getSession` instead and pass the request headers to it. 28 | 29 | ```tsx title="server.tsx" 30 | import { auth } from "./auth"; 31 | import { headers } from "next/headers"; 32 | 33 | const session = await auth.api.getSession({ 34 | headers: await headers() 35 | }) 36 | ``` 37 | 38 | if you need to use the auth client on the server for different purposes, you still can pass the request headers to it: 39 | 40 | ```tsx title="server.tsx" 41 | import { authClient } from "./auth-client"; 42 | import { headers } from "next/headers"; 43 | 44 | const session = await authClient.getSession({ 45 | fetchOptions:{ 46 | headers: await headers() 47 | } 48 | }) 49 | ``` 50 | </Accordion> 51 | 52 | <Accordion title="Adding custom fields to the users table"> 53 | 54 | Better Auth provides a type-safe way to extend the user and session schemas, take a look at our docs on <Link href="/docs/concepts/database#extending-core-schema">extending core schema</Link>. 55 | 56 | </Accordion> 57 | 58 | <Accordion title="Difference between getSession and useSession"> 59 | Both `useSession` and `getSession` instances are used fundamentally different based on the situation. 60 | 61 | `useSession` is a hook, meaning it can trigger re-renders whenever session data changes. 62 | 63 | If you have UI you need to change based on user or session data, you can use this hook. 64 | 65 | <Callout type="warn"> 66 | For performance reasons, do not use this hook on your `layout.tsx` file. We 67 | recommend using RSC and use your server auth instance to get the session data 68 | via `auth.api.getSession`. 69 | </Callout> 70 | 71 | `getSession` returns a promise containing data and error. 72 | 73 | For all other situations where you shouldn't use `useSession`, is when you should be using `getSession`. 74 | 75 | <Callout type="info"> 76 | `getSession` is available on both server and client auth instances. 77 | Not just the latter. 78 | </Callout> 79 | </Accordion> 80 | 81 | <Accordion title="Common TypeScript Errors"> 82 | If you're facing typescript errors, make sure your tsconfig has `strict` set to `true`: 83 | ```json title="tsconfig.json" 84 | { 85 | "compilerOptions": { 86 | "strict": true, 87 | } 88 | } 89 | ``` 90 | 91 | if you can't set strict to true, you can enable strictNullChecks: 92 | ```json title="tsconfig.json" 93 | { 94 | "compilerOptions": { 95 | "strictNullChecks": true, 96 | } 97 | } 98 | ``` 99 | 100 | You can learn more in our <Link href="/docs/concepts/typescript#typescript-config">TypeScript docs</Link>. 101 | </Accordion> 102 | <Accordion title="Can I remove `name`, `image`, or `email` fields from the user table?"> 103 | At this time, you can't remove the `name`, `image`, or `email` fields from the user table. 104 | 105 | We do plan to have more customizability in the future in this regard, but for now, you can't remove these fields. 106 | </Accordion> 107 | </Accordions> 108 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/oauth2/state.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { APIError } from "better-call"; 3 | import { generateRandomString } from "../crypto"; 4 | import type { GenericEndpointContext } from "@better-auth/core"; 5 | 6 | export async function generateState( 7 | c: GenericEndpointContext, 8 | link?: { 9 | email: string; 10 | userId: string; 11 | }, 12 | ) { 13 | const callbackURL = c.body?.callbackURL || c.context.options.baseURL; 14 | if (!callbackURL) { 15 | throw new APIError("BAD_REQUEST", { 16 | message: "callbackURL is required", 17 | }); 18 | } 19 | 20 | const codeVerifier = generateRandomString(128); 21 | const state = generateRandomString(32); 22 | const stateCookie = c.context.createAuthCookie("state", { 23 | maxAge: 5 * 60 * 1000, // 5 minutes 24 | }); 25 | await c.setSignedCookie( 26 | stateCookie.name, 27 | state, 28 | c.context.secret, 29 | stateCookie.attributes, 30 | ); 31 | const data = JSON.stringify({ 32 | callbackURL, 33 | codeVerifier, 34 | errorURL: c.body?.errorCallbackURL, 35 | newUserURL: c.body?.newUserCallbackURL, 36 | link, 37 | /** 38 | * This is the actual expiry time of the state 39 | */ 40 | expiresAt: Date.now() + 10 * 60 * 1000, 41 | requestSignUp: c.body?.requestSignUp, 42 | }); 43 | const expiresAt = new Date(); 44 | expiresAt.setMinutes(expiresAt.getMinutes() + 10); 45 | const verification = await c.context.internalAdapter.createVerificationValue({ 46 | value: data, 47 | identifier: state, 48 | expiresAt, 49 | }); 50 | if (!verification) { 51 | c.context.logger.error( 52 | "Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", 53 | ); 54 | throw new APIError("INTERNAL_SERVER_ERROR", { 55 | message: "Unable to create verification", 56 | }); 57 | } 58 | return { 59 | state: verification.identifier, 60 | codeVerifier, 61 | }; 62 | } 63 | 64 | export async function parseState(c: GenericEndpointContext) { 65 | const state = c.query.state || c.body.state; 66 | const data = await c.context.internalAdapter.findVerificationValue(state); 67 | if (!data) { 68 | c.context.logger.error("State Mismatch. Verification not found", { 69 | state, 70 | }); 71 | const errorURL = 72 | c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; 73 | throw c.redirect(`${errorURL}?error=please_restart_the_process`); 74 | } 75 | 76 | const parsedData = z 77 | .object({ 78 | callbackURL: z.string(), 79 | codeVerifier: z.string(), 80 | errorURL: z.string().optional(), 81 | newUserURL: z.string().optional(), 82 | expiresAt: z.number(), 83 | link: z 84 | .object({ 85 | email: z.string(), 86 | userId: z.coerce.string(), 87 | }) 88 | .optional(), 89 | requestSignUp: z.boolean().optional(), 90 | }) 91 | .parse(JSON.parse(data.value)); 92 | 93 | if (!parsedData.errorURL) { 94 | parsedData.errorURL = `${c.context.baseURL}/error`; 95 | } 96 | const stateCookie = c.context.createAuthCookie("state"); 97 | const stateCookieValue = await c.getSignedCookie( 98 | stateCookie.name, 99 | c.context.secret, 100 | ); 101 | /** 102 | * This is generally cause security issue and should only be used in 103 | * dev or staging environments. It's currently used by the oauth-proxy 104 | * plugin 105 | */ 106 | const skipStateCookieCheck = c.context.oauthConfig?.skipStateCookieCheck; 107 | if ( 108 | !skipStateCookieCheck && 109 | (!stateCookieValue || stateCookieValue !== state) 110 | ) { 111 | const errorURL = 112 | c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; 113 | throw c.redirect(`${errorURL}?error=state_mismatch`); 114 | } 115 | c.setCookie(stateCookie.name, "", { 116 | maxAge: 0, 117 | }); 118 | if (parsedData.expiresAt < Date.now()) { 119 | await c.context.internalAdapter.deleteVerificationValue(data.id); 120 | const errorURL = 121 | c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; 122 | throw c.redirect(`${errorURL}?error=please_restart_the_process`); 123 | } 124 | await c.context.internalAdapter.deleteVerificationValue(data.id); 125 | return parsedData; 126 | } 127 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/fastify.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Better Auth Fastify Integration Guide 3 | description: Learn how to seamlessly integrate Better Auth with your Fastify application. 4 | --- 5 | 6 | This guide provides step-by-step instructions for configuring both essential handlers and CORS settings. 7 | 8 | <Callout type="important"> 9 | A configured Better Auth instance is required before proceeding. If you haven't set this up yet, please consult our [Installation Guide](/docs/installation). 10 | </Callout> 11 | 12 | ### Prerequisites 13 | 14 | Verify the following requirements before integration: 15 | 16 | - **Node.js Environment**: v16 or later installed 17 | - **ES Module Support**: Enable ES modules in either: 18 | - `package.json`: `{ "type": "module" }` 19 | - TypeScript `tsconfig.json`: `{ "module": "ESNext" }` 20 | - **Fastify Dependencies**: 21 | ```package-install 22 | fastify @fastify/cors 23 | ``` 24 | 25 | <Callout type="tip"> For TypeScript: Ensure your `tsconfig.json` includes `"esModuleInterop": true` for optimal compatibility. </Callout> 26 | 27 | ### Authentication Handler Setup 28 | 29 | Configure Better Auth to process authentication requests by creating a catch-all route: 30 | 31 | ```ts title="server.ts" 32 | import Fastify from "fastify"; 33 | import { auth } from "./auth"; // Your configured Better Auth instance 34 | 35 | const fastify = Fastify({ logger: true }); 36 | 37 | // Register authentication endpoint 38 | fastify.route({ 39 | method: ["GET", "POST"], 40 | url: "/api/auth/*", 41 | async handler(request, reply) { 42 | try { 43 | // Construct request URL 44 | const url = new URL(request.url, `http://${request.headers.host}`); 45 | 46 | // Convert Fastify headers to standard Headers object 47 | const headers = new Headers(); 48 | Object.entries(request.headers).forEach(([key, value]) => { 49 | if (value) headers.append(key, value.toString()); 50 | }); 51 | 52 | // Create Fetch API-compatible request 53 | const req = new Request(url.toString(), { 54 | method: request.method, 55 | headers, 56 | body: request.body ? JSON.stringify(request.body) : undefined, 57 | }); 58 | 59 | // Process authentication request 60 | const response = await auth.handler(req); 61 | 62 | // Forward response to client 63 | reply.status(response.status); 64 | response.headers.forEach((value, key) => reply.header(key, value)); 65 | reply.send(response.body ? await response.text() : null); 66 | 67 | } catch (error) { 68 | fastify.log.error("Authentication Error:", error); 69 | reply.status(500).send({ 70 | error: "Internal authentication error", 71 | code: "AUTH_FAILURE" 72 | }); 73 | } 74 | } 75 | }); 76 | 77 | // Initialize server 78 | fastify.listen({ port: 4000 }, (err) => { 79 | if (err) { 80 | fastify.log.error(err); 81 | process.exit(1); 82 | } 83 | console.log("Server running on port 4000"); 84 | }); 85 | ``` 86 | 87 | ### Trusted origins 88 | 89 | When a request is made from a different origin, the request will be blocked by default. You can add trusted origins to the `auth` instance. 90 | 91 | ```ts 92 | export const auth = betterAuth({ 93 | trustedOrigins: ["http://localhost:3000", "https://example.com"], 94 | }); 95 | ``` 96 | 97 | ### Configuring CORS 98 | 99 | Secure your API endpoints with proper CORS configuration: 100 | 101 | ```ts 102 | import fastifyCors from "@fastify/cors"; 103 | 104 | // Configure CORS policies 105 | fastify.register(fastifyCors, { 106 | origin: process.env.CLIENT_ORIGIN || "http://localhost:3000", 107 | methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 108 | allowedHeaders: [ 109 | "Content-Type", 110 | "Authorization", 111 | "X-Requested-With" 112 | ], 113 | credentials: true, 114 | maxAge: 86400 115 | }); 116 | 117 | // Mount authentication handler after CORS registration 118 | // (Use previous handler configuration here) 119 | ``` 120 | 121 | <Callout type="warning"> Always restrict CORS origins in production environments. Use environment variables for dynamic configuration. </Callout> 122 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { XIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps<typeof DialogPrimitive.Root>) { 12 | return <DialogPrimitive.Root data-slot="dialog" {...props} />; 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { 18 | return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps<typeof DialogPrimitive.Portal>) { 24 | return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps<typeof DialogPrimitive.Close>) { 30 | return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { 37 | return ( 38 | <DialogPrimitive.Overlay 39 | data-slot="dialog-overlay" 40 | className={cn( 41 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", 42 | className, 43 | )} 44 | {...props} 45 | /> 46 | ); 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps<typeof DialogPrimitive.Content>) { 54 | return ( 55 | <DialogPortal data-slot="dialog-portal"> 56 | <DialogOverlay /> 57 | <DialogPrimitive.Content 58 | data-slot="dialog-content" 59 | className={cn( 60 | "fixed left-[50%] top-[50%] z-50 grid max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg w-11/12", 61 | className, 62 | )} 63 | {...props} 64 | > 65 | {children} 66 | <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"> 67 | <XIcon /> 68 | <span className="sr-only">Close</span> 69 | </DialogPrimitive.Close> 70 | </DialogPrimitive.Content> 71 | </DialogPortal> 72 | ); 73 | } 74 | 75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 | <div 78 | data-slot="dialog-header" 79 | className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 80 | {...props} 81 | /> 82 | ); 83 | } 84 | 85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 | <div 88 | data-slot="dialog-footer" 89 | className={cn( 90 | "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 91 | className, 92 | )} 93 | {...props} 94 | /> 95 | ); 96 | } 97 | 98 | function DialogTitle({ 99 | className, 100 | ...props 101 | }: React.ComponentProps<typeof DialogPrimitive.Title>) { 102 | return ( 103 | <DialogPrimitive.Title 104 | data-slot="dialog-title" 105 | className={cn("text-lg leading-none font-semibold", className)} 106 | {...props} 107 | /> 108 | ); 109 | } 110 | 111 | function DialogDescription({ 112 | className, 113 | ...props 114 | }: React.ComponentProps<typeof DialogPrimitive.Description>) { 115 | return ( 116 | <DialogPrimitive.Description 117 | data-slot="dialog-description" 118 | className={cn("text-muted-foreground text-sm", className)} 119 | {...props} 120 | /> 121 | ); 122 | } 123 | 124 | export { 125 | Dialog, 126 | DialogClose, 127 | DialogContent, 128 | DialogDescription, 129 | DialogFooter, 130 | DialogHeader, 131 | DialogOverlay, 132 | DialogPortal, 133 | DialogTitle, 134 | DialogTrigger, 135 | }; 136 | ``` -------------------------------------------------------------------------------- /docs/components/ui/dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { XIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps<typeof DialogPrimitive.Root>) { 12 | return <DialogPrimitive.Root data-slot="dialog" {...props} />; 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { 18 | return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps<typeof DialogPrimitive.Portal>) { 24 | return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps<typeof DialogPrimitive.Close>) { 30 | return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { 37 | return ( 38 | <DialogPrimitive.Overlay 39 | data-slot="dialog-overlay" 40 | className={cn( 41 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", 42 | className, 43 | )} 44 | {...props} 45 | /> 46 | ); 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps<typeof DialogPrimitive.Content>) { 54 | return ( 55 | <DialogPortal data-slot="dialog-portal"> 56 | <DialogOverlay /> 57 | <DialogPrimitive.Content 58 | data-slot="dialog-content" 59 | className={cn( 60 | "fixed left-[50%] top-[50%] z-50 grid max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg w-11/12", 61 | className, 62 | )} 63 | {...props} 64 | > 65 | {children} 66 | <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"> 67 | <XIcon /> 68 | <span className="sr-only">Close</span> 69 | </DialogPrimitive.Close> 70 | </DialogPrimitive.Content> 71 | </DialogPortal> 72 | ); 73 | } 74 | 75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 | <div 78 | data-slot="dialog-header" 79 | className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 80 | {...props} 81 | /> 82 | ); 83 | } 84 | 85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 | <div 88 | data-slot="dialog-footer" 89 | className={cn( 90 | "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 91 | className, 92 | )} 93 | {...props} 94 | /> 95 | ); 96 | } 97 | 98 | function DialogTitle({ 99 | className, 100 | ...props 101 | }: React.ComponentProps<typeof DialogPrimitive.Title>) { 102 | return ( 103 | <DialogPrimitive.Title 104 | data-slot="dialog-title" 105 | className={cn("text-lg leading-none font-semibold", className)} 106 | {...props} 107 | /> 108 | ); 109 | } 110 | 111 | function DialogDescription({ 112 | className, 113 | ...props 114 | }: React.ComponentProps<typeof DialogPrimitive.Description>) { 115 | return ( 116 | <DialogPrimitive.Description 117 | data-slot="dialog-description" 118 | className={cn("text-muted-foreground text-sm", className)} 119 | {...props} 120 | /> 121 | ); 122 | } 123 | 124 | export { 125 | Dialog, 126 | DialogClose, 127 | DialogContent, 128 | DialogDescription, 129 | DialogFooter, 130 | DialogHeader, 131 | DialogOverlay, 132 | DialogPortal, 133 | DialogTitle, 134 | DialogTrigger, 135 | }; 136 | ``` -------------------------------------------------------------------------------- /docs/components/ui/form.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { Slot } from "@radix-ui/react-slot"; 6 | import { 7 | Controller, 8 | FormProvider, 9 | useFormContext, 10 | useFormState, 11 | type ControllerProps, 12 | type FieldPath, 13 | type FieldValues, 14 | } from "react-hook-form"; 15 | 16 | import { cn } from "@/lib/utils"; 17 | import { Label } from "@/components/ui/label"; 18 | 19 | const Form = FormProvider; 20 | 21 | type FormFieldContextValue< 22 | TFieldValues extends FieldValues = FieldValues, 23 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 24 | > = { 25 | name: TName; 26 | }; 27 | 28 | const FormFieldContext = React.createContext<FormFieldContextValue>( 29 | {} as FormFieldContextValue, 30 | ); 31 | 32 | const FormField = < 33 | TFieldValues extends FieldValues = FieldValues, 34 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 35 | >({ 36 | ...props 37 | }: ControllerProps<TFieldValues, TName>) => { 38 | return ( 39 | <FormFieldContext.Provider value={{ name: props.name }}> 40 | <Controller {...props} /> 41 | </FormFieldContext.Provider> 42 | ); 43 | }; 44 | 45 | const useFormField = () => { 46 | const fieldContext = React.useContext(FormFieldContext); 47 | const itemContext = React.useContext(FormItemContext); 48 | const { getFieldState } = useFormContext(); 49 | const formState = useFormState({ name: fieldContext.name }); 50 | const fieldState = getFieldState(fieldContext.name, formState); 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within <FormField>"); 54 | } 55 | 56 | const { id } = itemContext; 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | }; 66 | }; 67 | 68 | type FormItemContextValue = { 69 | id: string; 70 | }; 71 | 72 | const FormItemContext = React.createContext<FormItemContextValue>( 73 | {} as FormItemContextValue, 74 | ); 75 | 76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 | const id = React.useId(); 78 | 79 | return ( 80 | <FormItemContext.Provider value={{ id }}> 81 | <div 82 | data-slot="form-item" 83 | className={cn("grid gap-2", className)} 84 | {...props} 85 | /> 86 | </FormItemContext.Provider> 87 | ); 88 | } 89 | 90 | function FormLabel({ 91 | className, 92 | ...props 93 | }: React.ComponentProps<typeof LabelPrimitive.Root>) { 94 | const { error, formItemId } = useFormField(); 95 | 96 | return ( 97 | <Label 98 | data-slot="form-label" 99 | data-error={!!error} 100 | className={cn("data-[error=true]:text-destructive-foreground", className)} 101 | htmlFor={formItemId} 102 | {...props} 103 | /> 104 | ); 105 | } 106 | 107 | function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { 108 | const { error, formItemId, formDescriptionId, formMessageId } = 109 | useFormField(); 110 | 111 | return ( 112 | <Slot 113 | data-slot="form-control" 114 | id={formItemId} 115 | aria-describedby={ 116 | !error 117 | ? `${formDescriptionId}` 118 | : `${formDescriptionId} ${formMessageId}` 119 | } 120 | aria-invalid={!!error} 121 | {...props} 122 | /> 123 | ); 124 | } 125 | 126 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) { 127 | const { formDescriptionId } = useFormField(); 128 | 129 | return ( 130 | <p 131 | data-slot="form-description" 132 | id={formDescriptionId} 133 | className={cn("text-muted-foreground text-sm", className)} 134 | {...props} 135 | /> 136 | ); 137 | } 138 | 139 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) { 140 | const { error, formMessageId } = useFormField(); 141 | const body = error ? String(error?.message ?? "") : props.children; 142 | 143 | if (!body) { 144 | return null; 145 | } 146 | 147 | return ( 148 | <p 149 | data-slot="form-message" 150 | id={formMessageId} 151 | className={cn("text-destructive-foreground text-sm", className)} 152 | {...props} 153 | > 154 | {body} 155 | </p> 156 | ); 157 | } 158 | 159 | export { 160 | useFormField, 161 | Form, 162 | FormItem, 163 | FormLabel, 164 | FormControl, 165 | FormDescription, 166 | FormMessage, 167 | FormField, 168 | }; 169 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Report an issue 2 | description: Create a report to help us improve 3 | body: 4 | - type: checkboxes 5 | attributes: 6 | label: Is this suited for github? 7 | description: Feel free to join the discord community [here](https://discord.gg/better-auth), we can usually respond faster to any questions. 8 | options: 9 | - label: Yes, this is suited for github 10 | - type: markdown 11 | attributes: 12 | value: | 13 | This template is used for reporting a issue with better-auth. 14 | 15 | Feature requests should be opened in [here](https://github.com/better-auth/better-auth/issues/new?assignees=&labels=&projects=&template=feature_request.md&title=). 16 | 17 | Before opening a new issue, please do a [search](https://github.com/better-auth/better-auth/issues) of existing issues and :+1: upvote the existing issue instead. This will result in a quicker resolution. 18 | - type: textarea 19 | attributes: 20 | label: To Reproduce 21 | description: A step-by-step description of how to reproduce the issue, based on the linked reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. 22 | placeholder: | 23 | Ex: 24 | 1. Create a backend 25 | 2. Create a frontend and use client 26 | 3. X will happen 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: Current vs. Expected behavior 32 | description: | 33 | A clear and concise description of what the bug is (e.g., screenshots, logs, etc.), and what you expected to happen. 34 | 35 | **Skipping this/failure to provide complete information of the bug will result in the issue being closed.** 36 | placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead.' 37 | validations: 38 | required: true 39 | - type: input 40 | attributes: 41 | label: What version of Better Auth are you using? 42 | description: Please provide the current version of `better-auth` that you are reporting the bug on 43 | placeholder: "1.x.x" 44 | validations: 45 | required: true 46 | - type: textarea 47 | attributes: 48 | label: System info 49 | description: Output of `npx @better-auth/cli info --json` 50 | render: bash 51 | placeholder: System and Better Auth info 52 | validations: 53 | required: true 54 | - type: dropdown 55 | attributes: 56 | label: Which area(s) are affected? (Select all that apply) 57 | multiple: true 58 | options: 59 | - 'Backend' 60 | - 'Client' 61 | - 'Types' 62 | - 'Documentation' 63 | - 'Package' 64 | - 'Other' 65 | validations: 66 | required: true 67 | - type: textarea 68 | attributes: 69 | label: Auth config (if applicable) 70 | description: If you haven't already shared a reproducible example or don't think it's unrelated, please include your auth config. Make sure to remove any sensitive information. 71 | render: typescript 72 | value: | 73 | import { betterAuth } from "better-auth" 74 | export const auth = betterAuth({ 75 | emailAndPassword: { 76 | enabled: true 77 | }, 78 | }); 79 | - type: textarea 80 | attributes: 81 | label: Additional context 82 | description: | 83 | Any extra information that might help us investigate. For example, is it only reproducible online, or locally too? Is the issue only happening in a specific browser? etc. 84 | placeholder: | 85 | I tested my reproduction against the latest release. 86 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/globals.css: -------------------------------------------------------------------------------- ```css 1 | @import "tailwindcss"; 2 | @config "../tailwind.config.ts"; 3 | @import "tw-animate-css"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | :root { 8 | --background: hsl(0 0% 100%); 9 | --foreground: hsl(20 14.3% 4.1%); 10 | --card: hsl(0 0% 100%); 11 | --card-foreground: hsl(20 14.3% 4.1%); 12 | --popover: hsl(0 0% 100%); 13 | --popover-foreground: hsl(20 14.3% 4.1%); 14 | --primary: hsl(24 9.8% 10%); 15 | --primary-foreground: hsl(60 9.1% 97.8%); 16 | --secondary: hsl(60 4.8% 95.9%); 17 | --secondary-foreground: hsl(24 9.8% 10%); 18 | --muted: hsl(60 4.8% 95.9%); 19 | --muted-foreground: hsl(25 5.3% 44.7%); 20 | --accent: hsl(60 4.8% 95.9%); 21 | --accent-foreground: hsl(24 9.8% 10%); 22 | --destructive: hsl(0 84.2% 60.2%); 23 | --destructive-foreground: hsl(60 9.1% 97.8%); 24 | --border: hsl(20 5.9% 90%); 25 | --input: hsl(20 5.9% 90%); 26 | --ring: hsl(20 14.3% 4.1%); 27 | --radius: 0rem; 28 | --chart-1: hsl(12 76% 61%); 29 | --chart-2: hsl(173 58% 39%); 30 | --chart-3: hsl(197 37% 24%); 31 | --chart-4: hsl(43 74% 66%); 32 | --chart-5: hsl(27 87% 67%); 33 | } 34 | 35 | .dark { 36 | --background: hsl(20 14.3% 4.1%); 37 | --foreground: hsl(60 9.1% 97.8%); 38 | --card: hsl(20 14.3% 4.1%); 39 | --card-foreground: hsl(60 9.1% 97.8%); 40 | --popover: hsl(20 14.3% 4.1%); 41 | --popover-foreground: hsl(60 9.1% 97.8%); 42 | --primary: hsl(60 9.1% 97.8%); 43 | --primary-foreground: hsl(24 9.8% 10%); 44 | --secondary: hsl(12 6.5% 15.1%); 45 | --secondary-foreground: hsl(60 9.1% 97.8%); 46 | --muted: hsl(12 6.5% 15.1%); 47 | --muted-foreground: hsl(24 5.4% 63.9%); 48 | --accent: hsl(12 6.5% 15.1%); 49 | --accent-foreground: hsl(60 9.1% 97.8%); 50 | --destructive: hsl(0 62.8% 30.6%); 51 | --destructive-foreground: hsl(60 9.1% 97.8%); 52 | --border: hsl(12 6.5% 15.1%); 53 | --input: hsl(12 6.5% 15.1%); 54 | --ring: hsl(24 5.7% 82.9%); 55 | --chart-1: hsl(220 70% 50%); 56 | --chart-2: hsl(160 60% 45%); 57 | --chart-3: hsl(30 80% 55%); 58 | --chart-4: hsl(280 65% 60%); 59 | --chart-5: hsl(340 75% 55%); 60 | } 61 | 62 | @theme inline { 63 | --color-background: var(--background); 64 | --color-foreground: var(--foreground); 65 | --color-card: var(--card); 66 | --color-card-foreground: var(--card-foreground); 67 | --color-popover: var(--popover); 68 | --color-popover-foreground: var(--popover-foreground); 69 | --color-primary: var(--primary); 70 | --color-primary-foreground: var(--primary-foreground); 71 | --color-secondary: var(--secondary); 72 | --color-secondary-foreground: var(--secondary-foreground); 73 | --color-muted: var(--muted); 74 | --color-muted-foreground: var(--muted-foreground); 75 | --color-accent: var(--accent); 76 | --color-accent-foreground: var(--accent-foreground); 77 | --color-destructive: var(--destructive); 78 | --color-destructive-foreground: var(--destructive-foreground); 79 | --color-border: var(--border); 80 | --color-input: var(--input); 81 | --color-ring: var(--ring); 82 | --color-chart-1: var(--chart-1); 83 | --color-chart-2: var(--chart-2); 84 | --color-chart-3: var(--chart-3); 85 | --color-chart-4: var(--chart-4); 86 | --color-chart-5: var(--chart-5); 87 | --radius-sm: calc(var(--radius) - 4px); 88 | --radius-md: calc(var(--radius) - 2px); 89 | --radius-lg: var(--radius); 90 | --radius-xl: calc(var(--radius) + 4px); 91 | --color-sidebar: var(--sidebar); 92 | --color-sidebar-foreground: var(--sidebar-foreground); 93 | --color-sidebar-primary: var(--sidebar-primary); 94 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 95 | --color-sidebar-accent: var(--sidebar-accent); 96 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 97 | --color-sidebar-border: var(--sidebar-border); 98 | --color-sidebar-ring: var(--sidebar-ring); 99 | } 100 | 101 | @layer base { 102 | * { 103 | @apply border-border; 104 | } 105 | body { 106 | @apply bg-background text-foreground; 107 | } 108 | } 109 | 110 | .no-visible-scrollbar { 111 | scrollbar-width: none; 112 | -ms-overflow-style: none; 113 | -webkit-overflow-scrolling: touch; 114 | } 115 | 116 | .no-visible-scrollbar::-webkit-scrollbar { 117 | display: none; 118 | } 119 | ``` -------------------------------------------------------------------------------- /docs/components/blocks/features.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { useId } from "react"; 2 | 3 | export function Features() { 4 | return ( 5 | <div className="py-2"> 6 | <div className="mt-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-10 md:gap-2 max-w-7xl mx-auto"> 7 | {grid.map((feature, i) => ( 8 | <div 9 | key={feature.title} 10 | className="relative bg-gradient-to-b dark:from-neutral-900 from-neutral-100 dark:to-neutral-950 to-white px-6 py-2 overflow-hidden" 11 | > 12 | <Grid size={i * 5 + 10} /> 13 | <p className="text-base font-bold text-neutral-800 dark:text-white relative z-0"> 14 | {feature.title} 15 | </p> 16 | <p className="text-neutral-600 dark:text-neutral-400 text-base font-normal relative z-0"> 17 | {feature.description} 18 | </p> 19 | </div> 20 | ))} 21 | </div> 22 | </div> 23 | ); 24 | } 25 | 26 | const grid = [ 27 | { 28 | title: "Framework Agnostic", 29 | description: "Support for most popular frameworks", 30 | }, 31 | { 32 | title: "Email & Password", 33 | description: 34 | "Built-in support for secure email and password authentication", 35 | }, 36 | { 37 | title: "Account & Session Management", 38 | description: "Manage user accounts and sessions with ease", 39 | }, 40 | { 41 | title: "Built-In Rate Limiter", 42 | description: "Built-in rate limiter with custom rules", 43 | }, 44 | { 45 | title: "Automatic Database Management", 46 | description: "Automatic database management and migrations", 47 | }, 48 | { 49 | title: "Social Sign-on", 50 | description: "Multiple social sign-on providers", 51 | }, 52 | { 53 | title: "Organization & Access Control", 54 | description: "Manage organizations and access control", 55 | }, 56 | { 57 | title: "Two Factor Authentication", 58 | description: "Secure your users with two factor authentication", 59 | }, 60 | { 61 | title: "Plugin Ecosystem", 62 | description: "Even more capabilities with plugins", 63 | }, 64 | ]; 65 | 66 | export const Grid = ({ 67 | pattern, 68 | size, 69 | }: { 70 | pattern?: number[][]; 71 | size?: number; 72 | }) => { 73 | const p = pattern ?? [ 74 | [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], 75 | [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], 76 | [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], 77 | [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], 78 | [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], 79 | ]; 80 | return ( 81 | <div className="pointer-events-none absolute left-1/2 top-0 -ml-20 -mt-2 h-full w-full [mask-image:linear-gradient(white,transparent)]"> 82 | <div className="absolute inset-0 bg-gradient-to-r [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 from-zinc-100/30 to-zinc-300/30 dark:to-zinc-900/30 opacity-100"> 83 | <GridPattern 84 | width={size ?? 20} 85 | height={size ?? 20} 86 | x="-12" 87 | y="4" 88 | squares={p} 89 | className="absolute inset-0 h-full w-full mix-blend-overlay dark:fill-white/10 dark:stroke-white/10 stroke-black/10 fill-black/10" 90 | /> 91 | </div> 92 | </div> 93 | ); 94 | }; 95 | 96 | export function GridPattern({ width, height, x, y, squares, ...props }: any) { 97 | const patternId = useId(); 98 | 99 | return ( 100 | <svg aria-hidden="true" {...props}> 101 | <defs> 102 | <pattern 103 | id={patternId} 104 | width={width} 105 | height={height} 106 | patternUnits="userSpaceOnUse" 107 | x={x} 108 | y={y} 109 | > 110 | <path d={`M.5 ${height}V.5H${width}`} fill="none" /> 111 | </pattern> 112 | </defs> 113 | <rect 114 | width="100%" 115 | height="100%" 116 | strokeWidth={0} 117 | fill={`url(#${patternId})`} 118 | /> 119 | {squares && ( 120 | <svg x={x} y={y} className="overflow-visible"> 121 | {squares.map(([x, y]: any, idx: number) => ( 122 | <rect 123 | strokeWidth="0" 124 | key={`${x}-${y}-${idx}`} 125 | width={width + 1} 126 | height={height + 1} 127 | x={x * width} 128 | y={y * height} 129 | /> 130 | ))} 131 | </svg> 132 | )} 133 | </svg> 134 | ); 135 | } 136 | ``` -------------------------------------------------------------------------------- /docs/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { 12 | return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { 18 | return ( 19 | <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> 20 | ); 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { 26 | return ( 27 | <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> 28 | ); 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { 35 | return ( 36 | <AlertDialogPrimitive.Overlay 37 | data-slot="alert-dialog-overlay" 38 | className={cn( 39 | "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", 40 | className, 41 | )} 42 | {...props} 43 | /> 44 | ); 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { 51 | return ( 52 | <AlertDialogPortal> 53 | <AlertDialogOverlay /> 54 | <AlertDialogPrimitive.Content 55 | data-slot="alert-dialog-content" 56 | className={cn( 57 | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", 58 | className, 59 | )} 60 | {...props} 61 | /> 62 | </AlertDialogPortal> 63 | ); 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 | <div 72 | data-slot="alert-dialog-header" 73 | className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 74 | {...props} 75 | /> 76 | ); 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 | <div 85 | data-slot="alert-dialog-footer" 86 | className={cn( 87 | "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 88 | className, 89 | )} 90 | {...props} 91 | /> 92 | ); 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { 99 | return ( 100 | <AlertDialogPrimitive.Title 101 | data-slot="alert-dialog-title" 102 | className={cn("text-lg font-semibold", className)} 103 | {...props} 104 | /> 105 | ); 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { 112 | return ( 113 | <AlertDialogPrimitive.Description 114 | data-slot="alert-dialog-description" 115 | className={cn("text-muted-foreground text-sm", className)} 116 | {...props} 117 | /> 118 | ); 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { 125 | return ( 126 | <AlertDialogPrimitive.Action 127 | className={cn(buttonVariants(), className)} 128 | {...props} 129 | /> 130 | ); 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { 137 | return ( 138 | <AlertDialogPrimitive.Cancel 139 | className={cn(buttonVariants({ variant: "outline" }), className)} 140 | {...props} 141 | /> 142 | ); 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | }; 158 | ``` -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import defaultTheme from "tailwindcss/defaultTheme"; 2 | import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; 3 | import svgToDataUri from "mini-svg-data-uri"; 4 | 5 | /** @type {import('tailwindcss').Config} */ 6 | export default { 7 | darkMode: ["class"], 8 | plugins: [ 9 | addVariablesForColors, 10 | function ({ matchUtilities, theme }) { 11 | matchUtilities( 12 | { 13 | "bg-grid": (value) => ({ 14 | backgroundImage: `url("${svgToDataUri( 15 | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`, 16 | )}")`, 17 | }), 18 | "bg-grid-small": (value) => ({ 19 | backgroundImage: `url("${svgToDataUri( 20 | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`, 21 | )}")`, 22 | }), 23 | "bg-dot": (value) => ({ 24 | backgroundImage: `url("${svgToDataUri( 25 | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`, 26 | )}")`, 27 | }), 28 | }, 29 | { 30 | values: flattenColorPalette(theme("backgroundColor")), 31 | type: "color", 32 | }, 33 | ); 34 | }, 35 | ], 36 | theme: { 37 | extend: { 38 | fontFamily: { 39 | sans: ["var(--font-geist-sans)"], 40 | mono: ["var(--font-geist-mono)"], 41 | display: [...defaultTheme.fontFamily.sans], 42 | }, 43 | borderRadius: { 44 | lg: "var(--radius)", 45 | md: "calc(var(--radius) - 2px)", 46 | sm: "calc(var(--radius) - 4px)", 47 | }, 48 | keyframes: { 49 | marquee: { 50 | from: { transform: "translateX(0)" }, 51 | to: { transform: "translateX(calc(-100% - var(--gap)))" }, 52 | }, 53 | "marquee-vertical": { 54 | from: { transform: "translateY(0)" }, 55 | to: { transform: "translateY(calc(-100% - var(--gap)))" }, 56 | }, 57 | "hrtl-scroll": { 58 | from: { transform: "translateX(0)" }, 59 | to: { transform: "translateX(calc(-95%))" }, 60 | }, 61 | "hrtl-scroll-reverse": { 62 | from: { transform: "translateX(calc(-95%))" }, 63 | to: { transform: "translateX(0)" }, 64 | }, 65 | ripple: { 66 | "0% , 100%": { 67 | transform: "translate(-50% , -50%) scale(1)", 68 | }, 69 | "50%": { 70 | transform: "translate(-50% , -50%) scale(0.9)", 71 | }, 72 | }, 73 | "accordion-down": { 74 | from: { 75 | height: "0", 76 | }, 77 | to: { 78 | height: "var(--radix-accordion-content-height)", 79 | }, 80 | }, 81 | "accordion-up": { 82 | from: { 83 | height: "var(--radix-accordion-content-height)", 84 | }, 85 | to: { 86 | height: "0", 87 | }, 88 | }, 89 | scroll: { 90 | to: { 91 | transform: "translate(calc(-50% - 0.5rem))", 92 | }, 93 | }, 94 | spotlight: { 95 | "0%": { 96 | opacity: 0, 97 | transform: "translate(-72%, -62%) scale(0.5)", 98 | }, 99 | "100%": { 100 | opacity: 1, 101 | transform: "translate(-50%,-40%) scale(1)", 102 | }, 103 | }, 104 | }, 105 | animation: { 106 | "accordion-down": "accordion-down 0.2s ease-out", 107 | "accordion-up": "accordion-up 0.2s ease-out", 108 | ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite", 109 | scroll: 110 | "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite", 111 | "hrtl-scroll": "hrtl-scroll var(--anime-duration,10s) linear infinite", 112 | "hrtl-scroll-reverse": 113 | "hrtl-scroll-reverse var(--anime-duration,10s) linear infinite", 114 | spotlight: "spotlight 2s ease .30s 1 forwards", 115 | }, 116 | }, 117 | }, 118 | }; 119 | 120 | function addVariablesForColors({ addBase, theme }) { 121 | let allColors = flattenColorPalette(theme("colors")); 122 | let newVars = Object.fromEntries( 123 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]), 124 | ); 125 | 126 | addBase({ 127 | ":root": newVars, 128 | }); 129 | } 130 | ``` -------------------------------------------------------------------------------- /demo/nextjs/hooks/use-toast.ts: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react"; 5 | 6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; 7 | 8 | const TOAST_LIMIT = 1; 9 | const TOAST_REMOVE_DELAY = 1000000; 10 | 11 | type ToasterToast = ToastProps & { 12 | id: string; 13 | title?: React.ReactNode; 14 | description?: React.ReactNode; 15 | action?: ToastActionElement; 16 | }; 17 | 18 | const actionTypes = { 19 | ADD_TOAST: "ADD_TOAST", 20 | UPDATE_TOAST: "UPDATE_TOAST", 21 | DISMISS_TOAST: "DISMISS_TOAST", 22 | REMOVE_TOAST: "REMOVE_TOAST", 23 | } as const; 24 | 25 | let count = 0; 26 | 27 | function genId() { 28 | count = (count + 1) % Number.MAX_SAFE_INTEGER; 29 | return count.toString(); 30 | } 31 | 32 | type ActionType = typeof actionTypes; 33 | 34 | type Action = 35 | | { 36 | type: ActionType["ADD_TOAST"]; 37 | toast: ToasterToast; 38 | } 39 | | { 40 | type: ActionType["UPDATE_TOAST"]; 41 | toast: Partial<ToasterToast>; 42 | } 43 | | { 44 | type: ActionType["DISMISS_TOAST"]; 45 | toastId?: ToasterToast["id"]; 46 | } 47 | | { 48 | type: ActionType["REMOVE_TOAST"]; 49 | toastId?: ToasterToast["id"]; 50 | }; 51 | 52 | interface State { 53 | toasts: ToasterToast[]; 54 | } 55 | 56 | const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); 57 | 58 | const addToRemoveQueue = (toastId: string) => { 59 | if (toastTimeouts.has(toastId)) { 60 | return; 61 | } 62 | 63 | const timeout = setTimeout(() => { 64 | toastTimeouts.delete(toastId); 65 | dispatch({ 66 | type: "REMOVE_TOAST", 67 | toastId: toastId, 68 | }); 69 | }, TOAST_REMOVE_DELAY); 70 | 71 | toastTimeouts.set(toastId, timeout); 72 | }; 73 | 74 | export const reducer = (state: State, action: Action): State => { 75 | switch (action.type) { 76 | case "ADD_TOAST": 77 | return { 78 | ...state, 79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 80 | }; 81 | 82 | case "UPDATE_TOAST": 83 | return { 84 | ...state, 85 | toasts: state.toasts.map((t) => 86 | t.id === action.toast.id ? { ...t, ...action.toast } : t, 87 | ), 88 | }; 89 | 90 | case "DISMISS_TOAST": { 91 | const { toastId } = action; 92 | 93 | // ! Side effects ! - This could be extracted into a dismissToast() action, 94 | // but I'll keep it here for simplicity 95 | if (toastId) { 96 | addToRemoveQueue(toastId); 97 | } else { 98 | state.toasts.forEach((toast) => { 99 | addToRemoveQueue(toast.id); 100 | }); 101 | } 102 | 103 | return { 104 | ...state, 105 | toasts: state.toasts.map((t) => 106 | t.id === toastId || toastId === undefined 107 | ? { 108 | ...t, 109 | open: false, 110 | } 111 | : t, 112 | ), 113 | }; 114 | } 115 | case "REMOVE_TOAST": 116 | if (action.toastId === undefined) { 117 | return { 118 | ...state, 119 | toasts: [], 120 | }; 121 | } 122 | return { 123 | ...state, 124 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 125 | }; 126 | } 127 | }; 128 | 129 | const listeners: Array<(state: State) => void> = []; 130 | 131 | let memoryState: State = { toasts: [] }; 132 | 133 | function dispatch(action: Action) { 134 | memoryState = reducer(memoryState, action); 135 | 136 | listeners.forEach((listener) => { 137 | listener(memoryState); 138 | }); 139 | } 140 | 141 | type Toast = Omit<ToasterToast, "id">; 142 | 143 | function toast({ ...props }: Toast) { 144 | const id = genId(); 145 | 146 | const update = (props: ToasterToast) => 147 | dispatch({ 148 | type: "UPDATE_TOAST", 149 | toast: { ...props, id }, 150 | }); 151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); 152 | 153 | dispatch({ 154 | type: "ADD_TOAST", 155 | toast: { 156 | ...props, 157 | id, 158 | open: true, 159 | // @ts-expect-error 160 | onOpenChange: (open) => { 161 | if (!open) dismiss(); 162 | }, 163 | }, 164 | }); 165 | 166 | return { 167 | id: id, 168 | dismiss, 169 | update, 170 | }; 171 | } 172 | 173 | function useToast() { 174 | const [state, setState] = React.useState<State>(memoryState); 175 | 176 | React.useEffect(() => { 177 | listeners.push(setState); 178 | return () => { 179 | const index = listeners.indexOf(setState); 180 | if (index > -1) { 181 | listeners.splice(index, 1); 182 | } 183 | }; 184 | }, [state]); 185 | 186 | return { 187 | ...state, 188 | toast, 189 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 190 | }; 191 | } 192 | 193 | export { useToast, toast }; 194 | ```