This is page 9 of 51. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/public/branding/better-auth-logo-wordmark-dark.svg: -------------------------------------------------------------------------------- ``` <svg width="1024" height="256" viewBox="0 0 1024 256" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect width="1024" height="256" fill="black"/> <rect x="96" y="79" width="34.6988" height="97.5904" fill="white"/> <rect x="203.133" y="79" width="36.8675" height="97.5904" fill="white"/> <rect x="238.916" y="79" width="31.4458" height="69.6144" transform="rotate(90 238.916 79)" fill="white"/> <rect x="240" y="145.145" width="31.4458" height="70.6988" transform="rotate(90 240 145.145)" fill="white"/> <rect x="169.301" y="110.446" width="34.6988" height="38.6024" transform="rotate(90 169.301 110.446)" fill="white"/> <path d="M281.832 162V93.84H305.256C313.32 93.84 319.368 95.312 323.4 98.256C327.432 101.2 329.448 105.84 329.448 112.176C329.448 116.016 328.36 119.248 326.184 121.872C324.072 124.432 321.128 126.064 317.352 126.768C322.024 127.408 325.672 129.232 328.296 132.24C330.984 135.184 332.328 138.864 332.328 143.28C332.328 149.488 330.312 154.16 326.28 157.296C322.248 160.432 316.52 162 309.096 162H281.832ZM290.088 123.312H305.256C310.248 123.312 314.088 122.384 316.776 120.528C319.464 118.608 320.808 115.952 320.808 112.56C320.808 105.456 315.624 101.904 305.256 101.904H290.088V123.312ZM290.088 153.936H309.096C313.768 153.936 317.352 152.976 319.848 151.056C322.408 149.136 323.688 146.384 323.688 142.8C323.688 139.216 322.408 136.432 319.848 134.448C317.352 132.4 313.768 131.376 309.096 131.376H290.088V153.936ZM345.301 162V93.84H388.117V101.904H353.557V123.888H386.965V131.76H353.557V153.936H388.885V162H345.301ZM416.681 162V101.904H395.465V93.84H446.153V101.904H424.937V162H416.681ZM470.587 162V101.904H449.371V93.84H500.059V101.904H478.843V162H470.587ZM507.113 162V93.84H549.929V101.904H515.369V123.888H548.777V131.76H515.369V153.936H550.697V162H507.113ZM564.02 162V93.84H589.844C597.012 93.84 602.676 95.696 606.836 99.408C610.996 103.12 613.076 108.144 613.076 114.48C613.076 117.104 612.532 119.504 611.444 121.68C610.356 123.792 608.948 125.584 607.22 127.056C605.492 128.528 603.604 129.552 601.556 130.128C604.564 130.64 606.932 131.856 608.66 133.776C610.452 135.696 611.508 138.416 611.828 141.936L613.748 162H605.396L603.667 142.8C603.412 139.984 602.388 137.904 600.596 136.56C598.868 135.216 596.02 134.544 592.052 134.544H572.276V162H564.02ZM572.276 126.48H590.9C595.06 126.48 598.356 125.424 600.788 123.312C603.22 121.2 604.436 118.192 604.436 114.288C604.436 110.32 603.188 107.28 600.692 105.168C598.196 102.992 594.58 101.904 589.844 101.904H572.276V126.48ZM623.912 137.808V130.224H655.688V137.808H623.912ZM661.826 162L686.402 93.84H697.538L722.114 162H713.09L706.274 142.608H677.666L670.85 162H661.826ZM680.45 134.544H703.49L691.97 101.04L680.45 134.544ZM755.651 163.536C750.403 163.536 745.827 162.512 741.923 160.464C738.083 158.416 735.107 155.504 732.995 151.728C730.947 147.888 729.923 143.376 729.923 138.192V93.744H738.179V138.192C738.179 143.696 739.683 147.952 742.691 150.96C745.763 153.968 750.083 155.472 755.651 155.472C761.155 155.472 765.411 153.968 768.419 150.96C771.491 147.952 773.027 143.696 773.027 138.192V93.744H781.283V138.192C781.283 143.376 780.227 147.888 778.115 151.728C776.067 155.504 773.123 158.416 769.283 160.464C765.443 162.512 760.899 163.536 755.651 163.536ZM811.087 162V101.904H789.871V93.84H840.559V101.904H819.343V162H811.087ZM847.613 162V93.84H855.869V123.696H890.141V93.84H898.397V162H890.141V131.76H855.869V162H847.613ZM911.443 162V151.152H922.291V162H911.443Z" fill="white"/> </svg> ``` -------------------------------------------------------------------------------- /docs/public/branding/better-auth-logo-wordmark-light.svg: -------------------------------------------------------------------------------- ``` <svg width="1024" height="256" viewBox="0 0 1024 256" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect width="1024" height="256" fill="#FFEAEA"/> <rect x="96" y="79" width="34.6988" height="97.5904" fill="black"/> <rect x="203.133" y="79" width="36.8675" height="97.5904" fill="black"/> <rect x="238.916" y="79" width="31.4458" height="69.6144" transform="rotate(90 238.916 79)" fill="black"/> <rect x="240" y="145.145" width="31.4458" height="70.6988" transform="rotate(90 240 145.145)" fill="black"/> <rect x="169.301" y="110.446" width="34.6988" height="38.6024" transform="rotate(90 169.301 110.446)" fill="black"/> <path d="M281.832 162V93.84H305.256C313.32 93.84 319.368 95.312 323.4 98.256C327.432 101.2 329.448 105.84 329.448 112.176C329.448 116.016 328.36 119.248 326.184 121.872C324.072 124.432 321.128 126.064 317.352 126.768C322.024 127.408 325.672 129.232 328.296 132.24C330.984 135.184 332.328 138.864 332.328 143.28C332.328 149.488 330.312 154.16 326.28 157.296C322.248 160.432 316.52 162 309.096 162H281.832ZM290.088 123.312H305.256C310.248 123.312 314.088 122.384 316.776 120.528C319.464 118.608 320.808 115.952 320.808 112.56C320.808 105.456 315.624 101.904 305.256 101.904H290.088V123.312ZM290.088 153.936H309.096C313.768 153.936 317.352 152.976 319.848 151.056C322.408 149.136 323.688 146.384 323.688 142.8C323.688 139.216 322.408 136.432 319.848 134.448C317.352 132.4 313.768 131.376 309.096 131.376H290.088V153.936ZM345.301 162V93.84H388.117V101.904H353.557V123.888H386.965V131.76H353.557V153.936H388.885V162H345.301ZM416.681 162V101.904H395.465V93.84H446.153V101.904H424.937V162H416.681ZM470.587 162V101.904H449.371V93.84H500.059V101.904H478.843V162H470.587ZM507.113 162V93.84H549.929V101.904H515.369V123.888H548.777V131.76H515.369V153.936H550.697V162H507.113ZM564.02 162V93.84H589.844C597.012 93.84 602.676 95.696 606.836 99.408C610.996 103.12 613.076 108.144 613.076 114.48C613.076 117.104 612.532 119.504 611.444 121.68C610.356 123.792 608.948 125.584 607.22 127.056C605.492 128.528 603.604 129.552 601.556 130.128C604.564 130.64 606.932 131.856 608.66 133.776C610.452 135.696 611.508 138.416 611.828 141.936L613.748 162H605.396L603.667 142.8C603.412 139.984 602.388 137.904 600.596 136.56C598.868 135.216 596.02 134.544 592.052 134.544H572.276V162H564.02ZM572.276 126.48H590.9C595.06 126.48 598.356 125.424 600.788 123.312C603.22 121.2 604.436 118.192 604.436 114.288C604.436 110.32 603.188 107.28 600.692 105.168C598.196 102.992 594.58 101.904 589.844 101.904H572.276V126.48ZM623.912 137.808V130.224H655.688V137.808H623.912ZM661.826 162L686.402 93.84H697.538L722.114 162H713.09L706.274 142.608H677.666L670.85 162H661.826ZM680.45 134.544H703.49L691.97 101.04L680.45 134.544ZM755.651 163.536C750.403 163.536 745.827 162.512 741.923 160.464C738.083 158.416 735.107 155.504 732.995 151.728C730.947 147.888 729.923 143.376 729.923 138.192V93.744H738.179V138.192C738.179 143.696 739.683 147.952 742.691 150.96C745.763 153.968 750.083 155.472 755.651 155.472C761.155 155.472 765.411 153.968 768.419 150.96C771.491 147.952 773.027 143.696 773.027 138.192V93.744H781.283V138.192C781.283 143.376 780.227 147.888 778.115 151.728C776.067 155.504 773.123 158.416 769.283 160.464C765.443 162.512 760.899 163.536 755.651 163.536ZM811.087 162V101.904H789.871V93.84H840.559V101.904H819.343V162H811.087ZM847.613 162V93.84H855.869V123.696H890.141V93.84H898.397V162H890.141V131.76H855.869V162H847.613ZM911.443 162V151.152H922.291V162H911.443Z" fill="black"/> </svg> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/custom-session/index.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { createAuthEndpoint, createAuthMiddleware, } from "@better-auth/core/api"; import { getSession } from "../../api"; import type { InferSession, InferUser } from "../../types"; import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core"; import { getEndpointResponse } from "../../utils/plugin-helper"; import type { GenericEndpointContext } from "@better-auth/core"; const getSessionQuerySchema = z.optional( z.object({ /** * If cookie cache is enabled, it will disable the cache * and fetch the session from the database */ disableCookieCache: z .boolean() .meta({ description: "Disable cookie cache and fetch session from database", }) .or(z.string().transform((v) => v === "true")) .optional(), disableRefresh: z .boolean() .meta({ description: "Disable session refresh. Useful for checking session status, without updating the session", }) .optional(), }), ); export type CustomSessionPluginOptions = { /** * This option is used to determine if the list-device-sessions endpoint should be mutated to the custom session data. * @default false */ shouldMutateListDeviceSessionsEndpoint?: boolean; }; export const customSession = < Returns extends Record<string, any>, O extends BetterAuthOptions = BetterAuthOptions, >( fn: ( session: { user: InferUser<O>; session: InferSession<O>; }, ctx: GenericEndpointContext, ) => Promise<Returns>, options?: O, pluginOptions?: CustomSessionPluginOptions, ) => { return { id: "custom-session", hooks: { after: [ { matcher: (ctx) => ctx.path === "/multi-session/list-device-sessions" && (pluginOptions?.shouldMutateListDeviceSessionsEndpoint ?? false), handler: createAuthMiddleware(async (ctx) => { const response = await getEndpointResponse<[]>(ctx); if (!response) return; const newResponse = await Promise.all( response.map(async (v) => await fn(v, ctx)), ); return ctx.json(newResponse); }), }, ], }, endpoints: { getSession: createAuthEndpoint( "/get-session", { method: "GET", query: getSessionQuerySchema, metadata: { CUSTOM_SESSION: true, openapi: { description: "Get custom session data", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", nullable: true, items: { $ref: "#/components/schemas/Session", }, }, }, }, }, }, }, }, requireHeaders: true, }, async (ctx): Promise<Returns | null> => { const session = await getSession()({ ...ctx, asResponse: false, headers: ctx.headers, returnHeaders: true, }).catch((e) => { return null; }); if (!session?.response) { return ctx.json(null); } const fnResult = await fn(session.response as any, ctx); const setCookie = session.headers.get("set-cookie"); if (setCookie) { ctx.setHeader("set-cookie", setCookie); session.headers.delete("set-cookie"); } session.headers.forEach((value, key) => { ctx.setHeader(key, value); }); return ctx.json(fnResult); }, ), }, $Infer: { Session: {} as Awaited<ReturnType<typeof fn>>, }, } satisfies BetterAuthPlugin; }; ``` -------------------------------------------------------------------------------- /demo/nextjs/components/account-switch.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandGroup, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; import { ChevronDown, PlusCircle } from "lucide-react"; import { Session } from "@/lib/auth-types"; import { client, useSession } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; export default function AccountSwitcher({ sessions }: { sessions: Session[] }) { const { data: currentUser } = useSession(); const [open, setOpen] = useState(false); const router = useRouter(); return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} aria-label="Select a user" className="w-[250px] justify-between" > <Avatar className="mr-2 h-6 w-6"> <AvatarImage src={currentUser?.user.image || undefined} alt={currentUser?.user.name} /> <AvatarFallback>{currentUser?.user.name.charAt(0)}</AvatarFallback> </Avatar> {currentUser?.user.name} <ChevronDown className="ml-auto h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-[250px] p-0"> <Command> <CommandList> <CommandGroup heading="Current Account"> <CommandItem onSelect={() => {}} className="text-sm w-full justify-between" key={currentUser?.user.id} > <div className="flex items-center"> <Avatar className="mr-2 h-5 w-5"> <AvatarImage src={currentUser?.user.image || undefined} alt={currentUser?.user.name} /> <AvatarFallback> {currentUser?.user.name.charAt(0)} </AvatarFallback> </Avatar> {currentUser?.user.name} </div> </CommandItem> </CommandGroup> <CommandSeparator /> <CommandGroup heading="Switch Account"> {sessions .filter((s) => s.user.id !== currentUser?.user.id) .map((u, i) => ( <CommandItem key={i} onSelect={async () => { await client.multiSession.setActive({ sessionToken: u.session.token, }); setOpen(false); }} className="text-sm" > <Avatar className="mr-2 h-5 w-5"> <AvatarImage src={u.user.image || undefined} alt={u.user.name} /> <AvatarFallback>{u.user.name.charAt(0)}</AvatarFallback> </Avatar> <div className="flex items-center justify-between w-full"> <div> <p>{u.user.name}</p> <p className="text-xs">({u.user.email})</p> </div> </div> </CommandItem> ))} </CommandGroup> </CommandList> <CommandSeparator /> <CommandList> <CommandGroup> <CommandItem onSelect={() => { router.push("/sign-in"); setOpen(false); }} className="cursor-pointer text-sm" > <PlusCircle className="mr-2 h-5 w-5" /> Add Account </CommandItem> </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> ); } ``` -------------------------------------------------------------------------------- /demo/nextjs/app/oauth/authorize/page.tsx: -------------------------------------------------------------------------------- ```typescript import { Metadata } from "next"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { ArrowLeftRight, ArrowUpRight, Mail, Users } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Logo } from "@/components/logo"; import Image from "next/image"; import { ConsentBtns } from "./concet-buttons"; export const metadata: Metadata = { title: "Authorize Application", description: "Grant access to your account", }; interface AuthorizePageProps { searchParams: Promise<{ redirect_uri: string; scope: string; cancel_uri: string; client_id: string; }>; } export default async function AuthorizePage({ searchParams, }: AuthorizePageProps) { const { redirect_uri, scope, client_id, cancel_uri } = await searchParams; const session = await auth.api.getSession({ headers: await headers(), }); // @ts-expect-error const clientDetails = await auth.api.getOAuthClient({ params: { id: client_id, }, headers: await headers(), }); return ( <div className="container mx-auto py-10"> <h1 className="text-2xl font-bold mb-6 text-center"> Authorize Application </h1> <div className="min-h-screen bg-black text-white flex flex-col"> <div className="flex flex-col items-center justify-center max-w-2xl mx-auto px-4"> <div className="flex items-center gap-8 mb-8"> <div className="w-16 h-16 border rounded-full flex items-center justify-center"> {clientDetails.icon ? ( <Image src={clientDetails.icon} alt="App Logo" className="object-cover" width={64} height={64} /> ) : ( <Logo /> )} </div> <ArrowLeftRight className="h-6 w-6" /> <div className="w-16 h-16 rounded-full overflow-hidden"> <Avatar className="hidden h-16 w-16 sm:flex "> <AvatarImage src={session?.user.image || "#"} alt="Avatar" className="object-cover" /> <AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback> </Avatar> </div> </div> <h1 className="text-3xl font-semibold text-center mb-8"> {clientDetails.name} is requesting access to your Better Auth account </h1> <Card className="w-full bg-zinc-900 border-zinc-800 rounded-none"> <CardContent className="p-6"> <div className="flex items-center justify-between p-4 bg-zinc-800 rounded-lg mb-6"> <div> <div className="font-medium">{session?.user.name}</div> <div className="text-zinc-400">{session?.user.email}</div> </div> <ArrowUpRight className="h-5 w-5 text-zinc-400" /> </div> <div className="flex flex-col gap-1"> <div className="text-lg mb-4"> Continuing will allow Sign in with {clientDetails.name} to: </div> {scope.includes("profile") && ( <div className="flex items-center gap-3 text-zinc-300"> <Users className="h-5 w-5" /> <span>Read your Better Auth user data.</span> </div> )} {scope.includes("email") && ( <div className="flex items-center gap-3 text-zinc-300"> <Mail className="h-5 w-5" /> <span>Read your email address.</span> </div> )} </div> </CardContent> <ConsentBtns /> </Card> </div> </div> </div> ); } ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/index.tsx: -------------------------------------------------------------------------------- ```typescript import Ionicons from "@expo/vector-icons/AntDesign"; import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Text } from "@/components/ui/text"; import { authClient } from "@/lib/auth-client"; import { Image, View } from "react-native"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; import { useEffect, useState } from "react"; import { router, useNavigationContainerRef } from "expo-router"; import { useStore } from "@nanostores/react"; export default function Index() { const { data: isAuthenticated } = useStore(authClient.useSession); const navContainerRef = useNavigationContainerRef(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); useEffect(() => { if (isAuthenticated) { if (navContainerRef.isReady()) { router.push("/dashboard"); } } }, [isAuthenticated, navContainerRef.isReady()]); return ( <Card className="z-50 mx-6 backdrop-blur-lg bg-gray-200/70"> <CardHeader className="flex items-center justify-center gap-8"> <Image source={require("../../assets/images/logo.png")} style={{ width: 40, height: 40, }} /> <CardTitle>Sign In to your account</CardTitle> </CardHeader> <View className="px-6 flex gap-2"> <Button onPress={() => { authClient.signIn.social({ provider: "google", callbackURL: "/dashboard", }); }} variant="secondary" className="flex flex-row gap-2 items-center bg-white/50" > <Ionicons name="google" size={16} /> <Text>Sign In with Google</Text> </Button> <Button variant="secondary" className="flex flex-row gap-2 items-center bg-white/50" onPress={() => { authClient.signIn.social({ provider: "github", callbackURL: "/dashboard", }); }} > <Ionicons name="github" size={16} /> <Text>Sign In with GitHub</Text> </Button> </View> <View className="flex-row gap-2 w-full items-center px-6 my-4"> <Separator className="flex-grow w-3/12" /> <Text>or continue with</Text> <Separator className="flex-grow w-3/12" /> </View> <View className="px-6"> <Input placeholder="Email Address" className="rounded-b-none border-b-0" value={email} onChangeText={(text) => { setEmail(text); }} /> <Input placeholder="Password" className="rounded-t-none" secureTextEntry value={password} onChangeText={(text) => { setPassword(text); }} /> </View> <CardFooter> <View className="w-full"> <Button variant="link" className="w-full" onPress={() => { router.push("/forget-password"); }} > <Text className="underline text-center">Forget Password?</Text> </Button> <Button onPress={() => { authClient.signIn.email( { email, password, }, { onError: (ctx) => { alert(ctx.error.message); }, }, ); }} > <Text>Continue</Text> </Button> <Text className="text-center mt-2"> Don't have an account?{" "} <Text className="underline" onPress={() => { router.push("/sign-up"); }} > Create Account </Text> </Text> </View> </CardFooter> </Card> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/types.ts: -------------------------------------------------------------------------------- ```typescript import type { BetterAuthPlugin } from "@better-auth/core"; import type { StripEmptyObjects, UnionToIntersection } from "../types/helper"; import type { Auth } from "../auth"; import type { InferRoutes } from "./path-to-object"; import type { Session, User } from "../types"; import type { InferFieldsInputClient, InferFieldsOutput } from "../db"; import type { ClientStore, ClientAtomListener, BetterAuthClientOptions, BetterAuthClientPlugin, } from "@better-auth/core"; export type { ClientStore, ClientAtomListener, BetterAuthClientOptions, BetterAuthClientPlugin, }; /** * @deprecated use type `BetterAuthClientOptions` instead. */ export type Store = ClientStore; /** * @deprecated use type `ClientAtomListener` instead. */ export type AtomListener = ClientAtomListener; /** * @deprecated use type `BetterAuthClientPlugin` instead. */ export type ClientOptions = BetterAuthClientOptions; export type InferClientAPI<O extends BetterAuthClientOptions> = InferRoutes< O["plugins"] extends Array<any> ? Auth["api"] & (O["plugins"] extends Array<infer Pl> ? UnionToIntersection< Pl extends { $InferServerPlugin: infer Plug; } ? Plug extends { endpoints: infer Endpoints; } ? Endpoints : {} : {} > : {}) : Auth["api"], O >; export type InferActions<O extends BetterAuthClientOptions> = (O["plugins"] extends Array<infer Plugin> ? UnionToIntersection< Plugin extends BetterAuthClientPlugin ? Plugin["getActions"] extends (...args: any) => infer Actions ? Actions : {} : {} > : {}) & //infer routes from auth config InferRoutes< O["$InferAuth"] extends { plugins: infer Plugins; } ? Plugins extends Array<infer Plugin> ? Plugin extends { endpoints: infer Endpoints; } ? Endpoints : {} : {} : {}, O >; export type InferErrorCodes<O extends BetterAuthClientOptions> = O["plugins"] extends Array<infer Plugin> ? UnionToIntersection< Plugin extends BetterAuthClientPlugin ? Plugin["$InferServerPlugin"] extends BetterAuthPlugin ? Plugin["$InferServerPlugin"]["$ERROR_CODES"] : {} : {} > : {}; /** * signals are just used to recall a computed value. * as a convention they start with "$" */ export type IsSignal<T> = T extends `$${infer _}` ? true : false; export type InferPluginsFromClient<O extends BetterAuthClientOptions> = O["plugins"] extends Array<BetterAuthClientPlugin> ? Array<O["plugins"][number]["$InferServerPlugin"]> : undefined; export type InferSessionFromClient<O extends BetterAuthClientOptions> = StripEmptyObjects< Session & UnionToIntersection<InferAdditionalFromClient<O, "session", "output">> >; export type InferUserFromClient<O extends BetterAuthClientOptions> = StripEmptyObjects< User & UnionToIntersection<InferAdditionalFromClient<O, "user", "output">> >; export type InferAdditionalFromClient< Options extends BetterAuthClientOptions, Key extends string, Format extends "input" | "output" = "output", > = Options["plugins"] extends Array<infer T> ? T extends BetterAuthClientPlugin ? T["$InferServerPlugin"] extends { schema: { [key in Key]: { fields: infer Field; }; }; } ? Format extends "input" ? InferFieldsInputClient<Field> : InferFieldsOutput<Field> : {} : {} : {}; export type SessionQueryParams = { disableCookieCache?: boolean; disableRefresh?: boolean; }; ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-up.test.ts: -------------------------------------------------------------------------------- ```typescript import { afterEach, describe, expect, vi } from "vitest"; import { getTestInstance } from "../../test-utils/test-instance"; describe("sign-up with custom fields", async (it) => { const mockFn = vi.fn(); const { auth, db } = await getTestInstance( { account: { fields: { providerId: "provider_id", accountId: "account_id", }, }, user: { additionalFields: { newField: { type: "string", required: false, }, newField2: { type: "string", required: false, }, isAdmin: { type: "boolean", defaultValue: true, input: false, }, role: { input: false, type: "string", required: false, }, }, }, emailVerification: { sendOnSignUp: true, sendVerificationEmail: mockFn, }, }, { disableTestUser: true, }, ); afterEach(() => { mockFn.mockReset(); }); it("should work with custom fields on account table", async () => { const res = await auth.api.signUpEmail({ body: { email: "[email protected]", password: "password", name: "Test Name", image: "https://picsum.photos/200", }, }); expect(res.token).toBeDefined(); const users = await db.findMany({ model: "user", }); const accounts = await db.findMany({ model: "account", }); expect(accounts).toHaveLength(1); expect("isAdmin" in (users[0] as any)).toBe(true); expect((users[0] as any).isAdmin).toBe(true); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith( expect.objectContaining({ token: expect.any(String), url: expect.any(String), user: expect.any(Object), }), ); }); it("should get the ipAddress and userAgent from headers", async () => { const res = await auth.api.signUpEmail({ body: { email: "[email protected]", password: "password", name: "Test Name", }, headers: new Headers({ "x-forwarded-for": "127.0.0.1", "user-agent": "test-user-agent", }), }); const session = await auth.api.getSession({ headers: new Headers({ authorization: `Bearer ${res.token}`, }), }); expect(session).toBeDefined(); expect(session!.session).toMatchObject({ userAgent: "test-user-agent", ipAddress: "127.0.0.1", }); }); it("should rollback when session creation fails", async ({ skip }) => { const ctx = await auth.$context; if (!ctx.adapter.options?.adapterConfig.transaction) { skip(); } const originalCreateSession = ctx.internalAdapter.createSession; ctx.internalAdapter.createSession = vi .fn() .mockRejectedValue(new Error("Session creation failed")); await expect( auth.api.signUpEmail({ body: { email: "[email protected]", password: "password", name: "Rollback Test", }, }), ).rejects.toThrow(); const users = await db.findMany({ model: "user" }); const rollbackUser = users.find( (u: any) => u.email === "[email protected]", ); expect(rollbackUser).toBeUndefined(); ctx.internalAdapter.createSession = originalCreateSession; }); it("should not allow user to set the field that is set to input: false", async () => { const res = await auth.api.signUpEmail({ body: { email: "[email protected]", password: "password", name: "Input False Test", //@ts-expect-error role: "admin", }, }); const session = await auth.api.getSession({ headers: new Headers({ authorization: `Bearer ${res.token}`, }), }); expect(session?.user.role).toBeNull(); }); }); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/error.ts: -------------------------------------------------------------------------------- ```typescript import { HIDE_METADATA } from "../../utils/hide-metadata"; import { createAuthEndpoint } from "@better-auth/core/api"; function sanitize(input: string): string { return input .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const html = (errorCode: string = "Unknown") => `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Authentication Error</title> <style> :root { --bg-color: #f8f9fa; --text-color: #212529; --accent-color: #000000; --error-color: #dc3545; --border-color: #e9ecef; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; line-height: 1.5; } .error-container { background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); padding: 2.5rem; text-align: center; max-width: 90%; width: 400px; } h1 { color: var(--error-color); font-size: 1.75rem; margin-bottom: 1rem; font-weight: 600; } p { margin-bottom: 1.5rem; color: #495057; } .btn { background-color: var(--accent-color); color: #ffffff; text-decoration: none; padding: 0.75rem 1.5rem; border-radius: 6px; transition: all 0.3s ease; display: inline-block; font-weight: 500; border: 2px solid var(--accent-color); } .btn:hover { background-color: #131721; } .error-code { font-size: 0.875rem; color: #6c757d; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color); } .icon { font-size: 3rem; margin-bottom: 1rem; } </style> </head> <body> <div class="error-container"> <div class="icon">⚠️</div> <h1>Better Auth Error</h1> <p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p> <a href="/" id="returnLink" class="btn">Return to Application</a> <div class="error-code">Error Code: <span id="errorCode">${sanitize( errorCode, )}</span></div> </div> </body> </html>`; export const error = createAuthEndpoint( "/error", { method: "GET", metadata: { ...HIDE_METADATA, openapi: { description: "Displays an error page", responses: { "200": { description: "Success", content: { "text/html": { schema: { type: "string", description: "The HTML content of the error page", }, }, }, }, }, }, }, }, async (c) => { const query = new URL(c.request?.url || "").searchParams.get("error") || "Unknown"; return new Response(html(query), { headers: { "Content-Type": "text/html", }, }); }, ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/config.ts: -------------------------------------------------------------------------------- ```typescript import { createFetch } from "@better-fetch/fetch"; import { getBaseURL } from "../utils/url"; import { type WritableAtom } from "nanostores"; import type { BetterAuthClientOptions, ClientAtomListener, } from "@better-auth/core"; import { redirectPlugin } from "./fetch-plugins"; import { getSessionAtom } from "./session-atom"; import { parseJSON } from "./parser"; export const getClientConfig = ( options?: BetterAuthClientOptions, loadEnv?: boolean, ) => { /* check if the credentials property is supported. Useful for cf workers */ const isCredentialsSupported = "credentials" in Request.prototype; const baseURL = getBaseURL(options?.baseURL, options?.basePath, undefined, loadEnv) ?? "/api/auth"; const pluginsFetchPlugins = options?.plugins ?.flatMap((plugin) => plugin.fetchPlugins) .filter((pl) => pl !== undefined) || []; const lifeCyclePlugin = { id: "lifecycle-hooks", name: "lifecycle-hooks", hooks: { onSuccess: options?.fetchOptions?.onSuccess, onError: options?.fetchOptions?.onError, onRequest: options?.fetchOptions?.onRequest, onResponse: options?.fetchOptions?.onResponse, }, }; const { onSuccess, onError, onRequest, onResponse, ...restOfFetchOptions } = options?.fetchOptions || {}; const $fetch = createFetch({ baseURL, ...(isCredentialsSupported ? { credentials: "include" } : {}), method: "GET", jsonParser(text) { if (!text) { return null as any; } return parseJSON(text, { strict: false, }); }, customFetchImpl: fetch, ...restOfFetchOptions, plugins: [ lifeCyclePlugin, ...(restOfFetchOptions.plugins || []), ...(options?.disableDefaultFetchPlugins ? [] : [redirectPlugin]), ...pluginsFetchPlugins, ], }); const { $sessionSignal, session } = getSessionAtom($fetch); const plugins = options?.plugins || []; let pluginsActions = {} as Record<string, any>; let pluginsAtoms = { $sessionSignal, session, } as Record<string, WritableAtom<any>>; let pluginPathMethods: Record<string, "POST" | "GET"> = { "/sign-out": "POST", "/revoke-sessions": "POST", "/revoke-other-sessions": "POST", "/delete-user": "POST", }; const atomListeners: ClientAtomListener[] = [ { signal: "$sessionSignal", matcher(path) { return ( path === "/sign-out" || path === "/update-user" || path.startsWith("/sign-in") || path.startsWith("/sign-up") || path === "/delete-user" || path === "/verify-email" ); }, }, ]; for (const plugin of plugins) { if (plugin.getAtoms) { Object.assign(pluginsAtoms, plugin.getAtoms?.($fetch)); } if (plugin.pathMethods) { Object.assign(pluginPathMethods, plugin.pathMethods); } if (plugin.atomListeners) { atomListeners.push(...plugin.atomListeners); } } const $store = { notify: (signal?: Omit<string, "$sessionSignal"> | "$sessionSignal") => { pluginsAtoms[signal as keyof typeof pluginsAtoms]!.set( !pluginsAtoms[signal as keyof typeof pluginsAtoms]!.get(), ); }, listen: ( signal: Omit<string, "$sessionSignal"> | "$sessionSignal", listener: (value: boolean, oldValue?: boolean | undefined) => void, ) => { pluginsAtoms[signal as keyof typeof pluginsAtoms]!.subscribe(listener); }, atoms: pluginsAtoms, }; for (const plugin of plugins) { if (plugin.getActions) { Object.assign( pluginsActions, plugin.getActions?.($fetch, $store, options), ); } } return { get baseURL() { return baseURL; }, pluginsActions, pluginsAtoms, pluginPathMethods, atomListeners, $fetch, $store, }; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/slack.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Slack description: Slack provider setup and usage. --- <Steps> <Step> ### Get your Slack credentials To use Slack as a social provider, you need to create a Slack app and get your credentials. 1. Go to [Your Apps on Slack API](https://api.slack.com/apps) and click "Create New App" 2. Choose "From scratch" and give your app a name and select a development workspace 3. In your app settings, navigate to "OAuth & Permissions" 4. Under "Redirect URLs", add your redirect URL: - For local development: `http://localhost:3000/api/auth/callback/slack` - For production: `https://yourdomain.com/api/auth/callback/slack` 5. Copy your Client ID and Client Secret from the "Basic Information" page <Callout> Slack requires HTTPS for redirect URLs in production. For local development, you can use tools like [ngrok](https://ngrok.com/) to create a secure tunnel. </Callout> </Step> <Step> ### Configure the provider To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.slack` in your auth configuration. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ socialProviders: { slack: { // [!code highlight] clientId: process.env.SLACK_CLIENT_ID as string, // [!code highlight] clientSecret: process.env.SLACK_CLIENT_SECRET as string, // [!code highlight] }, // [!code highlight] }, }) ``` </Step> </Steps> ## Usage ### Sign In with Slack To sign in with Slack, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: - `provider`: The provider to use. It should be set to `slack`. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; const authClient = createAuthClient(); const signIn = async () => { const data = await authClient.signIn.social({ provider: "slack" }); }; ``` ### Requesting Additional Scopes By default, Slack uses OpenID Connect scopes: `openid`, `profile`, and `email`. You can request additional Slack scopes during sign-in: ```ts title="auth-client.ts" const signInWithSlack = async () => { await authClient.signIn.social({ provider: "slack", scopes: ["channels:read", "chat:write"], // Additional Slack API scopes }); }; ``` ### Workspace-Specific Sign In If you want to restrict sign-in to a specific Slack workspace, you can pass the `team` parameter: ```ts title="auth.ts" socialProviders: { slack: { clientId: process.env.SLACK_CLIENT_ID as string, clientSecret: process.env.SLACK_CLIENT_SECRET as string, team: "T1234567890", // Your Slack workspace ID }, } ``` ### Using Slack API After Sign In After successful authentication, you can access the user's Slack information through the session. The access token can be used to make requests to the Slack API: ```ts const session = await authClient.getSession(); if (session?.user) { // Access Slack-specific data const slackUserId = session.user.id; // This is the Slack user ID // The access token is stored securely on the server } ``` <Callout> The Slack provider uses OpenID Connect by default, which provides basic user information. If you need to access other Slack APIs, make sure to request the appropriate scopes during sign-in. </Callout> ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/forget-password/page.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Alert, AlertDescription } from "@/components/ui/alert"; 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, ArrowLeft, CheckCircle2 } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; export default function Component() { const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); const [error, setError] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); setError(""); try { await client.requestPasswordReset({ email, redirectTo: "/reset-password", }); setIsSubmitted(true); } catch (err) { setError("An error occurred. Please try again."); } finally { setIsSubmitting(false); } }; if (isSubmitted) { return ( <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> <Card className="w-[350px]"> <CardHeader> <CardTitle>Check your email</CardTitle> <CardDescription> We've sent a password reset link to your email. </CardDescription> </CardHeader> <CardContent> <Alert variant="default"> <CheckCircle2 className="h-4 w-4" /> <AlertDescription> If you don't see the email, check your spam folder. </AlertDescription> </Alert> </CardContent> <CardFooter> <Button variant="outline" className="w-full" onClick={() => setIsSubmitted(false)} > <ArrowLeft className="mr-2 h-4 w-4" /> Back to reset password </Button> </CardFooter> </Card> </main> ); } return ( <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> {/* Radial gradient for the container to give a faded look */} <div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white mask-[radial-gradient(ellipse_at_center,transparent_20%,black)]"></div> <Card className="w-[350px]"> <CardHeader> <CardTitle>Forgot password</CardTitle> <CardDescription> Enter your email to reset your password </CardDescription> </CardHeader> <CardContent> <form onSubmit={handleSubmit}> <div className="grid w-full items-center gap-4"> <div className="flex flex-col space-y-1.5"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="Enter your email" value={email} onChange={(e) => setEmail(e.target.value)} required /> </div> </div> {error && ( <Alert variant="destructive" className="mt-4"> <AlertCircle className="h-4 w-4" /> <AlertDescription>{error}</AlertDescription> </Alert> )} <Button className="w-full mt-4" type="submit" disabled={isSubmitting} > {isSubmitting ? "Sending..." : "Send reset link"} </Button> </form> </CardContent> <CardFooter className="flex justify-center"> <Link href="/sign-in"> <Button variant="link" className="px-0"> Back to sign in </Button> </Link> </CardFooter> </Card> </main> ); } ``` -------------------------------------------------------------------------------- /docs/components/builder/code-tabs/index.tsx: -------------------------------------------------------------------------------- ```typescript import { useState } from "react"; import { TabBar } from "./tab-bar"; import { CodeEditor } from "./code-editor"; import { useAtom } from "jotai"; import { optionsAtom } from "../store"; import { js_beautify } from "js-beautify"; import { signUpString } from "../sign-up"; import { signInString } from "../sign-in"; export default function CodeTabs() { const [options] = useAtom(optionsAtom); const initialFiles = [ { id: "1", name: "auth.ts", content: `import { betterAuth } from 'better-auth'; export const auth = betterAuth({ ${ options.email ? `emailAndPassword: { enabled: true, ${ options.requestPasswordReset ? `async sendResetPassword(data, request) { // Send an email to the user with a link to reset their password },` : `` } },` : "" }${ options.socialProviders.length ? `socialProviders: ${JSON.stringify( options.socialProviders.reduce((acc, provider) => { return { ...acc, [provider]: { clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`, clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`, }, }; }, {}), ).replace(/"/g, "")},` : "" } ${ options.magicLink || options.passkey ? `plugins: [ ${ options.magicLink ? `magicLink({ async sendMagicLink(data) { // Send an email to the user with a magic link }, }),` : `${options.passkey ? `passkey(),` : ""}` } ${options.passkey && options.magicLink ? `passkey(),` : ""} ]` : "" } /** if no database is provided, the user data will be stored in memory. * Make sure to provide a database to persist user data **/ }); `, }, { id: "2", name: "auth-client.ts", content: `import { createAuthClient } from "better-auth/react"; ${ options.magicLink || options.passkey ? `import { ${options.magicLink ? "magicLinkClient, " : ""}, ${ options.passkey ? "passkeyClient" : "" } } from "better-auth/client/plugins";` : "" } export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_APP_URL, ${ options.magicLink || options.passkey ? `plugins: [${options.magicLink ? `magicLinkClient(),` : ""}${ options.passkey ? `passkeyClient(),` : "" }],` : "" } }) export const { signIn, signOut, signUp, useSession } = authClient; `, }, { id: "3", name: "sign-in.tsx", content: signInString(options), }, ]; if (options.email) { initialFiles.push({ id: "4", name: "sign-up.tsx", content: signUpString, }); } const [files, setFiles] = useState(initialFiles); const [activeFileId, setActiveFileId] = useState(files[0].id); const handleTabClick = (fileId: string) => { setActiveFileId(fileId); }; const handleTabClose = (fileId: string) => { setFiles(files.filter((file) => file.id !== fileId)); if (activeFileId === fileId) { setActiveFileId(files[0].id); } }; const activeFile = files.find((file) => file.id === activeFileId); return ( <div className="w-full mr-auto max-w-[45rem] mt-8 border border-border rounded-md overflow-hidden"> <TabBar files={files} activeFileId={activeFileId} onTabClick={handleTabClick} onTabClose={handleTabClose} /> <div className=""> {activeFile && ( <CodeEditor language="typescript" code={ activeFile.name.endsWith(".ts") ? js_beautify(activeFile.content) : activeFile.content.replace(/\n{3,}/g, "\n\n") } /> )} </div> </div> ); } ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-sqlite-number-id.txt: -------------------------------------------------------------------------------- ``` import { sql } from "drizzle-orm"; import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; export const custom_user = sqliteTable("custom_user", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: integer("email_verified", { mode: "boolean" }) .default(false) .notNull(), image: text("image"), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).default( false, ), username: text("username").unique(), displayUsername: text("display_username"), }); export const custom_session = sqliteTable("custom_session", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), token: text("token").notNull().unique(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .$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 = sqliteTable("custom_account", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 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: integer("access_token_expires_at", { mode: "timestamp_ms", }), refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp_ms", }), scope: text("scope"), password: text("password"), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const custom_verification = sqliteTable("custom_verification", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const twoFactor = sqliteTable("two_factor", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), secret: text("secret").notNull(), backupCodes: text("backup_codes").notNull(), userId: integer("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), }); ``` -------------------------------------------------------------------------------- /docs/content/docs/reference/faq.mdx: -------------------------------------------------------------------------------- ```markdown --- title: FAQ description: Frequently asked questions about Better Auth. --- This page contains frequently asked questions, common issues, and other helpful information about Better Auth. <Accordions> <Accordion title="Auth client not working"> When encountering `createAuthClient` related errors, make sure to have the correct import path as it varies based on environment. If you're using the auth client on react front-end, you'll need to import it from `/react`: ```ts title="component.ts" import { createAuthClient } from "better-auth/react"; ``` Where as if you're using the auth client in Next.js middleware, server-actions, server-components or anything server-related, you'll likely need to import it from `/client`: ```ts title="server.ts" import { createAuthClient } from "better-auth/client"; ``` </Accordion> <Accordion title="getSession not working"> If you try to call `authClient.getSession` on a server environment (e.g, a Next.js server component), it doesn't work since it can't access the cookies. You can use the `auth.api.getSession` instead and pass the request headers to it. ```tsx title="server.tsx" import { auth } from "./auth"; import { headers } from "next/headers"; const session = await auth.api.getSession({ headers: await headers() }) ``` if you need to use the auth client on the server for different purposes, you still can pass the request headers to it: ```tsx title="server.tsx" import { authClient } from "./auth-client"; import { headers } from "next/headers"; const session = await authClient.getSession({ fetchOptions:{ headers: await headers() } }) ``` </Accordion> <Accordion title="Adding custom fields to the users table"> Better Auth provides a type-safe way to extend the user and session schemas, take a look at our docs on <Link href="/docs/concepts/database#extending-core-schema">extending core schema</Link>. </Accordion> <Accordion title="Difference between getSession and useSession"> Both `useSession` and `getSession` instances are used fundamentally different based on the situation. `useSession` is a hook, meaning it can trigger re-renders whenever session data changes. If you have UI you need to change based on user or session data, you can use this hook. <Callout type="warn"> For performance reasons, do not use this hook on your `layout.tsx` file. We recommend using RSC and use your server auth instance to get the session data via `auth.api.getSession`. </Callout> `getSession` returns a promise containing data and error. For all other situations where you shouldn't use `useSession`, is when you should be using `getSession`. <Callout type="info"> `getSession` is available on both server and client auth instances. Not just the latter. </Callout> </Accordion> <Accordion title="Common TypeScript Errors"> If you're facing typescript errors, make sure your tsconfig has `strict` set to `true`: ```json title="tsconfig.json" { "compilerOptions": { "strict": true, } } ``` if you can't set strict to true, you can enable strictNullChecks: ```json title="tsconfig.json" { "compilerOptions": { "strictNullChecks": true, } } ``` You can learn more in our <Link href="/docs/concepts/typescript#typescript-config">TypeScript docs</Link>. </Accordion> <Accordion title="Can I remove `name`, `image`, or `email` fields from the user table?"> At this time, you can't remove the `name`, `image`, or `email` fields from the user table. We do plan to have more customizability in the future in this regard, but for now, you can't remove these fields. </Accordion> </Accordions> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/oauth2/state.ts: -------------------------------------------------------------------------------- ```typescript import * as z from "zod"; import { APIError } from "better-call"; import { generateRandomString } from "../crypto"; import type { GenericEndpointContext } from "@better-auth/core"; export async function generateState( c: GenericEndpointContext, link?: { email: string; userId: string; }, ) { const callbackURL = c.body?.callbackURL || c.context.options.baseURL; if (!callbackURL) { throw new APIError("BAD_REQUEST", { message: "callbackURL is required", }); } const codeVerifier = generateRandomString(128); const state = generateRandomString(32); const stateCookie = c.context.createAuthCookie("state", { maxAge: 5 * 60 * 1000, // 5 minutes }); await c.setSignedCookie( stateCookie.name, state, c.context.secret, stateCookie.attributes, ); const data = JSON.stringify({ callbackURL, codeVerifier, errorURL: c.body?.errorCallbackURL, newUserURL: c.body?.newUserCallbackURL, link, /** * This is the actual expiry time of the state */ expiresAt: Date.now() + 10 * 60 * 1000, requestSignUp: c.body?.requestSignUp, }); const expiresAt = new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 10); const verification = await c.context.internalAdapter.createVerificationValue({ value: data, identifier: state, expiresAt, }); if (!verification) { c.context.logger.error( "Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", ); throw new APIError("INTERNAL_SERVER_ERROR", { message: "Unable to create verification", }); } return { state: verification.identifier, codeVerifier, }; } export async function parseState(c: GenericEndpointContext) { const state = c.query.state || c.body.state; const data = await c.context.internalAdapter.findVerificationValue(state); if (!data) { c.context.logger.error("State Mismatch. Verification not found", { state, }); const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; throw c.redirect(`${errorURL}?error=please_restart_the_process`); } const parsedData = z .object({ callbackURL: z.string(), codeVerifier: z.string(), errorURL: z.string().optional(), newUserURL: z.string().optional(), expiresAt: z.number(), link: z .object({ email: z.string(), userId: z.coerce.string(), }) .optional(), requestSignUp: z.boolean().optional(), }) .parse(JSON.parse(data.value)); if (!parsedData.errorURL) { parsedData.errorURL = `${c.context.baseURL}/error`; } const stateCookie = c.context.createAuthCookie("state"); const stateCookieValue = await c.getSignedCookie( stateCookie.name, c.context.secret, ); /** * This is generally cause security issue and should only be used in * dev or staging environments. It's currently used by the oauth-proxy * plugin */ const skipStateCookieCheck = c.context.oauthConfig?.skipStateCookieCheck; if ( !skipStateCookieCheck && (!stateCookieValue || stateCookieValue !== state) ) { const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; throw c.redirect(`${errorURL}?error=state_mismatch`); } c.setCookie(stateCookie.name, "", { maxAge: 0, }); if (parsedData.expiresAt < Date.now()) { await c.context.internalAdapter.deleteVerificationValue(data.id); const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`; throw c.redirect(`${errorURL}?error=please_restart_the_process`); } await c.context.internalAdapter.deleteVerificationValue(data.id); return parsedData; } ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/fastify.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Better Auth Fastify Integration Guide description: Learn how to seamlessly integrate Better Auth with your Fastify application. --- This guide provides step-by-step instructions for configuring both essential handlers and CORS settings. <Callout type="important"> A configured Better Auth instance is required before proceeding. If you haven't set this up yet, please consult our [Installation Guide](/docs/installation). </Callout> ### Prerequisites Verify the following requirements before integration: - **Node.js Environment**: v16 or later installed - **ES Module Support**: Enable ES modules in either: - `package.json`: `{ "type": "module" }` - TypeScript `tsconfig.json`: `{ "module": "ESNext" }` - **Fastify Dependencies**: ```package-install fastify @fastify/cors ``` <Callout type="tip"> For TypeScript: Ensure your `tsconfig.json` includes `"esModuleInterop": true` for optimal compatibility. </Callout> ### Authentication Handler Setup Configure Better Auth to process authentication requests by creating a catch-all route: ```ts title="server.ts" import Fastify from "fastify"; import { auth } from "./auth"; // Your configured Better Auth instance const fastify = Fastify({ logger: true }); // Register authentication endpoint fastify.route({ method: ["GET", "POST"], url: "/api/auth/*", async handler(request, reply) { try { // Construct request URL const url = new URL(request.url, `http://${request.headers.host}`); // Convert Fastify headers to standard Headers object const headers = new Headers(); Object.entries(request.headers).forEach(([key, value]) => { if (value) headers.append(key, value.toString()); }); // Create Fetch API-compatible request const req = new Request(url.toString(), { method: request.method, headers, body: request.body ? JSON.stringify(request.body) : undefined, }); // Process authentication request const response = await auth.handler(req); // Forward response to client reply.status(response.status); response.headers.forEach((value, key) => reply.header(key, value)); reply.send(response.body ? await response.text() : null); } catch (error) { fastify.log.error("Authentication Error:", error); reply.status(500).send({ error: "Internal authentication error", code: "AUTH_FAILURE" }); } } }); // Initialize server fastify.listen({ port: 4000 }, (err) => { if (err) { fastify.log.error(err); process.exit(1); } console.log("Server running on port 4000"); }); ``` ### Trusted origins When a request is made from a different origin, the request will be blocked by default. You can add trusted origins to the `auth` instance. ```ts export const auth = betterAuth({ trustedOrigins: ["http://localhost:3000", "https://example.com"], }); ``` ### Configuring CORS Secure your API endpoints with proper CORS configuration: ```ts import fastifyCors from "@fastify/cors"; // Configure CORS policies fastify.register(fastifyCors, { origin: process.env.CLIENT_ORIGIN || "http://localhost:3000", methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: [ "Content-Type", "Authorization", "X-Requested-With" ], credentials: true, maxAge: 86400 }); // Mount authentication handler after CORS registration // (Use previous handler configuration here) ``` <Callout type="warning"> Always restrict CORS origins in production environments. Use environment variables for dynamic configuration. </Callout> ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/dialog.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />; } function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; } function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; } function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; } function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", className, )} {...props} /> ); } function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) { return ( <DialogPortal data-slot="dialog-portal"> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "fixed left-[50%] top-[50%] z-50 grid max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg w-11/12", className, )} {...props} > {children} <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"> <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> </DialogPrimitive.Content> </DialogPortal> ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className, )} {...props} /> ); } function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-lg leading-none font-semibold", className)} {...props} /> ); } function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, }; ``` -------------------------------------------------------------------------------- /docs/components/ui/dialog.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />; } function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; } function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; } function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; } function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", className, )} {...props} /> ); } function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) { return ( <DialogPortal data-slot="dialog-portal"> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "fixed left-[50%] top-[50%] z-50 grid max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg w-11/12", className, )} {...props} > {children} <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"> <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> </DialogPrimitive.Content> </DialogPortal> ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className, )} {...props} /> ); } function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-lg leading-none font-semibold", className)} {...props} /> ); } function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, }; ``` -------------------------------------------------------------------------------- /docs/components/ui/form.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import { Controller, FormProvider, useFormContext, useFormState, type ControllerProps, type FieldPath, type FieldValues, } from "react-hook-form"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > = { name: TName; }; const FormFieldContext = React.createContext<FormFieldContextValue>( {} as FormFieldContextValue, ); const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, >({ ...props }: ControllerProps<TFieldValues, TName>) => { return ( <FormFieldContext.Provider value={{ name: props.name }}> <Controller {...props} /> </FormFieldContext.Provider> ); }; const useFormField = () => { const fieldContext = React.useContext(FormFieldContext); const itemContext = React.useContext(FormItemContext); const { getFieldState } = useFormContext(); const formState = useFormState({ name: fieldContext.name }); const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { throw new Error("useFormField should be used within <FormField>"); } const { id } = itemContext; return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, }; }; type FormItemContextValue = { id: string; }; const FormItemContext = React.createContext<FormItemContextValue>( {} as FormItemContextValue, ); function FormItem({ className, ...props }: React.ComponentProps<"div">) { const id = React.useId(); return ( <FormItemContext.Provider value={{ id }}> <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} /> </FormItemContext.Provider> ); } function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { const { error, formItemId } = useFormField(); return ( <Label data-slot="form-label" data-error={!!error} className={cn("data-[error=true]:text-destructive-foreground", className)} htmlFor={formItemId} {...props} /> ); } function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { const { error, formItemId, formDescriptionId, formMessageId } = useFormField(); return ( <Slot data-slot="form-control" id={formItemId} aria-describedby={ !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}` } aria-invalid={!!error} {...props} /> ); } function FormDescription({ className, ...props }: React.ComponentProps<"p">) { const { formDescriptionId } = useFormField(); return ( <p data-slot="form-description" id={formDescriptionId} className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } function FormMessage({ className, ...props }: React.ComponentProps<"p">) { const { error, formMessageId } = useFormField(); const body = error ? String(error?.message ?? "") : props.children; if (!body) { return null; } return ( <p data-slot="form-message" id={formMessageId} className={cn("text-destructive-foreground text-sm", className)} {...props} > {body} </p> ); } export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, }; ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- ```yaml name: Report an issue description: Create a report to help us improve body: - type: checkboxes attributes: label: Is this suited for github? description: Feel free to join the discord community [here](https://discord.gg/better-auth), we can usually respond faster to any questions. options: - label: Yes, this is suited for github - type: markdown attributes: value: | This template is used for reporting a issue with better-auth. Feature requests should be opened in [here](https://github.com/better-auth/better-auth/issues/new?assignees=&labels=&projects=&template=feature_request.md&title=). Before opening a new issue, please do a [search](https://github.com/better-auth/better-auth/issues) of existing issues and :+1: upvote the existing issue instead. This will result in a quicker resolution. - type: textarea attributes: label: To Reproduce description: A step-by-step description of how to reproduce the issue, based on the linked reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. placeholder: | Ex: 1. Create a backend 2. Create a frontend and use client 3. X will happen validations: required: true - type: textarea attributes: label: Current vs. Expected behavior description: | A clear and concise description of what the bug is (e.g., screenshots, logs, etc.), and what you expected to happen. **Skipping this/failure to provide complete information of the bug will result in the issue being closed.** placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead.' validations: required: true - type: input attributes: label: What version of Better Auth are you using? description: Please provide the current version of `better-auth` that you are reporting the bug on placeholder: "1.x.x" validations: required: true - type: textarea attributes: label: System info description: Output of `npx @better-auth/cli info --json` render: bash placeholder: System and Better Auth info validations: required: true - type: dropdown attributes: label: Which area(s) are affected? (Select all that apply) multiple: true options: - 'Backend' - 'Client' - 'Types' - 'Documentation' - 'Package' - 'Other' validations: required: true - type: textarea attributes: label: Auth config (if applicable) description: If you haven't already shared a reproducible example or don't think it's unrelated, please include your auth config. Make sure to remove any sensitive information. render: typescript value: | import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); - type: textarea attributes: label: Additional context description: | Any extra information that might help us investigate. For example, is it only reproducible online, or locally too? Is the issue only happening in a specific browser? etc. placeholder: | I tested my reproduction against the latest release. ``` -------------------------------------------------------------------------------- /demo/nextjs/app/globals.css: -------------------------------------------------------------------------------- ```css @import "tailwindcss"; @config "../tailwind.config.ts"; @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); :root { --background: hsl(0 0% 100%); --foreground: hsl(20 14.3% 4.1%); --card: hsl(0 0% 100%); --card-foreground: hsl(20 14.3% 4.1%); --popover: hsl(0 0% 100%); --popover-foreground: hsl(20 14.3% 4.1%); --primary: hsl(24 9.8% 10%); --primary-foreground: hsl(60 9.1% 97.8%); --secondary: hsl(60 4.8% 95.9%); --secondary-foreground: hsl(24 9.8% 10%); --muted: hsl(60 4.8% 95.9%); --muted-foreground: hsl(25 5.3% 44.7%); --accent: hsl(60 4.8% 95.9%); --accent-foreground: hsl(24 9.8% 10%); --destructive: hsl(0 84.2% 60.2%); --destructive-foreground: hsl(60 9.1% 97.8%); --border: hsl(20 5.9% 90%); --input: hsl(20 5.9% 90%); --ring: hsl(20 14.3% 4.1%); --radius: 0rem; --chart-1: hsl(12 76% 61%); --chart-2: hsl(173 58% 39%); --chart-3: hsl(197 37% 24%); --chart-4: hsl(43 74% 66%); --chart-5: hsl(27 87% 67%); } .dark { --background: hsl(20 14.3% 4.1%); --foreground: hsl(60 9.1% 97.8%); --card: hsl(20 14.3% 4.1%); --card-foreground: hsl(60 9.1% 97.8%); --popover: hsl(20 14.3% 4.1%); --popover-foreground: hsl(60 9.1% 97.8%); --primary: hsl(60 9.1% 97.8%); --primary-foreground: hsl(24 9.8% 10%); --secondary: hsl(12 6.5% 15.1%); --secondary-foreground: hsl(60 9.1% 97.8%); --muted: hsl(12 6.5% 15.1%); --muted-foreground: hsl(24 5.4% 63.9%); --accent: hsl(12 6.5% 15.1%); --accent-foreground: hsl(60 9.1% 97.8%); --destructive: hsl(0 62.8% 30.6%); --destructive-foreground: hsl(60 9.1% 97.8%); --border: hsl(12 6.5% 15.1%); --input: hsl(12 6.5% 15.1%); --ring: hsl(24 5.7% 82.9%); --chart-1: hsl(220 70% 50%); --chart-2: hsl(160 60% 45%); --chart-3: hsl(30 80% 55%); --chart-4: hsl(280 65% 60%); --chart-5: hsl(340 75% 55%); } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } .no-visible-scrollbar { scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch; } .no-visible-scrollbar::-webkit-scrollbar { display: none; } ``` -------------------------------------------------------------------------------- /docs/components/blocks/features.tsx: -------------------------------------------------------------------------------- ```typescript import { useId } from "react"; export function Features() { return ( <div className="py-2"> <div className="mt-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-10 md:gap-2 max-w-7xl mx-auto"> {grid.map((feature, i) => ( <div key={feature.title} className="relative bg-gradient-to-b dark:from-neutral-900 from-neutral-100 dark:to-neutral-950 to-white px-6 py-2 overflow-hidden" > <Grid size={i * 5 + 10} /> <p className="text-base font-bold text-neutral-800 dark:text-white relative z-0"> {feature.title} </p> <p className="text-neutral-600 dark:text-neutral-400 text-base font-normal relative z-0"> {feature.description} </p> </div> ))} </div> </div> ); } const grid = [ { title: "Framework Agnostic", description: "Support for most popular frameworks", }, { title: "Email & Password", description: "Built-in support for secure email and password authentication", }, { title: "Account & Session Management", description: "Manage user accounts and sessions with ease", }, { title: "Built-In Rate Limiter", description: "Built-in rate limiter with custom rules", }, { title: "Automatic Database Management", description: "Automatic database management and migrations", }, { title: "Social Sign-on", description: "Multiple social sign-on providers", }, { title: "Organization & Access Control", description: "Manage organizations and access control", }, { title: "Two Factor Authentication", description: "Secure your users with two factor authentication", }, { title: "Plugin Ecosystem", description: "Even more capabilities with plugins", }, ]; export const Grid = ({ pattern, size, }: { pattern?: number[][]; size?: number; }) => { const p = pattern ?? [ [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], [Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1], ]; return ( <div className="pointer-events-none absolute left-1/2 top-0 -ml-20 -mt-2 h-full w-full [mask-image:linear-gradient(white,transparent)]"> <div className="absolute inset-0 bg-gradient-to-r [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 from-zinc-100/30 to-zinc-300/30 dark:to-zinc-900/30 opacity-100"> <GridPattern width={size ?? 20} height={size ?? 20} x="-12" y="4" squares={p} className="absolute inset-0 h-full w-full mix-blend-overlay dark:fill-white/10 dark:stroke-white/10 stroke-black/10 fill-black/10" /> </div> </div> ); }; export function GridPattern({ width, height, x, y, squares, ...props }: any) { const patternId = useId(); return ( <svg aria-hidden="true" {...props}> <defs> <pattern id={patternId} width={width} height={height} patternUnits="userSpaceOnUse" x={x} y={y} > <path d={`M.5 ${height}V.5H${width}`} fill="none" /> </pattern> </defs> <rect width="100%" height="100%" strokeWidth={0} fill={`url(#${patternId})`} /> {squares && ( <svg x={x} y={y} className="overflow-visible"> {squares.map(([x, y]: any, idx: number) => ( <rect strokeWidth="0" key={`${x}-${y}-${idx}`} width={width + 1} height={height + 1} x={x * width} y={y * height} /> ))} </svg> )} </svg> ); } ``` -------------------------------------------------------------------------------- /docs/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; } function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { return ( <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> ); } function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { return ( <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> ); } function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { return ( <AlertDialogPrimitive.Overlay data-slot="alert-dialog-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", className, )} {...props} /> ); } function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { return ( <AlertDialogPortal> <AlertDialogOverlay /> <AlertDialogPrimitive.Content data-slot="alert-dialog-content" className={cn( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", className, )} {...props} /> </AlertDialogPortal> ); } function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-dialog-header" className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> ); } function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="alert-dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className, )} {...props} /> ); } function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { return ( <AlertDialogPrimitive.Title data-slot="alert-dialog-title" className={cn("text-lg font-semibold", className)} {...props} /> ); } function AlertDialogDescription({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { return ( <AlertDialogPrimitive.Description data-slot="alert-dialog-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { return ( <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} /> ); } function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { return ( <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: "outline" }), className)} {...props} /> ); } export { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, }; ``` -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript import defaultTheme from "tailwindcss/defaultTheme"; import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; import svgToDataUri from "mini-svg-data-uri"; /** @type {import('tailwindcss').Config} */ export default { darkMode: ["class"], plugins: [ addVariablesForColors, function ({ matchUtilities, theme }) { matchUtilities( { "bg-grid": (value) => ({ backgroundImage: `url("${svgToDataUri( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`, )}")`, }), "bg-grid-small": (value) => ({ backgroundImage: `url("${svgToDataUri( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`, )}")`, }), "bg-dot": (value) => ({ backgroundImage: `url("${svgToDataUri( `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`, )}")`, }), }, { values: flattenColorPalette(theme("backgroundColor")), type: "color", }, ); }, ], theme: { extend: { fontFamily: { sans: ["var(--font-geist-sans)"], mono: ["var(--font-geist-mono)"], display: [...defaultTheme.fontFamily.sans], }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { marquee: { from: { transform: "translateX(0)" }, to: { transform: "translateX(calc(-100% - var(--gap)))" }, }, "marquee-vertical": { from: { transform: "translateY(0)" }, to: { transform: "translateY(calc(-100% - var(--gap)))" }, }, "hrtl-scroll": { from: { transform: "translateX(0)" }, to: { transform: "translateX(calc(-95%))" }, }, "hrtl-scroll-reverse": { from: { transform: "translateX(calc(-95%))" }, to: { transform: "translateX(0)" }, }, ripple: { "0% , 100%": { transform: "translate(-50% , -50%) scale(1)", }, "50%": { transform: "translate(-50% , -50%) scale(0.9)", }, }, "accordion-down": { from: { height: "0", }, to: { height: "var(--radix-accordion-content-height)", }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)", }, to: { height: "0", }, }, scroll: { to: { transform: "translate(calc(-50% - 0.5rem))", }, }, spotlight: { "0%": { opacity: 0, transform: "translate(-72%, -62%) scale(0.5)", }, "100%": { opacity: 1, transform: "translate(-50%,-40%) scale(1)", }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite", scroll: "scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite", "hrtl-scroll": "hrtl-scroll var(--anime-duration,10s) linear infinite", "hrtl-scroll-reverse": "hrtl-scroll-reverse var(--anime-duration,10s) linear infinite", spotlight: "spotlight 2s ease .30s 1 forwards", }, }, }, }; function addVariablesForColors({ addBase, theme }) { let allColors = flattenColorPalette(theme("colors")); let newVars = Object.fromEntries( Object.entries(allColors).map(([key, val]) => [`--${key}`, val]), ); addBase({ ":root": newVars, }); } ``` -------------------------------------------------------------------------------- /demo/nextjs/hooks/use-toast.ts: -------------------------------------------------------------------------------- ```typescript "use client"; // Inspired by react-hot-toast library import * as React from "react"; import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; const TOAST_LIMIT = 1; const TOAST_REMOVE_DELAY = 1000000; type ToasterToast = ToastProps & { id: string; title?: React.ReactNode; description?: React.ReactNode; action?: ToastActionElement; }; const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", } as const; let count = 0; function genId() { count = (count + 1) % Number.MAX_SAFE_INTEGER; return count.toString(); } type ActionType = typeof actionTypes; type Action = | { type: ActionType["ADD_TOAST"]; toast: ToasterToast; } | { type: ActionType["UPDATE_TOAST"]; toast: Partial<ToasterToast>; } | { type: ActionType["DISMISS_TOAST"]; toastId?: ToasterToast["id"]; } | { type: ActionType["REMOVE_TOAST"]; toastId?: ToasterToast["id"]; }; interface State { toasts: ToasterToast[]; } const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { return; } const timeout = setTimeout(() => { toastTimeouts.delete(toastId); dispatch({ type: "REMOVE_TOAST", toastId: toastId, }); }, TOAST_REMOVE_DELAY); toastTimeouts.set(toastId, timeout); }; export const reducer = (state: State, action: Action): State => { switch (action.type) { case "ADD_TOAST": return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), }; case "UPDATE_TOAST": return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t, ), }; case "DISMISS_TOAST": { const { toastId } = action; // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { addToRemoveQueue(toast.id); }); } return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t, ), }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, toasts: [], }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), }; } }; const listeners: Array<(state: State) => void> = []; let memoryState: State = { toasts: [] }; function dispatch(action: Action) { memoryState = reducer(memoryState, action); listeners.forEach((listener) => { listener(memoryState); }); } type Toast = Omit<ToasterToast, "id">; function toast({ ...props }: Toast) { const id = genId(); const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, }); const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); dispatch({ type: "ADD_TOAST", toast: { ...props, id, open: true, // @ts-expect-error onOpenChange: (open) => { if (!open) dismiss(); }, }, }); return { id: id, dismiss, update, }; } function useToast() { const [state, setState] = React.useState<State>(memoryState); React.useEffect(() => { listeners.push(setState); return () => { const index = listeners.indexOf(setState); if (index > -1) { listeners.splice(index, 1); } }; }, [state]); return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), }; } export { useToast, toast }; ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-sqlite-passkey-number-id.txt: -------------------------------------------------------------------------------- ``` import { sql } from "drizzle-orm"; import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; export const custom_user = sqliteTable("custom_user", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: integer("email_verified", { mode: "boolean" }) .default(false) .notNull(), image: text("image"), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const custom_session = sqliteTable("custom_session", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), token: text("token").notNull().unique(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .$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 = sqliteTable("custom_account", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), 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: integer("access_token_expires_at", { mode: "timestamp_ms", }), refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp_ms", }), scope: text("scope"), password: text("password"), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const custom_verification = sqliteTable("custom_verification", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), createdAt: integer("created_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .notNull(), updatedAt: integer("updated_at", { mode: "timestamp_ms" }) .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const passkey = sqliteTable("passkey", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), name: text("name"), publicKey: text("public_key").notNull(), userId: integer("user_id") .notNull() .references(() => custom_user.id, { onDelete: "cascade" }), credentialID: text("credential_id").notNull(), counter: integer("counter").notNull(), deviceType: text("device_type").notNull(), backedUp: integer("backed_up", { mode: "boolean" }).notNull(), transports: text("transports"), createdAt: integer("created_at", { mode: "timestamp_ms" }), aaguid: text("aaguid"), }); ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/svelte-kit.mdx: -------------------------------------------------------------------------------- ```markdown --- title: SvelteKit Integration description: Integrate Better Auth with SvelteKit. --- Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). ### Mount the handler We need to mount the handler to SvelteKit server hook. ```ts title="hooks.server.ts" import { auth } from "$lib/auth"; import { svelteKitHandler } from "better-auth/svelte-kit"; import { building } from "$app/environment"; export async function handle({ event, resolve }) { return svelteKitHandler({ event, resolve, auth, building }); } ``` ### Populate session data in the event (`event.locals`) The `svelteKitHandler` does not automatically populate `event.locals.user` or `event.locals.session`. If you want to access the current session in your server code (e.g., in `+layout.server.ts`, actions, or endpoints), populate `event.locals` in your `handle` hook: ```ts title="hooks.server.ts" import { auth } from "$lib/auth"; import { svelteKitHandler } from "better-auth/svelte-kit"; import { building } from "$app/environment"; export async function handle({ event, resolve }) { // Fetch current session from Better Auth const session = await auth.api.getSession({ headers: event.request.headers, }); // Make session and user available on server if (session) { event.locals.session = session.session; event.locals.user = session.user; } return svelteKitHandler({ event, resolve, auth, building }); } ``` ### Server Action Cookies To ensure cookies are properly set when you call functions like `signInEmail` or `signUpEmail` in a server action, you should use the `sveltekitCookies` plugin. This plugin will automatically handle setting cookies for you in SvelteKit. You need to add it as a plugin to your Better Auth instance. <Callout> The `getRequestEvent` function is available in SvelteKit `2.20.0` and later. Make sure you are using a compatible version. </Callout> ```ts title="lib/auth.ts" import { betterAuth } from "better-auth"; import { sveltekitCookies } from "better-auth/svelte-kit"; import { getRequestEvent } from "$app/server"; export const auth = betterAuth({ // ... your config plugins: [sveltekitCookies(getRequestEvent)], // make sure this is the last plugin in the array }); ``` ## Create a client Create a client instance. You can name the file anything you want. Here we are creating `client.ts` file inside the `lib/` directory. ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/svelte"; // make sure to import from better-auth/svelte export const authClient = createAuthClient({ // you can pass client configuration here }); ``` Once you have created the client, you can use it to sign up, sign in, and perform other actions. Some of the actions are reactive. The client use [nano-store](https://github.com/nanostores/nanostores) to store the state and reflect changes when there is a change like a user signing in or out affecting the session state. ### Example usage ```svelte <script lang="ts"> import { authClient } from "$lib/client"; const session = authClient.useSession(); </script> <div> {#if $session.data} <div> <p> {$session.data.user.name} </p> <button on:click={async () => { await authClient.signOut(); }} > Sign Out </button> </div> {:else} <button on:click={async () => { await authClient.signIn.social({ provider: "github", }); }} > Continue with GitHub </button> {/if} </div> ``` ``` -------------------------------------------------------------------------------- /docs/components/ui/drawer.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import * as React from "react"; import { Drawer as DrawerPrimitive } from "vaul"; import { cn } from "@/lib/utils"; function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) { return <DrawerPrimitive.Root data-slot="drawer" {...props} />; } function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />; } function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>) { return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />; } function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>) { return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />; } function DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { return ( <DrawerPrimitive.Overlay data-slot="drawer-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", className, )} {...props} /> ); } function DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>) { return ( <DrawerPortal data-slot="drawer-portal"> <DrawerOverlay /> <DrawerPrimitive.Content data-slot="drawer-content" className={cn( "group/drawer-content bg-background fixed z-50 flex h-auto flex-col", "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg", "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg", "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm", className, )} {...props} > <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> {children} </DrawerPrimitive.Content> </DrawerPortal> ); } function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} /> ); } function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> ); } function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) { return ( <DrawerPrimitive.Title data-slot="drawer-title" className={cn("text-foreground font-semibold", className)} {...props} /> ); } function DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>) { return ( <DrawerPrimitive.Description data-slot="drawer-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> ); } export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, }; ``` -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- ```json { "name": "@better-auth/docs", "version": "0.0.0", "private": true, "type": "module", "scripts": { "build": "next build && pnpm run scripts:sync-orama", "dev": "next dev", "start": "next start", "postinstall": "fumadocs-mdx", "scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts", "scripts:sync-orama": "node ./scripts/sync-orama.ts" }, "dependencies": { "@ai-sdk/openai-compatible": "^1.0.20", "@ai-sdk/react": "^2.0.64", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "catalog:", "@hookform/resolvers": "^5.2.1", "@oramacloud/client": "^2.1.4", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.7", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", "@scalar/nextjs-api-reference": "^0.8.17", "@vercel/analytics": "^1.5.0", "@vercel/og": "^0.8.5", "ai": "^5.0.64", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.1.1", "date-fns": "^4.1.0", "dotenv": "^17.2.2", "embla-carousel-react": "^8.6.0", "foxact": "^0.2.49", "framer-motion": "^12.23.12", "fumadocs-core": "15.8.3", "fumadocs-docgen": "2.1.0", "fumadocs-mdx": "11.8.3", "fumadocs-typescript": "^4.0.6", "fumadocs-ui": "15.8.3", "geist": "^1.4.2", "gray-matter": "^4.0.3", "hast-util-to-jsx-runtime": "^2.3.6", "highlight.js": "^11.11.1", "input-otp": "^1.4.2", "jotai": "^2.13.1", "js-beautify": "^1.15.4", "jsrsasign": "^11.1.0", "lucide-react": "^0.542.0", "motion": "^12.23.12", "next": "16.0.0-beta.0", "next-themes": "^0.4.6", "prism-react-renderer": "^2.4.1", "react": "19.2.0", "react-day-picker": "9.9.0", "react-dom": "19.2.0", "react-hook-form": "^7.62.0", "react-markdown": "^10.1.0", "react-remove-scroll": "^2.7.1", "react-resizable-panels": "^3.0.5", "react-use-measure": "^2.1.7", "recharts": "^3.1.2", "rehype-highlight": "^7.0.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", "shiki": "^3.13.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "unist-util-visit": "^5.0.0", "vaul": "^1.1.2", "zod": "^4.1.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", "@types/jsrsasign": "^10.5.15", "@types/mdx": "^2.0.13", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.1", "mini-svg-data-uri": "^1.4.4", "postcss": "^8.5.6", "tailwindcss": "^4.1.13", "typescript": "^5.9.2" } } ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/github.ts: -------------------------------------------------------------------------------- ```typescript import { betterFetch } from "@better-fetch/fetch"; import type { OAuthProvider, ProviderOptions } from "../oauth2"; import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, } from "../oauth2"; export interface GithubProfile { login: string; id: string; node_id: string; avatar_url: string; gravatar_id: string; url: string; html_url: string; followers_url: string; following_url: string; gists_url: string; starred_url: string; subscriptions_url: string; organizations_url: string; repos_url: string; events_url: string; received_events_url: string; type: string; site_admin: boolean; name: string; company: string; blog: string; location: string; email: string; hireable: boolean; bio: string; twitter_username: string; public_repos: string; public_gists: string; followers: string; following: string; created_at: string; updated_at: string; private_gists: string; total_private_repos: string; owned_private_repos: string; disk_usage: string; collaborators: string; two_factor_authentication: boolean; plan: { name: string; space: string; private_repos: string; collaborators: string; }; } export interface GithubOptions extends ProviderOptions<GithubProfile> { clientId: string; } export const github = (options: GithubOptions) => { const tokenEndpoint = "https://github.com/login/oauth/access_token"; return { id: "github", name: "GitHub", createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["read:user", "user:email"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return createAuthorizationURL({ id: "github", options, authorizationEndpoint: "https://github.com/login/oauth/authorize", scopes: _scopes, state, redirectURI, loginHint, prompt: options.prompt, }); }, 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: "https://github.com/login/oauth/access_token", }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await betterFetch<GithubProfile>( "https://api.github.com/user", { headers: { "User-Agent": "better-auth", authorization: `Bearer ${token.accessToken}`, }, }, ); if (error) { return null; } const { data: emails } = await betterFetch< { email: string; primary: boolean; verified: boolean; visibility: "public" | "private"; }[] >("https://api.github.com/user/emails", { headers: { Authorization: `Bearer ${token.accessToken}`, "User-Agent": "better-auth", }, }); if (!profile.email && emails) { profile.email = (emails.find((e) => e.primary) ?? emails[0]) ?.email as string; } const emailVerified = emails?.find((e) => e.email === profile.email)?.verified ?? false; const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.name || profile.login, email: profile.email, image: profile.avatar_url, emailVerified, ...userMap, }, data: profile, }; }, options, } satisfies OAuthProvider<GithubProfile>; }; ```