This is page 6 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/components/ui/background-boxes.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import React from "react"; 3 | import { motion } from "framer-motion"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export const BoxesCore = ({ className, ...rest }: { className?: string }) => { 7 | const rows = new Array(150).fill(1); 8 | const cols = new Array(100).fill(1); 9 | let colors = [ 10 | "--sky-300", 11 | "--pink-300", 12 | "--green-300", 13 | "--yellow-300", 14 | "--red-300", 15 | "--purple-300", 16 | "--blue-300", 17 | "--indigo-300", 18 | "--violet-300", 19 | ]; 20 | const getRandomColor = () => { 21 | return colors[Math.floor(Math.random() * colors.length)]; 22 | }; 23 | 24 | return ( 25 | <div 26 | style={{ 27 | transform: `translate(-40%,-60%) skewX(-48deg) skewY(14deg) scale(0.675) rotate(0deg) translateZ(0)`, 28 | }} 29 | className={cn( 30 | "absolute left-1/4 p-4 -top-1/4 flex -translate-x-1/2 -translate-y-1/2 w-full h-full z-0 ", 31 | className, 32 | )} 33 | {...rest} 34 | > 35 | {rows.map((_, i) => ( 36 | <motion.div 37 | key={`row` + i} 38 | className="w-16 h-8 border-l border-slate-700 relative" 39 | > 40 | {cols.map((_, j) => ( 41 | <motion.div 42 | whileHover={{ 43 | backgroundColor: `var(${getRandomColor()})`, 44 | transition: { duration: 0 }, 45 | }} 46 | animate={{ 47 | transition: { duration: 2 }, 48 | }} 49 | key={`col` + j} 50 | className="w-16 h-8 border-r border-t border-slate-700 relative" 51 | > 52 | {j % 2 === 0 && i % 2 === 0 ? ( 53 | <svg 54 | xmlns="http://www.w3.org/2000/svg" 55 | fill="none" 56 | viewBox="0 0 24 24" 57 | strokeWidth="1.5" 58 | stroke="currentColor" 59 | className="absolute h-6 w-10 -top-[14px] -left-[22px] text-slate-700 stroke-[1px] pointer-events-none" 60 | > 61 | <path 62 | strokeLinecap="round" 63 | strokeLinejoin="round" 64 | d="M12 6v12m6-6H6" 65 | /> 66 | </svg> 67 | ) : null} 68 | </motion.div> 69 | ))} 70 | </motion.div> 71 | ))} 72 | </div> 73 | ); 74 | }; 75 | 76 | export const Boxes = React.memo(BoxesCore); 77 | ``` -------------------------------------------------------------------------------- /packages/expo/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@better-auth/expo", 3 | "version": "1.4.0-beta.11", 4 | "type": "module", 5 | "description": "Better Auth integration for Expo and React Native applications.", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/better-auth/better-auth", 11 | "directory": "packages/expo" 12 | }, 13 | "homepage": "https://www.better-auth.com/docs/integrations/expo", 14 | "scripts": { 15 | "test": "vitest", 16 | "build": "tsdown", 17 | "dev": "tsdown --watch", 18 | "typecheck": "tsc --project tsconfig.json" 19 | }, 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.js", 24 | "require": "./dist/index.cjs" 25 | }, 26 | "./client": { 27 | "types": "./dist/client.d.ts", 28 | "import": "./dist/client.js", 29 | "require": "./dist/client.cjs" 30 | } 31 | }, 32 | "typesVersions": { 33 | "*": { 34 | "*": [ 35 | "./dist/index.d.ts" 36 | ], 37 | "client": [ 38 | "./dist/client.d.ts" 39 | ] 40 | } 41 | }, 42 | "keywords": [ 43 | "auth", 44 | "expo", 45 | "react-native", 46 | "typescript", 47 | "better-auth" 48 | ], 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "license": "MIT", 53 | "devDependencies": { 54 | "@better-fetch/fetch": "catalog:", 55 | "@types/better-sqlite3": "^7.6.13", 56 | "better-auth": "workspace:*", 57 | "better-sqlite3": "^12.2.0", 58 | "expo-constants": "~17.1.7", 59 | "expo-crypto": "^13.0.2", 60 | "expo-linking": "~7.1.7", 61 | "expo-secure-store": "~14.2.3", 62 | "expo-web-browser": "~14.2.0", 63 | "react-native": "~0.80.2", 64 | "tsdown": "catalog:" 65 | }, 66 | "peerDependencies": { 67 | "better-auth": "workspace:*", 68 | "expo-constants": ">=17.0.0", 69 | "expo-crypto": ">=13.0.0", 70 | "expo-linking": ">=7.0.0", 71 | "expo-secure-store": ">=14.0.0", 72 | "expo-web-browser": ">=14.0.0" 73 | }, 74 | "dependencies": { 75 | "@better-fetch/fetch": "catalog:", 76 | "zod": "^4.1.5" 77 | }, 78 | "files": [ 79 | "dist" 80 | ] 81 | } 82 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/url.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { createAuthClient } from "./vanilla"; 3 | import { testClientPlugin } from "./test-plugin"; 4 | 5 | describe("url", () => { 6 | it("should not require base url", async () => { 7 | const client = createAuthClient({ 8 | plugins: [testClientPlugin()], 9 | baseURL: "", 10 | fetchOptions: { 11 | customFetchImpl: async (url, init) => { 12 | return new Response(JSON.stringify({ hello: "world" })); 13 | }, 14 | }, 15 | }); 16 | const response = await client.test(); 17 | expect(response.data).toEqual({ hello: "world" }); 18 | }); 19 | 20 | it("should use base url and append `/api/auth` by default", async () => { 21 | const client = createAuthClient({ 22 | plugins: [testClientPlugin()], 23 | baseURL: "http://localhost:3000", 24 | fetchOptions: { 25 | customFetchImpl: async (url, init) => { 26 | return new Response(JSON.stringify({ url })); 27 | }, 28 | }, 29 | }); 30 | const response = await client.test(); 31 | expect(response.data).toEqual({ 32 | url: "http://localhost:3000/api/auth/test", 33 | }); 34 | }); 35 | 36 | it("should use base url and use the provider path if provided", async () => { 37 | const client = createAuthClient({ 38 | plugins: [testClientPlugin()], 39 | baseURL: "http://localhost:3000/auth", 40 | fetchOptions: { 41 | customFetchImpl: async (url, init) => { 42 | return new Response(JSON.stringify({ url })); 43 | }, 44 | }, 45 | }); 46 | const response = await client.test(); 47 | expect(response.data).toEqual({ 48 | url: "http://localhost:3000/auth/test", 49 | }); 50 | }); 51 | 52 | it("should use be able to detect `/` in the base url", async () => { 53 | const client = createAuthClient({ 54 | plugins: [testClientPlugin()], 55 | baseURL: "http://localhost:3000", 56 | basePath: "/", 57 | fetchOptions: { 58 | customFetchImpl: async (url, init) => { 59 | return new Response(JSON.stringify({ url })); 60 | }, 61 | }, 62 | }); 63 | const response = await client.test(); 64 | expect(response.data).toEqual({ 65 | url: "http://localhost:3000/test", 66 | }); 67 | }); 68 | }); 69 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { haveIBeenPwned } from "./index"; 4 | describe("have-i-been-pwned", async () => { 5 | const { client, auth } = await getTestInstance( 6 | { 7 | plugins: [haveIBeenPwned()], 8 | }, 9 | { 10 | disableTestUser: true, 11 | }, 12 | ); 13 | const ctx = await auth.$context; 14 | it("should prevent account creation with compromised password", async () => { 15 | const uniqueEmail = `test-${Date.now()}@example.com`; 16 | const compromisedPassword = "123456789"; 17 | 18 | const result = await client.signUp.email({ 19 | email: uniqueEmail, 20 | password: compromisedPassword, 21 | name: "Test User", 22 | }); 23 | const user = await ctx.internalAdapter.findUserByEmail(uniqueEmail); 24 | expect(user).toBeNull(); 25 | expect(result.error).not.toBeNull(); 26 | expect(result.error?.status).toBe(400); 27 | expect(result.error?.code).toBe("PASSWORD_COMPROMISED"); 28 | }); 29 | 30 | it("should allow account creation with strong, uncompromised password", async () => { 31 | const uniqueEmail = `test-${Date.now()}@example.com`; 32 | const strongPassword = `Str0ng!P@ssw0rd-${Date.now()}`; 33 | 34 | const result = await client.signUp.email({ 35 | email: uniqueEmail, 36 | password: strongPassword, 37 | name: "Test User", 38 | }); 39 | expect(result.data?.user).toBeDefined(); 40 | }); 41 | 42 | it("should prevent password update to compromised password", async () => { 43 | const uniqueEmail = `test-${Date.now()}@example.com`; 44 | const initialPassword = `Str0ng!P@ssw0rd-${Date.now()}`; 45 | 46 | const res = await client.signUp.email({ 47 | email: uniqueEmail, 48 | password: initialPassword, 49 | name: "Test User", 50 | }); 51 | const result = await client.changePassword( 52 | { 53 | currentPassword: initialPassword, 54 | newPassword: "123456789", 55 | }, 56 | { 57 | headers: { 58 | authorization: `Bearer ${res.data?.token}`, 59 | }, 60 | }, 61 | ); 62 | expect(result.error).toBeDefined(); 63 | expect(result.error?.status).toBe(400); 64 | }); 65 | }); 66 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/shim.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { AuthContext } from "@better-auth/core"; 2 | 3 | export const shimContext = <T extends Record<string, any>>( 4 | originalObject: T, 5 | newContext: Record<string, any>, 6 | ) => { 7 | const shimmedObj: Record<string, any> = {}; 8 | for (const [key, value] of Object.entries(originalObject)) { 9 | shimmedObj[key] = (ctx: Record<string, any>) => { 10 | return value({ 11 | ...ctx, 12 | context: { 13 | ...newContext, 14 | ...ctx.context, 15 | }, 16 | }); 17 | }; 18 | shimmedObj[key].path = value.path; 19 | shimmedObj[key].method = value.method; 20 | shimmedObj[key].options = value.options; 21 | shimmedObj[key].headers = value.headers; 22 | } 23 | return shimmedObj as T; 24 | }; 25 | 26 | export const shimEndpoint = (ctx: AuthContext, value: any) => { 27 | return async (context: any) => { 28 | for (const plugin of ctx.options.plugins || []) { 29 | if (plugin.hooks?.before) { 30 | for (const hook of plugin.hooks.before) { 31 | const match = hook.matcher({ 32 | ...context, 33 | ...value, 34 | }); 35 | if (match) { 36 | const hookRes = await hook.handler(context); 37 | if ( 38 | hookRes && 39 | typeof hookRes === "object" && 40 | "context" in hookRes 41 | ) { 42 | context = { 43 | ...context, 44 | ...(hookRes.context as any), 45 | ...value, 46 | }; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | const endpointRes = value({ 53 | ...context, 54 | context: { 55 | ...ctx, 56 | ...context.context, 57 | }, 58 | }); 59 | let response = endpointRes; 60 | for (const plugin of ctx.options.plugins || []) { 61 | if (plugin.hooks?.after) { 62 | for (const hook of plugin.hooks.after) { 63 | const match = hook.matcher(context); 64 | if (match) { 65 | const obj = Object.assign(context, { 66 | returned: endpointRes, 67 | }); 68 | const hookRes = await hook.handler(obj); 69 | if ( 70 | hookRes && 71 | typeof hookRes === "object" && 72 | "response" in hookRes 73 | ) { 74 | response = hookRes.response as any; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | return response; 81 | }; 82 | }; 83 | ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/theme-toggle.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { cva } from "class-variance-authority"; 3 | import { Moon, Sun, Airplay } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { type HTMLAttributes, useLayoutEffect, useState } from "react"; 6 | import { cn } from "../../../lib/utils"; 7 | 8 | const itemVariants = cva( 9 | "size-6.5 rounded-full p-1.5 text-fd-muted-foreground", 10 | { 11 | variants: { 12 | active: { 13 | true: "bg-fd-accent text-fd-accent-foreground", 14 | false: "text-fd-muted-foreground", 15 | }, 16 | }, 17 | }, 18 | ); 19 | 20 | const full = [ 21 | ["light", Sun] as const, 22 | ["dark", Moon] as const, 23 | ["system", Airplay] as const, 24 | ]; 25 | 26 | export function ThemeToggle({ 27 | className, 28 | mode = "light-dark", 29 | ...props 30 | }: HTMLAttributes<HTMLElement> & { 31 | mode?: "light-dark" | "light-dark-system"; 32 | }) { 33 | const { setTheme, theme, resolvedTheme } = useTheme(); 34 | const [mounted, setMounted] = useState(false); 35 | 36 | useLayoutEffect(() => { 37 | setMounted(true); 38 | }, []); 39 | 40 | const container = cn( 41 | "inline-flex items-center rounded-full border p-1", 42 | className, 43 | ); 44 | 45 | if (mode === "light-dark") { 46 | const value = mounted ? resolvedTheme : null; 47 | 48 | return ( 49 | <button 50 | className={container} 51 | aria-label={`Toggle Theme`} 52 | onClick={() => setTheme(value === "light" ? "dark" : "light")} 53 | data-theme-toggle="" 54 | {...props} 55 | > 56 | {full.map(([key, Icon]) => { 57 | if (key === "system") return; 58 | 59 | return ( 60 | <Icon 61 | key={key} 62 | fill="currentColor" 63 | className={cn(itemVariants({ active: value === key }))} 64 | /> 65 | ); 66 | })} 67 | </button> 68 | ); 69 | } 70 | 71 | const value = mounted ? theme : null; 72 | 73 | return ( 74 | <div className={container} data-theme-toggle="" {...props}> 75 | {full.map(([key, Icon]) => ( 76 | <button 77 | key={key} 78 | aria-label={key} 79 | className={cn(itemVariants({ active: value === key }))} 80 | onClick={() => setTheme(key)} 81 | > 82 | <Icon className="size-full" fill="currentColor" /> 83 | </button> 84 | ))} 85 | </div> 86 | ); 87 | } 88 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/spotify.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Spotify 3 | description: Spotify provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Spotify Credentials 9 | To use Spotify sign in, you need a client ID and client secret. You can get them from the [Spotify Developer Portal](https://developer.spotify.com/dashboard/applications). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/spotify` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | 21 | export const auth = betterAuth({ 22 | 23 | socialProviders: { 24 | spotify: { // [!code highlight] 25 | clientId: process.env.SPOTIFY_CLIENT_ID as string, // [!code highlight] 26 | clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, // [!code highlight] 27 | }, // [!code highlight] 28 | }, 29 | }) 30 | ``` 31 | </Step> 32 | <Step> 33 | ### Sign In with Spotify 34 | To sign in with Spotify, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 35 | - `provider`: The provider to use. It should be set to `spotify`. 36 | 37 | ```ts title="auth-client.ts" 38 | import { createAuthClient } from "better-auth/client" 39 | const authClient = createAuthClient() 40 | 41 | const signIn = async () => { 42 | const data = await authClient.signIn.social({ 43 | provider: "spotify" 44 | }) 45 | } 46 | ``` 47 | </Step> 48 | </Steps> 49 | ``` -------------------------------------------------------------------------------- /docs/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps<typeof buttonVariants> & { 45 | asChild?: boolean; 46 | }) { 47 | const Comp = asChild ? Slot : "button"; 48 | 49 | return ( 50 | <Comp 51 | data-slot="button" 52 | className={cn(buttonVariants({ variant, size, className }))} 53 | {...props} 54 | /> 55 | ); 56 | } 57 | 58 | export { Button, buttonVariants }; 59 | ``` -------------------------------------------------------------------------------- /docs/components/search-dialog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { 4 | SearchDialog, 5 | SearchDialogClose, 6 | SearchDialogContent, 7 | SearchDialogFooter, 8 | SearchDialogHeader, 9 | SearchDialogIcon, 10 | SearchDialogInput, 11 | SearchDialogList, 12 | SearchDialogOverlay, 13 | type SharedProps, 14 | } from "fumadocs-ui/components/dialog/search"; 15 | import { useDocsSearch } from "fumadocs-core/search/client"; 16 | import { OramaClient } from "@oramacloud/client"; 17 | import { useI18n } from "fumadocs-ui/contexts/i18n"; 18 | import { AIChatModal, aiChatModalAtom } from "./ai-chat-modal"; 19 | import { useAtom } from "jotai"; 20 | 21 | const client = new OramaClient({ 22 | endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!, 23 | api_key: process.env.NEXT_PUBLIC_ORAMA_PUBLIC_API_KEY!, 24 | }); 25 | 26 | export function CustomSearchDialog(props: SharedProps) { 27 | const { locale } = useI18n(); 28 | const [isAIModalOpen, setIsAIModalOpen] = useAtom(aiChatModalAtom); 29 | 30 | const { search, setSearch, query } = useDocsSearch({ 31 | type: "orama-cloud", 32 | client, 33 | locale, 34 | }); 35 | 36 | const handleAskAIClick = () => { 37 | props.onOpenChange?.(false); 38 | setIsAIModalOpen(true); 39 | }; 40 | 41 | const handleAIModalClose = () => { 42 | setIsAIModalOpen(false); 43 | }; 44 | 45 | return ( 46 | <> 47 | <SearchDialog 48 | search={search} 49 | onSearchChange={setSearch} 50 | isLoading={query.isLoading} 51 | {...props} 52 | > 53 | <SearchDialogOverlay /> 54 | <SearchDialogContent className="mt-12 md:mt-0"> 55 | <SearchDialogHeader> 56 | <SearchDialogIcon /> 57 | <SearchDialogInput /> 58 | <SearchDialogClose className="hidden md:block" /> 59 | </SearchDialogHeader> 60 | <SearchDialogList 61 | items={query.data !== "empty" ? query.data : null} 62 | /> 63 | <SearchDialogFooter> 64 | <a 65 | href="https://orama.com" 66 | rel="noreferrer noopener" 67 | className="ms-auto text-xs text-fd-muted-foreground" 68 | > 69 | Search powered by Orama 70 | </a> 71 | </SearchDialogFooter> 72 | </SearchDialogContent> 73 | </SearchDialog> 74 | 75 | <AIChatModal isOpen={isAIModalOpen} onClose={handleAIModalClose} /> 76 | </> 77 | ); 78 | } 79 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type Session, type User } from "../../types"; 2 | import { type InferOptionSchema } from "../../types"; 3 | import { type AccessControl, type Role } from "../access"; 4 | import { type AdminSchema } from "./schema"; 5 | 6 | export interface UserWithRole extends User { 7 | role?: string; 8 | banned?: boolean | null; 9 | banReason?: string | null; 10 | banExpires?: Date | null; 11 | } 12 | 13 | export interface SessionWithImpersonatedBy extends Session { 14 | impersonatedBy?: string; 15 | } 16 | 17 | export interface AdminOptions { 18 | /** 19 | * The default role for a user 20 | * 21 | * @default "user" 22 | */ 23 | defaultRole?: string; 24 | /** 25 | * Roles that are considered admin roles. 26 | * 27 | * Any user role that isn't in this list, even if they have the permission, 28 | * will not be considered an admin. 29 | * 30 | * @default ["admin"] 31 | */ 32 | adminRoles?: string | string[]; 33 | /** 34 | * A default ban reason 35 | * 36 | * By default, no reason is provided 37 | */ 38 | defaultBanReason?: string; 39 | /** 40 | * Number of seconds until the ban expires 41 | * 42 | * By default, the ban never expires 43 | */ 44 | defaultBanExpiresIn?: number; 45 | /** 46 | * Duration of the impersonation session in seconds 47 | * 48 | * By default, the impersonation session lasts 1 hour 49 | */ 50 | impersonationSessionDuration?: number; 51 | /** 52 | * Custom schema for the admin plugin 53 | */ 54 | schema?: InferOptionSchema<AdminSchema>; 55 | /** 56 | * Configure the roles and permissions for the admin 57 | * plugin. 58 | */ 59 | ac?: AccessControl; 60 | /** 61 | * Custom permissions for roles. 62 | */ 63 | roles?: { 64 | [key in string]?: Role; 65 | }; 66 | /** 67 | * List of user ids that should have admin access 68 | * 69 | * If this is set, the `adminRole` option is ignored 70 | */ 71 | adminUserIds?: string[]; 72 | /** 73 | * Message to show when a user is banned 74 | * 75 | * By default, the message is "You have been banned from this application" 76 | */ 77 | bannedUserMessage?: string; 78 | } 79 | 80 | export type InferAdminRolesFromOption<O extends AdminOptions | undefined> = 81 | O extends { roles: Record<string, unknown> } 82 | ? keyof O["roles"] 83 | : "user" | "admin"; 84 | ``` -------------------------------------------------------------------------------- /docs/components/ui/resizable.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { GripVerticalIcon } from "lucide-react"; 5 | import * as ResizablePrimitive from "react-resizable-panels"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function ResizablePanelGroup({ 10 | className, 11 | ...props 12 | }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) { 13 | return ( 14 | <ResizablePrimitive.PanelGroup 15 | data-slot="resizable-panel-group" 16 | className={cn( 17 | "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", 18 | className, 19 | )} 20 | {...props} 21 | /> 22 | ); 23 | } 24 | 25 | function ResizablePanel({ 26 | ...props 27 | }: React.ComponentProps<typeof ResizablePrimitive.Panel>) { 28 | return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />; 29 | } 30 | 31 | function ResizableHandle({ 32 | withHandle, 33 | className, 34 | ...props 35 | }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { 36 | withHandle?: boolean; 37 | }) { 38 | return ( 39 | <ResizablePrimitive.PanelResizeHandle 40 | data-slot="resizable-handle" 41 | className={cn( 42 | "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", 43 | className, 44 | )} 45 | {...props} 46 | > 47 | {withHandle && ( 48 | <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border"> 49 | <GripVerticalIcon className="size-2.5" /> 50 | </div> 51 | )} 52 | </ResizablePrimitive.PanelResizeHandle> 53 | ); 54 | } 55 | 56 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 57 | ``` -------------------------------------------------------------------------------- /docs/components/ui/accordion.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDownIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps<typeof AccordionPrimitive.Root>) { 12 | return <AccordionPrimitive.Root data-slot="accordion" {...props} />; 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps<typeof AccordionPrimitive.Item>) { 19 | return ( 20 | <AccordionPrimitive.Item 21 | data-slot="accordion-item" 22 | className={cn("border-b last:border-b-0", className)} 23 | {...props} 24 | /> 25 | ); 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) { 33 | return ( 34 | <AccordionPrimitive.Header className="flex"> 35 | <AccordionPrimitive.Trigger 36 | data-slot="accordion-trigger" 37 | className={cn( 38 | "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", 39 | className, 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> 45 | </AccordionPrimitive.Trigger> 46 | </AccordionPrimitive.Header> 47 | ); 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps<typeof AccordionPrimitive.Content>) { 55 | return ( 56 | <AccordionPrimitive.Content 57 | data-slot="accordion-content" 58 | className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" 59 | {...props} 60 | > 61 | <div className={cn("pt-0 pb-4", className)}>{children}</div> 62 | </AccordionPrimitive.Content> 63 | ); 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 67 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/types/models.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthOptions } from "@better-auth/core"; 2 | import type { Auth } from "../auth"; 3 | import type { InferFieldsFromOptions, InferFieldsFromPlugins } from "../db"; 4 | import type { StripEmptyObjects, UnionToIntersection } from "./helper"; 5 | import type { BetterAuthPlugin } from "@better-auth/core"; 6 | import type { User, Session } from "@better-auth/core/db"; 7 | 8 | export type AdditionalUserFieldsInput<Options extends BetterAuthOptions> = 9 | InferFieldsFromPlugins<Options, "user", "input"> & 10 | InferFieldsFromOptions<Options, "user", "input">; 11 | 12 | export type AdditionalUserFieldsOutput<Options extends BetterAuthOptions> = 13 | InferFieldsFromPlugins<Options, "user"> & 14 | InferFieldsFromOptions<Options, "user">; 15 | 16 | export type AdditionalSessionFieldsInput<Options extends BetterAuthOptions> = 17 | InferFieldsFromPlugins<Options, "session", "input"> & 18 | InferFieldsFromOptions<Options, "session", "input">; 19 | 20 | export type AdditionalSessionFieldsOutput<Options extends BetterAuthOptions> = 21 | InferFieldsFromPlugins<Options, "session"> & 22 | InferFieldsFromOptions<Options, "session">; 23 | 24 | export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection< 25 | StripEmptyObjects< 26 | User & 27 | (O extends BetterAuthOptions 28 | ? AdditionalUserFieldsOutput<O> 29 | : O extends Auth 30 | ? AdditionalUserFieldsOutput<O["options"]> 31 | : {}) 32 | > 33 | >; 34 | 35 | export type InferSession<O extends BetterAuthOptions | Auth> = 36 | UnionToIntersection< 37 | StripEmptyObjects< 38 | Session & 39 | (O extends BetterAuthOptions 40 | ? AdditionalSessionFieldsOutput<O> 41 | : O extends Auth 42 | ? AdditionalSessionFieldsOutput<O["options"]> 43 | : {}) 44 | > 45 | >; 46 | 47 | export type InferPluginTypes<O extends BetterAuthOptions> = 48 | O["plugins"] extends Array<infer P> 49 | ? UnionToIntersection< 50 | P extends BetterAuthPlugin 51 | ? P["$Infer"] extends Record<string, any> 52 | ? P["$Infer"] 53 | : {} 54 | : {} 55 | > 56 | : {}; 57 | 58 | export type { 59 | User, 60 | Account, 61 | Session, 62 | Verification, 63 | RateLimit, 64 | } from "@better-auth/core/db"; 65 | ``` -------------------------------------------------------------------------------- /demo/expo-example/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | // NOTE: Update this to include the paths to all of your component files. 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | presets: [require("nativewind/preset")], 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: "2rem", 10 | screens: { 11 | "2xl": "1400px", 12 | }, 13 | }, 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border))", 17 | input: "hsl(var(--input))", 18 | ring: "hsl(var(--ring))", 19 | background: "hsl(var(--background))", 20 | foreground: "hsl(var(--foreground))", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary))", 23 | foreground: "hsl(var(--primary-foreground))", 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary))", 27 | foreground: "hsl(var(--secondary-foreground))", 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive))", 31 | foreground: "hsl(var(--destructive-foreground))", 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted))", 35 | foreground: "hsl(var(--muted-foreground))", 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent))", 39 | foreground: "hsl(var(--accent-foreground))", 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover))", 43 | foreground: "hsl(var(--popover-foreground))", 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)", 54 | }, 55 | keyframes: { 56 | "accordion-down": { 57 | from: { height: "0" }, 58 | to: { height: "var(--radix-accordion-content-height)" }, 59 | }, 60 | "accordion-up": { 61 | from: { height: "var(--radix-accordion-content-height)" }, 62 | to: { height: "0" }, 63 | }, 64 | }, 65 | animation: { 66 | "accordion-down": "accordion-down 0.2s ease-out", 67 | "accordion-up": "accordion-up 0.2s ease-out", 68 | }, 69 | boxShadow: { 70 | input: `0px 2px 3px -1px rgba(0,0,0,0.1), 0px 1px 0px 0px rgba(25,28,33,0.02), 0px 0px 0px 1px rgba(25,28,33,0.08)`, 71 | }, 72 | }, 73 | }, 74 | plugins: [], 75 | }; 76 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/admin/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthClientPlugin } from "@better-auth/core"; 2 | import { type AccessControl, type Role } from "../access"; 3 | import { adminAc, defaultStatements, userAc } from "./access"; 4 | import type { admin } from "./admin"; 5 | import { hasPermission } from "./has-permission"; 6 | 7 | interface AdminClientOptions { 8 | ac?: AccessControl; 9 | roles?: { 10 | [key in string]: Role; 11 | }; 12 | } 13 | 14 | export const adminClient = <O extends AdminClientOptions>(options?: O) => { 15 | type DefaultStatements = typeof defaultStatements; 16 | type Statements = O["ac"] extends AccessControl<infer S> 17 | ? S 18 | : DefaultStatements; 19 | type PermissionType = { 20 | [key in keyof Statements]?: Array< 21 | Statements[key] extends readonly unknown[] 22 | ? Statements[key][number] 23 | : never 24 | >; 25 | }; 26 | type PermissionExclusive = 27 | | { 28 | /** 29 | * @deprecated Use `permissions` instead 30 | */ 31 | permission: PermissionType; 32 | permissions?: never; 33 | } 34 | | { 35 | permissions: PermissionType; 36 | permission?: never; 37 | }; 38 | 39 | const roles = { 40 | admin: adminAc, 41 | user: userAc, 42 | ...options?.roles, 43 | }; 44 | 45 | return { 46 | id: "admin-client", 47 | $InferServerPlugin: {} as ReturnType< 48 | typeof admin<{ 49 | ac: O["ac"] extends AccessControl 50 | ? O["ac"] 51 | : AccessControl<DefaultStatements>; 52 | roles: O["roles"] extends Record<string, Role> 53 | ? O["roles"] 54 | : { 55 | admin: Role; 56 | user: Role; 57 | }; 58 | }> 59 | >, 60 | getActions: () => ({ 61 | admin: { 62 | checkRolePermission: < 63 | R extends O extends { roles: any } 64 | ? keyof O["roles"] 65 | : "admin" | "user", 66 | >( 67 | data: PermissionExclusive & { 68 | role: R; 69 | }, 70 | ) => { 71 | const isAuthorized = hasPermission({ 72 | role: data.role as string, 73 | options: { 74 | ac: options?.ac, 75 | roles: roles, 76 | }, 77 | permissions: (data.permissions ?? data.permission) as any, 78 | }); 79 | return isAuthorized; 80 | }, 81 | }, 82 | }), 83 | pathMethods: { 84 | "/admin/list-users": "GET", 85 | "/admin/stop-impersonating": "POST", 86 | }, 87 | } satisfies BetterAuthClientPlugin; 88 | }; 89 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/huggingface.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Hugging Face 3 | description: Hugging Face provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Hugging Face credentials 9 | To use Hugging Face sign in, you need a client ID and client secret. [Hugging Face OAuth documentation](https://huggingface.co/docs/hub/oauth). Make sure the created oauth app on Hugging Face has the "email" scope. 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/huggingface` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Step> 15 | ### Configure the provider 16 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 17 | 18 | ```ts title="auth.ts" 19 | import { betterAuth } from "better-auth" 20 | 21 | export const auth = betterAuth({ 22 | socialProviders: { 23 | huggingface: { // [!code highlight] 24 | clientId: process.env.HUGGINGFACE_CLIENT_ID as string, // [!code highlight] 25 | clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET as string, // [!code highlight] 26 | }, // [!code highlight] 27 | }, 28 | }) 29 | ``` 30 | </Step> 31 | <Step> 32 | ### Sign In with Hugging Face 33 | To sign in with Hugging Face, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 34 | - `provider`: The provider to use. It should be set to `huggingface`. 35 | 36 | ```ts title="auth-client.ts" 37 | import { createAuthClient } from "better-auth/client" 38 | const authClient = createAuthClient() 39 | 40 | const signIn = async () => { 41 | const data = await authClient.signIn.social({ 42 | provider: "huggingface" 43 | }) 44 | } 45 | ``` 46 | </Step> 47 | </Steps> 48 | ``` -------------------------------------------------------------------------------- /docs/components/docs/layout/toc-thumb.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { type HTMLAttributes, type RefObject, useEffect, useRef } from "react"; 2 | import * as Primitive from "fumadocs-core/toc"; 3 | import { useOnChange } from "fumadocs-core/utils/use-on-change"; 4 | import { useEffectEvent } from "fumadocs-core/utils/use-effect-event"; 5 | 6 | export type TOCThumb = [top: number, height: number]; 7 | 8 | function calc(container: HTMLElement, active: string[]): TOCThumb { 9 | if (active.length === 0 || container.clientHeight === 0) { 10 | return [0, 0]; 11 | } 12 | 13 | let upper = Number.MAX_VALUE, 14 | lower = 0; 15 | 16 | for (const item of active) { 17 | const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`); 18 | if (!element) continue; 19 | 20 | const styles = getComputedStyle(element); 21 | upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop)); 22 | lower = Math.max( 23 | lower, 24 | element.offsetTop + 25 | element.clientHeight - 26 | parseFloat(styles.paddingBottom), 27 | ); 28 | } 29 | 30 | return [upper, lower - upper]; 31 | } 32 | 33 | function update(element: HTMLElement, info: TOCThumb): void { 34 | element.style.setProperty("--fd-top", `${info[0]}px`); 35 | element.style.setProperty("--fd-height", `${info[1]}px`); 36 | } 37 | 38 | export function TocThumb({ 39 | containerRef, 40 | ...props 41 | }: HTMLAttributes<HTMLDivElement> & { 42 | containerRef: RefObject<HTMLElement | null>; 43 | }) { 44 | const active = Primitive.useActiveAnchors(); 45 | const thumbRef = useRef<HTMLDivElement>(null); 46 | 47 | const onResize = useEffectEvent(() => { 48 | if (!containerRef.current || !thumbRef.current) return; 49 | 50 | update(thumbRef.current, calc(containerRef.current, active)); 51 | }); 52 | 53 | useEffect(() => { 54 | if (!containerRef.current) return; 55 | const container = containerRef.current; 56 | 57 | onResize(); 58 | const observer = new ResizeObserver(onResize); 59 | observer.observe(container); 60 | 61 | return () => { 62 | observer.disconnect(); 63 | }; 64 | }, [containerRef, onResize]); 65 | 66 | useOnChange(active, () => { 67 | if (!containerRef.current || !thumbRef.current) return; 68 | 69 | update(thumbRef.current, calc(containerRef.current, active)); 70 | }); 71 | 72 | return <div ref={thumbRef} role="none" {...props} />; 73 | } 74 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/accordion.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = ({ 12 | ref, 13 | className, 14 | ...props 15 | }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & { 16 | ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Item>>; 17 | }) => ( 18 | <AccordionPrimitive.Item 19 | ref={ref} 20 | className={cn("border-b", className)} 21 | {...props} 22 | /> 23 | ); 24 | AccordionItem.displayName = "AccordionItem"; 25 | 26 | const AccordionTrigger = ({ 27 | ref, 28 | className, 29 | children, 30 | ...props 31 | }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { 32 | ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Trigger>>; 33 | }) => ( 34 | <AccordionPrimitive.Header className="flex"> 35 | <AccordionPrimitive.Trigger 36 | ref={ref} 37 | className={cn( 38 | "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", 39 | className, 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | <ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" /> 45 | </AccordionPrimitive.Trigger> 46 | </AccordionPrimitive.Header> 47 | ); 48 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 49 | 50 | const AccordionContent = ({ 51 | ref, 52 | className, 53 | children, 54 | ...props 55 | }: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> & { 56 | ref: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Content>>; 57 | }) => ( 58 | <AccordionPrimitive.Content 59 | ref={ref} 60 | className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" 61 | {...props} 62 | > 63 | <div className={cn("pb-4 pt-0", className)}>{children}</div> 64 | </AccordionPrimitive.Content> 65 | ); 66 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 67 | 68 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 69 | ``` -------------------------------------------------------------------------------- /demo/expo-example/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "expo-example", 3 | "main": "index.ts", 4 | "private": true, 5 | "version": "1.0.0", 6 | "scripts": { 7 | "clean": "git clean -xdf .cache .expo .turbo android ios node_modules", 8 | "start": "expo start", 9 | "dev": "expo run:android", 10 | "ios": "expo run:ios", 11 | "web": "expo start --web", 12 | "lint": "expo lint", 13 | "android": "expo run:android" 14 | }, 15 | "dependencies": { 16 | "@better-auth/expo": "workspace:*", 17 | "@expo/metro-runtime": "^6.1.2", 18 | "@expo/vector-icons": "^15.0.2", 19 | "@nanostores/react": "^1.0.0", 20 | "@react-native-async-storage/async-storage": "2.2.0", 21 | "@react-navigation/native": "^7.1.17", 22 | "@rn-primitives/avatar": "^1.1.0", 23 | "@rn-primitives/separator": "^1.1.0", 24 | "@rn-primitives/slot": "^1.1.0", 25 | "@rn-primitives/types": "^1.1.0", 26 | "@types/better-sqlite3": "^7.6.12", 27 | "babel-plugin-transform-import-meta": "^2.2.1", 28 | "better-auth": "workspace:*", 29 | "better-sqlite3": "^11.6.0", 30 | "expo": "~54.0.10", 31 | "expo-constants": "~18.0.9", 32 | "expo-crypto": "^15.0.7", 33 | "expo-font": "~14.0.8", 34 | "expo-linking": "~8.0.8", 35 | "expo-router": "~6.0.8", 36 | "expo-secure-store": "~15.0.7", 37 | "expo-splash-screen": "~31.0.10", 38 | "expo-status-bar": "~3.0.8", 39 | "expo-system-ui": "~6.0.7", 40 | "expo-web-browser": "~15.0.7", 41 | "nanostores": "^0.11.3", 42 | "nativewind": "^4.1.23", 43 | "pg": "^8.13.1", 44 | "react": "^19.2.0", 45 | "react-dom": "^19.2.0", 46 | "react-native": "~0.81.4", 47 | "react-native-css-interop": "^0.2.1", 48 | "react-native-gesture-handler": "~2.28.0", 49 | "react-native-reanimated": "~4.1.2", 50 | "react-native-safe-area-context": "5.6.1", 51 | "react-native-screens": "4.16.0", 52 | "react-native-svg": "^15.12.1", 53 | "react-native-web": "~0.21.1", 54 | "react-native-worklets": "^0.5.1", 55 | "tailwindcss": "^3.4.16" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.26.0", 59 | "@babel/preset-env": "^7.26.0", 60 | "@babel/runtime": "^7.26.0", 61 | "@types/babel__core": "^7.20.5", 62 | "@types/react": "^19.2.2" 63 | } 64 | } 65 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/card.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { TextRef, ViewRef } from "@rn-primitives/types"; 2 | import * as React from "react"; 3 | import { Text, type TextProps, View, type ViewProps } from "react-native"; 4 | import { cn } from "@/lib/utils"; 5 | import { TextClassContext } from "@/components/ui/text"; 6 | 7 | const Card = React.forwardRef<ViewRef, ViewProps>( 8 | ({ className, ...props }, ref) => ( 9 | <View 10 | ref={ref} 11 | className={cn( 12 | "rounded-lg border border-border bg-card shadow-sm shadow-foreground/10", 13 | className, 14 | )} 15 | {...props} 16 | /> 17 | ), 18 | ); 19 | Card.displayName = "Card"; 20 | 21 | const CardHeader = React.forwardRef<ViewRef, ViewProps>( 22 | ({ className, ...props }, ref) => ( 23 | <View 24 | ref={ref} 25 | className={cn("flex flex-col space-y-1.5 p-6", className)} 26 | {...props} 27 | /> 28 | ), 29 | ); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef<TextRef, TextProps>( 33 | ({ className, ...props }, ref) => ( 34 | <Text 35 | role="heading" 36 | aria-level={3} 37 | ref={ref} 38 | className={cn( 39 | "text-2xl text-card-foreground font-semibold leading-none tracking-tight", 40 | className, 41 | )} 42 | {...props} 43 | /> 44 | ), 45 | ); 46 | CardTitle.displayName = "CardTitle"; 47 | 48 | const CardDescription = React.forwardRef<TextRef, TextProps>( 49 | ({ className, ...props }, ref) => ( 50 | <Text 51 | ref={ref} 52 | className={cn("text-sm text-muted-foreground", className)} 53 | {...props} 54 | /> 55 | ), 56 | ); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef<ViewRef, ViewProps>( 60 | ({ className, ...props }, ref) => ( 61 | <TextClassContext.Provider value="text-card-foreground"> 62 | <View ref={ref} className={cn("p-6 pt-0", className)} {...props} /> 63 | </TextClassContext.Provider> 64 | ), 65 | ); 66 | CardContent.displayName = "CardContent"; 67 | 68 | const CardFooter = React.forwardRef<ViewRef, ViewProps>( 69 | ({ className, ...props }, ref) => ( 70 | <View 71 | ref={ref} 72 | className={cn("flex flex-row items-center p-6 pt-0", className)} 73 | {...props} 74 | /> 75 | ), 76 | ); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle, 86 | }; 87 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/fixtures/cloudflare/src/auth-schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 2 | 3 | export const user = sqliteTable("user", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | email: text("email").notNull().unique(), 7 | emailVerified: integer("email_verified", { mode: "boolean" }).notNull(), 8 | image: text("image"), 9 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 10 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 11 | }); 12 | 13 | export const session = sqliteTable("session", { 14 | id: text("id").primaryKey(), 15 | expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), 16 | token: text("token").notNull().unique(), 17 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 18 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 19 | ipAddress: text("ip_address"), 20 | userAgent: text("user_agent"), 21 | userId: text("user_id") 22 | .notNull() 23 | .references(() => user.id, { onDelete: "cascade" }), 24 | }); 25 | 26 | export const account = sqliteTable("account", { 27 | id: text("id").primaryKey(), 28 | accountId: text("account_id").notNull(), 29 | providerId: text("provider_id").notNull(), 30 | userId: text("user_id") 31 | .notNull() 32 | .references(() => user.id, { onDelete: "cascade" }), 33 | accessToken: text("access_token"), 34 | refreshToken: text("refresh_token"), 35 | idToken: text("id_token"), 36 | accessTokenExpiresAt: integer("access_token_expires_at", { 37 | mode: "timestamp", 38 | }), 39 | refreshTokenExpiresAt: integer("refresh_token_expires_at", { 40 | mode: "timestamp", 41 | }), 42 | scope: text("scope"), 43 | password: text("password"), 44 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 45 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 46 | }); 47 | 48 | export const verification = sqliteTable("verification", { 49 | id: text("id").primaryKey(), 50 | identifier: text("identifier").notNull(), 51 | value: text("value").notNull(), 52 | expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), 53 | createdAt: integer("created_at", { mode: "timestamp" }), 54 | updatedAt: integer("updated_at", { mode: "timestamp" }), 55 | }); 56 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/drizzle-adapter/test/adapter.drizzle.sqlite.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Database from "better-sqlite3"; 2 | import { drizzleAdapter } from "../drizzle-adapter"; 3 | import { testAdapter } from "../../test-adapter"; 4 | import { 5 | authFlowTestSuite, 6 | normalTestSuite, 7 | numberIdTestSuite, 8 | performanceTestSuite, 9 | transactionsTestSuite, 10 | } from "../../tests"; 11 | import { drizzle } from "drizzle-orm/better-sqlite3"; 12 | import path from "path"; 13 | import { 14 | clearSchemaCache, 15 | generateDrizzleSchema, 16 | resetGenerationCount, 17 | } from "./generate-schema"; 18 | import fs from "fs/promises"; 19 | import { execSync } from "child_process"; 20 | 21 | const dbFilePath = path.join(import.meta.dirname, "test.db"); 22 | let sqliteDB = new Database(dbFilePath); 23 | 24 | const { execute } = await testAdapter({ 25 | adapter: async (options) => { 26 | const { schema } = await generateDrizzleSchema(sqliteDB, options, "sqlite"); 27 | return drizzleAdapter(drizzle(sqliteDB), { 28 | debugLogs: { isRunningAdapterTests: true }, 29 | schema, 30 | provider: "sqlite", 31 | }); 32 | }, 33 | async runMigrations(betterAuthOptions) { 34 | sqliteDB.close(); 35 | try { 36 | await fs.unlink(dbFilePath); 37 | } catch { 38 | console.log("db file not found"); 39 | } 40 | sqliteDB = new Database(dbFilePath); 41 | 42 | const { fileName } = await generateDrizzleSchema( 43 | sqliteDB, 44 | betterAuthOptions, 45 | "sqlite", 46 | ); 47 | 48 | const command = `npx drizzle-kit push --dialect=sqlite --schema=${fileName}.ts --url=./test.db`; 49 | console.log(`Running: ${command}`); 50 | console.log(`Options:`, betterAuthOptions); 51 | try { 52 | // wait for the above console.log to be printed 53 | await new Promise((resolve) => setTimeout(resolve, 10)); 54 | execSync(command, { 55 | cwd: import.meta.dirname, 56 | stdio: "inherit", 57 | }); 58 | } catch (error) { 59 | console.error("Failed to push drizzle schema (sqlite):", error); 60 | throw error; 61 | } 62 | }, 63 | prefixTests: "sqlite", 64 | tests: [ 65 | normalTestSuite(), 66 | transactionsTestSuite({ disableTests: { ALL: true } }), 67 | authFlowTestSuite(), 68 | numberIdTestSuite(), 69 | performanceTestSuite({ dialect: "sqlite" }), 70 | ], 71 | async onFinish() { 72 | clearSchemaCache(); 73 | resetGenerationCount(); 74 | }, 75 | }); 76 | 77 | execute(); 78 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/drizzle-adapter/test/adapter.drizzle.pg.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { drizzleAdapter } from "../drizzle-adapter"; 2 | import { testAdapter } from "../../test-adapter"; 3 | import { 4 | authFlowTestSuite, 5 | normalTestSuite, 6 | numberIdTestSuite, 7 | performanceTestSuite, 8 | transactionsTestSuite, 9 | } from "../../tests"; 10 | import { drizzle } from "drizzle-orm/node-postgres"; 11 | import { generateDrizzleSchema, resetGenerationCount } from "./generate-schema"; 12 | import { Pool } from "pg"; 13 | import { execSync } from "child_process"; 14 | 15 | const pgDB = new Pool({ 16 | connectionString: "postgres://user:password@localhost:5432/better_auth", 17 | }); 18 | 19 | const cleanupDatabase = async (shouldDestroy = false) => { 20 | await pgDB.query(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`); 21 | if (shouldDestroy) { 22 | await pgDB.end(); 23 | } 24 | }; 25 | 26 | const { execute } = await testAdapter({ 27 | adapter: async (options) => { 28 | const { schema } = await generateDrizzleSchema(pgDB, options, "pg"); 29 | return drizzleAdapter(drizzle(pgDB), { 30 | debugLogs: { isRunningAdapterTests: true }, 31 | schema, 32 | provider: "pg", 33 | transaction: true, 34 | }); 35 | }, 36 | async runMigrations(betterAuthOptions) { 37 | await cleanupDatabase(); 38 | const { fileName } = await generateDrizzleSchema( 39 | pgDB, 40 | betterAuthOptions, 41 | "pg", 42 | ); 43 | 44 | const command = `npx drizzle-kit push --dialect=postgresql --schema=${fileName}.ts --url=postgres://user:password@localhost:5432/better_auth`; 45 | console.log(`Running: ${command}`); 46 | console.log(`Options:`, betterAuthOptions); 47 | try { 48 | // wait for the above console.log to be printed 49 | await new Promise((resolve) => setTimeout(resolve, 10)); 50 | execSync(command, { 51 | cwd: import.meta.dirname, 52 | stdio: "inherit", 53 | }); 54 | } catch (error) { 55 | console.error("Failed to push drizzle schema (pg):", error); 56 | throw error; 57 | } 58 | }, 59 | prefixTests: "pg", 60 | tests: [ 61 | normalTestSuite(), 62 | transactionsTestSuite({ disableTests: { ALL: true } }), 63 | authFlowTestSuite(), 64 | numberIdTestSuite(), 65 | performanceTestSuite({ dialect: "pg" }), 66 | ], 67 | async onFinish() { 68 | await cleanupDatabase(true); 69 | resetGenerationCount(); 70 | }, 71 | }); 72 | 73 | execute(); 74 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/input-otp.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { DashIcon } from "@radix-ui/react-icons"; 5 | import { OTPInput, OTPInputContext } from "input-otp"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const InputOTP = ({ 10 | ref, 11 | className, 12 | containerClassName, 13 | ...props 14 | }: React.ComponentPropsWithoutRef<typeof OTPInput> & { 15 | ref: React.RefObject<React.ElementRef<typeof OTPInput>>; 16 | }) => ( 17 | <OTPInput 18 | ref={ref} 19 | containerClassName={cn( 20 | "flex items-center gap-2 has-disabled:opacity-50", 21 | containerClassName, 22 | )} 23 | className={cn("disabled:cursor-not-allowed", className)} 24 | {...props} 25 | /> 26 | ); 27 | InputOTP.displayName = "InputOTP"; 28 | 29 | const InputOTPGroup = ({ 30 | ref, 31 | className, 32 | ...props 33 | }: React.ComponentPropsWithoutRef<"div"> & { 34 | ref: React.RefObject<React.ElementRef<"div">>; 35 | }) => ( 36 | <div ref={ref} className={cn("flex items-center", className)} {...props} /> 37 | ); 38 | InputOTPGroup.displayName = "InputOTPGroup"; 39 | 40 | const InputOTPSlot = ({ ref, index, className, ...props }) => { 41 | const inputOTPContext = React.useContext(OTPInputContext); 42 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; 43 | 44 | return ( 45 | <div 46 | ref={ref} 47 | className={cn( 48 | "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", 49 | isActive && "z-10 ring-1 ring-ring", 50 | className, 51 | )} 52 | {...props} 53 | > 54 | {char} 55 | {hasFakeCaret && ( 56 | <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> 57 | <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> 58 | </div> 59 | )} 60 | </div> 61 | ); 62 | }; 63 | InputOTPSlot.displayName = "InputOTPSlot"; 64 | 65 | const InputOTPSeparator = ({ 66 | ref, 67 | ...props 68 | }: React.ComponentPropsWithoutRef<"div"> & { 69 | ref: React.RefObject<React.ElementRef<"div">>; 70 | }) => ( 71 | <div ref={ref} role="separator" {...props}> 72 | <DashIcon /> 73 | </div> 74 | ); 75 | InputOTPSeparator.displayName = "InputOTPSeparator"; 76 | 77 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; 78 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/prisma.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Prisma 3 | description: Integrate Better Auth with Prisma. 4 | --- 5 | 6 | Prisma ORM is an open-source database toolkit that simplifies database access and management in applications by providing a type-safe query builder and an intuitive data modeling interface. 7 | Read more [here](https://www.prisma.io/). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have Prisma installed and configured. 12 | Then, you can use the Prisma adapter to connect to your database. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { prismaAdapter } from "better-auth/adapters/prisma"; 17 | import { PrismaClient } from "@prisma/client"; 18 | 19 | const prisma = new PrismaClient(); 20 | 21 | export const auth = betterAuth({ 22 | database: prismaAdapter(prisma, { 23 | provider: "sqlite", 24 | }), 25 | }); 26 | ``` 27 | 28 | <Callout type="warning"> 29 | If you have configured a custom output directory in your `schema.prisma` file (e.g., `output = "../src/generated/prisma"`), make sure to import the Prisma client from that location instead of `@prisma/client`. Learn more about custom output directories in the [Prisma documentation](https://www.prisma.io/docs/guides/nextjs#21-install-prisma-orm-and-create-your-first-models). 30 | </Callout> 31 | 32 | ## Schema generation & migration 33 | 34 | The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate 35 | your database schema based on your Better Auth configuration and plugins. 36 | 37 | <table> 38 | <thead> 39 | <tr className="border-b"> 40 | <th> 41 | <p className="font-bold text-[16px] mb-1">Prisma Schema Generation</p> 42 | </th> 43 | <th> 44 | <p className="font-bold text-[16px] mb-1">Prisma Schema Migration</p> 45 | </th> 46 | </tr> 47 | </thead> 48 | <tbody> 49 | <tr className="h-10"> 50 | <td>✅ Supported</td> 51 | <td>❌ Not Supported</td> 52 | </tr> 53 | </tbody> 54 | </table> 55 | 56 | ```bash title="Schema Generation" 57 | npx @better-auth/cli@latest generate 58 | ``` 59 | 60 | ## Additional Information 61 | 62 | If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. 63 | ``` -------------------------------------------------------------------------------- /docs/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function ScrollArea({ 9 | className, 10 | children, 11 | ...props 12 | }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { 13 | return ( 14 | <ScrollAreaPrimitive.Root 15 | data-slot="scroll-area" 16 | className={cn("relative", className)} 17 | {...props} 18 | > 19 | <ScrollAreaPrimitive.Viewport 20 | data-slot="scroll-area-viewport" 21 | className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1" 22 | > 23 | {children} 24 | </ScrollAreaPrimitive.Viewport> 25 | <ScrollBar /> 26 | <ScrollAreaPrimitive.Corner /> 27 | </ScrollAreaPrimitive.Root> 28 | ); 29 | } 30 | const ScrollViewport = React.forwardRef< 31 | React.ComponentRef<typeof ScrollAreaPrimitive.Viewport>, 32 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> 33 | >(({ className, children, ...props }, ref) => ( 34 | <ScrollAreaPrimitive.Viewport 35 | ref={ref} 36 | className={cn("size-full rounded-[inherit]", className)} 37 | {...props} 38 | > 39 | {children} 40 | </ScrollAreaPrimitive.Viewport> 41 | )); 42 | 43 | ScrollViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; 44 | 45 | function ScrollBar({ 46 | className, 47 | orientation = "vertical", 48 | ...props 49 | }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { 50 | return ( 51 | <ScrollAreaPrimitive.ScrollAreaScrollbar 52 | data-slot="scroll-area-scrollbar" 53 | orientation={orientation} 54 | className={cn( 55 | "flex touch-none p-px transition-colors select-none", 56 | orientation === "vertical" && 57 | "h-full w-2.5 border-l border-l-transparent", 58 | orientation === "horizontal" && 59 | "h-2.5 flex-col border-t border-t-transparent", 60 | className, 61 | )} 62 | {...props} 63 | > 64 | <ScrollAreaPrimitive.ScrollAreaThumb 65 | data-slot="scroll-area-thumb" 66 | className="bg-border relative flex-1 rounded-full" 67 | /> 68 | </ScrollAreaPrimitive.ScrollAreaScrollbar> 69 | ); 70 | } 71 | 72 | export { ScrollArea, ScrollBar, ScrollViewport }; 73 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/kick.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2"; 3 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 4 | 5 | export interface KickProfile { 6 | /** 7 | * The user id of the user 8 | */ 9 | user_id: string; 10 | /** 11 | * The name of the user 12 | */ 13 | name: string; 14 | /** 15 | * The email of the user 16 | */ 17 | email: string; 18 | /** 19 | * The picture of the user 20 | */ 21 | profile_picture: string; 22 | } 23 | 24 | export interface KickOptions extends ProviderOptions<KickProfile> { 25 | clientId: string; 26 | } 27 | 28 | export const kick = (options: KickOptions) => { 29 | return { 30 | id: "kick", 31 | name: "Kick", 32 | createAuthorizationURL({ state, scopes, redirectURI, codeVerifier }) { 33 | const _scopes = options.disableDefaultScope ? [] : ["user:read"]; 34 | options.scope && _scopes.push(...options.scope); 35 | scopes && _scopes.push(...scopes); 36 | 37 | return createAuthorizationURL({ 38 | id: "kick", 39 | redirectURI, 40 | options, 41 | authorizationEndpoint: "https://id.kick.com/oauth/authorize", 42 | scopes: _scopes, 43 | codeVerifier, 44 | state, 45 | }); 46 | }, 47 | async validateAuthorizationCode({ code, redirectURI, codeVerifier }) { 48 | return validateAuthorizationCode({ 49 | code, 50 | redirectURI, 51 | options, 52 | tokenEndpoint: "https://id.kick.com/oauth/token", 53 | codeVerifier, 54 | }); 55 | }, 56 | async getUserInfo(token) { 57 | if (options.getUserInfo) { 58 | return options.getUserInfo(token); 59 | } 60 | 61 | const { data, error } = await betterFetch<{ 62 | data: KickProfile[]; 63 | }>("https://api.kick.com/public/v1/users", { 64 | method: "GET", 65 | headers: { 66 | Authorization: `Bearer ${token.accessToken}`, 67 | }, 68 | }); 69 | 70 | if (error) { 71 | return null; 72 | } 73 | 74 | const profile = data.data[0]!; 75 | 76 | const userMap = await options.mapProfileToUser?.(profile); 77 | 78 | return { 79 | user: { 80 | id: profile.user_id, 81 | name: profile.name, 82 | email: profile.email, 83 | image: profile.profile_picture, 84 | emailVerified: true, 85 | ...userMap, 86 | }, 87 | data: profile, 88 | }; 89 | }, 90 | options, 91 | } satisfies OAuthProvider<KickProfile>; 92 | }; 93 | ``` -------------------------------------------------------------------------------- /e2e/integration/vanilla-node/e2e/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Page } from "@playwright/test"; 2 | import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; 3 | import { fileURLToPath } from "node:url"; 4 | import { createAuthServer } from "./app"; 5 | import { terminate } from "@better-auth/test-utils/playwright"; 6 | 7 | const root = fileURLToPath(new URL("../", import.meta.url)); 8 | 9 | export async function runClient<R>( 10 | page: Page, 11 | fn: ({ client }: { client: Window["client"] }) => R, 12 | ): Promise<R> { 13 | const client = await page.evaluateHandle<Window["client"]>("window.client"); 14 | return page.evaluate(fn, { client }); 15 | } 16 | 17 | export function setup() { 18 | let server: Awaited<ReturnType<typeof createAuthServer>>; 19 | let clientChild: ChildProcessWithoutNullStreams; 20 | const ref: { 21 | clientPort: number; 22 | serverPort: number; 23 | } = { 24 | clientPort: -1, 25 | serverPort: -1, 26 | }; 27 | return { 28 | ref, 29 | start: async () => { 30 | server = await createAuthServer(); 31 | clientChild = spawn("pnpm", ["run", "start:client"], { 32 | cwd: root, 33 | stdio: "pipe", 34 | }); 35 | clientChild.stderr.on("data", (data) => { 36 | const message = data.toString(); 37 | console.error(message); 38 | }); 39 | clientChild.stdout.on("data", (data) => { 40 | const message = data.toString(); 41 | console.log(message); 42 | }); 43 | 44 | await Promise.all([ 45 | new Promise<void>((resolve) => { 46 | server.listen(0, "0.0.0.0", () => { 47 | const address = server.address(); 48 | if (address && typeof address === "object") { 49 | ref.serverPort = address.port; 50 | resolve(); 51 | } 52 | }); 53 | }), 54 | new Promise<void>((resolve) => { 55 | clientChild.stdout.on("data", (data) => { 56 | const message = data.toString(); 57 | // find: http://localhost:5173/ 58 | if (message.includes("http://localhost:")) { 59 | const port: string = message 60 | .split("http://localhost:")[1] 61 | .split("/")[0] 62 | .trim(); 63 | ref.clientPort = Number(port.replace(/\x1b\[[0-9;]*m/g, "")); 64 | resolve(); 65 | } 66 | }); 67 | }), 68 | ]); 69 | }, 70 | clean: async () => { 71 | await terminate(clientChild.pid!); 72 | server.close(); 73 | }, 74 | }; 75 | } 76 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/crypto/password.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from "vitest"; 2 | import { hashPassword, verifyPassword } from "./password"; 3 | 4 | describe("Password hashing and verification", () => { 5 | it("should hash a password", async () => { 6 | const password = "mySecurePassword123!"; 7 | const hash = await hashPassword(password); 8 | expect(hash).toBeTruthy(); 9 | expect(hash.split(":").length).toBe(2); 10 | }); 11 | 12 | it("should verify a correct password", async () => { 13 | const password = "correctPassword123!"; 14 | const hash = await hashPassword(password); 15 | const isValid = await verifyPassword({ hash, password }); 16 | expect(isValid).toBe(true); 17 | }); 18 | 19 | it("should reject an incorrect password", async () => { 20 | const correctPassword = "correctPassword123!"; 21 | const incorrectPassword = "wrongPassword456!"; 22 | const hash = await hashPassword(correctPassword); 23 | const isValid = await verifyPassword({ hash, password: incorrectPassword }); 24 | expect(isValid).toBe(false); 25 | }); 26 | 27 | it("should generate different hashes for the same password", async () => { 28 | const password = "samePassword123!"; 29 | const hash1 = await hashPassword(password); 30 | const hash2 = await hashPassword(password); 31 | expect(hash1).not.toBe(hash2); 32 | }); 33 | 34 | it("should handle long passwords", async () => { 35 | const password = "a".repeat(1000); 36 | const hash = await hashPassword(password); 37 | const isValid = await verifyPassword({ hash, password }); 38 | expect(isValid).toBe(true); 39 | }); 40 | 41 | it("should be case-sensitive", async () => { 42 | const password = "CaseSensitivePassword123!"; 43 | const hash = await hashPassword(password); 44 | const isValidLower = await verifyPassword({ 45 | hash, 46 | password: password.toLowerCase(), 47 | }); 48 | const isValidUpper = await verifyPassword({ 49 | hash, 50 | password: password.toUpperCase(), 51 | }); 52 | expect(isValidLower).toBe(false); 53 | expect(isValidUpper).toBe(false); 54 | }); 55 | 56 | it("should handle Unicode characters", async () => { 57 | const password = "пароль123!"; 58 | const hash = await hashPassword(password); 59 | const isValid = await verifyPassword({ hash, password }); 60 | expect(isValid).toBe(true); 61 | }); 62 | }); 63 | ``` -------------------------------------------------------------------------------- /demo/nextjs/public/logo.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect width="200" height="200" fill="#D9D9D9"/> 3 | <line x1="21.5" y1="2.18557e-08" x2="21.5" y2="200" stroke="#C4C4C4"/> 4 | <line x1="173.5" y1="2.18557e-08" x2="173.5" y2="200" stroke="#C4C4C4"/> 5 | <line x1="200" y1="176.5" x2="-4.37114e-08" y2="176.5" stroke="#C4C4C4"/> 6 | <line x1="200" y1="24.5" x2="-4.37114e-08" y2="24.5" stroke="#C4C4C4"/> 7 | <path d="M64.4545 135V65.1818H88.8636C93.7273 65.1818 97.7386 66.0227 100.898 67.7045C104.057 69.3636 106.409 71.6023 107.955 74.4205C109.5 77.2159 110.273 80.3182 110.273 83.7273C110.273 86.7273 109.739 89.2045 108.67 91.1591C107.625 93.1136 106.239 94.6591 104.511 95.7955C102.807 96.9318 100.955 97.7727 98.9545 98.3182V99C101.091 99.1364 103.239 99.8864 105.398 101.25C107.557 102.614 109.364 104.568 110.818 107.114C112.273 109.659 113 112.773 113 116.455C113 119.955 112.205 123.102 110.614 125.898C109.023 128.693 106.511 130.909 103.08 132.545C99.6477 134.182 95.1818 135 89.6818 135H64.4545ZM72.9091 127.5H89.6818C95.2045 127.5 99.125 126.432 101.443 124.295C103.784 122.136 104.955 119.523 104.955 116.455C104.955 114.091 104.352 111.909 103.148 109.909C101.943 107.886 100.227 106.273 98 105.068C95.7727 103.841 93.1364 103.227 90.0909 103.227H72.9091V127.5ZM72.9091 95.8636H88.5909C91.1364 95.8636 93.4318 95.3636 95.4773 94.3636C97.5455 93.3636 99.1818 91.9545 100.386 90.1364C101.614 88.3182 102.227 86.1818 102.227 83.7273C102.227 80.6591 101.159 78.0568 99.0227 75.9205C96.8864 73.7614 93.5 72.6818 88.8636 72.6818H72.9091V95.8636ZM131.665 135.545C129.983 135.545 128.54 134.943 127.335 133.739C126.131 132.534 125.528 131.091 125.528 129.409C125.528 127.727 126.131 126.284 127.335 125.08C128.54 123.875 129.983 123.273 131.665 123.273C133.347 123.273 134.79 123.875 135.994 125.08C137.199 126.284 137.801 127.727 137.801 129.409C137.801 130.523 137.517 131.545 136.949 132.477C136.403 133.409 135.665 134.159 134.733 134.727C133.824 135.273 132.801 135.545 131.665 135.545Z" fill="#302208"/> 8 | </svg> 9 | ``` -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect width="200" height="200" fill="#D9D9D9"/> 3 | <line x1="21.5" y1="2.18557e-08" x2="21.5" y2="200" stroke="#C4C4C4"/> 4 | <line x1="173.5" y1="2.18557e-08" x2="173.5" y2="200" stroke="#C4C4C4"/> 5 | <line x1="200" y1="176.5" x2="-4.37114e-08" y2="176.5" stroke="#C4C4C4"/> 6 | <line x1="200" y1="24.5" x2="-4.37114e-08" y2="24.5" stroke="#C4C4C4"/> 7 | <path d="M64.4545 135V65.1818H88.8636C93.7273 65.1818 97.7386 66.0227 100.898 67.7045C104.057 69.3636 106.409 71.6023 107.955 74.4205C109.5 77.2159 110.273 80.3182 110.273 83.7273C110.273 86.7273 109.739 89.2045 108.67 91.1591C107.625 93.1136 106.239 94.6591 104.511 95.7955C102.807 96.9318 100.955 97.7727 98.9545 98.3182V99C101.091 99.1364 103.239 99.8864 105.398 101.25C107.557 102.614 109.364 104.568 110.818 107.114C112.273 109.659 113 112.773 113 116.455C113 119.955 112.205 123.102 110.614 125.898C109.023 128.693 106.511 130.909 103.08 132.545C99.6477 134.182 95.1818 135 89.6818 135H64.4545ZM72.9091 127.5H89.6818C95.2045 127.5 99.125 126.432 101.443 124.295C103.784 122.136 104.955 119.523 104.955 116.455C104.955 114.091 104.352 111.909 103.148 109.909C101.943 107.886 100.227 106.273 98 105.068C95.7727 103.841 93.1364 103.227 90.0909 103.227H72.9091V127.5ZM72.9091 95.8636H88.5909C91.1364 95.8636 93.4318 95.3636 95.4773 94.3636C97.5455 93.3636 99.1818 91.9545 100.386 90.1364C101.614 88.3182 102.227 86.1818 102.227 83.7273C102.227 80.6591 101.159 78.0568 99.0227 75.9205C96.8864 73.7614 93.5 72.6818 88.8636 72.6818H72.9091V95.8636ZM131.665 135.545C129.983 135.545 128.54 134.943 127.335 133.739C126.131 132.534 125.528 131.091 125.528 129.409C125.528 127.727 126.131 126.284 127.335 125.08C128.54 123.875 129.983 123.273 131.665 123.273C133.347 123.273 134.79 123.875 135.994 125.08C137.199 126.284 137.801 127.727 137.801 129.409C137.801 130.523 137.517 131.545 136.949 132.477C136.403 133.409 135.665 134.159 134.733 134.727C133.824 135.273 132.801 135.545 131.665 135.545Z" fill="#302208"/> 8 | </svg> 9 | ``` -------------------------------------------------------------------------------- /e2e/smoke/test/fixtures/cloudflare/test/index.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SELF } from "cloudflare:test"; 2 | import { describe, it, expect } from "vitest"; 3 | 4 | describe("Cloudflare Worker compatibly basic tests", () => { 5 | const randomEmail = `${crypto.randomUUID()}@test.com`; 6 | const randomUserName = crypto.randomUUID().replaceAll("-", "").slice(6); 7 | const randomPassword = crypto.randomUUID(); 8 | 9 | it("can sign up and login", async () => { 10 | // Sign Up 11 | let response = await SELF.fetch( 12 | "http://localhost:8787/api/auth/sign-up/email", 13 | { 14 | method: "POST", 15 | body: JSON.stringify({ 16 | email: randomEmail, 17 | password: randomPassword, 18 | name: randomUserName, 19 | }), 20 | headers: { 21 | "content-type": "application/json", 22 | }, 23 | }, 24 | ); 25 | expect(response.status).toBe(200); 26 | 27 | // Login with correct password 28 | response = await SELF.fetch( 29 | "http://localhost:8787/api/auth/sign-in/email", 30 | { 31 | method: "POST", 32 | body: JSON.stringify({ 33 | email: randomEmail, 34 | password: randomPassword, 35 | }), 36 | headers: { 37 | "content-type": "application/json", 38 | }, 39 | }, 40 | ); 41 | 42 | expect(response.status).toBe(200); 43 | expect(response.headers.get("set-cookie")).toContain("better-auth.session"); 44 | 45 | const token = response.headers.get("set-cookie")?.split(";")[0]; 46 | expect(token).toBeDefined(); 47 | 48 | // Get Auth Status 49 | response = await SELF.fetch("http://localhost:8787/", { 50 | headers: { 51 | Cookie: token!, 52 | }, 53 | }); 54 | expect(response.status).toBe(200); 55 | expect(await response.text()!).toBe(`Hello ${randomUserName}`); 56 | 57 | // Try login with wrong password 58 | response = await SELF.fetch( 59 | "http://localhost:8787/api/auth/sign-in/email", 60 | { 61 | method: "POST", 62 | body: JSON.stringify({ 63 | email: randomEmail, 64 | // wrong password 65 | password: crypto.randomUUID(), 66 | }), 67 | headers: { 68 | "content-type": "application/json", 69 | }, 70 | }, 71 | ); 72 | expect(response.status).toBe(401); 73 | expect(response.headers.get("set-cookie")).toBeNull(); 74 | 75 | response = await SELF.fetch("http://localhost:8787/"); 76 | expect(response.status).toBe(200); 77 | expect(await response.text()).toBe("Not logged in"); 78 | }); 79 | }); 80 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/roblox.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Roblox 3 | description: Roblox provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Roblox Credentials 9 | Get your Roblox credentials from the [Roblox Creator Hub](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/roblox` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | 13 | <Callout type="info"> 14 | The Roblox API does not provide email addresses. As a workaround, the user's `email` field uses the `preferred_username` value instead. 15 | </Callout> 16 | </Step> 17 | 18 | <Step> 19 | ### Configure the provider 20 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 21 | 22 | ```ts title="auth.ts" 23 | import { betterAuth } from "better-auth" 24 | 25 | export const auth = betterAuth({ 26 | socialProviders: { 27 | roblox: { // [!code highlight] 28 | clientId: process.env.ROBLOX_CLIENT_ID as string, // [!code highlight] 29 | clientSecret: process.env.ROBLOX_CLIENT_SECRET as string, // [!code highlight] 30 | }, // [!code highlight] 31 | }, 32 | }) 33 | ``` 34 | </Step> 35 | <Step> 36 | ### Sign In with Roblox 37 | To sign in with Roblox, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 38 | - `provider`: The provider to use. It should be set to `roblox`. 39 | 40 | ```ts title="auth-client.ts" 41 | import { createAuthClient } from "better-auth/client" 42 | const authClient = createAuthClient() 43 | 44 | const signIn = async () => { 45 | const data = await authClient.signIn.social({ 46 | provider: "roblox" 47 | }) 48 | } 49 | ``` 50 | </Step> 51 | </Steps> 52 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/other-relational-databases.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Other Relational Databases 3 | description: Integrate Better Auth with other relational databases. 4 | --- 5 | 6 | Better Auth supports a wide range of database dialects out of the box thanks to <Link href="https://kysely.dev/">Kysely</Link>. 7 | 8 | Any dialect supported by Kysely can be utilized with Better Auth, including capabilities for generating and migrating database schemas through the <Link href="/docs/concepts/cli">CLI</Link>. 9 | 10 | ## Core Dialects 11 | 12 | - [MySQL](/docs/adapters/mysql) 13 | - [SQLite](/docs/adapters/sqlite) 14 | - [PostgreSQL](/docs/adapters/postgresql) 15 | - [MS SQL](/docs/adapters/mssql) 16 | 17 | ## Kysely Organization Dialects 18 | 19 | - [Postgres.js](https://github.com/kysely-org/kysely-postgres-js) 20 | - [SingleStore Data API](https://github.com/kysely-org/kysely-singlestore) 21 | - [Supabase](https://github.com/kysely-org/kysely-supabase) 22 | 23 | ## Kysely Community dialects 24 | 25 | - [PlanetScale Serverless Driver](https://github.com/depot/kysely-planetscale) 26 | - [Cloudflare D1](https://github.com/aidenwallis/kysely-d1) 27 | - [AWS RDS Data API](https://github.com/serverless-stack/kysely-data-api) 28 | - [SurrealDB](https://github.com/igalklebanov/kysely-surrealdb) 29 | - [Neon](https://github.com/seveibar/kysely-neon) 30 | - [Xata](https://github.com/xataio/client-ts/tree/main/packages/plugin-client-kysely) 31 | - [AWS S3 Select](https://github.com/igalklebanov/kysely-s3-select) 32 | - [libSQL/sqld](https://github.com/libsql/kysely-libsql) 33 | - [Fetch driver](https://github.com/andersgee/kysely-fetch-driver) 34 | - [SQLite WASM](https://github.com/DallasHoff/sqlocal) 35 | - [Deno SQLite](https://gitlab.com/soapbox-pub/kysely-deno-sqlite) 36 | - [TiDB Cloud Serverless Driver](https://github.com/tidbcloud/kysely) 37 | - [Capacitor SQLite Kysely](https://github.com/DawidWetzler/capacitor-sqlite-kysely) 38 | - [BigQuery](https://github.com/maktouch/kysely-bigquery) 39 | - [Clickhouse](https://github.com/founderpathcom/kysely-clickhouse) 40 | - [PGLite](https://github.com/czeidler/kysely-pglite-dialect) 41 | 42 | <Callout> 43 | You can see the full list of supported Kysely dialects{" "} 44 | <Link href="https://kysely.dev/docs/dialects">here</Link>. 45 | </Callout> 46 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/access/access.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { createAccessControl } from "./access"; 3 | 4 | describe("access", () => { 5 | const ac = createAccessControl({ 6 | project: ["create", "update", "delete", "delete-many"], 7 | ui: ["view", "edit", "comment", "hide"], 8 | }); 9 | 10 | const role1 = ac.newRole({ 11 | project: ["create", "update", "delete"], 12 | ui: ["view", "edit", "comment"], 13 | }); 14 | 15 | it("should validate permissions", async () => { 16 | const response = role1.authorize({ 17 | project: ["create"], 18 | }); 19 | expect(response.success).toBe(true); 20 | 21 | const failedResponse = role1.authorize({ 22 | project: ["delete-many"], 23 | }); 24 | expect(failedResponse.success).toBe(false); 25 | }); 26 | 27 | it("should validate multiple resource permissions", async () => { 28 | const response = role1.authorize({ 29 | project: ["create"], 30 | ui: ["view"], 31 | }); 32 | expect(response.success).toBe(true); 33 | 34 | const failedResponse = role1.authorize({ 35 | project: ["delete-many"], 36 | ui: ["view"], 37 | }); 38 | expect(failedResponse.success).toBe(false); 39 | }); 40 | 41 | it("should validate multiple resource multiple permissions", async () => { 42 | const response = role1.authorize({ 43 | project: ["create", "delete"], 44 | ui: ["view", "edit"], 45 | }); 46 | expect(response.success).toBe(true); 47 | const failedResponse = role1.authorize({ 48 | project: ["create", "delete-many"], 49 | ui: ["view", "edit"], 50 | }); 51 | expect(failedResponse.success).toBe(false); 52 | }); 53 | 54 | it("should validate using or connector", () => { 55 | const response = role1.authorize( 56 | { 57 | project: ["create", "delete-many"], 58 | ui: ["view", "edit"], 59 | }, 60 | "OR", 61 | ); 62 | expect(response.success).toBe(true); 63 | }); 64 | 65 | it("should validate using or connector for a specific resource", () => { 66 | const response = role1.authorize({ 67 | project: { 68 | connector: "OR", 69 | actions: ["create", "delete-many"], 70 | }, 71 | ui: ["view", "edit"], 72 | }); 73 | expect(response.success).toBe(true); 74 | 75 | const failedResponse = role1.authorize({ 76 | project: { 77 | connector: "OR", 78 | actions: ["create", "delete-many"], 79 | }, 80 | ui: ["view", "edit", "hide"], 81 | }); 82 | expect(failedResponse.success).toBe(false); 83 | }); 84 | }); 85 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/twitter.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Twitter (X) 3 | description: Twitter provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Twitter Credentials 9 | Get your Twitter credentials from the [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/twitter` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | 13 | <Callout type="info"> 14 | Twitter API v2 now supports email address retrieval. Make sure to request the `user.email` scope when configuring your Twitter app to enable this feature. 15 | </Callout> 16 | </Step> 17 | 18 | <Step> 19 | ### Configure the provider 20 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 21 | 22 | ```ts title="auth.ts" 23 | import { betterAuth } from "better-auth" 24 | 25 | export const auth = betterAuth({ 26 | socialProviders: { 27 | twitter: { // [!code highlight] 28 | clientId: process.env.TWITTER_CLIENT_ID as string, // [!code highlight] 29 | clientSecret: process.env.TWITTER_CLIENT_SECRET as string, // [!code highlight] 30 | }, // [!code highlight] 31 | }, 32 | }) 33 | ``` 34 | </Step> 35 | <Step> 36 | ### Sign In with Twitter 37 | To sign in with Twitter, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 38 | - `provider`: The provider to use. It should be set to `twitter`. 39 | 40 | ```ts title="auth-client.ts" 41 | import { createAuthClient } from "better-auth/client" 42 | const authClient = createAuthClient() 43 | 44 | const signIn = async () => { 45 | const data = await authClient.signIn.social({ 46 | provider: "twitter" 47 | }) 48 | } 49 | ``` 50 | </Step> 51 | </Steps> 52 | ``` -------------------------------------------------------------------------------- /packages/cli/test/migrate.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { migrateAction } from "../src/commands/migrate"; 3 | import * as config from "../src/utils/get-config"; 4 | import { betterAuth, type BetterAuthPlugin } from "better-auth"; 5 | import Database from "better-sqlite3"; 6 | 7 | describe("migrate base auth instance", () => { 8 | const db = new Database(":memory:"); 9 | 10 | const auth = betterAuth({ 11 | baseURL: "http://localhost:3000", 12 | database: db, 13 | emailAndPassword: { 14 | enabled: true, 15 | }, 16 | }); 17 | 18 | beforeEach(() => { 19 | vi.spyOn(process, "exit").mockImplementation((code) => { 20 | return code as never; 21 | }); 22 | vi.spyOn(config, "getConfig").mockImplementation(async () => auth.options); 23 | }); 24 | 25 | afterEach(async () => { 26 | vi.restoreAllMocks(); 27 | }); 28 | 29 | it("should migrate the database and sign-up a user", async () => { 30 | await migrateAction({ 31 | cwd: process.cwd(), 32 | config: "test/auth.ts", 33 | y: true, 34 | }); 35 | const signUpRes = await auth.api.signUpEmail({ 36 | body: { 37 | name: "test", 38 | email: "[email protected]", 39 | password: "password", 40 | }, 41 | }); 42 | expect(signUpRes.token).toBeDefined(); 43 | }); 44 | }); 45 | 46 | describe("migrate auth instance with plugins", () => { 47 | const db = new Database(":memory:"); 48 | const testPlugin = { 49 | id: "plugin", 50 | schema: { 51 | plugin: { 52 | fields: { 53 | test: { 54 | type: "string", 55 | fieldName: "test", 56 | }, 57 | }, 58 | }, 59 | }, 60 | } satisfies BetterAuthPlugin; 61 | 62 | const auth = betterAuth({ 63 | baseURL: "http://localhost:3000", 64 | database: db, 65 | emailAndPassword: { 66 | enabled: true, 67 | }, 68 | plugins: [testPlugin], 69 | }); 70 | 71 | beforeEach(() => { 72 | vi.spyOn(process, "exit").mockImplementation((code) => { 73 | return code as never; 74 | }); 75 | vi.spyOn(config, "getConfig").mockImplementation(async () => auth.options); 76 | }); 77 | 78 | afterEach(async () => { 79 | vi.restoreAllMocks(); 80 | }); 81 | 82 | it("should migrate the database and sign-up a user", async () => { 83 | await migrateAction({ 84 | cwd: process.cwd(), 85 | config: "test/auth.ts", 86 | y: true, 87 | }); 88 | const res = db 89 | .prepare("INSERT INTO plugin (id, test) VALUES (?, ?)") 90 | .run("1", "test"); 91 | expect(res.changes).toBe(1); 92 | }); 93 | }); 94 | ``` -------------------------------------------------------------------------------- /docs/components/promo-card.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Card, CardContent, CardFooter } from "@/components/ui/card"; 6 | import { Progress } from "@/components/ui/progress"; 7 | import { Badge } from "@/components/ui/badge"; 8 | import { 9 | Tooltip, 10 | TooltipContent, 11 | TooltipProvider, 12 | TooltipTrigger, 13 | } from "@/components/ui/tooltip"; 14 | import { Sparkles, Clock, ArrowRight } from "lucide-react"; 15 | 16 | export default function PromoCard() { 17 | const [isHovered, setIsHovered] = useState(false); 18 | 19 | return ( 20 | <TooltipProvider> 21 | <Card 22 | className="w-full overflow-hidden bg-gradient-to-br from-purple-500 to-indigo-600 text-white" 23 | onMouseEnter={() => setIsHovered(true)} 24 | onMouseLeave={() => setIsHovered(false)} 25 | > 26 | <CardContent className="p-6 pb-0"> 27 | <div className="flex justify-between items-start mb-4"> 28 | <Badge className="bg-yellow-500 text-black hover:bg-yellow-600"> 29 | New 30 | </Badge> 31 | <Tooltip> 32 | <TooltipTrigger> 33 | <Clock className="h-5 w-5 text-white/80" /> 34 | </TooltipTrigger> 35 | <TooltipContent> 36 | <p>Limited time offer</p> 37 | </TooltipContent> 38 | </Tooltip> 39 | </div> 40 | <h3 className="text-2xl font-bold mb-2">Unlock Pro Features</h3> 41 | <p className="text-sm text-white/80 mb-4"> 42 | Supercharge your workflow with our advanced tools and exclusive 43 | content. 44 | </p> 45 | <div className="relative"> 46 | <Progress value={67} className="h-2 mb-2" /> 47 | <span className="text-xs text-white/80">67% of slots filled</span> 48 | </div> 49 | </CardContent> 50 | <CardFooter className="p-6 pt-4"> 51 | <Button 52 | className={`w-full bg-white text-purple-600 hover:bg-white/90 transition-all duration-300 ${ 53 | isHovered ? "translate-y-[-2px] shadow-lg" : "" 54 | }`} 55 | > 56 | <span className="mr-2">Upgrade Now</span> 57 | <Sparkles className="h-4 w-4 mr-2" /> 58 | <ArrowRight 59 | className={`h-4 w-4 transition-transform duration-300 ${ 60 | isHovered ? "translate-x-1" : "" 61 | }`} 62 | /> 63 | </Button> 64 | </CardFooter> 65 | </Card> 66 | </TooltipProvider> 67 | ); 68 | } 69 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/dropbox.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Dropbox 3 | description: Dropbox provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Dropbox credentials 9 | To use Dropbox sign in, you need a client ID and client secret. You can get them from the [Dropbox Developer Portal](https://www.dropbox.com/developers). You can Allow "Implicit Grant & PKCE" for the application in the App Console. 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/dropbox` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | If you need deeper dive into Dropbox Authentication, you can check out the [official documentation](https://developers.dropbox.com/oauth-guide). 15 | 16 | <Step> 17 | ### Configure the provider 18 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 19 | 20 | ```ts title="auth.ts" 21 | import { betterAuth } from "better-auth" 22 | 23 | export const auth = betterAuth({ 24 | socialProviders: { 25 | dropbox: { // [!code highlight] 26 | clientId: process.env.DROPBOX_CLIENT_ID as string, // [!code highlight] 27 | clientSecret: process.env.DROPBOX_CLIENT_SECRET as string, // [!code highlight] 28 | }, // [!code highlight] 29 | }, 30 | }) 31 | ``` 32 | </Step> 33 | <Step> 34 | ### Sign In with Dropbox 35 | To sign in with Dropbox, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 36 | - `provider`: The provider to use. It should be set to `dropbox`. 37 | 38 | ```ts title="auth-client.ts" 39 | import { createAuthClient } from "better-auth/client" 40 | const authClient = createAuthClient() 41 | 42 | const signIn = async () => { 43 | const data = await authClient.signIn.social({ 44 | provider: "dropbox" 45 | }) 46 | } 47 | ``` 48 | </Step> 49 | 50 | </Steps> 51 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/haveibeenpwned/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { APIError } from "../../api"; 2 | import { createHash } from "@better-auth/utils/hash"; 3 | import { betterFetch } from "@better-fetch/fetch"; 4 | import type { BetterAuthPlugin } from "@better-auth/core"; 5 | import { defineErrorCodes } from "@better-auth/core/utils"; 6 | 7 | const ERROR_CODES = defineErrorCodes({ 8 | PASSWORD_COMPROMISED: 9 | "The password you entered has been compromised. Please choose a different password.", 10 | }); 11 | 12 | async function checkPasswordCompromise( 13 | password: string, 14 | customMessage?: string, 15 | ) { 16 | if (!password) return; 17 | 18 | const sha1Hash = ( 19 | await createHash("SHA-1", "hex").digest(password) 20 | ).toUpperCase(); 21 | const prefix = sha1Hash.substring(0, 5); 22 | const suffix = sha1Hash.substring(5); 23 | try { 24 | const { data, error } = await betterFetch<string>( 25 | `https://api.pwnedpasswords.com/range/${prefix}`, 26 | { 27 | headers: { 28 | "Add-Padding": "true", 29 | "User-Agent": "BetterAuth Password Checker", 30 | }, 31 | }, 32 | ); 33 | 34 | if (error) { 35 | throw new APIError("INTERNAL_SERVER_ERROR", { 36 | message: `Failed to check password. Status: ${error.status}`, 37 | }); 38 | } 39 | const lines = data.split("\n"); 40 | const found = lines.some( 41 | (line) => line.split(":")[0]!.toUpperCase() === suffix.toUpperCase(), 42 | ); 43 | 44 | if (found) { 45 | throw new APIError("BAD_REQUEST", { 46 | message: customMessage || ERROR_CODES.PASSWORD_COMPROMISED, 47 | code: "PASSWORD_COMPROMISED", 48 | }); 49 | } 50 | } catch (error) { 51 | if (error instanceof APIError) throw error; 52 | throw new APIError("INTERNAL_SERVER_ERROR", { 53 | message: "Failed to check password. Please try again later.", 54 | }); 55 | } 56 | } 57 | 58 | export interface HaveIBeenPwnedOptions { 59 | customPasswordCompromisedMessage?: string; 60 | } 61 | 62 | export const haveIBeenPwned = (options?: HaveIBeenPwnedOptions) => 63 | ({ 64 | id: "haveIBeenPwned", 65 | init(ctx) { 66 | return { 67 | context: { 68 | password: { 69 | ...ctx.password, 70 | async hash(password) { 71 | await checkPasswordCompromise( 72 | password, 73 | options?.customPasswordCompromisedMessage, 74 | ); 75 | return ctx.password.hash(password); 76 | }, 77 | }, 78 | }, 79 | }; 80 | }, 81 | $ERROR_CODES: ERROR_CODES, 82 | }) satisfies BetterAuthPlugin; 83 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/oidc-provider/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db"; 2 | 3 | export const schema = { 4 | oauthApplication: { 5 | modelName: "oauthApplication", 6 | fields: { 7 | name: { 8 | type: "string", 9 | }, 10 | icon: { 11 | type: "string", 12 | required: false, 13 | }, 14 | metadata: { 15 | type: "string", 16 | required: false, 17 | }, 18 | clientId: { 19 | type: "string", 20 | unique: true, 21 | }, 22 | clientSecret: { 23 | type: "string", 24 | required: false, 25 | }, 26 | redirectURLs: { 27 | type: "string", 28 | }, 29 | type: { 30 | type: "string", 31 | }, 32 | disabled: { 33 | type: "boolean", 34 | required: false, 35 | defaultValue: false, 36 | }, 37 | userId: { 38 | type: "string", 39 | required: false, 40 | references: { 41 | model: "user", 42 | field: "id", 43 | onDelete: "cascade", 44 | }, 45 | }, 46 | createdAt: { 47 | type: "date", 48 | }, 49 | updatedAt: { 50 | type: "date", 51 | }, 52 | }, 53 | }, 54 | oauthAccessToken: { 55 | modelName: "oauthAccessToken", 56 | fields: { 57 | accessToken: { 58 | type: "string", 59 | unique: true, 60 | }, 61 | refreshToken: { 62 | type: "string", 63 | unique: true, 64 | }, 65 | accessTokenExpiresAt: { 66 | type: "date", 67 | }, 68 | refreshTokenExpiresAt: { 69 | type: "date", 70 | }, 71 | clientId: { 72 | type: "string", 73 | references: { 74 | model: "oauthApplication", 75 | field: "clientId", 76 | onDelete: "cascade", 77 | }, 78 | }, 79 | userId: { 80 | type: "string", 81 | required: false, 82 | references: { 83 | model: "user", 84 | field: "id", 85 | onDelete: "cascade", 86 | }, 87 | }, 88 | scopes: { 89 | type: "string", 90 | }, 91 | createdAt: { 92 | type: "date", 93 | }, 94 | updatedAt: { 95 | type: "date", 96 | }, 97 | }, 98 | }, 99 | oauthConsent: { 100 | modelName: "oauthConsent", 101 | fields: { 102 | clientId: { 103 | type: "string", 104 | references: { 105 | model: "oauthApplication", 106 | field: "clientId", 107 | onDelete: "cascade", 108 | }, 109 | }, 110 | userId: { 111 | type: "string", 112 | references: { 113 | model: "user", 114 | field: "id", 115 | onDelete: "cascade", 116 | }, 117 | }, 118 | scopes: { 119 | type: "string", 120 | }, 121 | createdAt: { 122 | type: "date", 123 | }, 124 | updatedAt: { 125 | type: "date", 126 | }, 127 | consentGiven: { 128 | type: "boolean", 129 | }, 130 | }, 131 | }, 132 | } satisfies BetterAuthPluginDBSchema; 133 | ``` -------------------------------------------------------------------------------- /docs/components/ui/input-otp.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { OTPInput, OTPInputContext } from "input-otp"; 5 | import { MinusIcon } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function InputOTP({ 10 | className, 11 | containerClassName, 12 | ...props 13 | }: React.ComponentProps<typeof OTPInput> & { 14 | containerClassName?: string; 15 | }) { 16 | return ( 17 | <OTPInput 18 | data-slot="input-otp" 19 | containerClassName={cn( 20 | "flex items-center gap-2 has-disabled:opacity-50", 21 | containerClassName, 22 | )} 23 | className={cn("disabled:cursor-not-allowed", className)} 24 | {...props} 25 | /> 26 | ); 27 | } 28 | 29 | function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { 30 | return ( 31 | <div 32 | data-slot="input-otp-group" 33 | className={cn("flex items-center", className)} 34 | {...props} 35 | /> 36 | ); 37 | } 38 | 39 | function InputOTPSlot({ 40 | index, 41 | className, 42 | ...props 43 | }: React.ComponentProps<"div"> & { 44 | index: number; 45 | }) { 46 | const inputOTPContext = React.useContext(OTPInputContext); 47 | const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}; 48 | 49 | return ( 50 | <div 51 | data-slot="input-otp-slot" 52 | data-active={isActive} 53 | className={cn( 54 | "border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", 55 | className, 56 | )} 57 | {...props} 58 | > 59 | {char} 60 | {hasFakeCaret && ( 61 | <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> 62 | <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" /> 63 | </div> 64 | )} 65 | </div> 66 | ); 67 | } 68 | 69 | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { 70 | return ( 71 | <div data-slot="input-otp-separator" role="separator" {...props}> 72 | <MinusIcon /> 73 | </div> 74 | ); 75 | } 76 | 77 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; 78 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/has-permission.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defaultRoles } from "./access"; 2 | import type { Role } from "../access"; 3 | import * as z from "zod"; 4 | import { APIError } from "../../api"; 5 | import type { OrganizationRole } from "./schema"; 6 | import { 7 | cacheAllRoles, 8 | hasPermissionFn, 9 | type HasPermissionBaseInput, 10 | } from "./permission"; 11 | import type { GenericEndpointContext } from "@better-auth/core"; 12 | 13 | export const hasPermission = async ( 14 | input: { 15 | organizationId: string; 16 | /** 17 | * If true, will use the in-memory cache of the roles. 18 | * Keep in mind to use this in a stateless mindset, the purpose of this is to avoid unnecessary database calls when running multiple 19 | * hasPermission calls in a row. 20 | * 21 | * @default false 22 | */ 23 | useMemoryCache?: boolean; 24 | } & HasPermissionBaseInput, 25 | ctx: GenericEndpointContext, 26 | ) => { 27 | let acRoles: { 28 | [x: string]: Role<any> | undefined; 29 | } = { ...(input.options.roles || defaultRoles) }; 30 | 31 | if ( 32 | ctx && 33 | input.organizationId && 34 | input.options.dynamicAccessControl?.enabled && 35 | input.options.ac && 36 | !input.useMemoryCache 37 | ) { 38 | // Load roles from database 39 | const roles = await ctx.context.adapter.findMany< 40 | OrganizationRole & { permission: string } 41 | >({ 42 | model: "organizationRole", 43 | where: [ 44 | { 45 | field: "organizationId", 46 | value: input.organizationId, 47 | }, 48 | ], 49 | }); 50 | 51 | for (const { role, permission: permissionsString } of roles) { 52 | // If it's for an existing role, skip as we shouldn't override hard-coded roles. 53 | if (role in acRoles) continue; 54 | 55 | const result = z 56 | .record(z.string(), z.array(z.string())) 57 | .safeParse(JSON.parse(permissionsString)); 58 | 59 | if (!result.success) { 60 | ctx.context.logger.error( 61 | "[hasPermission] Invalid permissions for role " + role, 62 | { 63 | permissions: JSON.parse(permissionsString), 64 | }, 65 | ); 66 | throw new APIError("INTERNAL_SERVER_ERROR", { 67 | message: "Invalid permissions for role " + role, 68 | }); 69 | } 70 | 71 | acRoles[role] = input.options.ac.newRole(result.data); 72 | } 73 | } 74 | 75 | if (input.useMemoryCache) { 76 | acRoles = cacheAllRoles.get(input.organizationId) || acRoles; 77 | } 78 | cacheAllRoles.set(input.organizationId, acRoles); 79 | 80 | return hasPermissionFn(input, acRoles); 81 | }; 82 | ```