This is page 27 of 52. 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 │ └── stateless │ ├── .env.example │ ├── .gitignore │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ │ └── user │ │ │ │ └── route.ts │ │ │ ├── dashboard │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── lib │ │ ├── auth-client.ts │ │ └── auth.ts │ ├── tailwind.config.ts │ └── tsconfig.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-declaration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── username.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-custom-schema.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-schema.test.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 │ │ └── 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.base.json ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /demo/nextjs/app/dashboard/organization-card.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { organization, useListOrganizations, useSession, } from "@/lib/auth-client"; import { ActiveOrganization, Session } from "@/lib/auth-types"; import { ChevronDownIcon, PlusIcon } from "@radix-ui/react-icons"; import { Loader2, MailPlus } from "lucide-react"; import { useState, useEffect } from "react"; import { toast } from "sonner"; import { AnimatePresence, motion } from "framer-motion"; import CopyButton from "@/components/ui/copy-button"; import Image from "next/image"; export function OrganizationCard(props: { session: Session | null; activeOrganization: ActiveOrganization | null; }) { const organizations = useListOrganizations(); const [optimisticOrg, setOptimisticOrg] = useState<ActiveOrganization | null>( props.activeOrganization, ); const [isRevoking, setIsRevoking] = useState<string[]>([]); const inviteVariants = { hidden: { opacity: 0, height: 0 }, visible: { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, }; const { data } = useSession(); const session = data || props.session; const currentMember = optimisticOrg?.members.find( (member) => member.userId === session?.user.id, ); return ( <Card> <CardHeader> <CardTitle>Organization</CardTitle> <div className="flex justify-between"> <DropdownMenu> <DropdownMenuTrigger asChild> <div className="flex items-center gap-1 cursor-pointer"> <p className="text-sm"> <span className="font-bold"></span>{" "} {optimisticOrg?.name || "Personal"} </p> <ChevronDownIcon /> </div> </DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuItem className=" py-1" onClick={async () => { organization.setActive({ organizationId: null, }); setOptimisticOrg(null); }} > <p className="text-sm sm">Personal</p> </DropdownMenuItem> {organizations.data?.map((org) => ( <DropdownMenuItem className=" py-1" key={org.id} onClick={async () => { if (org.id === optimisticOrg?.id) { return; } setOptimisticOrg({ members: [], invitations: [], ...org, }); const { data } = await organization.setActive({ organizationId: org.id, }); setOptimisticOrg(data); }} > <p className="text-sm sm">{org.name}</p> </DropdownMenuItem> ))} </DropdownMenuContent> </DropdownMenu> <div> <CreateOrganizationDialog /> </div> </div> <div className="flex items-center gap-2"> <Avatar className="rounded-none"> <AvatarImage className="object-cover w-full h-full rounded-none" src={optimisticOrg?.logo || undefined} /> <AvatarFallback className="rounded-none"> {optimisticOrg?.name?.charAt(0) || "P"} </AvatarFallback> </Avatar> <div> <p>{optimisticOrg?.name || "Personal"}</p> <p className="text-xs text-muted-foreground"> {optimisticOrg?.members.length || 1} members </p> </div> </div> </CardHeader> <CardContent> <div className="flex gap-8 flex-col md:flex-row"> <div className="flex flex-col gap-2 grow"> <p className="font-medium border-b-2 border-b-foreground/10"> Members </p> <div className="flex flex-col gap-2"> {optimisticOrg?.members.map((member) => ( <div key={member.id} className="flex justify-between items-center" > <div className="flex items-center gap-2"> <Avatar className="sm:flex w-9 h-9"> <AvatarImage src={member.user.image || undefined} className="object-cover" /> <AvatarFallback> {member.user.name?.charAt(0)} </AvatarFallback> </Avatar> <div> <p className="text-sm">{member.user.name}</p> <p className="text-xs text-muted-foreground"> {member.role} </p> </div> </div> {member.role !== "owner" && (currentMember?.role === "owner" || currentMember?.role === "admin") && ( <Button size="sm" variant="destructive" onClick={() => { organization.removeMember({ memberIdOrEmail: member.id, }); }} > {currentMember?.id === member.id ? "Leave" : "Remove"} </Button> )} </div> ))} {!optimisticOrg?.id && ( <div> <div className="flex items-center gap-2"> <Avatar> <AvatarImage src={session?.user.image || undefined} /> <AvatarFallback> {session?.user.name?.charAt(0)} </AvatarFallback> </Avatar> <div> <p className="text-sm">{session?.user.name}</p> <p className="text-xs text-muted-foreground">Owner</p> </div> </div> </div> )} </div> </div> <div className="flex flex-col gap-2 grow"> <p className="font-medium border-b-2 border-b-foreground/10"> Invites </p> <div className="flex flex-col gap-2"> <AnimatePresence> {optimisticOrg?.invitations .filter((invitation) => invitation.status === "pending") .map((invitation) => ( <motion.div key={invitation.id} className="flex items-center justify-between" variants={inviteVariants} initial="hidden" animate="visible" exit="exit" layout > <div> <p className="text-sm">{invitation.email}</p> <p className="text-xs text-muted-foreground"> {invitation.role} </p> </div> <div className="flex items-center gap-2"> <Button disabled={isRevoking.includes(invitation.id)} size="sm" variant="destructive" onClick={() => { organization.cancelInvitation( { invitationId: invitation.id, }, { onRequest: () => { setIsRevoking([...isRevoking, invitation.id]); }, onSuccess: () => { toast.message( "Invitation revoked successfully", ); setIsRevoking( isRevoking.filter( (id) => id !== invitation.id, ), ); setOptimisticOrg({ ...optimisticOrg, invitations: optimisticOrg?.invitations.filter( (inv) => inv.id !== invitation.id, ), }); }, onError: (ctx) => { toast.error(ctx.error.message); setIsRevoking( isRevoking.filter( (id) => id !== invitation.id, ), ); }, }, ); }} > {isRevoking.includes(invitation.id) ? ( <Loader2 className="animate-spin" size={16} /> ) : ( "Revoke" )} </Button> <div> <CopyButton textToCopy={`${window.location.origin}/accept-invitation/${invitation.id}`} /> </div> </div> </motion.div> ))} </AnimatePresence> {optimisticOrg?.invitations.length === 0 && ( <motion.p className="text-sm text-muted-foreground" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > No Active Invitations </motion.p> )} {!optimisticOrg?.id && ( <Label className="text-xs text-muted-foreground"> You can't invite members to your personal workspace. </Label> )} </div> </div> </div> <div className="flex justify-end w-full mt-4"> <div> <div> {optimisticOrg?.id && ( <InviteMemberDialog setOptimisticOrg={setOptimisticOrg} optimisticOrg={optimisticOrg} /> )} </div> </div> </div> </CardContent> </Card> ); } function CreateOrganizationDialog() { const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [isSlugEdited, setIsSlugEdited] = useState(false); const [logo, setLogo] = useState<string | null>(null); useEffect(() => { if (!isSlugEdited) { const generatedSlug = name.trim().toLowerCase().replace(/\s+/g, "-"); setSlug(generatedSlug); } }, [name, isSlugEdited]); useEffect(() => { if (open) { setName(""); setSlug(""); setIsSlugEdited(false); setLogo(null); } }, [open]); const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; const reader = new FileReader(); reader.onloadend = () => { setLogo(reader.result as string); }; reader.readAsDataURL(file); } }; return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button size="sm" className="w-full gap-2" variant="default"> <PlusIcon /> <p>New Organization</p> </Button> </DialogTrigger> <DialogContent className="sm:max-w-[425px] w-11/12"> <DialogHeader> <DialogTitle>New Organization</DialogTitle> <DialogDescription> Create a new organization to collaborate with your team. </DialogDescription> </DialogHeader> <div className="flex flex-col gap-4"> <div className="flex flex-col gap-2"> <Label>Organization Name</Label> <Input placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} /> </div> <div className="flex flex-col gap-2"> <Label>Organization Slug</Label> <Input value={slug} onChange={(e) => { setSlug(e.target.value); setIsSlugEdited(true); }} placeholder="Slug" /> </div> <div className="flex flex-col gap-2"> <Label>Logo</Label> <Input type="file" accept="image/*" onChange={handleLogoChange} /> {logo && ( <div className="mt-2"> <Image src={logo} alt="Logo preview" className="w-16 h-16 object-cover" width={16} height={16} /> </div> )} </div> </div> <DialogFooter> <Button disabled={loading} onClick={async () => { setLoading(true); await organization.create( { name: name, slug: slug, logo: logo || undefined, }, { onResponse: () => { setLoading(false); }, onSuccess: () => { toast.success("Organization created successfully"); setOpen(false); }, onError: (error) => { toast.error(error.error.message); setLoading(false); }, }, ); }} > {loading ? ( <Loader2 className="animate-spin" size={16} /> ) : ( "Create" )} </Button> </DialogFooter> </DialogContent> </Dialog> ); } function InviteMemberDialog({ setOptimisticOrg, optimisticOrg, }: { setOptimisticOrg: (org: ActiveOrganization | null) => void; optimisticOrg: ActiveOrganization | null; }) { const [open, setOpen] = useState(false); const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const [loading, setLoading] = useState(false); return ( <Dialog> <DialogTrigger asChild> <Button size="sm" className="w-full gap-2" variant="secondary"> <MailPlus size={16} /> <p>Invite Member</p> </Button> </DialogTrigger> <DialogContent className="sm:max-w-[425px] w-11/12"> <DialogHeader> <DialogTitle>Invite Member</DialogTitle> <DialogDescription> Invite a member to your organization. </DialogDescription> </DialogHeader> <div className="flex flex-col gap-2"> <Label>Email</Label> <Input placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <Label>Role</Label> <Select value={role} onValueChange={setRole}> <SelectTrigger> <SelectValue placeholder="Select a role" /> </SelectTrigger> <SelectContent> <SelectItem value="admin">Admin</SelectItem> <SelectItem value="member">Member</SelectItem> </SelectContent> </Select> </div> <DialogFooter> <DialogClose> <Button disabled={loading} onClick={async () => { const invite = organization.inviteMember({ email: email, role: role as "member", fetchOptions: { throw: true, onSuccess: (ctx) => { if (optimisticOrg) { setOptimisticOrg({ ...optimisticOrg, invitations: [ ...(optimisticOrg?.invitations || []), ctx.data, ], }); } }, }, }); toast.promise(invite, { loading: "Inviting member...", success: "Member invited successfully", error: (error) => error.error.message, }); }} > Invite </Button> </DialogClose> </DialogFooter> </DialogContent> </Dialog> ); } ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/create-test-suite.ts: -------------------------------------------------------------------------------- ```typescript import type { User, Session, Verification, Account } from "../types"; import type { BetterAuthOptions } from "@better-auth/core"; import type { DBAdapter } from "@better-auth/core/db/adapter"; import { createAdapterFactory } from "./adapter-factory"; import { test } from "vitest"; import { generateId } from "../utils"; import type { Logger } from "./test-adapter"; import { TTY_COLORS } from "@better-auth/core/env"; import { betterAuth } from "../auth"; import { deepmerge } from "./utils"; type GenerateFn = <M extends "user" | "session" | "verification" | "account">( Model: M, ) => Promise< M extends "user" ? User : M extends "session" ? Session : M extends "verification" ? Verification : M extends "account" ? Account : undefined >; type Success<T> = { data: T; error: null; }; type Failure<E> = { data: null; error: E; }; type Result<T, E = Error> = Success<T> | Failure<E>; async function tryCatch<T, E = Error>( promise: Promise<T>, ): Promise<Result<T, E>> { try { const data = await promise; return { data, error: null }; } catch (error) { return { data: null, error: error as E }; } } export type InsertRandomFn = < M extends "user" | "session" | "verification" | "account", Count extends number = 1, >( model: M, count?: Count, ) => Promise< Count extends 1 ? M extends "user" ? [User] : M extends "session" ? [User, Session] : M extends "verification" ? [Verification] : M extends "account" ? [User, Account] : [undefined] : Array< M extends "user" ? [User] : M extends "session" ? [User, Session] : M extends "verification" ? [Verification] : M extends "account" ? [User, Account] : [undefined] > >; export const createTestSuite = < Tests extends Record< string, (context: { /** * Mark tests as skipped. All execution after this call will be skipped. * This function throws an error, so make sure you are not catching it accidentally. * @see {@link https://vitest.dev/guide/test-context#skip} */ readonly skip: { (note?: string): never; (condition: boolean, note?: string): void; }; }) => Promise<void> >, AdditionalOptions extends Record<string, any> = {}, >( suiteName: string, config: { defaultBetterAuthOptions?: BetterAuthOptions; /** * Helpful if the default better auth options require migrations to be run. */ alwaysMigrate?: boolean; prefixTests?: string; }, tests: ( helpers: { adapter: DBAdapter<BetterAuthOptions>; log: Logger; generate: GenerateFn; insertRandom: InsertRandomFn; /** * A light cleanup function that will only delete rows it knows about. */ cleanup: () => Promise<void>; /** * A hard cleanup function that will delete all rows from the database. */ hardCleanup: () => Promise<void>; modifyBetterAuthOptions: ( options: BetterAuthOptions, shouldRunMigrations: boolean, ) => Promise<BetterAuthOptions>; getBetterAuthOptions: () => BetterAuthOptions; sortModels: ( models: Array< Record<string, any> & { id: string; } >, by?: "id" | "createdAt", ) => (Record<string, any> & { id: string; })[]; getAuth: () => Promise<ReturnType<typeof betterAuth>>; tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>>; customIdGenerator?: () => string | Promise<string>; }, additionalOptions?: AdditionalOptions, ) => Tests, ) => { return ( options?: { disableTests?: Partial<Record<keyof Tests, boolean> & { ALL?: boolean }>; } & AdditionalOptions, ) => { return async (helpers: { adapter: () => Promise<DBAdapter<BetterAuthOptions>>; log: Logger; adapterDisplayName: string; getBetterAuthOptions: () => BetterAuthOptions; modifyBetterAuthOptions: ( options: BetterAuthOptions, ) => Promise<BetterAuthOptions>; cleanup: () => Promise<void>; runMigrations: () => Promise<void>; prefixTests?: string; onTestFinish: () => Promise<void>; customIdGenerator?: () => string | Promise<string>; }) => { const createdRows: Record<string, any[]> = {}; let adapter = await helpers.adapter(); const wrapperAdapter = (overrideOptions?: BetterAuthOptions) => { const options = deepmerge( deepmerge( helpers.getBetterAuthOptions(), config?.defaultBetterAuthOptions || {}, ), overrideOptions || {}, ); const adapterConfig = { adapterId: helpers.adapterDisplayName, ...(adapter.options?.adapterConfig || {}), adapterName: `Wrapped ${adapter.options?.adapterConfig.adapterName}`, disableTransformOutput: true, disableTransformInput: true, }; const adapterCreator = ( options: BetterAuthOptions, ): DBAdapter<BetterAuthOptions> => createAdapterFactory({ config: { ...adapterConfig, transaction: adapter.transaction, }, adapter: ({ getDefaultModelName }) => { adapter.transaction = undefined as any; return { count: adapter.count, deleteMany: adapter.deleteMany, delete: adapter.delete, findOne: adapter.findOne, findMany: adapter.findMany, update: adapter.update as any, updateMany: adapter.updateMany, createSchema: adapter.createSchema as any, async create({ data, model, select }) { const defaultModelName = getDefaultModelName(model); adapter = await helpers.adapter(); const res = await adapter.create({ data: data, model: defaultModelName, select, forceAllowId: true, }); createdRows[model] = [...(createdRows[model] || []), res]; return res as any; }, options: adapter.options, }; }, })(options); return adapterCreator(options); }; const resetDebugLogs = () => { //@ts-expect-error wrapperAdapter()?.adapterTestDebugLogs?.resetDebugLogs(); }; const printDebugLogs = () => { //@ts-expect-error wrapperAdapter()?.adapterTestDebugLogs?.printDebugLogs(); }; const cleanupCreatedRows = async () => { adapter = await helpers.adapter(); for (const model of Object.keys(createdRows)) { for (const row of createdRows[model]!) { try { await adapter.delete({ model, where: [{ field: "id", value: row.id }], }); } catch (error) { // We ignore any failed attempts to delete the created rows. } if (createdRows[model]!.length === 1) { delete createdRows[model]; } } } }; let didMigrateOnOptionsModify = false; const resetBetterAuthOptions = async () => { adapter = await helpers.adapter(); await helpers.modifyBetterAuthOptions( config.defaultBetterAuthOptions || {}, ); if (didMigrateOnOptionsModify) { didMigrateOnOptionsModify = false; await helpers.runMigrations(); adapter = await helpers.adapter(); } }; const generateModel: GenerateFn = async (model: string) => { const id = (await helpers.customIdGenerator?.()) || generateId(); const randomDate = new Date( Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 365, ); if (model === "user") { const user: User = { id, createdAt: randomDate, updatedAt: new Date(), email: `user-${id}@email.com`.toLowerCase(), emailVerified: true, name: `user-${id}`, image: null, }; return user as any; } if (model === "session") { const session: Session = { id, createdAt: randomDate, updatedAt: new Date(), expiresAt: new Date(), token: generateId(32), userId: generateId(), ipAddress: "127.0.0.1", userAgent: "Some User Agent", }; return session as any; } if (model === "verification") { const verification: Verification = { id, createdAt: randomDate, updatedAt: new Date(), expiresAt: new Date(), identifier: `test:${generateId()}`, value: generateId(), }; return verification as any; } if (model === "account") { const account: Account = { id, createdAt: randomDate, updatedAt: new Date(), accountId: generateId(), providerId: "test", userId: generateId(), accessToken: generateId(), refreshToken: generateId(), idToken: generateId(), accessTokenExpiresAt: new Date(), refreshTokenExpiresAt: new Date(), scope: "test", }; return account as any; } // This should never happen given the type constraints, but TypeScript needs an exhaustive check throw new Error(`Unknown model type: ${model}`); }; const insertRandom: InsertRandomFn = async < M extends "user" | "session" | "verification" | "account", Count extends number = 1, >( model: M, count: Count = 1 as Count, ) => { let res: any[] = []; const a = wrapperAdapter(); for (let i = 0; i < count; i++) { const modelResults = []; if (model === "user") { const user = await generateModel("user"); modelResults.push( await a.create({ data: user, model: "user", forceAllowId: true, }), ); } if (model === "session") { const user = await generateModel("user"); const userRes = await a.create({ data: user, model: "user", forceAllowId: true, }); const session = await generateModel("session"); session.userId = userRes.id; const sessionRes = await a.create({ data: session, model: "session", forceAllowId: true, }); modelResults.push(userRes, sessionRes); } if (model === "verification") { const verification = await generateModel("verification"); modelResults.push( await a.create({ data: verification, model: "verification", forceAllowId: true, }), ); } if (model === "account") { const user = await generateModel("user"); const account = await generateModel("account"); const userRes = await a.create({ data: user, model: "user", forceAllowId: true, }); account.userId = userRes.id; const accRes = await a.create({ data: account, model: "account", forceAllowId: true, }); modelResults.push(userRes, accRes); } res.push(modelResults); } return res.length === 1 ? res[0] : (res as any); }; const sortModels = ( models: Array<Record<string, any> & { id: string }>, by: "id" | "createdAt" = "id", ) => { return models.sort((a, b) => { if (by === "createdAt") { return ( new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); } return a.id.localeCompare(b.id); }); }; const modifyBetterAuthOptions = async ( opts: BetterAuthOptions, shouldRunMigrations: boolean, ) => { const res = helpers.modifyBetterAuthOptions( deepmerge(config?.defaultBetterAuthOptions || {}, opts), ); if (config.alwaysMigrate || shouldRunMigrations) { didMigrateOnOptionsModify = true; await helpers.runMigrations(); } return res; }; const additionalOptions = { ...options }; additionalOptions.disableTests = undefined; const fullTests = tests( { adapter: new Proxy({} as any, { get(target, prop) { const adapter = wrapperAdapter(); if (prop === "transaction") { return adapter.transaction; } const value = adapter[prop as keyof typeof adapter]; if (typeof value === "function") { return value.bind(adapter); } return value; }, }), getAuth: async () => { adapter = await helpers.adapter(); const auth = betterAuth({ ...helpers.getBetterAuthOptions(), ...(config?.defaultBetterAuthOptions || {}), database: (options: BetterAuthOptions) => { const adapter = wrapperAdapter(options); return adapter; }, }); return auth; }, log: helpers.log, generate: generateModel, cleanup: cleanupCreatedRows, hardCleanup: helpers.cleanup, insertRandom, modifyBetterAuthOptions, getBetterAuthOptions: helpers.getBetterAuthOptions, sortModels, tryCatch, customIdGenerator: helpers.customIdGenerator, }, additionalOptions as AdditionalOptions, ); const dash = `─`; const allDisabled: boolean = options?.disableTests?.ALL ?? false; // Here to display a label in the tests showing the suite name test(`\n${TTY_COLORS.fg.white}${" ".repeat(3)}${dash.repeat(35)} [${TTY_COLORS.fg.magenta}${suiteName}${TTY_COLORS.fg.white}] ${dash.repeat(35)}`, async () => { try { await helpers.cleanup(); } catch {} if (config.defaultBetterAuthOptions && !allDisabled) { await helpers.modifyBetterAuthOptions( config.defaultBetterAuthOptions, ); if (config.alwaysMigrate) { await helpers.runMigrations(); } } }); const onFinish = async (testName: string) => { await cleanupCreatedRows(); await resetBetterAuthOptions(); // Check if this is the last test by comparing current test index with total tests const testEntries = Object.entries(fullTests); const currentTestIndex = testEntries.findIndex( ([name]) => name === testName.replace(/\[.*?\] /, "").replace(/ ─ /g, " - "), ); const isLastTest = currentTestIndex === testEntries.length - 1; if (isLastTest) { await helpers.onTestFinish(); } }; if (allDisabled) { await resetBetterAuthOptions(); } for (let [testName, testFn] of Object.entries(fullTests)) { let shouldSkip = (allDisabled && options?.disableTests?.[testName] !== false) || (options?.disableTests?.[testName] ?? false); testName = testName.replace( " - ", ` ${TTY_COLORS.dim}${dash}${TTY_COLORS.undim} `, ); if (config.prefixTests) { testName = `${config.prefixTests} ${TTY_COLORS.dim}>${TTY_COLORS.undim} ${testName}`; } if (helpers.prefixTests) { testName = `[${TTY_COLORS.dim}${helpers.prefixTests}${TTY_COLORS.undim}] ${testName}`; } test.skipIf(shouldSkip)( testName, { timeout: 10000 }, async ({ onTestFailed, skip }) => { resetDebugLogs(); onTestFailed(async () => { printDebugLogs(); await onFinish(testName); }); await testFn({ skip }); await onFinish(testName); }, ); } }; }; }; ``` -------------------------------------------------------------------------------- /docs/content/docs/basic-usage.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Basic Usage description: Getting started with Better Auth --- Better Auth provides built-in authentication support for: - **Email and password** - **Social provider (Google, GitHub, Apple, and more)** But also can easily be extended using plugins, such as: [username](/docs/plugins/username), [magic link](/docs/plugins/magic-link), [passkey](/docs/plugins/passkey), [email-otp](/docs/plugins/email-otp), and more. ## Email & Password To enable email and password authentication: ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { // [!code highlight] enabled: true // [!code highlight] } // [!code highlight] }) ``` ### Sign Up To sign up a user you need to call the client method `signUp.email` with the user's information. ```ts title="sign-up.ts" import { authClient } from "@/lib/auth-client"; //import the auth client // [!code highlight] const { data, error } = await authClient.signUp.email({ email, // user email address password, // user password -> min 8 characters by default name, // user display name image, // User image URL (optional) callbackURL: "/dashboard" // A URL to redirect to after the user verifies their email (optional) }, { onRequest: (ctx) => { //show loading }, onSuccess: (ctx) => { //redirect to the dashboard or sign in page }, onError: (ctx) => { // display the error message alert(ctx.error.message); }, }); ``` By default, the users are automatically signed in after they successfully sign up. To disable this behavior you can set `autoSignIn` to `false`. ```ts title="auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true, autoSignIn: false //defaults to true // [!code highlight] }, }) ``` ### Sign In To sign a user in, you can use the `signIn.email` function provided by the client. ```ts title="sign-in" const { data, error } = await authClient.signIn.email({ /** * The user email */ email, /** * The user password */ password, /** * A URL to redirect to after the user verifies their email (optional) */ callbackURL: "/dashboard", /** * remember the user session after the browser is closed. * @default true */ rememberMe: false }, { //callbacks }) ``` <Callout type="warn"> Always invoke client methods from the client side. Don't call them from the server. </Callout> ### Server-Side Authentication To authenticate a user on the server, you can use the `auth.api` methods. ```ts title="server.ts" import { auth } from "./auth"; // path to your Better Auth server instance const response = await auth.api.signInEmail({ body: { email, password }, asResponse: true // returns a response object instead of data }); ``` <Callout> If the server cannot return a response object, you'll need to manually parse and set cookies. But for frameworks like Next.js we provide [a plugin](/docs/integrations/next#server-action-cookies) to handle this automatically </Callout> ## Social Sign-On Better Auth supports multiple social providers, including Google, GitHub, Apple, Discord, and more. To use a social provider, you need to configure the ones you need in the `socialProviders` option on your `auth` object. ```ts title="auth.ts" import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { // [!code highlight] github: { // [!code highlight] clientId: process.env.GITHUB_CLIENT_ID!, // [!code highlight] clientSecret: process.env.GITHUB_CLIENT_SECRET!, // [!code highlight] } // [!code highlight] }, // [!code highlight] }) ``` ### Sign in with social providers To sign in using a social provider you need to call `signIn.social`. It takes an object with the following properties: ```ts title="sign-in.ts" import { authClient } from "@/lib/auth-client"; //import the auth client // [!code highlight] await authClient.signIn.social({ /** * The social provider ID * @example "github", "google", "apple" */ provider: "github", /** * A URL to redirect after the user authenticates with the provider * @default "/" */ callbackURL: "/dashboard", /** * A URL to redirect if an error occurs during the sign in process */ errorCallbackURL: "/error", /** * A URL to redirect if the user is newly registered */ newUserCallbackURL: "/welcome", /** * disable the automatic redirect to the provider. * @default false */ disableRedirect: true, }); ``` You can also authenticate using `idToken` or `accessToken` from the social provider instead of redirecting the user to the provider's site. See social providers documentation for more details. ## Signout To signout a user, you can use the `signOut` function provided by the client. ```ts title="user-card.tsx" await authClient.signOut(); ``` you can pass `fetchOptions` to redirect onSuccess ```ts title="user-card.tsx" await authClient.signOut({ fetchOptions: { onSuccess: () => { router.push("/login"); // redirect to login page }, }, }); ``` ## Session Once a user is signed in, you'll want to access the user session. Better Auth allows you to easily access the session data from both the server and client sides. ### Client Side #### Use Session Better Auth provides a `useSession` hook to easily access session data on the client side. This hook is implemented using nanostore and has support for each supported framework and vanilla client, ensuring that any changes to the session (such as signing out) are immediately reflected in your UI. <Tabs items={["React", "Vue","Svelte", "Solid", "Vanilla"]} defaultValue="react"> <Tab value="React"> ```tsx title="user.tsx" import { authClient } from "@/lib/auth-client" // import the auth client // [!code highlight] export function User(){ const { // [!code highlight] data: session, // [!code highlight] isPending, //loading state // [!code highlight] error, //error object // [!code highlight] refetch //refetch the session } = authClient.useSession() // [!code highlight] return ( //... ) } ``` </Tab> <Tab value="Vue"> ```vue title="index.vue" <script setup lang="ts"> import { authClient } from "~/lib/auth-client" // [!code highlight] const session = authClient.useSession() // [!code highlight] </script> <template> <div> <div> <pre>{{ session.data }}</pre> <button v-if="session.data" @click="authClient.signOut()"> Sign out </button> </div> </div> </template> ``` </Tab> <Tab value="Svelte"> ```svelte title="user.svelte" <script lang="ts"> import { authClient } from "$lib/auth-client"; // [!code highlight] const session = authClient.useSession(); // [!code highlight] </script> <p> {$session.data?.user.email} </p> ``` </Tab> <Tab value="Vanilla"> ```ts title="user.svelte" import { authClient } from "~/lib/auth-client"; //import the auth client authClient.useSession.subscribe((value)=>{ //do something with the session // }) ``` </Tab> <Tab value="Solid"> ```tsx title="user.tsx" import { authClient } from "~/lib/auth-client"; // [!code highlight] export default function Home() { const session = authClient.useSession() // [!code highlight] return ( <pre>{JSON.stringify(session(), null, 2)}</pre> ); } ``` </Tab> </Tabs> #### Get Session If you prefer not to use the hook, you can use the `getSession` method provided by the client. ```ts title="user.tsx" import { authClient } from "@/lib/auth-client" // import the auth client // [!code highlight] const { data: session, error } = await authClient.getSession() ``` You can also use it with client-side data-fetching libraries like [TanStack Query](https://tanstack.com/query/latest). ### Server Side The server provides a `session` object that you can use to access the session data. It requires request headers object to be passed to the `getSession` method. **Example: Using some popular frameworks** <Tabs items={["Next.js", "Nuxt", "Svelte", "Astro", "Hono", "TanStack"]}> <Tab value="Next.js"> ```ts title="server.ts" import { auth } from "./auth"; // path to your Better Auth server instance import { headers } from "next/headers"; const session = await auth.api.getSession({ headers: await headers() // you need to pass the headers object. }) ``` </Tab> <Tab value="Remix"> ```ts title="route.ts" import { auth } from "lib/auth"; // path to your Better Auth server instance export async function loader({ request }: LoaderFunctionArgs) { const session = await auth.api.getSession({ headers: request.headers }) return json({ session }) } ``` </Tab> <Tab value="Astro"> ```astro title="index.astro" --- import { auth } from "./auth"; const session = await auth.api.getSession({ headers: Astro.request.headers, }); --- <!-- Your Astro Template --> ``` </Tab> <Tab value="Svelte"> ```ts title="+page.ts" import { auth } from "./auth"; export async function load({ request }) { const session = await auth.api.getSession({ headers: request.headers }) return { props: { session } } } ``` </Tab> <Tab value="Hono"> ```ts title="index.ts" import { auth } from "./auth"; const app = new Hono(); app.get("/path", async (c) => { const session = await auth.api.getSession({ headers: c.req.raw.headers }) }); ``` </Tab> <Tab value="Nuxt"> ```ts title="server/session.ts" import { auth } from "~/utils/auth"; export default defineEventHandler((event) => { const session = await auth.api.getSession({ headers: event.headers, }) }); ``` </Tab> <Tab value="TanStack"> ```ts title="app/routes/api/index.ts" import { auth } from "./auth"; import { createAPIFileRoute } from "@tanstack/start/api"; export const APIRoute = createAPIFileRoute("/api/$")({ GET: async ({ request }) => { const session = await auth.api.getSession({ headers: request.headers }) }, }); ``` </Tab> </Tabs> <Callout> For more details check [session-management](/docs/concepts/session-management) documentation. </Callout> ## Using Plugins One of the unique features of Better Auth is a plugins ecosystem. It allows you to add complex auth related functionality with small lines of code. Below is an example of how to add two factor authentication using two factor plugin. <Steps> <Step> ### Server Configuration To add a plugin, you need to import the plugin and pass it to the `plugins` option of the auth instance. For example, to add two factor authentication, you can use the following code: ```ts title="auth.ts" import { betterAuth } from "better-auth" import { twoFactor } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ //...rest of the options plugins: [ // [!code highlight] twoFactor() // [!code highlight] ] // [!code highlight] }) ``` now two factor related routes and method will be available on the server. </Step> <Step> ### Migrate Database After adding the plugin, you'll need to add the required tables to your database. You can do this by running the `migrate` command, or by using the `generate` command to create the schema and handle the migration manually. generating the schema: ```bash title="terminal" npx @better-auth/cli generate ``` using the `migrate` command: ```bash title="terminal" npx @better-auth/cli migrate ``` <Callout> If you prefer adding the schema manually, you can check the schema required on the [two factor plugin](/docs/plugins/2fa#schema) documentation. </Callout> </Step> <Step> ### Client Configuration Once we're done with the server, we need to add the plugin to the client. To do this, you need to import the plugin and pass it to the `plugins` option of the auth client. For example, to add two factor authentication, you can use the following code: ```ts title="auth-client.ts" import { createAuthClient } from "better-auth/client"; import { twoFactorClient } from "better-auth/client/plugins"; // [!code highlight] const authClient = createAuthClient({ plugins: [ // [!code highlight] twoFactorClient({ // [!code highlight] twoFactorPage: "/two-factor" // the page to redirect if a user needs to verify 2nd factor // [!code highlight] }) // [!code highlight] ] // [!code highlight] }) ``` now two factor related methods will be available on the client. ```ts title="profile.ts" import { authClient } from "./auth-client" const enableTwoFactor = async() => { const data = await authClient.twoFactor.enable({ password // the user password is required }) // this will enable two factor } const disableTwoFactor = async() => { const data = await authClient.twoFactor.disable({ password // the user password is required }) // this will disable two factor } const signInWith2Factor = async() => { const data = await authClient.signIn.email({ //... }) //if the user has two factor enabled, it will redirect to the two factor page } const verifyTOTP = async() => { const data = await authClient.twoFactor.verifyTOTP({ code: "123456", // the code entered by the user /** * If the device is trusted, the user won't * need to pass 2FA again on the same device */ trustDevice: true }) } ``` </Step> <Step> Next step: See the <Link href="/docs/plugins/2fa">two factor plugin documentation</Link>. </Step> </Steps> ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/sign-in.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import * as z from "zod"; import { createAuthEndpoint } from "@better-auth/core/api"; import { setSessionCookie } from "../../cookies"; import { createEmailVerificationToken } from "./email-verification"; import { generateState } from "../../utils"; import { handleOAuthUserInfo } from "../../oauth2/link-account"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; import { SocialProviderListEnum } from "@better-auth/core/social-providers"; export const signInSocial = createAuthEndpoint( "/sign-in/social", { method: "POST", body: z.object({ /** * Callback URL to redirect to after the user * has signed in. */ callbackURL: z .string() .meta({ description: "Callback URL to redirect to after the user has signed in", }) .optional(), /** * callback url to redirect if the user is newly registered. * * useful if you have different routes for existing users and new users */ newUserCallbackURL: z.string().optional(), /** * Callback url to redirect to if an error happens * * If it's initiated from the client sdk this defaults to * the current url. */ errorCallbackURL: z .string() .meta({ description: "Callback URL to redirect to if an error happens", }) .optional(), /** * OAuth2 provider to use` */ provider: SocialProviderListEnum, /** * Disable automatic redirection to the provider * * This is useful if you want to handle the redirection * yourself like in a popup or a different tab. */ disableRedirect: z .boolean() .meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself", }) .optional(), /** * ID token from the provider * * This is used to sign in the user * if the user is already signed in with the * provider in the frontend. * * Only applicable if the provider supports * it. Currently only `apple` and `google` is * supported out of the box. */ idToken: z.optional( z.object({ /** * ID token from the provider */ token: z.string().meta({ description: "ID token from the provider", }), /** * The nonce used to generate the token */ nonce: z .string() .meta({ description: "Nonce used to generate the token", }) .optional(), /** * Access token from the provider */ accessToken: z .string() .meta({ description: "Access token from the provider", }) .optional(), /** * Refresh token from the provider */ refreshToken: z .string() .meta({ description: "Refresh token from the provider", }) .optional(), /** * Expiry date of the token */ expiresAt: z .number() .meta({ description: "Expiry date of the token", }) .optional(), }), ), scopes: z .array(z.string()) .meta({ description: "Array of scopes to request from the provider. This will override the default scopes passed.", }) .optional(), /** * Explicitly request sign-up * * Should be used to allow sign up when * disableImplicitSignUp for this provider is * true */ requestSignUp: z .boolean() .meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider", }) .optional(), /** * The login hint to use for the authorization code request */ loginHint: z .string() .meta({ description: "The login hint to use for the authorization code request", }) .optional(), }), metadata: { openapi: { description: "Sign in with a social provider", operationId: "socialSignIn", responses: { "200": { description: "Success - Returns either session details or redirect URL", content: { "application/json": { schema: { // todo: we need support for multiple schema type: "object", description: "Session response when idToken is provided", properties: { redirect: { type: "boolean", enum: [false], }, token: { type: "string", description: "Session token", url: { type: "null", nullable: true, }, user: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, name: { type: "string", nullable: true, }, image: { type: "string", nullable: true, }, emailVerified: { type: "boolean", }, createdAt: { type: "string", format: "date-time", }, updatedAt: { type: "string", format: "date-time", }, }, required: [ "id", "email", "emailVerified", "createdAt", "updatedAt", ], }, }, }, required: ["redirect", "token", "user"], }, }, }, }, }, }, }, }, async (c) => { const provider = c.context.socialProviders.find( (p) => p.id === c.body.provider, ); if (!provider) { c.context.logger.error( "Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider, }, ); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND, }); } if (c.body.idToken) { if (!provider.verifyIdToken) { c.context.logger.error( "Provider does not support id token verification", { provider: c.body.provider, }, ); throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED, }); } const { token, nonce } = c.body.idToken; const valid = await provider.verifyIdToken(token, nonce); if (!valid) { c.context.logger.error("Invalid id token", { provider: c.body.provider, }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN, }); } const userInfo = await provider.getUserInfo({ idToken: token, accessToken: c.body.idToken.accessToken, refreshToken: c.body.idToken.refreshToken, }); if (!userInfo || !userInfo?.user) { c.context.logger.error("Failed to get user info", { provider: c.body.provider, }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO, }); } if (!userInfo.user.email) { c.context.logger.error("User email not found", { provider: c.body.provider, }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND, }); } const data = await handleOAuthUserInfo(c, { userInfo: { ...userInfo.user, email: userInfo.user.email, id: String(userInfo.user.id), name: userInfo.user.name || "", image: userInfo.user.image, emailVerified: userInfo.user.emailVerified || false, }, account: { providerId: provider.id, accountId: String(userInfo.user.id), accessToken: c.body.idToken.accessToken, }, callbackURL: c.body.callbackURL, disableSignUp: (provider.disableImplicitSignUp && !c.body.requestSignUp) || provider.disableSignUp, }); if (data.error) { throw new APIError("UNAUTHORIZED", { message: data.error, }); } await setSessionCookie(c, data.data!); return c.json({ redirect: false, token: data.data!.session.token, url: undefined, user: { id: data.data!.user.id, email: data.data!.user.email, name: data.data!.user.name, image: data.data!.user.image, emailVerified: data.data!.user.emailVerified, createdAt: data.data!.user.createdAt, updatedAt: data.data!.user.updatedAt, }, }); } const { codeVerifier, state } = await generateState(c); const url = await provider.createAuthorizationURL({ state, codeVerifier, redirectURI: `${c.context.baseURL}/callback/${provider.id}`, scopes: c.body.scopes, loginHint: c.body.loginHint, }); return c.json({ url: url.toString(), redirect: !c.body.disableRedirect, }); }, ); export const signInEmail = createAuthEndpoint( "/sign-in/email", { method: "POST", body: z.object({ /** * Email of the user */ email: z.string().meta({ description: "Email of the user", }), /** * Password of the user */ password: z.string().meta({ description: "Password of the user", }), /** * Callback URL to use as a redirect for email * verification and for possible redirects */ callbackURL: z .string() .meta({ description: "Callback URL to use as a redirect for email verification", }) .optional(), /** * If this is false, the session will not be remembered * @default true */ rememberMe: z .boolean() .meta({ description: "If this is false, the session will not be remembered. Default is `true`.", }) .default(true) .optional(), }), metadata: { openapi: { description: "Sign in with email and password", responses: { "200": { description: "Success - Returns either session details or redirect URL", content: { "application/json": { schema: { // todo: we need support for multiple schema type: "object", description: "Session response when idToken is provided", properties: { redirect: { type: "boolean", enum: [false], }, token: { type: "string", description: "Session token", }, url: { type: "null", nullable: true, }, user: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, name: { type: "string", nullable: true, }, image: { type: "string", nullable: true, }, emailVerified: { type: "boolean", }, createdAt: { type: "string", format: "date-time", }, updatedAt: { type: "string", format: "date-time", }, }, required: [ "id", "email", "emailVerified", "createdAt", "updatedAt", ], }, }, required: ["redirect", "token", "user"], }, }, }, }, }, }, }, }, async (ctx) => { if (!ctx.context.options?.emailAndPassword?.enabled) { ctx.context.logger.error( "Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!", ); throw new APIError("BAD_REQUEST", { message: "Email and password is not enabled", }); } const { email, password } = ctx.body; const isValidEmail = z.string().email().safeParse(email); if (!isValidEmail.success) { throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL, }); } const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true, }); if (!user) { // Hash password to prevent timing attacks from revealing valid email addresses // By hashing passwords for invalid emails, we ensure consistent response times await ctx.context.password.hash(password); ctx.context.logger.error("User not found", { email }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD, }); } const credentialAccount = user.accounts.find( (a) => a.providerId === "credential", ); if (!credentialAccount) { ctx.context.logger.error("Credential account not found", { email }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD, }); } const currentPassword = credentialAccount?.password; if (!currentPassword) { ctx.context.logger.error("Password not found", { email }); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD, }); } const validPassword = await ctx.context.password.verify({ hash: currentPassword, password, }); if (!validPassword) { ctx.context.logger.error("Invalid password"); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD, }); } if ( ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.user.emailVerified ) { if (!ctx.context.options?.emailVerification?.sendVerificationEmail) { throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED, }); } if (ctx.context.options?.emailVerification?.sendOnSignIn) { const token = await createEmailVerificationToken( ctx.context.secret, user.user.email, undefined, ctx.context.options.emailVerification?.expiresIn, ); const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/"); const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`; await ctx.context.options.emailVerification.sendVerificationEmail( { user: user.user, url, token, }, ctx.request, ); } throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED, }); } const session = await ctx.context.internalAdapter.createSession( user.user.id, ctx.body.rememberMe === false, ); if (!session) { ctx.context.logger.error("Failed to create session"); throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION, }); } await setSessionCookie( ctx, { session, user: user.user, }, ctx.body.rememberMe === false, ); return ctx.json({ redirect: !!ctx.body.callbackURL, token: session.token, url: ctx.body.callbackURL, user: { id: user.user.id, email: user.user.email, name: user.user.name, image: user.user.image, emailVerified: user.user.emailVerified, createdAt: user.user.createdAt, updatedAt: user.user.updatedAt, }, }); }, ); ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/session.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from "better-call"; import { createAuthEndpoint, createAuthMiddleware, } from "@better-auth/core/api"; import { getDate } from "../../utils/date"; import { deleteSessionCookie, setCookieCache, setSessionCookie, } from "../../cookies"; import * as z from "zod"; import type { InferSession, InferUser, Session, User } from "../../types"; import type { BetterAuthOptions } from "@better-auth/core"; import type { Prettify } from "../../types/helper"; import { safeJSONParse } from "../../utils/json"; import { BASE_ERROR_CODES } from "@better-auth/core/error"; import { createHMAC } from "@better-auth/utils/hmac"; import { base64Url } from "@better-auth/utils/base64"; import { binary } from "@better-auth/utils/binary"; import type { GenericEndpointContext } from "@better-auth/core"; export 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.coerce .boolean() .meta({ description: "Disable cookie cache and fetch session from database", }) .optional(), disableRefresh: z.coerce .boolean() .meta({ description: "Disable session refresh. Useful for checking session status, without updating the session", }) .optional(), }), ); export const getSession = <Option extends BetterAuthOptions>() => createAuthEndpoint( "/get-session", { method: "GET", query: getSessionQuerySchema, requireHeaders: true, metadata: { openapi: { description: "Get the current session", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { session: { $ref: "#/components/schemas/Session", }, user: { $ref: "#/components/schemas/User", }, }, required: ["session", "user"], }, }, }, }, }, }, }, }, async (ctx) => { try { const sessionCookieToken = await ctx.getSignedCookie( ctx.context.authCookies.sessionToken.name, ctx.context.secret, ); if (!sessionCookieToken) { return null; } const sessionDataCookie = ctx.getCookie( ctx.context.authCookies.sessionData.name, ); const sessionDataPayload = sessionDataCookie ? safeJSONParse<{ session: { session: Session; user: User; }; signature: string; expiresAt: number; }>(binary.decode(base64Url.decode(sessionDataCookie))) : null; if (sessionDataPayload) { const isValid = await createHMAC("SHA-256", "base64urlnopad").verify( ctx.context.secret, JSON.stringify({ ...sessionDataPayload.session, expiresAt: sessionDataPayload.expiresAt, }), sessionDataPayload.signature, ); if (!isValid) { const dataCookie = ctx.context.authCookies.sessionData.name; ctx.setCookie(dataCookie, "", { maxAge: 0, }); return ctx.json(null); } } const dontRememberMe = await ctx.getSignedCookie( ctx.context.authCookies.dontRememberToken.name, ctx.context.secret, ); /** * If session data is present in the cookie, return it */ if ( sessionDataPayload?.session && ctx.context.options.session?.cookieCache?.enabled && !ctx.query?.disableCookieCache ) { const session = sessionDataPayload.session; const hasExpired = sessionDataPayload.expiresAt < Date.now() || session.session.expiresAt < new Date(); if (!hasExpired) { ctx.context.session = session; return ctx.json( session as { session: InferSession<Option>; user: InferUser<Option>; }, ); } else { const dataCookie = ctx.context.authCookies.sessionData.name; ctx.setCookie(dataCookie, "", { maxAge: 0, }); } } const session = await ctx.context.internalAdapter.findSession(sessionCookieToken); ctx.context.session = session; if (!session || session.session.expiresAt < new Date()) { deleteSessionCookie(ctx); if (session) { /** * if session expired clean up the session */ await ctx.context.internalAdapter.deleteSession( session.session.token, ); } return ctx.json(null); } /** * We don't need to update the session if the user doesn't want to be remembered * or if the session refresh is disabled */ if (dontRememberMe || ctx.query?.disableRefresh) { return ctx.json( session as unknown as { session: InferSession<Option>; user: InferUser<Option>; }, ); } const expiresIn = ctx.context.sessionConfig.expiresIn; const updateAge = ctx.context.sessionConfig.updateAge; /** * Calculate last updated date to throttle write updates to database * Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge * * e.g. ({expiry date} - 30 days) + 1 hour * * inspired by: https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/lib/actions/session.ts */ const sessionIsDueToBeUpdatedDate = session.session.expiresAt.valueOf() - expiresIn * 1000 + updateAge * 1000; const shouldBeUpdated = sessionIsDueToBeUpdatedDate <= Date.now(); if ( shouldBeUpdated && (!ctx.query?.disableRefresh || !ctx.context.options.session?.disableSessionRefresh) ) { const updatedSession = await ctx.context.internalAdapter.updateSession( session.session.token, { expiresAt: getDate(ctx.context.sessionConfig.expiresIn, "sec"), updatedAt: new Date(), }, ); if (!updatedSession) { /** * Handle case where session update fails (e.g., concurrent deletion) */ deleteSessionCookie(ctx); return ctx.json(null, { status: 401 }); } const maxAge = (updatedSession.expiresAt.valueOf() - Date.now()) / 1000; await setSessionCookie( ctx, { session: updatedSession, user: session.user, }, false, { maxAge, }, ); return ctx.json({ session: updatedSession, user: session.user, } as unknown as { session: InferSession<Option>; user: InferUser<Option>; }); } await setCookieCache(ctx, session, !!dontRememberMe); return ctx.json( session as unknown as { session: InferSession<Option>; user: InferUser<Option>; }, ); } catch (error) { ctx.context.logger.error("INTERNAL_SERVER_ERROR", error); throw new APIError("INTERNAL_SERVER_ERROR", { message: BASE_ERROR_CODES.FAILED_TO_GET_SESSION, }); } }, ); export const getSessionFromCtx = async < U extends Record<string, any> = Record<string, any>, S extends Record<string, any> = Record<string, any>, >( ctx: GenericEndpointContext, config?: { disableCookieCache?: boolean; disableRefresh?: boolean; }, ) => { if (ctx.context.session) { return ctx.context.session as { session: S & Session; user: U & User; }; } const session = await getSession()({ ...ctx, asResponse: false, headers: ctx.headers!, returnHeaders: false, query: { ...config, ...ctx.query, }, }).catch((e) => { return null; }); ctx.context.session = session; return session as { session: S & Session; user: U & User; } | null; }; /** * The middleware forces the endpoint to require a valid session. */ export const sessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session) { throw new APIError("UNAUTHORIZED"); } return { session, }; }); /** * This middleware forces the endpoint to require a valid session and ignores cookie cache. * This should be used for sensitive operations like password changes, account deletion, etc. * to ensure that revoked sessions cannot be used even if they're still cached in cookies. */ export const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx, { disableCookieCache: true }); if (!session?.session) { throw new APIError("UNAUTHORIZED"); } return { session, }; }); /** * This middleware allows you to call the endpoint on the client if session is valid. * However, if called on the server, no session is required. */ export const requestOnlySessionMiddleware = createAuthMiddleware( async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session && (ctx.request || ctx.headers)) { throw new APIError("UNAUTHORIZED"); } return { session }; }, ); /** * This middleware forces the endpoint to require a valid session, * as well as making sure the session is fresh before proceeding. * * Session freshness check will be skipped if the session config's freshAge * is set to 0 */ export const freshSessionMiddleware = createAuthMiddleware(async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session?.session) { throw new APIError("UNAUTHORIZED"); } if (ctx.context.sessionConfig.freshAge === 0) { return { session, }; } const freshAge = ctx.context.sessionConfig.freshAge; const lastUpdated = session.session.updatedAt?.valueOf() || session.session.createdAt.valueOf(); const now = Date.now(); const isFresh = now - lastUpdated < freshAge * 1000; if (!isFresh) { throw new APIError("FORBIDDEN", { message: "Session is not fresh", }); } return { session, }; }); /** * user active sessions list */ export const listSessions = <Option extends BetterAuthOptions>() => createAuthEndpoint( "/list-sessions", { method: "GET", use: [sessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "List all active sessions for the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Session", }, }, }, }, }, }, }, }, }, async (ctx) => { try { const sessions = await ctx.context.internalAdapter.listSessions( ctx.context.session.user.id, ); const activeSessions = sessions.filter((session) => { return session.expiresAt > new Date(); }); return ctx.json( activeSessions as unknown as Prettify<InferSession<Option>>[], ); } catch (e: any) { ctx.context.logger.error(e); throw ctx.error("INTERNAL_SERVER_ERROR"); } }, ); /** * revoke a single session */ export const revokeSession = createAuthEndpoint( "/revoke-session", { method: "POST", body: z.object({ token: z.string().meta({ description: "The token to revoke", }), }), use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "Revoke a single session", requestBody: { content: { "application/json": { schema: { type: "object", properties: { token: { type: "string", description: "The token to revoke", }, }, required: ["token"], }, }, }, }, responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if the session was revoked successfully", }, }, required: ["status"], }, }, }, }, }, }, }, }, async (ctx) => { const token = ctx.body.token; const findSession = await ctx.context.internalAdapter.findSession(token); if (!findSession) { throw new APIError("BAD_REQUEST", { message: "Session not found", }); } if (findSession.session.userId !== ctx.context.session.user.id) { throw new APIError("UNAUTHORIZED"); } try { await ctx.context.internalAdapter.deleteSession(token); } catch (error) { ctx.context.logger.error( error && typeof error === "object" && "name" in error ? (error.name as string) : "", error, ); throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true, }); }, ); /** * revoke all user sessions */ export const revokeSessions = createAuthEndpoint( "/revoke-sessions", { method: "POST", use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { description: "Revoke all sessions for the user", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if all sessions were revoked successfully", }, }, required: ["status"], }, }, }, }, }, }, }, }, async (ctx) => { try { await ctx.context.internalAdapter.deleteSessions( ctx.context.session.user.id, ); } catch (error) { ctx.context.logger.error( error && typeof error === "object" && "name" in error ? (error.name as string) : "", error, ); throw new APIError("INTERNAL_SERVER_ERROR"); } return ctx.json({ status: true, }); }, ); export const revokeOtherSessions = createAuthEndpoint( "/revoke-other-sessions", { method: "POST", requireHeaders: true, use: [sensitiveSessionMiddleware], metadata: { openapi: { description: "Revoke all other sessions for the user except the current one", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { status: { type: "boolean", description: "Indicates if all other sessions were revoked successfully", }, }, required: ["status"], }, }, }, }, }, }, }, }, async (ctx) => { const session = ctx.context.session; if (!session.user) { throw new APIError("UNAUTHORIZED"); } const sessions = await ctx.context.internalAdapter.listSessions( session.user.id, ); const activeSessions = sessions.filter((session) => { return session.expiresAt > new Date(); }); const otherSessions = activeSessions.filter( (session) => session.token !== ctx.context.session.session.token, ); await Promise.all( otherSessions.map((session) => ctx.context.internalAdapter.deleteSession(session.token), ), ); return ctx.json({ status: true, }); }, ); ``` -------------------------------------------------------------------------------- /demo/nextjs/lib/auth.ts: -------------------------------------------------------------------------------- ```typescript import { betterAuth } from "better-auth"; import { bearer, admin, multiSession, organization, twoFactor, oneTap, oAuthProxy, openAPI, customSession, deviceAuthorization, lastLoginMethod, } from "better-auth/plugins"; import { reactInvitationEmail } from "./email/invitation"; import { LibsqlDialect } from "@libsql/kysely-libsql"; import { reactResetPasswordEmail } from "./email/reset-password"; import { resend } from "./email/resend"; import { MysqlDialect } from "kysely"; import { createPool } from "mysql2/promise"; import { nextCookies } from "better-auth/next-js"; import { passkey } from "better-auth/plugins/passkey"; import { stripe } from "@better-auth/stripe"; import { sso } from "@better-auth/sso"; import { Stripe } from "stripe"; const from = process.env.BETTER_AUTH_EMAIL || "[email protected]"; const to = process.env.TEST_EMAIL || ""; const dialect = (() => { if (process.env.USE_MYSQL) { if (!process.env.MYSQL_DATABASE_URL) { throw new Error( "Using MySQL dialect without MYSQL_DATABASE_URL. Please set it in your environment variables.", ); } return new MysqlDialect(createPool(process.env.MYSQL_DATABASE_URL || "")); } else { if (process.env.TURSO_DATABASE_URL && process.env.TURSO_AUTH_TOKEN) { return new LibsqlDialect({ url: process.env.TURSO_DATABASE_URL, authToken: process.env.TURSO_AUTH_TOKEN, }); } } return null; })(); if (!dialect) { throw new Error("No dialect found"); } const baseURL: string | undefined = process.env.VERCEL === "1" ? process.env.VERCEL_ENV === "production" ? process.env.BETTER_AUTH_URL : process.env.VERCEL_ENV === "preview" ? `https://${process.env.VERCEL_URL}` : undefined : undefined; const cookieDomain: string | undefined = process.env.VERCEL === "1" ? process.env.VERCEL_ENV === "production" ? ".better-auth.com" : process.env.VERCEL_ENV === "preview" ? `.${process.env.VERCEL_URL}` : undefined : undefined; export const auth = betterAuth({ appName: "Better Auth Demo", baseURL, database: { dialect, type: "sqlite", }, emailVerification: { async sendVerificationEmail({ user, url }) { const res = await resend.emails.send({ from, to: to || user.email, subject: "Verify your email address", html: `<a href="${url}">Verify your email address</a>`, }); console.log(res, user.email); }, }, account: { accountLinking: { trustedProviders: ["google", "github", "demo-app", "sso"], }, }, emailAndPassword: { enabled: true, async sendResetPassword({ user, url }) { await resend.emails.send({ from, to: user.email, subject: "Reset your password", react: reactResetPasswordEmail({ username: user.email, resetLink: url, }), }); }, }, socialProviders: { facebook: { clientId: process.env.FACEBOOK_CLIENT_ID || "", clientSecret: process.env.FACEBOOK_CLIENT_SECRET || "", }, github: { clientId: process.env.GITHUB_CLIENT_ID || "", clientSecret: process.env.GITHUB_CLIENT_SECRET || "", }, google: { clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }, discord: { clientId: process.env.DISCORD_CLIENT_ID || "", clientSecret: process.env.DISCORD_CLIENT_SECRET || "", }, microsoft: { clientId: process.env.MICROSOFT_CLIENT_ID || "", clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "", }, twitch: { clientId: process.env.TWITCH_CLIENT_ID || "", clientSecret: process.env.TWITCH_CLIENT_SECRET || "", }, twitter: { clientId: process.env.TWITTER_CLIENT_ID || "", clientSecret: process.env.TWITTER_CLIENT_SECRET || "", }, paypal: { clientId: process.env.PAYPAL_CLIENT_ID || "", clientSecret: process.env.PAYPAL_CLIENT_SECRET || "", }, }, plugins: [ organization({ async sendInvitationEmail(data) { await resend.emails.send({ from, to: data.email, subject: "You've been invited to join an organization", react: reactInvitationEmail({ username: data.email, invitedByUsername: data.inviter.user.name, invitedByEmail: data.inviter.user.email, teamName: data.organization.name, inviteLink: process.env.NODE_ENV === "development" ? `http://localhost:3000/accept-invitation/${data.id}` : `${ process.env.BETTER_AUTH_URL || "https://demo.better-auth.com" }/accept-invitation/${data.id}`, }), }); }, }), twoFactor({ otpOptions: { async sendOTP({ user, otp }) { await resend.emails.send({ from, to: user.email, subject: "Your OTP", html: `Your OTP is ${otp}`, }); }, }, }), passkey(), openAPI(), bearer(), admin({ adminUserIds: ["EXD5zjob2SD6CBWcEQ6OpLRHcyoUbnaB"], }), multiSession(), oAuthProxy(), nextCookies(), oneTap(), customSession(async (session) => { return { ...session, user: { ...session.user, dd: "test", }, }; }), stripe({ stripeClient: new Stripe(process.env.STRIPE_KEY || "sk_test_"), stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, subscription: { enabled: true, allowReTrialsForDifferentPlans: true, plans: () => { const PRO_PRICE_ID = { default: process.env.STRIPE_PRO_PRICE_ID ?? "price_1RoxnRHmTADgihIt4y8c0lVE", annual: process.env.STRIPE_PRO_ANNUAL_PRICE_ID ?? "price_1RoxnoHmTADgihItzFvVP8KT", }; const PLUS_PRICE_ID = { default: process.env.STRIPE_PLUS_PRICE_ID ?? "price_1RoxnJHmTADgihIthZTLmrPn", annual: process.env.STRIPE_PLUS_ANNUAL_PRICE_ID ?? "price_1Roxo5HmTADgihItEbJu5llL", }; return [ { name: "Plus", priceId: PLUS_PRICE_ID.default, annualDiscountPriceId: PLUS_PRICE_ID.annual, freeTrial: { days: 7, }, }, { name: "Pro", priceId: PRO_PRICE_ID.default, annualDiscountPriceId: PRO_PRICE_ID.annual, freeTrial: { days: 7, }, }, ]; }, }, }), sso({ defaultSSO: [ { domain: "http://localhost:3000", providerId: "sso", samlConfig: { issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata", entryPoint: "https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435", cert: `-----BEGIN CERTIFICATE----- MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g 0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7 mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm 2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9 vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+ jzGhYL6m9gFTm/8= -----END CERTIFICATE-----`, spMetadata: { metadata: ` <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/api/auth/sso/saml2/sp/metadata"> <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:KeyDescriptor use="signing"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:KeyDescriptor use="encryption"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/sls"/> <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/> </md:SPSSODescriptor> <md:Organization> <md:OrganizationName xml:lang="en-US">Organization Name</md:OrganizationName> <md:OrganizationDisplayName xml:lang="en-US">Organization DisplayName</md:OrganizationDisplayName> <md:OrganizationURL xml:lang="en-US">http://localhost:3000/</md:OrganizationURL> </md:Organization> <md:ContactPerson contactType="technical"> <md:GivenName>Technical Contact Name</md:GivenName> <md:EmailAddress>[email protected]</md:EmailAddress> </md:ContactPerson> <md:ContactPerson contactType="support"> <md:GivenName>Support Contact Name</md:GivenName> <md:EmailAddress>[email protected]</md:EmailAddress> </md:ContactPerson> </md:EntityDescriptor> `, }, idpMetadata: { entityURL: "https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/metadata", entityID: "https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435", redirectURL: "https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso", singleSignOnService: [ { Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Location: "https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso", }, ], cert: `-----BEGIN CERTIFICATE----- MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g 0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7 mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm 2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9 vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+ jzGhYL6m9gFTm/8= -----END CERTIFICATE-----`, }, callbackUrl: "/dashboard", }, }, ], }), deviceAuthorization({ expiresIn: "3min", interval: "5s", }), lastLoginMethod(), ], trustedOrigins: ["exp://"], advanced: { crossSubDomainCookies: { enabled: process.env.NODE_ENV === "production", domain: cookieDomain, }, }, }); ``` -------------------------------------------------------------------------------- /docs/app/community/_components/stats.tsx: -------------------------------------------------------------------------------- ```typescript "use client"; import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { kFormatter } from "@/lib/utils"; export default function Stats({ npmDownloads, githubStars, }: { npmDownloads: number; githubStars: number; }) { return ( <div className="relative"> <div className="md:mx-auto w-full"> <div className="border border-b-0 rounded-none overflow-hidden border-l-0 border-r-0"> <div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-input"> <div className="flex pt-5 dark:[box-shadow:0_-20px_80px_-20px_#dfbf9f1f_inset] flex-col items-center justify-between"> <div className="relative flex flex-col p-3"> <div className="inline-flex dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#8686f01f_inset] border rounded-full items-center justify-center p-1 w-[4.0em] h-[4.0em] mx-auto mb-4"> <svg xmlns="http://www.w3.org/2000/svg" width="4em" height="4em" viewBox="0 0 24 24" className="my-2" > <path fill="currentColor" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" ></path> </svg> </div> <span className="text-xl uppercase tracking-tighter font-bold font-mono bg-gradient-to-b dark:from-stone-200 dark:via-stone-400 dark:to-stone-700 bg-clip-text text-transparent drop-shadow-[0_0_10px_rgba(255,255,255,0.1)] from-stone-800 via-stone-600 to-stone-400"> Discord </span> </div> <div className="flex items-end w-full gap-2 mt-4 text-gray-400"> <Link className="w-full" href="https://discord.gg/better-auth" rel="noopener noreferrer" target="_blank" > <Button variant="outline" className="group duration-500 cursor-pointer text-gray-400 flex items-center gap-2 text-md hover:bg-transparent border-l-input/50 border-r-input/50 md:border-r-0 md:border-l-0 border-t-[1px] border-t-input py-7 w-full hover:text-black dark:hover:text-white" > <span className="uppercase font-mono group-hover:text-black duration-300 dark:group-hover:text-white"> Join Our Discord </span> <ArrowUpRight className="w-6 h-6 opacity-20 ml-2 group-hover:opacity-300 duration-300 text-black group-hover:duration-700 dark:text-white" /> </Button> </Link> </div> </div> <div className="flex pt-5 dark:[box-shadow:0_-20px_80px_-20px_#dfbf9f1f_inset] flex-col items-center justify-between"> <div className="relative flex flex-col p-3"> <div className="inline-flex dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#8686f01f_inset] border rounded-full items-center justify-center p-1 w-[4.0em] h-[4.0em] mx-auto mb-4"> <svg xmlns="http://www.w3.org/2000/svg" width="4em" height="4em" viewBox="0 0 28 28" className="mt-1 ml-1" > <path fill="currentColor" d="M25.418 12v.03c0 .543-.156 1.05-.425 1.479l.007-.012a2.77 2.77 0 0 1-1.112 1.021l-.016.007c.108.403.17.865.17 1.343v.018v-.001a6.33 6.33 0 0 1-1.518 4.08l.007-.009a10.2 10.2 0 0 1-4.052 2.936l-.069.024c-1.635.686-3.535 1.085-5.529 1.085L12.728 24h.008l-.146.001c-1.991 0-3.888-.399-5.617-1.121l.096.036a10.26 10.26 0 0 1-4.101-2.944l-.013-.016a6.3 6.3 0 0 1-1.51-4.069v-.007q.002-.707.161-1.366l-.008.04a2.86 2.86 0 0 1-1.156-1.029l-.007-.011a2.8 2.8 0 0 1-.44-1.512c0-.777.314-1.481.823-1.991a2.7 2.7 0 0 1 1.952-.83h.05h-.003h.039c.799 0 1.519.343 2.019.889l.002.002a13.14 13.14 0 0 1 7.296-2.298h.008l1.646-7.39a.48.48 0 0 1 .211-.296l.002-.001a.46.46 0 0 1 .372-.071l-.003-.001l5.234 1.149c.174-.353.435-.639.757-.838l.009-.005c.319-.2.707-.319 1.123-.319c.585 0 1.116.235 1.501.617c.385.369.624.888.624 1.463v.036v-.002v.03c0 .578-.239 1.1-.624 1.472l-.001.001a2.1 2.1 0 0 1-1.504.624a2.12 2.12 0 0 1-1.497-.617a2.03 2.03 0 0 1-.617-1.461v-.038v.002l-4.738-1.05l-1.475 6.694c2.747.02 5.293.865 7.407 2.3l-.047-.03a2.8 2.8 0 0 1 2.031-.865c.78 0 1.486.317 1.997.83c.509.496.825 1.189.825 1.955v.039V12zM5.929 14.822v.032c0 .576.236 1.097.617 1.471a2.02 2.02 0 0 0 1.463.624h.036h-.002a2.13 2.13 0 0 0 2.128-2.128v-.034c0-.575-.239-1.094-.624-1.462l-.001-.001a2.06 2.06 0 0 0-1.471-.617h-.034h.002a2.13 2.13 0 0 0-2.114 2.113v.001zm11.489 5.036a.513.513 0 0 0 0-.738a.48.48 0 0 0-.341-.142h-.014h.001h-.008a.53.53 0 0 0-.361.142a3.54 3.54 0 0 1-1.694.876l-.023.004a9.26 9.26 0 0 1-4.604-.014l.064.014a3.55 3.55 0 0 1-1.721-.882l.002.002a.53.53 0 0 0-.361-.142h-.019a.48.48 0 0 0-.341.142a.47.47 0 0 0-.16.352v.014c0 .146.061.278.16.372a4.2 4.2 0 0 0 1.65.957l.03.008a8 8 0 0 0 1.695.414l.043.005q.666.064 1.29.064t1.29-.064a8.4 8.4 0 0 0 1.796-.437l-.058.019a4.2 4.2 0 0 0 1.685-.966l-.002.002zm-.042-2.908h.034c.575 0 1.094-.239 1.462-.624l.001-.001c.381-.374.617-.895.617-1.471v-.034v.002a2.13 2.13 0 0 0-2.113-2.114h-.033c-.576 0-1.097.236-1.471.617a2.02 2.02 0 0 0-.624 1.463v.036v-.002a2.13 2.13 0 0 0 2.128 2.128z" /> </svg> </div> <span className="text-xl uppercase tracking-tighter font-bold font-mono bg-gradient-to-b dark:from-stone-200 dark:via-stone-400 dark:to-stone-700 bg-clip-text text-transparent drop-shadow-[0_0_10px_rgba(255,255,255,0.1)] from-stone-800 via-stone-600 to-stone-400"> Reddit </span> </div> <div className="flex items-end w-full gap-2 mt-4 text-gray-400"> <Link className="w-full" href="https://reddit.com/r/better_auth" rel="noopener noreferrer" target="_blank" > <Button variant="outline" className="group duration-500 cursor-pointer text-gray-400 flex items-center gap-2 text-md hover:bg-transparent border-l-input/50 border-r-input/50 md:border-r-0 md:border-l-0 border-t-[1px] border-t-input py-7 w-full hover:text-black dark:hover:text-white" > <span className="uppercase font-mono group-hover:text-black duration-300 dark:group-hover:text-white"> Join Subreddit </span> <ArrowUpRight className="w-6 h-6 opacity-20 ml-2 group-hover:opacity-300 duration-300 text-black group-hover:duration-700 dark:text-white" /> </Button> </Link> </div> </div> <div className="flex pt-5 dark:[box-shadow:0_-20px_80px_-20px_#dfbf9f1f_inset] flex-col items-center justify-between"> <div className="relative flex flex-col p-3"> <div className="flex dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#8686f01f_inset] border rounded-full items-center justify-center p-1 w-[4.0em] h-[4.0em] mx-auto mb-4"> <svg xmlns="http://www.w3.org/2000/svg" width="4em" height="4em" viewBox="0 0 19 19" className="my-2 mx-auto pl-2 pt-1" > <path fill="currentColor" d="M9.294 6.928L14.357 1h-1.2L8.762 6.147L5.25 1H1.2l5.31 7.784L1.2 15h1.2l4.642-5.436L10.751 15h4.05zM7.651 8.852l-.538-.775L2.832 1.91h1.843l3.454 4.977l.538.775l4.491 6.47h-1.843z" /> </svg> </div> <span className="text-xl uppercase tracking-tighter font-bold font-mono bg-gradient-to-b dark:from-stone-200 dark:via-stone-400 dark:to-stone-700 bg-clip-text text-transparent drop-shadow-[0_0_10px_rgba(255,255,255,0.1)] from-stone-800 via-stone-600 to-stone-400"> Twitter </span> </div> <div className="flex items-end w-full gap-2 mt-4 text-gray-400"> <Link className="w-full" href="https://x.com/better_auth" rel="noopener noreferrer" target="_blank" > <Button variant="outline" className="group duration-500 cursor-pointer text-gray-400 flex items-center gap-2 text-md hover:bg-transparent border-l-input/50 border-r-input/50 md:border-r-0 md:border-l-0 border-t-[1px] border-t-input py-7 w-full hover:text-black dark:hover:text-white" > <span className="uppercase font-mono group-hover:text-black duration-300 dark:group-hover:text-white"> Follow on 𝕏 </span> <ArrowUpRight className="w-6 h-6 opacity-20 ml-2 group-hover:opacity-300 duration-300 text-black group-hover:duration-700 dark:text-white" /> </Button> </Link> </div> </div> </div> </div> <div> <div className="flex md:flex-row flex-col w-full dark:[box-shadow:0_-20px_80px_-20px_#dfbf9f1f_inset]"> <div className="w-full text-center border-r pt-5"> <div className="relative p-3 "> <span className="text-[70px] tracking-tighter font-bold font-mono bg-gradient-to-b dark:from-stone-200 dark:via-stone-400 dark:to-stone-700 bg-clip-text text-transparent drop-shadow-[0_0_10px_rgba(255,255,255,0.1)] from-stone-800 via-stone-600 to-stone-400"> {kFormatter(npmDownloads)} </span> </div> <div className="flex items-end w-full gap-2 mt-4 text-gray-400"> <Link className="w-full" href="https://www.npmjs.com/package/better-auth" rel="noopener noreferrer" target="_blank" > <Button variant="outline" className="group duration-500 cursor-pointer text-gray-400 flex items-center gap-2 text-md hover:bg-transparent border-l-input/50 border-r-input/50 md:border-r-0 md:border-l-0 border-t-[1px] border-t-input py-7 w-full hover:text-black dark:hover:text-white" > <svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 128 128" > <path fill="#000" d="M0 7.062C0 3.225 3.225 0 7.062 0h113.88c3.838 0 7.063 3.225 7.063 7.062v113.88c0 3.838-3.225 7.063-7.063 7.063H7.062c-3.837 0-7.062-3.225-7.062-7.063zm23.69 97.518h40.395l.05-58.532h19.494l-.05 58.581h19.543l.05-78.075l-78.075-.1l-.1 78.126z" ></path> <path fill="#fff" d="M25.105 65.52V26.512H40.96c8.72 0 26.274.034 39.008.075l23.153.075v77.866H83.645v-58.54H64.057v58.54H25.105z" ></path> </svg> <span className="uppercase font-mono group-hover:text-black duration-300 dark:group-hover:text-white"> Downloads </span> <ArrowUpRight className="w-6 h-6 opacity-20 ml-2 group-hover:opacity-300 duration-300 text-black group-hover:duration-700 dark:text-white" /> </Button> </Link> </div> </div> <div className="w-full text-center pt-5"> <div className="relative p-3"> <span className="text-[70px] tracking-tighter font-bold font-mono bg-gradient-to-b dark:from-stone-200 dark:via-stone-400 dark:to-stone-700 bg-clip-text text-transparent drop-shadow-[0_0_10px_rgba(255,255,255,0.1)] from-stone-800 via-stone-600 to-stone-400"> {kFormatter(githubStars)} </span> </div> <div className="flex -p-8 items-end w-full gap-2 mt-4 text-gray-400"> <Link className="w-full" href="https://github.com/better-auth/better-auth" rel="noopener noreferrer" target="_blank" > <Button variant="outline" className="group duration-500 cursor-pointer text-gray-400 flex items-center gap-2 text-md hover:bg-transparent border-l-input/50 border-r-input/50 md:border-r-0 md:border-l-0 border-t-[1px] border-t-input py-7 w-full hover:text-black dark:hover:text-white" > <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" > <g fill="none"> <path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /> <path fill="currentColor" d="M6.315 6.176c-.25-.638-.24-1.367-.129-2.034a6.8 6.8 0 0 1 2.12 1.07c.28.214.647.283.989.18A9.3 9.3 0 0 1 12 5c.961 0 1.874.14 2.703.391c.342.104.709.034.988-.18a6.8 6.8 0 0 1 2.119-1.07c.111.667.12 1.396-.128 2.033c-.15.384-.075.826.208 1.14C18.614 8.117 19 9.04 19 10c0 2.114-1.97 4.187-5.134 4.818c-.792.158-1.101 1.155-.495 1.726c.389.366.629.882.629 1.456v3a1 1 0 0 0 2 0v-3c0-.57-.12-1.112-.334-1.603C18.683 15.35 21 12.993 21 10c0-1.347-.484-2.585-1.287-3.622c.21-.82.191-1.646.111-2.28c-.071-.568-.17-1.312-.57-1.756c-.595-.659-1.58-.271-2.28-.032a9 9 0 0 0-2.125 1.045A11.4 11.4 0 0 0 12 3c-.994 0-1.953.125-2.851.356a9 9 0 0 0-2.125-1.045c-.7-.24-1.686-.628-2.281.031c-.408.452-.493 1.137-.566 1.719l-.005.038c-.08.635-.098 1.462.112 2.283C3.484 7.418 3 8.654 3 10c0 2.992 2.317 5.35 5.334 6.397A4 4 0 0 0 8 17.98l-.168.034c-.717.099-1.176.01-1.488-.122c-.76-.322-1.152-1.133-1.63-1.753c-.298-.385-.732-.866-1.398-1.088a1 1 0 0 0-.632 1.898c.558.186.944 1.142 1.298 1.566c.373.448.869.916 1.58 1.218c.682.29 1.483.393 2.438.276V21a1 1 0 0 0 2 0v-3c0-.574.24-1.09.629-1.456c.607-.572.297-1.568-.495-1.726C6.969 14.187 5 12.114 5 10c0-.958.385-1.881 1.108-2.684c.283-.314.357-.756.207-1.14" /> </g> </svg> <span className="uppercase font-mono group-hover:text-black duration-300 dark:group-hover:text-white"> Stars </span> <ArrowUpRight className="w-6 h-6 opacity-20 ml-2 group-hover:opacity-300 duration-300 text-black group-hover:duration-700 dark:text-white" /> </Button> </Link> </div> </div> </div> </div> </div> </div> ); } ```