This is page 6 of 49. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── 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 -------------------------------------------------------------------------------- /packages/core/src/oauth2/client-credentials-token.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import { base64Url } from "@better-auth/utils/base64"; import type { OAuth2Tokens, ProviderOptions } from "./oauth-provider"; export function createClientCredentialsTokenRequest({ options, scope, authentication, resource, }: { options: ProviderOptions & { clientSecret: string }; scope?: string; authentication?: "basic" | "post"; resource?: string | string[]; }) { const body = new URLSearchParams(); const headers: Record<string, any> = { "content-type": "application/x-www-form-urlencoded", accept: "application/json", }; body.set("grant_type", "client_credentials"); scope && body.set("scope", scope); if (resource) { if (typeof resource === "string") { body.append("resource", resource); } else { for (const _resource of resource) { body.append("resource", _resource); } } } if (authentication === "basic") { const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId; const encodedCredentials = base64Url.encode( `${primaryClientId}:${options.clientSecret}`, ); headers["authorization"] = `Basic ${encodedCredentials}`; } else { const primaryClientId = Array.isArray(options.clientId) ? options.clientId[0] : options.clientId; body.set("client_id", primaryClientId); body.set("client_secret", options.clientSecret); } return { body, headers, }; } export async function clientCredentialsToken({ options, tokenEndpoint, scope, authentication, resource, }: { options: ProviderOptions & { clientSecret: string }; tokenEndpoint: string; scope: string; authentication?: "basic" | "post"; resource?: string | string[]; }): Promise<OAuth2Tokens> { const { body, headers } = createClientCredentialsTokenRequest({ options, scope, authentication, resource, }); const { data, error } = await betterFetch<{ access_token: string; expires_in?: number; token_type?: string; scope?: string; }>(tokenEndpoint, { method: "POST", body, headers, }); if (error) { throw error; } const tokens: OAuth2Tokens = { accessToken: data.access_token, tokenType: data.token_type, scopes: data.scope?.split(" "), }; if (data.expires_in) { const now = new Date(); tokens.accessTokenExpiresAt = new Date( now.getTime() + data.expires_in * 1000, ); } return tokens; } ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/icons.tsx: -------------------------------------------------------------------------------- ```typescript export function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> </svg> ); } export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> </svg> ); } export function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path fillRule="evenodd" clipRule="evenodd" d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" /> </svg> ); } export function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> </svg> ); } ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/icons.tsx: -------------------------------------------------------------------------------- ```typescript export function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M7 3.41a1 1 0 0 0-.668-.943L2.275 1.039a.987.987 0 0 0-.877.166c-.25.192-.398.493-.398.812V12.2c0 .454.296.853.725.977l3.948 1.365A1 1 0 0 0 7 13.596V3.41ZM9 13.596a1 1 0 0 0 1.327.946l3.948-1.365c.429-.124.725-.523.725-.977V2.017c0-.32-.147-.62-.398-.812a.987.987 0 0 0-.877-.166L9.668 2.467A1 1 0 0 0 9 3.41v10.186Z" /> </svg> ); } export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M8 .198a8 8 0 0 0-8 8 7.999 7.999 0 0 0 5.47 7.59c.4.076.547-.172.547-.384 0-.19-.007-.694-.01-1.36-2.226.482-2.695-1.074-2.695-1.074-.364-.923-.89-1.17-.89-1.17-.725-.496.056-.486.056-.486.803.056 1.225.824 1.225.824.714 1.224 1.873.87 2.33.666.072-.518.278-.87.507-1.07-1.777-.2-3.644-.888-3.644-3.954 0-.873.31-1.586.823-2.146-.09-.202-.36-1.016.07-2.118 0 0 .67-.214 2.2.82a7.67 7.67 0 0 1 2-.27 7.67 7.67 0 0 1 2 .27c1.52-1.034 2.19-.82 2.19-.82.43 1.102.16 1.916.08 2.118.51.56.82 1.273.82 2.146 0 3.074-1.87 3.75-3.65 3.947.28.24.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.14.46.55.38A7.972 7.972 0 0 0 16 8.199a8 8 0 0 0-8-8Z" /> </svg> ); } export function FeedIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path fillRule="evenodd" clipRule="evenodd" d="M2.5 3a.5.5 0 0 1 .5-.5h.5c5.523 0 10 4.477 10 10v.5a.5.5 0 0 1-.5.5h-.5a.5.5 0 0 1-.5-.5v-.5A8.5 8.5 0 0 0 3.5 4H3a.5.5 0 0 1-.5-.5V3Zm0 4.5A.5.5 0 0 1 3 7h.5A5.5 5.5 0 0 1 9 12.5v.5a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5v-.5a4 4 0 0 0-4-4H3a.5.5 0 0 1-.5-.5v-.5Zm0 5a1 1 0 1 1 2 0 1 1 0 0 1-2 0Z" /> </svg> ); } export function XIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" {...props}> <path d="M9.51762 6.77491L15.3459 0H13.9648L8.90409 5.88256L4.86212 0H0.200195L6.31244 8.89547L0.200195 16H1.58139L6.92562 9.78782L11.1942 16H15.8562L9.51728 6.77491H9.51762ZM7.62588 8.97384L7.00658 8.08805L2.07905 1.03974H4.20049L8.17706 6.72795L8.79636 7.61374L13.9654 15.0075H11.844L7.62588 8.97418V8.97384Z" /> </svg> ); } ``` -------------------------------------------------------------------------------- /docs/components/ui/calendar.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { DayPicker } from "react-day-picker"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; function Calendar({ className, classNames, showOutsideDays = true, ...props }: React.ComponentProps<typeof DayPicker>) { return ( <DayPicker showOutsideDays={showOutsideDays} className={cn("p-3", className)} classNames={{ months: "flex flex-col sm:flex-row gap-2", month: "flex flex-col gap-4", caption: "flex justify-center pt-1 relative items-center w-full", caption_label: "text-sm font-medium", nav: "flex items-center gap-1", nav_button: cn( buttonVariants({ variant: "outline" }), "size-7 bg-transparent p-0 opacity-50 hover:opacity-100", ), nav_button_previous: "absolute left-1", nav_button_next: "absolute right-1", table: "w-full border-collapse space-x-1", head_row: "flex", head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", row: "flex w-full mt-2", cell: cn( "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md", props.mode === "range" ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" : "[&:has([aria-selected])]:rounded-md", ), day: cn( buttonVariants({ variant: "ghost" }), "size-8 p-0 font-normal aria-selected:opacity-100", ), day_range_start: "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground", day_range_end: "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground", day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", day_today: "bg-accent text-accent-foreground", day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground", day_disabled: "text-muted-foreground opacity-50", day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", day_hidden: "invisible", ...classNames, }} {...props} /> ); } export { Calendar }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/captcha/index.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPlugin } from "@better-auth/core"; import type { CaptchaOptions } from "./types"; import { defaultEndpoints, Providers, siteVerifyMap } from "./constants"; import { EXTERNAL_ERROR_CODES, INTERNAL_ERROR_CODES } from "./error-codes"; import { middlewareResponse } from "../../utils/middleware-response"; import * as verifyHandlers from "./verify-handlers"; export const captcha = (options: CaptchaOptions) => ({ id: "captcha", onRequest: async (request, ctx) => { try { const endpoints = options.endpoints?.length ? options.endpoints : defaultEndpoints; if (!endpoints.some((endpoint) => request.url.includes(endpoint))) return undefined; if (!options.secretKey) { throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY); } const captchaResponse = request.headers.get("x-captcha-response"); const remoteUserIP = request.headers.get("x-captcha-user-remote-ip") ?? undefined; if (!captchaResponse) { return middlewareResponse({ message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE, status: 400, }); } const siteVerifyURL = options.siteVerifyURLOverride || siteVerifyMap[options.provider]; const handlerParams = { siteVerifyURL, captchaResponse, secretKey: options.secretKey, remoteIP: remoteUserIP, }; if (options.provider === Providers.CLOUDFLARE_TURNSTILE) { return await verifyHandlers.cloudflareTurnstile(handlerParams); } if (options.provider === Providers.GOOGLE_RECAPTCHA) { return await verifyHandlers.googleRecaptcha({ ...handlerParams, minScore: options.minScore, }); } if (options.provider === Providers.HCAPTCHA) { return await verifyHandlers.hCaptcha({ ...handlerParams, siteKey: options.siteKey, }); } if (options.provider === Providers.CAPTCHAFOX) { return await verifyHandlers.captchaFox({ ...handlerParams, siteKey: options.siteKey, }); } } catch (_error) { const errorMessage = _error instanceof Error ? _error.message : undefined; ctx.logger.error(errorMessage ?? "Unknown error", { endpoint: request.url, message: _error, }); return middlewareResponse({ message: EXTERNAL_ERROR_CODES.UNKNOWN_ERROR, status: 500, }); } }, }) satisfies BetterAuthPlugin; ``` -------------------------------------------------------------------------------- /docs/components/api-method-tabs.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { cn } from "@/lib/utils"; const provider = React.createContext<{ current: string | null; setCurrent: (value: string | null) => void; }>({ current: null, setCurrent: () => {}, }); function ApiMethodTabs({ className, ...props }: React.ComponentProps<"div"> & { defaultValue: string | null }) { const [current, setCurrent] = React.useState<string | null>( props.defaultValue || null, ); return ( <provider.Provider value={{ current, setCurrent }}> <div data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} /> </provider.Provider> ); } const useApiMethodTabs = () => { return React.useContext(provider); }; function ApiMethodTabsList({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="tabs-list" className={cn( "inline-flex justify-center items-center p-1 h-9 rounded-lg bg-muted text-muted-foreground w-fit", className, )} {...props} /> ); } function ApiMethodTabsTrigger({ className, ...props }: React.ComponentProps<"button"> & { value: string }) { const { setCurrent, current } = useApiMethodTabs(); return ( <button data-slot="tabs-trigger" className={cn( "data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} data-state={props.value === current ? "active" : "inactive"} onClick={() => { setCurrent(props.value); }} {...props} /> ); } function ApiMethodTabsContent({ className, ...props }: React.ComponentProps<"div"> & { value: string }) { const { current } = useApiMethodTabs(); return ( <div data-slot="tabs-content" className={cn( "flex-1 outline-none", className, props.value === current && "block", props.value !== current && "hidden", )} {...props} /> ); } export { ApiMethodTabs, ApiMethodTabsList, ApiMethodTabsTrigger, ApiMethodTabsContent, }; ``` -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- ```typescript import { Navbar } from "@/components/nav-bar"; import "./global.css"; import { RootProvider } from "fumadocs-ui/provider"; import type { ReactNode } from "react"; import { NavbarProvider } from "@/components/nav-mobile"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; import { baseUrl, createMetadata } from "@/lib/metadata"; import { Analytics } from "@vercel/analytics/react"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { CustomSearchDialog } from "@/components/search-dialog"; import { AnchorScroll } from "@/components/anchor-scroll-fix"; export const metadata = createMetadata({ title: { template: "%s | Better Auth", default: "Better Auth", }, description: "The most comprehensive authentication library for TypeScript.", metadataBase: baseUrl, }); export default function Layout({ children }: { children: ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <link rel="icon" href="/favicon/favicon.ico" sizes="any" /> <script dangerouslySetInnerHTML={{ __html: ` try { if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.querySelector('meta[name="theme-color"]').setAttribute('content') } } catch (_) {} `, }} /> </head> <body className={`${GeistSans.variable} ${GeistMono.variable} bg-background font-sans relative `} > <ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange > <RootProvider theme={{ enableSystem: true, defaultTheme: "dark", }} search={{ enabled: true, SearchDialog: process.env.ORAMA_PRIVATE_API_KEY ? CustomSearchDialog : undefined, }} > <AnchorScroll /> <NavbarProvider> <Navbar /> {children} <Toaster toastOptions={{ style: { borderRadius: "0px", fontSize: "11px", }, }} /> </NavbarProvider> </RootProvider> <Analytics /> </ThemeProvider> </body> </html> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/next-js.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPlugin } from "@better-auth/core"; import { parseSetCookieHeader } from "../cookies"; import { createAuthMiddleware } from "@better-auth/core/api"; export function toNextJsHandler( auth: | { handler: (request: Request) => Promise<Response>; } | ((request: Request) => Promise<Response>), ) { const handler = async (request: Request) => { return "handler" in auth ? auth.handler(request) : auth(request); }; return { GET: handler, POST: handler, }; } export const nextCookies = () => { return { id: "next-cookies", hooks: { after: [ { matcher(ctx) { return true; }, handler: createAuthMiddleware(async (ctx) => { const returned = ctx.context.responseHeaders; if ("_flag" in ctx && ctx._flag === "router") { return; } if (returned instanceof Headers) { const setCookies = returned?.get("set-cookie"); if (!setCookies) return; const parsed = parseSetCookieHeader(setCookies); const { cookies } = await import("next/headers"); let cookieHelper: Awaited<ReturnType<typeof cookies>>; try { cookieHelper = await cookies(); } catch (error) { if ( error instanceof Error && error.message.startsWith( "`cookies` was called outside a request scope.", ) ) { // If error it means the `cookies` was called outside request scope. // NextJS docs on this: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context // This often gets called in a monorepo workspace (outside of NextJS), // so we will try to catch this suppress it, and ignore using next-cookies. return; } // If it's an unexpected error, throw it. throw error; } parsed.forEach((value, key) => { if (!key) return; const opts = { sameSite: value.samesite, secure: value.secure, maxAge: value["max-age"], httpOnly: value.httponly, domain: value.domain, path: value.path, } as const; try { cookieHelper.set(key, decodeURIComponent(value.value), opts); } catch (e) { // this will fail if the cookie is being set on server component } }); return; } }), }, ], }, } satisfies BetterAuthPlugin; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/linkedin.mdx: -------------------------------------------------------------------------------- ```markdown --- title: LinkedIn description: LinkedIn Provider --- <Steps> <Step> ### Get your LinkedIn credentials To use LinkedIn sign in, you need a client ID and client secret. You can get them from the [LinkedIn Developer Portal](https://www.linkedin.com/developers/). Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/linkedin` 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. </Step> <Callout type="info"> In the LinkedIn portal under products you need the **Sign In with LinkedIn using OpenID Connect** product. </Callout> There are some different Guides here: [Authorization Code Flow (3-legged OAuth) (Outdated)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow) [Sign In with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin%2Fconsumer%2Fcontext) <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { linkedin: { // [!code highlight] clientId: process.env.LINKEDIN_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> <Step> ### Sign In with LinkedIn To sign in with LinkedIn, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `linkedin`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "linkedin" }) } ``` </Step> </Steps> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/test-plugin.ts: -------------------------------------------------------------------------------- ```typescript import { atom, computed } from "nanostores"; import type { BetterAuthClientPlugin } from "@better-auth/core"; import type { BetterAuthPlugin } from "@better-auth/core"; import { createAuthEndpoint } from "@better-auth/core/api"; import { useAuthQuery } from "./query"; import z from "zod"; const serverPlugin = { id: "test", endpoints: { test: createAuthEndpoint( "/test", { method: "GET", error: z.object({ code: z.number(), message: z.string(), test: z.boolean(), }), }, async (c) => { return { data: "test", }; }, ), testSignOut2: createAuthEndpoint( "/test-2/sign-out", { method: "POST", }, async (c) => { return null; }, ), }, schema: { user: { fields: { testField: { type: "string", required: false, }, testField2: { type: "number", required: false, }, testField3: { type: "string", returned: false, }, testField4: { type: "string", defaultValue: "test", }, }, }, }, } satisfies BetterAuthPlugin; export const testClientPlugin = () => { const $test = atom(false); let testValue = 0; const computedAtom = computed($test, () => { return testValue++; }); return { id: "test" as const, getActions($fetch) { return { setTestAtom(value: boolean) { $test.set(value); }, test: { signOut: async () => {}, }, }; }, getAtoms($fetch) { const $signal = atom(false); const queryAtom = useAuthQuery<any>($signal, "/test", $fetch, { method: "GET", }); return { $test, $signal, computedAtom, queryAtom, }; }, $InferServerPlugin: {} as typeof serverPlugin, atomListeners: [ { matcher: (path) => path === "/test", signal: "$test", }, { matcher: (path) => path === "/test2/sign-out", signal: "$sessionSignal", }, ], } satisfies BetterAuthClientPlugin; }; export const testClientPlugin2 = () => { const $test2 = atom(false); let testValue = 0; const anotherAtom = computed($test2, () => { return testValue++; }); return { id: "test", getAtoms($fetch) { return { $test2, anotherAtom, }; }, atomListeners: [ { matcher: (path) => path === "/test", signal: "$test", }, { matcher: (path) => path === "/test2/sign-out", signal: "$sessionSignal", }, ], } satisfies BetterAuthClientPlugin; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/line.mdx: -------------------------------------------------------------------------------- ```markdown --- title: LINE description: LINE provider setup and usage. --- <Steps> <Step> ### Get your LINE credentials 1. Create a channel in the LINE Developers Console. 2. Note your Channel ID (client_id) and Channel secret (client_secret). 3. In the channel settings, add your Redirect URI, e.g. `http://localhost:3000/api/auth/callback/line` for local development. 4. Enable required scopes (at least `openid`; add `profile`, `email` if you need name, avatar, email). See LINE Login v2.1 reference for details: [`https://developers.line.biz/en/reference/line-login/#issue-access-token`] </Step> <Step> ### Configure the provider Add your LINE credentials to `socialProviders.line` in your auth configuration. ```ts title="auth.ts" import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { line: { clientId: process.env.LINE_CLIENT_ID as string, clientSecret: process.env.LINE_CLIENT_SECRET as string, // Optional: override redirect if needed // redirectURI: "https://your.app/api/auth/callback/line", // scopes are prefilled: ["openid","profile","email"]. Append if needed }, }, }); ``` </Step> </Steps> ## Usage ### Sign In with LINE Use the client `signIn.social` with `provider: "line"`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; const authClient = createAuthClient(); async function signInWithLINE() { const res = await authClient.signIn.social({ provider: "line" }); } ``` ### Sign In with LINE using ID Token (optional) If you obtain the LINE ID token on the client, you can sign in directly without redirection. ```ts title="auth-client.ts" await authClient.signIn.social({ provider: "line", idToken: { token: "<LINE_ID_TOKEN>", accessToken: "<LINE_ACCESS_TOKEN>", }, }); ``` ### Notes - Default scopes include `openid profile email`. Adjust as needed via provider options. - Verify redirect URI exactly matches the value configured in LINE Developers Console. - LINE ID token verification uses the official endpoint and checks audience and optional nonce per spec. Designing a login button? Follow LINE's button [guidelines](https://developers.line.biz/en/docs/line-login/login-button/). ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/to-zod.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import type { ZodType } from "zod"; import type { DBFieldAttribute } from "@better-auth/core/db"; export function toZodSchema< Fields extends Record<string, DBFieldAttribute | never>, IsClientSide extends boolean, >({ fields, isClientSide, }: { fields: Fields; /** * If true, then any fields that have `input: false` will be removed from the schema to prevent user input. */ isClientSide: IsClientSide; }) { const zodFields = Object.keys(fields).reduce((acc, key) => { const field = fields[key]; if (!field) { return acc; } if (isClientSide && field.input === false) { return acc; } let schema: ZodType; if (field.type === "json") { schema = (z as any).json ? (z as any).json() : z.any(); } else if (field.type === "string[]" || field.type === "number[]") { schema = z.array(field.type === "string[]" ? z.string() : z.number()); } else if (Array.isArray(field.type)) { schema = z.any(); } else { schema = z[field.type](); } if (field?.required === false) { schema = schema.optional(); } if (field?.returned === false) { return acc; } return { ...acc, [key]: schema, }; }, {}); const schema = z.object(zodFields); return schema as z.ZodObject< RemoveNeverProps<{ [key in keyof Fields]: FieldAttributeToSchema<Fields[key], IsClientSide>; }>, z.core.$strip >; } export type FieldAttributeToSchema< Field extends DBFieldAttribute | Record<string, never>, // if it's client side, then field attributes of `input` that are false should be removed isClientSide extends boolean = false, > = Field extends { type: any } ? GetInput<isClientSide, Field, GetRequired<Field, GetType<Field>>> : Record<string, never>; type GetType<F extends DBFieldAttribute> = F extends { type: "string"; } ? z.ZodString : F extends { type: "number" } ? z.ZodNumber : F extends { type: "boolean" } ? z.ZodBoolean : F extends { type: "date" } ? z.ZodDate : z.ZodAny; type GetRequired< F extends DBFieldAttribute, Schema extends z.core.SomeType, > = F extends { required: true; } ? Schema : z.ZodOptional<Schema>; type GetInput< isClientSide extends boolean, Field extends DBFieldAttribute, Schema extends z.core.SomeType, > = Field extends { input: false; } ? isClientSide extends true ? never : Schema : Schema; type RemoveNeverProps<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K]; }; ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: pull_request: types: - opened - synchronize push: branches: - main - canary merge_group: jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: [22.x, 24.x] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Cache turbo build setup uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .turbo key: ${{ runner.os }}-turbo-${{ github.sha }} restore-keys: | ${{ runner.os }}-turbo- - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' cache: pnpm - name: Install run: pnpm install - name: Build env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm build - name: Start Docker Containers run: | docker compose up -d # Wait for services to be ready (optional) sleep 10 - name: Lint env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm lint - name: Test env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} run: pnpm test - name: Typecheck env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM || github.repository_owner }} TURBO_CACHE: remote:rw run: pnpm typecheck - name: Stop Docker Containers run: docker compose down ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/url.ts: -------------------------------------------------------------------------------- ```typescript import { env } from "@better-auth/core/env"; import { BetterAuthError } from "@better-auth/core/error"; function checkHasPath(url: string): boolean { try { const parsedUrl = new URL(url); const pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/"; return pathname !== "/"; } catch (error) { throw new BetterAuthError( `Invalid base URL: ${url}. Please provide a valid base URL.`, ); } } function withPath(url: string, path = "/api/auth") { const hasPath = checkHasPath(url); if (hasPath) { return url; } const trimmedUrl = url.replace(/\/+$/, ""); if (!path || path === "/") { return trimmedUrl; } path = path.startsWith("/") ? path : `/${path}`; return `${trimmedUrl}${path}`; } export function getBaseURL( url?: string, path?: string, request?: Request, loadEnv?: boolean, ) { if (url) { return withPath(url, path); } if (loadEnv !== false) { const fromEnv = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL || (env.BASE_URL !== "/" ? env.BASE_URL : undefined); if (fromEnv) { return withPath(fromEnv, path); } } const fromRequest = request?.headers.get("x-forwarded-host"); const fromRequestProto = request?.headers.get("x-forwarded-proto"); if (fromRequest && fromRequestProto) { return withPath(`${fromRequestProto}://${fromRequest}`, path); } if (request) { const url = getOrigin(request.url); if (!url) { throw new BetterAuthError( "Could not get origin from request. Please provide a valid base URL.", ); } return withPath(url, path); } if (typeof window !== "undefined" && window.location) { return withPath(window.location.origin, path); } return undefined; } export function getOrigin(url: string) { try { const parsedUrl = new URL(url); // For custom URL schemes (like exp://), the origin property returns the string "null" // instead of null. We need to handle this case and return null so the fallback logic works. return parsedUrl.origin === "null" ? null : parsedUrl.origin; } catch (error) { return null; } } export function getProtocol(url: string) { try { const parsedUrl = new URL(url); return parsedUrl.protocol; } catch (error) { return null; } } export function getHost(url: string) { try { const parsedUrl = new URL(url); return parsedUrl.host; } catch (error) { return url; } } ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/calendar.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { DayPicker } from "react-day-picker"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; export type CalendarProps = React.ComponentProps<typeof DayPicker>; function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { return ( <DayPicker showOutsideDays={showOutsideDays} className={cn("p-3", className)} classNames={{ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", month: "space-y-4", caption: "flex justify-center pt-1 relative items-center", caption_label: "text-sm font-medium", nav: "space-x-1 flex items-center", nav_button: cn( buttonVariants({ variant: "outline" }), "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", ), nav_button_previous: "absolute left-1", nav_button_next: "absolute right-1", table: "w-full border-collapse space-y-1", head_row: "flex", head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", row: "flex w-full mt-2", cell: cn( "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", props.mode === "range" ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" : "[&:has([aria-selected])]:rounded-md", ), day: cn( buttonVariants({ variant: "ghost" }), "h-8 w-8 p-0 font-normal aria-selected:opacity-100", ), day_range_start: "day-range-start", day_range_end: "day-range-end", day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", day_today: "bg-accent text-accent-foreground", day_outside: "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", day_disabled: "text-muted-foreground opacity-50", day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", day_hidden: "invisible", ...classNames, }} {...props} /> ); } Calendar.displayName = "Calendar"; export { Calendar }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; import { Slot } from "@radix-ui/react-slot"; import { cn } from "@/lib/utils"; const Breadcrumb = ({ ref, ...props }) => ( <nav ref={ref} aria-label="breadcrumb" {...props} /> ); Breadcrumb.displayName = "Breadcrumb"; const BreadcrumbList = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<"ol"> & { ref: React.RefObject<HTMLOListElement>; }) => ( <ol ref={ref} className={cn( "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", className, )} {...props} /> ); BreadcrumbList.displayName = "BreadcrumbList"; const BreadcrumbItem = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<"li"> & { ref: React.RefObject<HTMLLIElement>; }) => ( <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} /> ); BreadcrumbItem.displayName = "BreadcrumbItem"; const BreadcrumbLink = ({ ref, asChild, className, ...props }) => { const Comp = asChild ? Slot : "a"; return ( <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} /> ); }; BreadcrumbLink.displayName = "BreadcrumbLink"; const BreadcrumbPage = ({ ref, className, ...props }: React.ComponentPropsWithoutRef<"span"> & { ref: React.RefObject<HTMLSpanElement>; }) => ( <span ref={ref} role="link" aria-disabled="true" aria-current="page" className={cn("font-normal text-foreground", className)} {...props} /> ); BreadcrumbPage.displayName = "BreadcrumbPage"; const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => ( <li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props} > {children ?? <ChevronRightIcon />} </li> ); BreadcrumbSeparator.displayName = "BreadcrumbSeparator"; const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( <span role="presentation" aria-hidden="true" className={cn("flex h-9 w-9 items-center justify-center", className)} {...props} > <DotsHorizontalIcon className="h-4 w-4" /> <span className="sr-only">More</span> </span> ); BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"; export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis, }; ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/sign-up.tsx: -------------------------------------------------------------------------------- ```typescript import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Text } from "@/components/ui/text"; import { authClient } from "@/lib/auth-client"; import { KeyboardAvoidingView, View } from "react-native"; import { Image } from "react-native"; import { useRouter } from "expo-router"; import { useState } from "react"; export default function SignUp() { const router = useRouter(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [name, setName] = useState(""); return ( <Card className="z-50 mx-6"> <CardHeader className="flex items-center justify-center gap-8"> <Image source={require("../../assets/images/logo.png")} style={{ width: 40, height: 40, }} /> <CardTitle>Create new Account</CardTitle> </CardHeader> <View className="px-6"> <KeyboardAvoidingView> <Input placeholder="Name" className="rounded-b-none border-b-0" value={name} onChangeText={(text) => { setName(text); }} /> </KeyboardAvoidingView> <KeyboardAvoidingView> <Input placeholder="Email" className="rounded-b-none border-b-0" value={email} onChangeText={(text) => { setEmail(text); }} autoCapitalize="none" /> </KeyboardAvoidingView> <KeyboardAvoidingView> <Input placeholder="Password" secureTextEntry className="rounded-t-none" value={password} onChangeText={(text) => { setPassword(text); }} /> </KeyboardAvoidingView> </View> <CardFooter> <View className="w-full mt-2"> <Button onPress={async () => { const res = await authClient.signUp.email( { email, password, name, }, { onError: (ctx) => { alert(ctx.error.message); }, onSuccess: (ctx) => { router.push("/dashboard"); }, }, ); console.log(res); }} > <Text>Sign Up</Text> </Button> <Text className="text-center mt-2"> Already have an account?{" "} <Text className="underline" onPress={() => { router.push("/"); }} > Sign In </Text> </Text> </View> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/figma.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Figma description: Figma provider setup and usage. --- <Steps> <Step> ### Get your Credentials 1. Sign in to your Figma account and go to the [Developer Apps page](https://www.figma.com/developers/apps) 2. Click "Create new app" 3. Fill out the app details (name, description, etc.) 4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/figma`) 5. Note your Client ID and Client Secret <Callout type="info"> - The default scope is `file_read`. For additional scopes like `file_write`, refer to the [Figma OAuth documentation](https://www.figma.com/developers/api#oauth2). </Callout> Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { figma: { // [!code highlight] clientId: process.env.FIGMA_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.FIGMA_CLIENT_SECRET as string, // [!code highlight] clientKey: process.env.FIGMA_CLIENT_KEY as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> <Step> ### Sign In with Figma To sign in with Figma, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `figma`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "figma" }) } ``` <Callout type="info"> For more information about Figma's OAuth scopes and API capabilities, refer to the [official Figma API documentation](https://www.figma.com/developers/api). </Callout> </Step> </Steps> ``` -------------------------------------------------------------------------------- /docs/components/ui/dynamic-code-block.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { CodeBlock, type CodeBlockProps, Pre, } from "@/components/ui/code-block"; import type { HighlightOptions, HighlightOptionsCommon, HighlightOptionsThemes, } from "fumadocs-core/highlight"; import { useShiki } from "fumadocs-core/highlight/client"; import { cn } from "@/lib/utils"; import { type ComponentProps, createContext, type FC, Suspense, use, } from "react"; export interface DynamicCodeblockProps { lang: string; code: string; /** * Extra props for the underlying `<CodeBlock />` component. * * Ignored if you defined your own `pre` component in `options.components`. */ codeblock?: CodeBlockProps; /** * Wrap in React `<Suspense />` and provide a fallback. * * @defaultValue true */ wrapInSuspense?: boolean; /** * Allow to copy code with copy button * * @defaultValue true */ allowCopy?: boolean; options?: Omit<HighlightOptionsCommon, "lang"> & HighlightOptionsThemes; } const PropsContext = createContext<CodeBlockProps | undefined>(undefined); function DefaultPre(props: ComponentProps<"pre">) { const extraProps = use(PropsContext); return ( <CodeBlock {...props} {...extraProps} className={cn( "my-0 border-t-0 rounded-none", props.className, extraProps?.className, )} > <Pre className="py-2">{props.children}</Pre> </CodeBlock> ); } export function DynamicCodeBlock({ lang, code, codeblock, options, wrapInSuspense = true, allowCopy = true, }: DynamicCodeblockProps) { const shikiOptions = { lang, ...options, components: { pre: DefaultPre, ...options?.components, }, } satisfies HighlightOptions; let children = <Internal code={code} options={shikiOptions} />; if (wrapInSuspense) children = ( <Suspense fallback={ <Placeholder code={code} components={shikiOptions.components} /> } > {children} </Suspense> ); return ( <PropsContext value={{ ...codeblock, allowCopy }}>{children}</PropsContext> ); } function Placeholder({ code, components = {}, }: { code: string; components: HighlightOptions["components"]; }) { const { pre: Pre = "pre", code: Code = "code" } = components as Record< string, FC >; return ( <Pre> <Code> {code.split("\n").map((line, i) => ( <span key={i} className="line"> {line} </span> ))} </Code> </Pre> ); } function Internal({ code, options, }: { code: string; options: HighlightOptions; }) { return useShiki(code, options); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/drizzle-adapter/test/generate-schema.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthOptions } from "@better-auth/core"; import type { DBAdapter } from "@better-auth/core/db/adapter"; import { drizzleAdapter } from "../drizzle-adapter"; import fs from "fs/promises"; import { join } from "path"; let generationCount = 0; const schemaCache = new Map<string, { count: number; schema: any }>(); /** * generates a drizzle schema based on BetterAuthOptions & a given dialect. * * Useful for testing the Drizzle adapter. */ export const generateDrizzleSchema = async ( db: any, options: BetterAuthOptions, dialect: "sqlite" | "mysql" | "pg", ) => { const cacheKey = `${dialect}-${JSON.stringify(options)}`; if (schemaCache.has(cacheKey)) { const { count, schema } = schemaCache.get(cacheKey)!; return { schema, fileName: `./.tmp/generated-${dialect}-schema-${count}`, }; } generationCount++; let thisCount = generationCount; const i = async (x: string) => { // Clear the Node.js module cache for the generated schema file to ensure fresh import try { const resolvedPath = require?.resolve?.(x) || (import.meta && new URL(x, import.meta.url).pathname); if (resolvedPath && typeof resolvedPath === "string" && require?.cache) { delete require.cache[resolvedPath]; } } catch (error) {} return await import(x); }; const { generateSchema } = (await i( "./../../../../../cli/src/generators/index", )) as { generateSchema: (opts: { adapter: DBAdapter<BetterAuthOptions>; file?: string; options: BetterAuthOptions; }) => Promise<{ code: string | undefined; fileName: string; overwrite: boolean | undefined; }>; }; const exists = await fs .access(join(import.meta.dirname, `/.tmp`)) .then(() => true) .catch(() => false); if (!exists) { await fs.mkdir(join(import.meta.dirname, `/.tmp`), { recursive: true }); } let adapter = drizzleAdapter(db, { provider: dialect })(options); let { code } = await generateSchema({ adapter, options, }); await fs.writeFile( join( import.meta.dirname, `/.tmp/generated-${dialect}-schema-${thisCount}.ts`, ), code || "", "utf-8", ); const res = await i(`./.tmp/generated-${dialect}-schema-${thisCount}`); schemaCache.set(cacheKey, { count: thisCount, schema: res, }); return { schema: res, fileName: `./.tmp/generated-${dialect}-schema-${thisCount}`, }; }; export const clearSchemaCache = () => { schemaCache.clear(); }; export const resetGenerationCount = () => { generationCount = 0; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/atlassian.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Atlassian description: Atlassian provider setup and usage. --- <Steps> <Step> ### Get your Credentials 1. Sign in to your Atlassian account and go to the [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/) 2. Click "Create new app" 3. Fill out the app details 4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/atlassian`) 5. Note your Client ID and Client Secret <Callout type="info"> - The default scope is `read:jira-user` and `offline_access`. For additional scopes, refer to the [Atlassian OAuth documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/). </Callout> Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly. </Step> <Step> ### Configure the provider To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { atlassian: { // [!code highlight] clientId: process.env.ATLASSIAN_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.ATLASSIAN_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> <Step> ### Sign In with Atlassian To sign in with Atlassian, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `atlassian`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client" const authClient = createAuthClient() const signIn = async () => { const data = await authClient.signIn.social({ provider: "atlassian" }) } ``` <Callout type="info"> For more information about Atlassian's OAuth scopes and API capabilities, refer to the [official Atlassian OAuth 2.0 (3LO) apps documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/). </Callout> </Step> </Steps> ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-mysql-enum.txt: -------------------------------------------------------------------------------- ``` import { mysqlTable, varchar, text, timestamp, boolean, mysqlEnum, } from "drizzle-orm/mysql-core"; export const user = mysqlTable("user", { id: varchar("id", { length: 36 }).primaryKey(), name: text("name").notNull(), email: varchar("email", { length: 255 }).notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { fsp: 3 }) .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), status: mysqlEnum(["active", "inactive", "pending"]), }); export const session = mysqlTable("session", { id: varchar("id", { length: 36 }).primaryKey(), expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(), token: varchar("token", { length: 255 }).notNull().unique(), createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { fsp: 3 }) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: varchar("user_id", { length: 36 }) .notNull() .references(() => user.id, { onDelete: "cascade" }), }); export const account = mysqlTable("account", { id: varchar("id", { length: 36 }).primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: varchar("user_id", { length: 36 }) .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }), refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { fsp: 3 }) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const verification = mysqlTable("verification", { id: varchar("id", { length: 36 }).primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(), createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { fsp: 3 }) .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/linkedin.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, validateAuthorizationCode, refreshAccessToken, } from "../oauth2"; export interface LinkedInProfile { sub: string; name: string; given_name: string; family_name: string; picture: string; locale: { country: string; language: string; }; email: string; email_verified: boolean; } export interface LinkedInOptions extends ProviderOptions<LinkedInProfile> { clientId: string; } export const linkedin = (options: LinkedInOptions) => { const authorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization"; const tokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken"; return { id: "linkedin", name: "Linkedin", createAuthorizationURL: async ({ state, scopes, redirectURI, loginHint, }) => { const _scopes = options.disableDefaultScope ? [] : ["profile", "email", "openid"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await createAuthorizationURL({ id: "linkedin", options, authorizationEndpoint, scopes: _scopes, state, loginHint, redirectURI, }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return await validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint, }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint, }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<LinkedInProfile>( "https://api.linkedin.com/v2/userinfo", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}`, }, }, ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name: profile.name, email: profile.email, emailVerified: profile.email_verified || false, image: profile.picture, ...userMap, }, data: profile, }; }, options, } satisfies OAuthProvider<LinkedInProfile>; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/svelte/index.ts: -------------------------------------------------------------------------------- ```typescript import { getClientConfig } from "../config"; import { capitalizeFirstLetter } from "../../utils/misc"; import type { InferActions, InferClientAPI, InferErrorCodes, IsSignal, } from "../types"; import type { BetterAuthClientPlugin, BetterAuthClientOptions, } from "@better-auth/core"; import { createDynamicPathProxy } from "../proxy"; import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; import type { Atom } from "nanostores"; import type { BetterFetchError, BetterFetchResponse, } from "@better-fetch/fetch"; import type { BASE_ERROR_CODES } from "@better-auth/core/error"; type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { plugins: Array<infer Plugin>; } ? UnionToIntersection< Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Atoms[key]; } : {} : {} : {} > : {}; export function createAuthClient<Option extends BetterAuthClientOptions>( options?: Option, ) { const { pluginPathMethods, pluginsActions, pluginsAtoms, $fetch, atomListeners, $store, } = getClientConfig(options); let resolvedHooks: Record<string, any> = {}; for (const [key, value] of Object.entries(pluginsAtoms)) { resolvedHooks[`use${capitalizeFirstLetter(key)}`] = () => value; } const routes = { ...pluginsActions, ...resolvedHooks, $fetch, $store, }; const proxy = createDynamicPathProxy( routes, $fetch, pluginPathMethods, pluginsAtoms, atomListeners, ); type ClientAPI = InferClientAPI<Option>; type Session = ClientAPI extends { getSession: () => Promise<infer Res>; } ? Res extends BetterFetchResponse<infer S> ? S : Res extends Record<string, any> ? Res : never : never; return proxy as UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & { useSession: () => Atom<{ data: Session; error: BetterFetchError | null; isPending: boolean; isRefetching: boolean; }>; $fetch: typeof $fetch; $store: typeof $store; $Infer: { Session: NonNullable<Session>; }; $ERROR_CODES: PrettifyDeep< InferErrorCodes<Option> & typeof BASE_ERROR_CODES >; }; } export type * from "@better-fetch/fetch"; export type * from "nanostores"; ``` -------------------------------------------------------------------------------- /docs/components/ui/pagination.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button, buttonVariants } from "@/components/ui/button"; function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return ( <nav role="navigation" aria-label="pagination" data-slot="pagination" className={cn("mx-auto flex w-full justify-center", className)} {...props} /> ); } function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) { return ( <ul data-slot="pagination-content" className={cn("flex flex-row items-center gap-1", className)} {...props} /> ); } function PaginationItem({ ...props }: React.ComponentProps<"li">) { return <li data-slot="pagination-item" {...props} />; } type PaginationLinkProps = { isActive?: boolean; } & Pick<React.ComponentProps<typeof Button>, "size"> & React.ComponentProps<"a">; function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) { return ( <a aria-current={isActive ? "page" : undefined} data-slot="pagination-link" data-active={isActive} className={cn( buttonVariants({ variant: isActive ? "outline" : "ghost", size, }), className, )} {...props} /> ); } function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) { return ( <PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 px-2.5 sm:pl-2.5", className)} {...props} > <ChevronLeftIcon /> <span className="hidden sm:block">Previous</span> </PaginationLink> ); } function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) { return ( <PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 px-2.5 sm:pr-2.5", className)} {...props} > <span className="hidden sm:block">Next</span> <ChevronRightIcon /> </PaginationLink> ); } function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) { return ( <span aria-hidden data-slot="pagination-ellipsis" className={cn("flex size-9 items-center justify-center", className)} {...props} > <MoreHorizontalIcon className="size-4" /> <span className="sr-only">More pages</span> </span> ); } export { Pagination, PaginationContent, PaginationLink, PaginationItem, PaginationPrevious, PaginationNext, PaginationEllipsis, }; ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/postgresql.mdx: -------------------------------------------------------------------------------- ```markdown --- title: PostgreSQL description: Integrate Better Auth with PostgreSQL. --- PostgreSQL is a powerful, open-source relational database management system known for its advanced features, extensibility, and support for complex queries and large datasets. Read more [here](https://www.postgresql.org/). ## Example Usage Make sure you have PostgreSQL installed and configured. Then, you can connect it straight into Better Auth. ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { Pool } from "pg"; export const auth = betterAuth({ database: new Pool({ connectionString: "postgres://user:password@localhost:5432/database", }), }); ``` <Callout> For more information, read Kysely's documentation to the [PostgresDialect](https://kysely-org.github.io/kysely-apidoc/classes/PostgresDialect.html). </Callout> ## Schema generation & migration The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate your database schema based on your Better Auth configuration and plugins. <table> <thead> <tr className="border-b"> <th> <p className="font-bold text-[16px] mb-1">PostgreSQL Schema Generation</p> </th> <th> <p className="font-bold text-[16px] mb-1">PostgreSQL Schema Migration</p> </th> </tr> </thead> <tbody> <tr className="h-10"> <td>✅ Supported</td> <td>✅ Supported</td> </tr> </tbody> </table> ```bash title="Schema Generation" npx @better-auth/cli@latest generate ``` ```bash title="Schema Migration" npx @better-auth/cli@latest migrate ``` ## Use a non-default schema In most cases, the default schema is `public`. To have Better Auth use a non-default schema (e.g., `auth`) for its tables, set the PostgreSQL user's default schema before generating or migrating: ```sql ALTER USER authuser SET SEARCH_PATH TO auth; ``` alternatively, append the option to your connection URI, for example: ``` postgres://<DATABASE_URL>?option=-c search_path=auth ``` URL-encode if needed: `?option=-c%20search_path%3Dauth`. Ensure the target schema exists and the database user has the required permissions. ## Additional Information PostgreSQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (<Link href="/docs/adapters/other-relational-databases">Read more here</Link>) If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. ``` -------------------------------------------------------------------------------- /packages/telemetry/src/index.ts: -------------------------------------------------------------------------------- ```typescript import { ENV, getBooleanEnvVar, isTest } from "@better-auth/core/env"; import { getProjectId } from "./project-id"; import type { BetterAuthOptions } from "@better-auth/core"; import { detectEnvironment, detectRuntime } from "./detectors/detect-runtime"; import { detectDatabase } from "./detectors/detect-database"; import { detectFramework } from "./detectors/detect-framework"; import { detectSystemInfo } from "./detectors/detect-system-info"; import { detectPackageManager } from "./detectors/detect-project-info"; import { betterFetch } from "@better-fetch/fetch"; import type { TelemetryContext, TelemetryEvent } from "./types"; import { logger } from "@better-auth/core/env"; import { getTelemetryAuthConfig } from "./detectors/detect-auth-config"; export { getTelemetryAuthConfig }; export type { TelemetryEvent } from "./types"; export async function createTelemetry( options: BetterAuthOptions, context?: TelemetryContext, ) { const debugEnabled = options.telemetry?.debug || getBooleanEnvVar("BETTER_AUTH_TELEMETRY_DEBUG", false); const TELEMETRY_ENDPOINT = ENV.BETTER_AUTH_TELEMETRY_ENDPOINT; const track = async (event: TelemetryEvent) => { if (context?.customTrack) { await context.customTrack(event).catch(logger.error); } else { if (debugEnabled) { logger.info("telemetry event", JSON.stringify(event, null, 2)); } else { await betterFetch(TELEMETRY_ENDPOINT, { method: "POST", body: event, }).catch(logger.error); } } }; const isEnabled = async () => { const telemetryEnabled = options.telemetry?.enabled !== undefined ? options.telemetry.enabled : false; const envEnabled = getBooleanEnvVar("BETTER_AUTH_TELEMETRY", false); return ( (envEnabled || telemetryEnabled) && (context?.skipTestCheck || !isTest()) ); }; const enabled = await isEnabled(); let anonymousId: string | undefined; if (enabled) { anonymousId = await getProjectId(options.baseURL); const payload = { config: getTelemetryAuthConfig(options), runtime: detectRuntime(), database: await detectDatabase(), framework: await detectFramework(), environment: detectEnvironment(), systemInfo: await detectSystemInfo(), packageManager: detectPackageManager(), }; void track({ type: "init", payload, anonymousId }); } return { publish: async (event: TelemetryEvent) => { if (!enabled) return; if (!anonymousId) { anonymousId = await getProjectId(options.baseURL); } await track({ type: event.type, payload: event.payload, anonymousId, }); }, }; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/svelte-kit.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthOptions } from "../types"; import type { BetterAuthPlugin } from "../types"; import { createAuthMiddleware } from "@better-auth/core/api"; import { parseSetCookieHeader } from "../cookies"; import type { RequestEvent } from "@sveltejs/kit"; export const toSvelteKitHandler = (auth: { handler: (request: Request) => Response | Promise<Response>; options: BetterAuthOptions; }) => { return (event: { request: Request }) => auth.handler(event.request); }; export const svelteKitHandler = async ({ auth, event, resolve, building, }: { auth: { handler: (request: Request) => Response | Promise<Response>; options: BetterAuthOptions; }; event: RequestEvent; resolve: (event: RequestEvent) => Response | Promise<Response>; building: boolean; }) => { if (building) { return resolve(event); } const { request, url } = event; if (isAuthPath(url.toString(), auth.options)) { return auth.handler(request); } return resolve(event); }; export function isAuthPath(url: string, options: BetterAuthOptions) { const _url = new URL(url); const baseURL = new URL( `${options.baseURL || _url.origin}${options.basePath || "/api/auth"}`, ); if (_url.origin !== baseURL.origin) return false; if ( !_url.pathname.startsWith( baseURL.pathname.endsWith("/") ? baseURL.pathname : `${baseURL.pathname}/`, ) ) return false; return true; } export const sveltekitCookies = ( getRequestEvent: () => RequestEvent<any, any>, ) => { return { id: "sveltekit-cookies", hooks: { after: [ { matcher() { return true; }, handler: createAuthMiddleware(async (ctx) => { const returned = ctx.context.responseHeaders; if ("_flag" in ctx && ctx._flag === "router") { return; } if (returned instanceof Headers) { const setCookies = returned?.get("set-cookie"); if (!setCookies) return; const event = getRequestEvent(); if (!event) return; const parsed = parseSetCookieHeader(setCookies); for (const [name, { value, ...ops }] of parsed) { try { event.cookies.set(name, decodeURIComponent(value), { sameSite: ops.samesite, path: ops.path || "/", expires: ops.expires, secure: ops.secure, httpOnly: ops.httponly, domain: ops.domain, maxAge: ops["max-age"], }); } catch (e) { // this will avoid any issue related to already streamed response } } } }), }, ], }, } satisfies BetterAuthPlugin; }; ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema.txt: -------------------------------------------------------------------------------- ``` import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; export const custom_user = pgTable("custom_user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), twoFactorEnabled: boolean("two_factor_enabled").default(false), username: text("username").unique(), displayUsername: text("display_username"), }); export const custom_session = pgTable("custom_session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), }); export const custom_account = pgTable("custom_account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const custom_verification = pgTable("custom_verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const twoFactor = pgTable("two_factor", { id: text("id").primaryKey(), secret: text("secret").notNull(), backupCodes: text("backup_codes").notNull(), userId: text("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), }); ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/two-factor/page.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { client } from "@/lib/auth-client"; import { AlertCircle, CheckCircle2 } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; export default function Component() { const [totpCode, setTotpCode] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(false); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (totpCode.length !== 6 || !/^\d+$/.test(totpCode)) { setError("TOTP code must be 6 digits"); return; } client.twoFactor .verifyTotp({ code: totpCode, }) .then((res) => { if (res.data?.token) { setSuccess(true); setError(""); } else { setError("Invalid TOTP code"); } }); }; return ( <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> <Card className="w-[350px]"> <CardHeader> <CardTitle>TOTP Verification</CardTitle> <CardDescription> Enter your 6-digit TOTP code to authenticate </CardDescription> </CardHeader> <CardContent> {!success ? ( <form onSubmit={handleSubmit}> <div className="space-y-2"> <Label htmlFor="totp">TOTP Code</Label> <Input id="totp" type="text" inputMode="numeric" pattern="\d{6}" maxLength={6} value={totpCode} onChange={(e) => setTotpCode(e.target.value)} placeholder="Enter 6-digit code" required /> </div> {error && ( <div className="flex items-center mt-2 text-red-500"> <AlertCircle className="w-4 h-4 mr-2" /> <span className="text-sm">{error}</span> </div> )} <Button type="submit" className="w-full mt-4"> Verify </Button> </form> ) : ( <div className="flex flex-col items-center justify-center space-y-2"> <CheckCircle2 className="w-12 h-12 text-green-500" /> <p className="text-lg font-semibold">Verification Successful</p> </div> )} </CardContent> <CardFooter className="text-sm text-muted-foreground gap-2"> <Link href="/two-factor/otp"> <Button variant="link" size="sm"> Switch to Email Verification </Button> </Link> </CardFooter> </Card> </main> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/utils.ts: -------------------------------------------------------------------------------- ```typescript import { getAuthTables } from "."; import { BetterAuthError } from "@better-auth/core/error"; import type { BetterAuthOptions } from "@better-auth/core"; import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect"; import { kyselyAdapter } from "../adapters/kysely-adapter"; import { memoryAdapter, type MemoryDB } from "../adapters/memory-adapter"; import { logger } from "@better-auth/core/env"; import type { DBFieldAttribute } from "@better-auth/core/db"; import type { DBAdapter } from "@better-auth/core/db/adapter"; export async function getAdapter( options: BetterAuthOptions, ): Promise<DBAdapter<BetterAuthOptions>> { let adapter: DBAdapter<BetterAuthOptions>; if (!options.database) { const tables = getAuthTables(options); const memoryDB = Object.keys(tables).reduce<MemoryDB>((acc, key) => { acc[key] = []; return acc; }, {}); logger.warn( "No database configuration provided. Using memory adapter in development", ); adapter = memoryAdapter(memoryDB)(options); } else if (typeof options.database === "function") { adapter = options.database(options); } else { const { kysely, databaseType, transaction } = await createKyselyAdapter(options); if (!kysely) { throw new BetterAuthError("Failed to initialize database adapter"); } adapter = kyselyAdapter(kysely, { type: databaseType || "sqlite", debugLogs: "debugLogs" in options.database ? options.database.debugLogs : false, transaction: transaction, })(options); } // patch for 1.3.x to ensure we have a transaction function in the adapter if (!adapter.transaction) { logger.warn( "Adapter does not correctly implement transaction function, patching it automatically. Please update your adapter implementation.", ); adapter.transaction = async (cb) => { return cb(adapter); }; } return adapter; } export function convertToDB<T extends Record<string, any>>( fields: Record<string, DBFieldAttribute>, values: T, ) { let result: Record<string, any> = values.id ? { id: values.id, } : {}; for (const key in fields) { const field = fields[key]!; const value = values[key]; if (value === undefined) { continue; } result[field.fieldName || key] = value; } return result as T; } export function convertFromDB<T extends Record<string, any>>( fields: Record<string, DBFieldAttribute>, values: T | null, ) { if (!values) { return null; } let result: Record<string, any> = { id: values.id, }; for (const [key, value] of Object.entries(fields)) { result[key] = values[value.fieldName || key]; } return result as T; } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/solid/index.ts: -------------------------------------------------------------------------------- ```typescript import { getClientConfig } from "../config"; import { createDynamicPathProxy } from "../proxy"; import { capitalizeFirstLetter } from "../../utils/misc"; import type { InferActions, InferClientAPI, InferErrorCodes, IsSignal, } from "../types"; import type { BetterAuthClientPlugin, BetterAuthClientOptions, } from "@better-auth/core"; import type { Accessor } from "solid-js"; import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; import type { BetterFetchError, BetterFetchResponse, } from "@better-fetch/fetch"; import { useStore } from "./solid-store"; import type { BASE_ERROR_CODES } from "@better-auth/core/error"; function getAtomKey(str: string) { return `use${capitalizeFirstLetter(str)}`; } type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { plugins: Array<infer Plugin>; } ? UnionToIntersection< Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => Accessor<ReturnType<Atoms[key]["get"]>>; } : {} : {} : {} > : {}; export function createAuthClient<Option extends BetterAuthClientOptions>( options?: Option, ) { const { pluginPathMethods, pluginsActions, pluginsAtoms, $fetch, atomListeners, } = getClientConfig(options); let resolvedHooks: Record<string, any> = {}; for (const [key, value] of Object.entries(pluginsAtoms)) { resolvedHooks[getAtomKey(key)] = () => useStore(value); } const routes = { ...pluginsActions, ...resolvedHooks, }; const proxy = createDynamicPathProxy( routes, $fetch, pluginPathMethods, pluginsAtoms, atomListeners, ); type ClientAPI = InferClientAPI<Option>; type Session = ClientAPI extends { getSession: () => Promise<infer Res>; } ? Res extends BetterFetchResponse<infer S> ? S : Res extends Record<string, any> ? Res : never : never; return proxy as UnionToIntersection<InferResolvedHooks<Option>> & InferClientAPI<Option> & InferActions<Option> & { useSession: () => Accessor<{ data: Session; isPending: boolean; isRefetching: boolean; error: BetterFetchError | null; }>; $Infer: { Session: NonNullable<Session>; }; $fetch: typeof $fetch; $ERROR_CODES: PrettifyDeep< InferErrorCodes<Option> & typeof BASE_ERROR_CODES >; }; } export type * from "@better-fetch/fetch"; export type * from "nanostores"; ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/notion.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, } from "../oauth2"; export interface NotionProfile { object: "user"; id: string; type: "person" | "bot"; name?: string; avatar_url?: string; person?: { email?: string; }; } export interface NotionOptions extends ProviderOptions<NotionProfile> { clientId: string; } export const notion = (options: NotionOptions) => { const tokenEndpoint = "https://api.notion.com/v1/oauth/token"; return { id: "notion", name: "Notion", createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { const _scopes: string[] = options.disableDefaultScope ? [] : []; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return createAuthorizationURL({ id: "notion", options, authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize", scopes: _scopes, state, redirectURI, loginHint, additionalParams: { owner: "user", }, }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint, authentication: "basic", }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint, }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<{ bot: { owner: { user: NotionProfile; }; }; }>("https://api.notion.com/v1/users/me", { headers: { Authorization: `Bearer ${token.accessToken}`, "Notion-Version": "2022-06-28", }, }); if (error || !profile) { return null; } const userProfile = profile.bot?.owner?.user; if (!userProfile) { return null; } const userMap = await options.mapProfileToUser?.(userProfile); return { user: { id: userProfile.id, name: userProfile.name || "Notion User", email: userProfile.person?.email || null, image: userProfile.avatar_url, emailVerified: !!userProfile.person?.email, ...userMap, }, data: userProfile, }; }, options, } satisfies OAuthProvider<NotionProfile>; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/mssql.mdx: -------------------------------------------------------------------------------- ```markdown --- title: MS SQL description: Integrate Better Auth with MS SQL. --- Microsoft SQL Server is a relational database management system developed by Microsoft, designed for enterprise-level data storage, management, and analytics with robust security and scalability features. Read more [here](https://en.wikipedia.org/wiki/Microsoft_SQL_Server). ## Example Usage Make sure you have MS SQL installed and configured. Then, you can connect it straight into Better Auth. ```ts title="auth.ts" import { betterAuth } from "better-auth"; import { MssqlDialect } from "kysely"; import * as Tedious from 'tedious' import * as Tarn from 'tarn' const dialect = new MssqlDialect({ tarn: { ...Tarn, options: { min: 0, max: 10, }, }, tedious: { ...Tedious, connectionFactory: () => new Tedious.Connection({ authentication: { options: { password: 'password', userName: 'username', }, type: 'default', }, options: { database: 'some_db', port: 1433, trustServerCertificate: true, }, server: 'localhost', }), }, TYPES: { ...Tedious.TYPES, DateTime: Tedious.TYPES.DateTime2, }, }) export const auth = betterAuth({ database: { dialect, type: "mssql" } }); ``` <Callout> For more information, read Kysely's documentation to the [MssqlDialect](https://kysely-org.github.io/kysely-apidoc/classes/MssqlDialect.html). </Callout> ## Schema generation & migration The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate your database schema based on your Better Auth configuration and plugins. <table> <thead> <tr className="border-b"> <th> <p className="font-bold text-[16px] mb-1">MS SQL Schema Generation</p> </th> <th> <p className="font-bold text-[16px] mb-1">MS SQL Schema Migration</p> </th> </tr> </thead> <tbody> <tr className="h-10"> <td>✅ Supported</td> <td>✅ Supported</td> </tr> </tbody> </table> ```bash title="Schema Generation" npx @better-auth/cli@latest generate ``` ```bash title="Schema Migration" npx @better-auth/cli@latest migrate ``` ## Additional Information MS SQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (<Link href="/docs/adapters/other-relational-databases">Read more here</Link>) If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-number-id.txt: -------------------------------------------------------------------------------- ``` import { pgTable, text, timestamp, boolean, integer, serial, } from "drizzle-orm/pg-core"; export const custom_user = pgTable("custom_user", { id: serial("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), twoFactorEnabled: boolean("two_factor_enabled").default(false), username: text("username").unique(), displayUsername: text("display_username"), }); export const custom_session = pgTable("custom_session", { id: serial("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: integer("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), }); export const custom_account = pgTable("custom_account", { id: serial("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: integer("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const custom_verification = pgTable("custom_verification", { id: serial("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const twoFactor = pgTable("two_factor", { id: serial("id").primaryKey(), secret: text("secret").notNull(), backupCodes: text("backup_codes").notNull(), userId: integer("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/auth.ts: -------------------------------------------------------------------------------- ```typescript import { getEndpoints, router } from "./api"; import { init } from "./init"; import type { BetterAuthOptions } from "@better-auth/core"; import type { InferPluginErrorCodes, InferPluginTypes, InferSession, InferUser, InferAPI, } from "./types"; import type { PrettifyDeep, Expand } from "./types/helper"; import { getBaseURL, getOrigin } from "./utils/url"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; import { BetterAuthError } from "@better-auth/core/error"; import { runWithAdapter } from "@better-auth/core/context"; import type { AuthContext } from "@better-auth/core"; export type WithJsDoc<T, D> = Expand<T & D>; export const betterAuth = <Options extends BetterAuthOptions>( options: Options & // fixme(alex): do we need Record<never, never> here? Record<never, never>, ): Auth<Options> => { const authContext = init(options); const { api } = getEndpoints(authContext, options); const errorCodes = options.plugins?.reduce((acc, plugin) => { if (plugin.$ERROR_CODES) { return { ...acc, ...plugin.$ERROR_CODES, }; } return acc; }, {}); return { handler: async (request: Request) => { const ctx = await authContext; const basePath = ctx.options.basePath || "/api/auth"; if (!ctx.options.baseURL) { const baseURL = getBaseURL(undefined, basePath, request); if (baseURL) { ctx.baseURL = baseURL; ctx.options.baseURL = getOrigin(ctx.baseURL) || undefined; } else { throw new BetterAuthError( "Could not get base URL from request. Please provide a valid base URL.", ); } } ctx.trustedOrigins = [ ...(options.trustedOrigins ? Array.isArray(options.trustedOrigins) ? options.trustedOrigins : await options.trustedOrigins(request) : []), ctx.options.baseURL!, ]; const { handler } = router(ctx, options); return runWithAdapter(ctx.adapter, () => handler(request)); }, api, options: options, $context: authContext, $ERROR_CODES: { ...errorCodes, ...BASE_ERROR_CODES, }, } as any; }; export type Auth<Options extends BetterAuthOptions = BetterAuthOptions> = { handler: (request: Request) => Promise<Response>; api: InferAPI<ReturnType<typeof router<Options>>["endpoints"]>; options: Options; $ERROR_CODES: InferPluginErrorCodes<Options> & typeof BASE_ERROR_CODES; $context: Promise<AuthContext>; /** * Share types */ $Infer: InferPluginTypes<Options> extends { Session: any; } ? InferPluginTypes<Options> : { Session: { session: PrettifyDeep<InferSession<Options>>; user: PrettifyDeep<InferUser<Options>>; }; } & InferPluginTypes<Options>; }; ``` -------------------------------------------------------------------------------- /demo/nextjs/app/device/page.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useState, useTransition } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { client } from "@/lib/auth-client"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2 } from "lucide-react"; export default function DeviceAuthorizationPage() { const router = useRouter(); const params = useSearchParams(); const user_code = params.get("user_code"); const [userCode, setUserCode] = useState<string>(user_code ? user_code : ""); const [isPending, startTransition] = useTransition(); const [error, setError] = useState<string | null>(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(null); startTransition(async () => { try { const finalCode = userCode.trim().replaceAll(/-/g, "").toUpperCase(); // Get the device authorization status const response = await client.device({ query: { user_code: finalCode, }, }); if (response.data) { router.push(`/device/approve?user_code=${finalCode}`); } } catch (err: any) { setError( err.error?.message || "Invalid code. Please check and try again.", ); } }); }; return ( <div className="flex min-h-screen items-center justify-center p-4"> <Card className="w-full max-w-md p-6"> <div className="space-y-4"> <div className="text-center"> <h1 className="text-2xl font-bold">Device Authorization</h1> <p className="text-muted-foreground mt-2"> Enter the code displayed on your device </p> </div> <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="userCode">Device Code</Label> <Input id="userCode" type="text" placeholder="XXXX-XXXX" value={userCode} onChange={(e) => setUserCode(e.target.value)} className="text-center text-lg font-mono uppercase" maxLength={9} disabled={isPending} required /> </div> {error && ( <Alert variant="destructive"> <AlertDescription>{error}</AlertDescription> </Alert> )} <Button type="submit" className="w-full" disabled={isPending}> {isPending ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Verifying... </> ) : ( "Continue" )} </Button> </form> </div> </Card> </div> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/routes/index.ts: -------------------------------------------------------------------------------- ```typescript import type { apiKeySchema } from "../schema"; import type { ApiKey, ApiKeyOptions } from "../types"; import { createApiKey } from "./create-api-key"; import { deleteApiKey } from "./delete-api-key"; import { getApiKey } from "./get-api-key"; import { updateApiKey } from "./update-api-key"; import { verifyApiKey } from "./verify-api-key"; import { listApiKeys } from "./list-api-keys"; import { deleteAllExpiredApiKeysEndpoint } from "./delete-all-expired-api-keys"; import { API_KEY_TABLE_NAME } from ".."; import type { AuthContext } from "@better-auth/core"; export type PredefinedApiKeyOptions = ApiKeyOptions & Required< Pick< ApiKeyOptions, | "apiKeyHeaders" | "defaultKeyLength" | "keyExpiration" | "rateLimit" | "maximumPrefixLength" | "minimumPrefixLength" | "maximumNameLength" | "disableKeyHashing" | "minimumNameLength" | "requireName" | "enableMetadata" | "enableSessionForAPIKeys" | "startingCharactersConfig" > > & { keyExpiration: Required<ApiKeyOptions["keyExpiration"]>; startingCharactersConfig: Required< ApiKeyOptions["startingCharactersConfig"] >; }; let lastChecked: Date | null = null; export async function deleteAllExpiredApiKeys( ctx: AuthContext, byPassLastCheckTime = false, ): Promise<void> { if (lastChecked && !byPassLastCheckTime) { const now = new Date(); const diff = now.getTime() - lastChecked.getTime(); if (diff < 10000) { return; } } lastChecked = new Date(); await ctx.adapter .deleteMany({ model: API_KEY_TABLE_NAME, where: [ { field: "expiresAt" satisfies keyof ApiKey, operator: "lt", value: new Date(), }, { field: "expiresAt", operator: "ne", value: null, }, ], }) .catch((error) => { ctx.logger.error(`Failed to delete expired API keys:`, error); }); } export function createApiKeyRoutes({ keyGenerator, opts, schema, }: { keyGenerator: (options: { length: number; prefix: string | undefined; }) => Promise<string> | string; opts: PredefinedApiKeyOptions; schema: ReturnType<typeof apiKeySchema>; }) { return { createApiKey: createApiKey({ keyGenerator, opts, schema, deleteAllExpiredApiKeys, }), verifyApiKey: verifyApiKey({ opts, schema, deleteAllExpiredApiKeys }), getApiKey: getApiKey({ opts, schema, deleteAllExpiredApiKeys }), updateApiKey: updateApiKey({ opts, schema, deleteAllExpiredApiKeys }), deleteApiKey: deleteApiKey({ opts, schema, deleteAllExpiredApiKeys }), listApiKeys: listApiKeys({ opts, schema, deleteAllExpiredApiKeys }), deleteAllExpiredApiKeys: deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys, }), }; } ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/twitch.ts: -------------------------------------------------------------------------------- ```typescript import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { logger } from "../env"; import { createAuthorizationURL, validateAuthorizationCode, refreshAccessToken, } from "../oauth2"; import { decodeJwt } from "jose"; /** * @see https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#requesting-claims */ export interface TwitchProfile { /** * The sub of the user */ sub: string; /** * The preferred username of the user */ preferred_username: string; /** * The email of the user */ email: string; /** * Indicate if this user has a verified email. */ email_verified: boolean; /** * The picture of the user */ picture: string; } export interface TwitchOptions extends ProviderOptions<TwitchProfile> { clientId: string; claims?: string[]; } export const twitch = (options: TwitchOptions) => { return { id: "twitch", name: "Twitch", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["user:read:email", "openid"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return createAuthorizationURL({ id: "twitch", redirectURI, options, authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize", scopes: _scopes, state, claims: options.claims || [ "email", "email_verified", "preferred_username", "picture", ], }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://id.twitch.tv/oauth2/token", }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint: "https://id.twitch.tv/oauth2/token", }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const idToken = token.idToken; if (!idToken) { logger.error("No idToken found in token"); return null; } const profile = decodeJwt(idToken) as TwitchProfile; const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name: profile.preferred_username, email: profile.email, image: profile.picture, emailVerified: profile.email_verified, ...userMap, }, data: profile, }; }, options, } satisfies OAuthProvider<TwitchProfile>; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { APIError, sessionMiddleware } from "../../../api"; import { ERROR_CODES } from ".."; import type { apiKeySchema } from "../schema"; import type { ApiKey } from "../types"; import type { PredefinedApiKeyOptions } from "."; import { API_KEY_TABLE_NAME } from ".."; import type { AuthContext } from "@better-auth/core"; import { createAuthEndpoint } from "@better-auth/core/api"; export function deleteApiKey({ opts, schema, deleteAllExpiredApiKeys, }: { opts: PredefinedApiKeyOptions; schema: ReturnType<typeof apiKeySchema>; deleteAllExpiredApiKeys( ctx: AuthContext, byPassLastCheckTime?: boolean, ): void; }) { return createAuthEndpoint( "/api-key/delete", { method: "POST", body: z.object({ keyId: z.string().meta({ description: "The id of the Api Key", }), }), use: [sessionMiddleware], metadata: { openapi: { description: "Delete an existing API key", requestBody: { content: { "application/json": { schema: { type: "object", properties: { keyId: { type: "string", description: "The id of the API key to delete", }, }, required: ["keyId"], }, }, }, }, responses: { "200": { description: "API key deleted successfully", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean", description: "Indicates if the API key was successfully deleted", }, }, required: ["success"], }, }, }, }, }, }, }, }, async (ctx) => { const { keyId } = ctx.body; const session = ctx.context.session; if (session.user.banned === true) { throw new APIError("UNAUTHORIZED", { message: ERROR_CODES.USER_BANNED, }); } const apiKey = await ctx.context.adapter.findOne<ApiKey>({ model: API_KEY_TABLE_NAME, where: [ { field: "id", value: keyId, }, ], }); if (!apiKey || apiKey.userId !== session.user.id) { throw new APIError("NOT_FOUND", { message: ERROR_CODES.KEY_NOT_FOUND, }); } try { await ctx.context.adapter.delete<ApiKey>({ model: API_KEY_TABLE_NAME, where: [ { field: "id", value: apiKey.id, }, ], }); } catch (error: any) { throw new APIError("INTERNAL_SERVER_ERROR", { message: error?.message, }); } deleteAllExpiredApiKeys(ctx.context); return ctx.json({ success: true, }); }, ); } ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { Pressable } from "react-native"; import { cn } from "@/lib/utils"; import { TextClassContext } from "@/components/ui/text"; const buttonVariants = cva( "group flex items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2", { variants: { variant: { default: "bg-primary web:hover:opacity-90 active:opacity-90", destructive: "bg-destructive web:hover:opacity-90 active:opacity-90", outline: "border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent", secondary: "bg-secondary web:hover:opacity-80 active:opacity-80", ghost: "web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent", link: "web:underline-offset-4 web:hover:underline web:focus:underline ", }, size: { default: "h-10 px-4 py-2 native:h-12 native:px-5 native:py-3", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8 native:h-14", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); const buttonTextVariants = cva( "web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors", { variants: { variant: { default: "text-primary-foreground", destructive: "text-destructive-foreground", outline: "group-active:text-accent-foreground", secondary: "text-secondary-foreground group-active:text-secondary-foreground", ghost: "group-active:text-accent-foreground", link: "text-primary group-active:underline", }, size: { default: "", sm: "", lg: "native:text-lg", icon: "", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> & VariantProps<typeof buttonVariants>; const Button = React.forwardRef< React.ElementRef<typeof Pressable>, ButtonProps >(({ className, variant, size, ...props }, ref) => { return ( <TextClassContext.Provider value={buttonTextVariants({ variant, size, className: "web:pointer-events-none", })} > <Pressable className={cn( props.disabled && "opacity-50 web:pointer-events-none", buttonVariants({ variant, size, className }), )} ref={ref} role="button" {...props} /> </TextClassContext.Provider> ); }); Button.displayName = "Button"; export { Button, buttonTextVariants, buttonVariants }; export type { ButtonProps }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/lynx/index.ts: -------------------------------------------------------------------------------- ```typescript import { getClientConfig } from "../config"; import type { InferActions, InferClientAPI, InferErrorCodes, IsSignal, } from "../types"; import type { BetterAuthClientPlugin, BetterAuthClientOptions, } from "@better-auth/core"; import { createDynamicPathProxy } from "../proxy"; import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; import type { BetterFetchError, BetterFetchResponse, } from "@better-fetch/fetch"; import { useStore } from "./lynx-store"; import type { BASE_ERROR_CODES } from "@better-auth/core/error"; import type { SessionQueryParams } from "../types"; function getAtomKey(str: string) { return `use${capitalizeFirstLetter(str)}`; } export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { plugins: Array<infer Plugin>; } ? UnionToIntersection< Plugin extends BetterAuthClientPlugin ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms ? Atoms extends Record<string, any> ? { [key in keyof Atoms as IsSignal<key> extends true ? never : key extends string ? `use${Capitalize<key>}` : never]: () => ReturnType<Atoms[key]["get"]>; } : {} : {} : {} > : {}; export function createAuthClient<Option extends BetterAuthClientOptions>( options?: Option, ) { const { pluginPathMethods, pluginsActions, pluginsAtoms, $fetch, $store, atomListeners, } = getClientConfig(options); let resolvedHooks: Record<string, any> = {}; for (const [key, value] of Object.entries(pluginsAtoms)) { resolvedHooks[getAtomKey(key)] = () => useStore(value); } const routes = { ...pluginsActions, ...resolvedHooks, $fetch, $store, }; const proxy = createDynamicPathProxy( routes, $fetch, pluginPathMethods, pluginsAtoms, atomListeners, ); type ClientAPI = InferClientAPI<Option>; type Session = ClientAPI extends { getSession: () => Promise<infer Res>; } ? Res extends BetterFetchResponse<infer S> ? S : Res : never; return proxy as UnionToIntersection<InferResolvedHooks<Option>> & ClientAPI & InferActions<Option> & { useSession: () => { data: Session; isPending: boolean; error: BetterFetchError | null; refetch: (queryParams?: { query?: SessionQueryParams }) => void; }; $Infer: { Session: NonNullable<Session>; }; $fetch: typeof $fetch; $store: typeof $store; $ERROR_CODES: PrettifyDeep< InferErrorCodes<Option> & typeof BASE_ERROR_CODES >; }; } export { useStore }; export type * from "@better-fetch/fetch"; export type * from "nanostores"; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/microsoft.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Microsoft description: Microsoft provider setup and usage. --- Enabling OAuth with Microsoft Azure Entra ID (formerly Active Directory) allows your users to sign in and sign up to your application with their Microsoft account. <Steps> <Step> ### Get your Microsoft credentials To use Microsoft as a social provider, you need to get your Microsoft credentials. Which involves generating your own Client ID and Client Secret using your Microsoft Entra ID dashboard account. Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/microsoft` for local development. For production, you should change it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. see the [Microsoft Entra ID documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more information. </Step> <Step> ### Configure the provider To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.microsoft` in your auth configuration. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { microsoft: { // [!code highlight] clientId: process.env.MICROSOFT_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string, // [!code highlight] // Optional tenantId: 'common', // [!code highlight] authority: "https://login.microsoftonline.com", // Authentication authority URL // [!code highlight] prompt: "select_account", // Forces account selection // [!code highlight] }, // [!code highlight] }, }) ``` **Authority URL**: Use the default `https://login.microsoftonline.com` for standard Entra ID scenarios or `https://<tenant-id>.ciamlogin.com` for CIAM (Customer Identity and Access Management) scenarios. </Step> </Steps> ## Sign In with Microsoft To sign in with Microsoft, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `microsoft`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; const authClient = createAuthClient(); const signIn = async () => { const data = await authClient.signIn.social({ provider: "microsoft", callbackURL: "/dashboard", // The URL to redirect to after the sign in }); }; ``` ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/linear.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, } from "../oauth2"; interface LinearUser { id: string; name: string; email: string; avatarUrl?: string; active: boolean; createdAt: string; updatedAt: string; } export interface LinearProfile { data: { viewer: LinearUser; }; } export interface LinearOptions extends ProviderOptions<LinearUser> { clientId: string; } export const linear = (options: LinearOptions) => { const tokenEndpoint = "https://api.linear.app/oauth/token"; return { id: "linear", name: "Linear", createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["read"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return createAuthorizationURL({ id: "linear", options, authorizationEndpoint: "https://linear.app/oauth/authorize", scopes: _scopes, state, redirectURI, loginHint, }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint, }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret, }, tokenEndpoint, }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<LinearProfile>( "https://api.linear.app/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token.accessToken}`, }, body: JSON.stringify({ query: ` query { viewer { id name email avatarUrl active createdAt updatedAt } } `, }), }, ); if (error || !profile?.data?.viewer) { return null; } const userData = profile.data.viewer; const userMap = await options.mapProfileToUser?.(userData); return { user: { id: profile.data.viewer.id, name: profile.data.viewer.name, email: profile.data.viewer.email, image: profile.data.viewer.avatarUrl, emailVerified: true, ...userMap, }, data: userData, }; }, options, } satisfies OAuthProvider<LinearUser>; }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/pagination.tsx: -------------------------------------------------------------------------------- ```typescript import * as React from "react"; import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon, } from "@radix-ui/react-icons"; import { cn } from "@/lib/utils"; import { ButtonProps, buttonVariants } from "@/components/ui/button"; const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( <nav role="navigation" aria-label="pagination" className={cn("mx-auto flex w-full justify-center", className)} {...props} /> ); Pagination.displayName = "Pagination"; const PaginationContent = ({ ref, className, ...props }: React.ComponentProps<"ul"> & { ref: React.RefObject<HTMLUListElement>; }) => ( <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} /> ); PaginationContent.displayName = "PaginationContent"; const PaginationItem = ({ ref, className, ...props }: React.ComponentProps<"li"> & { ref: React.RefObject<HTMLLIElement>; }) => <li ref={ref} className={cn("", className)} {...props} />; PaginationItem.displayName = "PaginationItem"; type PaginationLinkProps = { isActive?: boolean; } & Pick<ButtonProps, "size"> & React.ComponentProps<"a">; const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => ( <a aria-current={isActive ? "page" : undefined} className={cn( buttonVariants({ variant: isActive ? "outline" : "ghost", size, }), className, )} {...props} /> ); PaginationLink.displayName = "PaginationLink"; const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( <PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props} > <ChevronLeftIcon className="h-4 w-4" /> <span>Previous</span> </PaginationLink> ); PaginationPrevious.displayName = "PaginationPrevious"; const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( <PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props} > <span>Next</span> <ChevronRightIcon className="h-4 w-4" /> </PaginationLink> ); PaginationNext.displayName = "PaginationNext"; const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( <span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props} > <DotsHorizontalIcon className="h-4 w-4" /> <span className="sr-only">More pages</span> </span> ); PaginationEllipsis.displayName = "PaginationEllipsis"; export { Pagination, PaginationContent, PaginationLink, PaginationItem, PaginationPrevious, PaginationNext, PaginationEllipsis, }; ``` -------------------------------------------------------------------------------- /docs/scripts/endpoint-to-doc/input.ts: -------------------------------------------------------------------------------- ```typescript //@ts-nocheck import { createAuthEndpoint, sessionMiddleware, referenceMiddleware, } from "./index"; import { z } from "zod"; export const restoreSubscription = createAuthEndpoint( "/subscription/restore", { method: "POST", body: z.object({ referenceId: z .string({ description: "Reference id of the subscription to restore. Eg: '123'", }) .optional(), subscriptionId: z.string({ description: "The id of the subscription to restore. Eg: 'sub_123'", }), }), use: [sessionMiddleware, referenceMiddleware("restore-subscription")], }, async (ctx) => { const referenceId = ctx.body?.referenceId || ctx.context.session.user.id; const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne<Subscription>({ model: "subscription", where: [ { field: "id", value: ctx.body.subscriptionId, }, ], }) : await ctx.context.adapter .findMany<Subscription>({ model: "subscription", where: [ { field: "referenceId", value: referenceId, }, ], }) .then((subs) => subs.find( (sub) => sub.status === "active" || sub.status === "trialing", ), ); if (!subscription || !subscription.stripeCustomerId) { throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, }); } if (subscription.status != "active" && subscription.status != "trialing") { throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE, }); } if (!subscription.cancelAtPeriodEnd) { throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION, }); } const activeSubscription = await client.subscriptions .list({ customer: subscription.stripeCustomerId, }) .then( (res) => res.data.filter( (sub) => sub.status === "active" || sub.status === "trialing", )[0], ); if (!activeSubscription) { throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND, }); } try { const newSub = await client.subscriptions.update(activeSubscription.id, { cancel_at_period_end: false, }); await ctx.context.adapter.update({ model: "subscription", update: { cancelAtPeriodEnd: false, updatedAt: new Date(), }, where: [ { field: "id", value: subscription.id, }, ], }); return ctx.json(newSub); } catch (error) { ctx.context.logger.error("Error restoring subscription", error); throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER, }); } }, ); ```