This is page 27 of 71. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── nextjs │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app │ │ │ ├── (auth) │ │ │ │ ├── forget-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── sign-in │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── two-factor │ │ │ │ ├── otp │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── accept-invitation │ │ │ │ └── [id] │ │ │ │ ├── invitation-error.tsx │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── auth │ │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ │ ├── apps │ │ │ │ └── register │ │ │ │ └── page.tsx │ │ │ ├── client-test │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── change-plan.tsx │ │ │ │ ├── client.tsx │ │ │ │ ├── organization-card.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── upgrade-button.tsx │ │ │ │ └── user-card.tsx │ │ │ ├── device │ │ │ │ ├── approve │ │ │ │ │ └── page.tsx │ │ │ │ ├── denied │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── success │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── features.tsx │ │ │ ├── fonts │ │ │ │ ├── GeistMonoVF.woff │ │ │ │ └── GeistVF.woff │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── oauth │ │ │ │ └── authorize │ │ │ │ ├── concet-buttons.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── pricing │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── account-switch.tsx │ │ │ ├── blocks │ │ │ │ └── pricing.tsx │ │ │ ├── logo.tsx │ │ │ ├── one-tap.tsx │ │ │ ├── sign-in-btn.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-toggle.tsx │ │ │ ├── tier-labels.tsx │ │ │ ├── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── carousel.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── copy-button.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── menubar.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── password-input.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── tabs2.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toast.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── wrapper.tsx │ │ ├── components.json │ │ ├── hooks │ │ │ └── use-toast.ts │ │ ├── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth-types.ts │ │ │ ├── auth.ts │ │ │ ├── email │ │ │ │ ├── invitation.tsx │ │ │ │ ├── resend.ts │ │ │ │ └── reset-password.tsx │ │ │ ├── metadata.ts │ │ │ ├── shared.ts │ │ │ └── utils.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── proxy.ts │ │ ├── public │ │ │ ├── __og.png │ │ │ ├── _og.png │ │ │ ├── favicon │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── light │ │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ │ ├── apple-touch-icon.png │ │ │ │ │ ├── favicon-16x16.png │ │ │ │ │ ├── favicon-32x32.png │ │ │ │ │ ├── favicon.ico │ │ │ │ │ └── site.webmanifest │ │ │ │ └── site.webmanifest │ │ │ ├── logo.svg │ │ │ └── og.png │ │ ├── README.md │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── turbo.json │ └── 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 │ │ │ └── index.ts │ │ ├── test │ │ │ └── expo.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.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 -------------------------------------------------------------------------------- /docs/content/docs/plugins/username.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Username 3 | description: Username plugin 4 | --- 5 | 6 | The username plugin is a lightweight plugin that adds username support to the email and password authenticator. This allows users to sign in and sign up with their username instead of their email. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ### Add Plugin to the server 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth" 16 | import { username } from "better-auth/plugins" 17 | 18 | export const auth = betterAuth({ 19 | plugins: [ // [!code highlight] 20 | username() // [!code highlight] 21 | ] // [!code highlight] 22 | }) 23 | ``` 24 | </Step> 25 | <Step> 26 | ### Migrate the database 27 | 28 | Run the migration or generate the schema to add the necessary fields and tables to the database. 29 | 30 | <Tabs items={["migrate", "generate"]}> 31 | <Tab value="migrate"> 32 | ```bash 33 | npx @better-auth/cli migrate 34 | ``` 35 | </Tab> 36 | <Tab value="generate"> 37 | ```bash 38 | npx @better-auth/cli generate 39 | ``` 40 | </Tab> 41 | </Tabs> 42 | See the [Schema](#schema) section to add the fields manually. 43 | </Step> 44 | <Step> 45 | ### Add the client plugin 46 | 47 | ```ts title="auth-client.ts" 48 | import { createAuthClient } from "better-auth/client" 49 | import { usernameClient } from "better-auth/client/plugins" 50 | 51 | export const authClient = createAuthClient({ 52 | plugins: [ // [!code highlight] 53 | usernameClient() // [!code highlight] 54 | ] // [!code highlight] 55 | }) 56 | ``` 57 | </Step> 58 | </Steps> 59 | 60 | ## Usage 61 | 62 | ### Sign up 63 | 64 | To sign up a user with username, you can use the existing `signUp.email` function provided by the client. 65 | The `signUp` function should take a new `username` property in the object. 66 | 67 | <APIMethod path="/sign-up/email" method="POST"> 68 | ```ts 69 | type signUpEmail = { 70 | /** 71 | * The email of the user. 72 | */ 73 | email: string = "[email protected]" 74 | /** 75 | * The name of the user. 76 | */ 77 | name: string = "Test User" 78 | /** 79 | * The password of the user. 80 | */ 81 | password: string = "password1234" 82 | /** 83 | * The username of the user. 84 | */ 85 | username: string = "test" 86 | /** 87 | * An optional display username of the user. 88 | */ 89 | displayUsername?: string = "Test User123" 90 | } 91 | ``` 92 | </APIMethod> 93 | 94 | 95 | <Callout type="info"> 96 | If only `username` is provided, the `displayUsername` will be set to the pre normalized version of the `username`. You can see the [Username Normalization](#username-normalization) and [Display Username Normalization](#display-username-normalization) sections for more details. 97 | </Callout> 98 | 99 | ### Sign in 100 | 101 | To sign in a user with username, you can use the `signIn.username` function provided by the client. 102 | 103 | <APIMethod path="/sign-in/username" method="POST"> 104 | ```ts 105 | type signInUsername = { 106 | /** 107 | * The username of the user. 108 | */ 109 | username: string = "test" 110 | /** 111 | * The password of the user. 112 | */ 113 | password: string = "password1234" 114 | } 115 | ``` 116 | </APIMethod> 117 | 118 | ### Update username 119 | 120 | To update the username of a user, you can use the `updateUser` function provided by the client. 121 | 122 | <APIMethod path="/update-user" method="POST"> 123 | ```ts 124 | type updateUser = { 125 | /** 126 | * The username to update. 127 | */ 128 | username?: string = "new-username" 129 | } 130 | ``` 131 | </APIMethod> 132 | 133 | ### Check if username is available 134 | 135 | To check if a username is available, you can use the `isUsernameAvailable` function provided by the client. 136 | 137 | <APIMethod path="/is-username-available" method="POST" resultVariable="response"> 138 | ```ts 139 | type isUsernameAvailable = { 140 | /** 141 | * The username to check. 142 | */ 143 | username: string = "new-username" 144 | } 145 | 146 | if(response?.available) { 147 | console.log("Username is available"); 148 | } else { 149 | console.log("Username is not available"); 150 | } 151 | ``` 152 | </APIMethod> 153 | 154 | ## Options 155 | 156 | ### Min Username Length 157 | 158 | The minimum length of the username. Default is `3`. 159 | 160 | ```ts title="auth.ts" 161 | import { betterAuth } from "better-auth" 162 | import { username } from "better-auth/plugins" 163 | 164 | const auth = betterAuth({ 165 | plugins: [ 166 | username({ 167 | minUsernameLength: 5 168 | }) 169 | ] 170 | }) 171 | ``` 172 | 173 | ### Max Username Length 174 | 175 | The maximum length of the username. Default is `30`. 176 | 177 | ```ts title="auth.ts" 178 | import { betterAuth } from "better-auth" 179 | import { username } from "better-auth/plugins" 180 | 181 | const auth = betterAuth({ 182 | plugins: [ 183 | username({ 184 | maxUsernameLength: 100 185 | }) 186 | ] 187 | }) 188 | ``` 189 | 190 | ### Username Validator 191 | 192 | A function that validates the username. The function should return false if the username is invalid. By default, the username should only contain alphanumeric characters, underscores, and dots. 193 | 194 | ```ts title="auth.ts" 195 | import { betterAuth } from "better-auth" 196 | import { username } from "better-auth/plugins" 197 | 198 | const auth = betterAuth({ 199 | plugins: [ 200 | username({ 201 | usernameValidator: (username) => { 202 | if (username === "admin") { 203 | return false 204 | } 205 | return true 206 | } 207 | }) 208 | ] 209 | }) 210 | ``` 211 | 212 | ### Display Username Validator 213 | 214 | A function that validates the display username. The function should return false if the display username is invalid. By default, no validation is applied to display username. 215 | 216 | ```ts title="auth.ts" 217 | import { betterAuth } from "better-auth" 218 | import { username } from "better-auth/plugins" 219 | 220 | const auth = betterAuth({ 221 | plugins: [ 222 | username({ 223 | displayUsernameValidator: (displayUsername) => { 224 | // Allow only alphanumeric characters, underscores, and hyphens 225 | return /^[a-zA-Z0-9_-]+$/.test(displayUsername) 226 | } 227 | }) 228 | ] 229 | }) 230 | ``` 231 | 232 | ### Username Normalization 233 | 234 | A function that normalizes the username, or `false` if you want to disable normalization. 235 | 236 | By default, usernames are normalized to lowercase, so "TestUser" and "testuser", for example, are considered the same username. The `username` field will contain the normalized (lower case) username, while `displayUsername` will contain the original `username`. 237 | 238 | ```ts title="auth.ts" 239 | import { betterAuth } from "better-auth" 240 | import { username } from "better-auth/plugins" 241 | 242 | const auth = betterAuth({ 243 | plugins: [ 244 | username({ 245 | usernameNormalization: (username) => { 246 | return username.toLowerCase() 247 | .replaceAll("0", "o") 248 | .replaceAll("3", "e") 249 | .replaceAll("4", "a"); 250 | } 251 | }) 252 | ] 253 | }) 254 | ``` 255 | 256 | ### Display Username Normalization 257 | 258 | A function that normalizes the display username, or `false` to disable normalization. 259 | 260 | By default, display usernames are not normalized. When only `username` is provided during signup or update, the `displayUsername` will be set to match the original `username` value (before normalization). You can also explicitly set a `displayUsername` which will be preserved as-is. For custom normalization, provide a function that takes the display username as input and returns the normalized version. 261 | 262 | ```ts title="auth.ts" 263 | import { betterAuth } from "better-auth" 264 | import { username } from "better-auth/plugins" 265 | 266 | const auth = betterAuth({ 267 | plugins: [ 268 | username({ 269 | displayUsernameNormalization: (displayUsername) => displayUsername.toLowerCase(), 270 | }) 271 | ] 272 | }) 273 | ``` 274 | 275 | ### Validation Order 276 | 277 | By default, username and display username are validated before normalization. You can change this behavior by setting `validationOrder` to `post-normalization`. 278 | 279 | ```ts title="auth.ts" 280 | import { betterAuth } from "better-auth" 281 | import { username } from "better-auth/plugins" 282 | 283 | const auth = betterAuth({ 284 | plugins: [ 285 | username({ 286 | validationOrder: { 287 | username: "post-normalization", 288 | displayUsername: "post-normalization", 289 | } 290 | }) 291 | ] 292 | }) 293 | ``` 294 | 295 | ## Schema 296 | 297 | The plugin requires 2 fields to be added to the user table: 298 | 299 | <DatabaseTable 300 | fields={[ 301 | { 302 | name: "username", 303 | type: "string", 304 | description: "The username of the user", 305 | isUnique: true 306 | }, 307 | { 308 | name: "displayUsername", 309 | type: "string", 310 | description: "Non normalized username of the user", 311 | isUnique: true 312 | }, 313 | ]} 314 | /> 315 | ``` -------------------------------------------------------------------------------- /docs/components/ui/sparkles.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { useId } from "react"; 3 | import { useEffect, useState } from "react"; 4 | import Particles, { initParticlesEngine } from "@tsparticles/react"; 5 | import type { Container, SingleOrMultiple } from "@tsparticles/engine"; 6 | import { loadSlim } from "@tsparticles/slim"; 7 | import { cn } from "@/lib/utils"; 8 | import { motion, useAnimation } from "framer-motion"; 9 | 10 | type ParticlesProps = { 11 | id?: string; 12 | className?: string; 13 | background?: string; 14 | particleSize?: number; 15 | minSize?: number; 16 | maxSize?: number; 17 | speed?: number; 18 | particleColor?: string; 19 | particleDensity?: number; 20 | }; 21 | export const SparklesCore = (props: ParticlesProps) => { 22 | const { 23 | id, 24 | className, 25 | background, 26 | minSize, 27 | maxSize, 28 | speed, 29 | particleColor, 30 | particleDensity, 31 | } = props; 32 | const [init, setInit] = useState(false); 33 | useEffect(() => { 34 | initParticlesEngine(async (engine) => { 35 | await loadSlim(engine); 36 | }).then(() => { 37 | setInit(true); 38 | }); 39 | }, []); 40 | const controls = useAnimation(); 41 | 42 | const particlesLoaded = async (container?: Container) => { 43 | if (container) { 44 | console.log(container); 45 | // biome-ignore lint/nursery/noFloatingPromises: add error handling is not important 46 | controls.start({ 47 | opacity: 1, 48 | transition: { 49 | duration: 1, 50 | }, 51 | }); 52 | } 53 | }; 54 | 55 | const generatedId = useId(); 56 | return ( 57 | <motion.div animate={controls} className={cn("opacity-0", className)}> 58 | {init && ( 59 | <Particles 60 | id={id || generatedId} 61 | className={cn("h-full w-full")} 62 | particlesLoaded={particlesLoaded} 63 | options={{ 64 | background: { 65 | color: { 66 | value: background || "#0d47a1", 67 | }, 68 | }, 69 | fullScreen: { 70 | enable: false, 71 | zIndex: 1, 72 | }, 73 | 74 | fpsLimit: 120, 75 | interactivity: { 76 | events: { 77 | onClick: { 78 | enable: true, 79 | mode: "push", 80 | }, 81 | onHover: { 82 | enable: false, 83 | mode: "repulse", 84 | }, 85 | resize: true as any, 86 | }, 87 | modes: { 88 | push: { 89 | quantity: 4, 90 | }, 91 | repulse: { 92 | distance: 200, 93 | duration: 0.4, 94 | }, 95 | }, 96 | }, 97 | particles: { 98 | bounce: { 99 | horizontal: { 100 | value: 1, 101 | }, 102 | vertical: { 103 | value: 1, 104 | }, 105 | }, 106 | collisions: { 107 | absorb: { 108 | speed: 2, 109 | }, 110 | bounce: { 111 | horizontal: { 112 | value: 1, 113 | }, 114 | vertical: { 115 | value: 1, 116 | }, 117 | }, 118 | enable: false, 119 | maxSpeed: 50, 120 | mode: "bounce", 121 | overlap: { 122 | enable: true, 123 | retries: 0, 124 | }, 125 | }, 126 | color: { 127 | value: particleColor || "#ffffff", 128 | animation: { 129 | h: { 130 | count: 0, 131 | enable: false, 132 | speed: 1, 133 | decay: 0, 134 | delay: 0, 135 | sync: true, 136 | offset: 0, 137 | }, 138 | s: { 139 | count: 0, 140 | enable: false, 141 | speed: 1, 142 | decay: 0, 143 | delay: 0, 144 | sync: true, 145 | offset: 0, 146 | }, 147 | l: { 148 | count: 0, 149 | enable: false, 150 | speed: 1, 151 | decay: 0, 152 | delay: 0, 153 | sync: true, 154 | offset: 0, 155 | }, 156 | }, 157 | }, 158 | effect: { 159 | close: true, 160 | fill: true, 161 | options: {}, 162 | type: {} as SingleOrMultiple<string> | undefined, 163 | }, 164 | groups: {}, 165 | move: { 166 | angle: { 167 | offset: 0, 168 | value: 90, 169 | }, 170 | attract: { 171 | distance: 200, 172 | enable: false, 173 | rotate: { 174 | x: 3000, 175 | y: 3000, 176 | }, 177 | }, 178 | center: { 179 | x: 50, 180 | y: 50, 181 | mode: "percent", 182 | radius: 0, 183 | }, 184 | decay: 0, 185 | distance: {}, 186 | direction: "none", 187 | drift: 0, 188 | enable: true, 189 | gravity: { 190 | acceleration: 9.81, 191 | enable: false, 192 | inverse: false, 193 | maxSpeed: 50, 194 | }, 195 | path: { 196 | clamp: true, 197 | delay: { 198 | value: 0, 199 | }, 200 | enable: false, 201 | options: {}, 202 | }, 203 | outModes: { 204 | default: "out", 205 | }, 206 | random: false, 207 | size: false, 208 | speed: { 209 | min: 0.1, 210 | max: 1, 211 | }, 212 | spin: { 213 | acceleration: 0, 214 | enable: false, 215 | }, 216 | straight: false, 217 | trail: { 218 | enable: false, 219 | length: 10, 220 | fill: {}, 221 | }, 222 | vibrate: false, 223 | warp: false, 224 | }, 225 | number: { 226 | density: { 227 | enable: true, 228 | width: 400, 229 | height: 400, 230 | }, 231 | limit: { 232 | mode: "delete", 233 | value: 0, 234 | }, 235 | value: particleDensity || 120, 236 | }, 237 | opacity: { 238 | value: { 239 | min: 0.1, 240 | max: 1, 241 | }, 242 | animation: { 243 | count: 0, 244 | enable: true, 245 | speed: speed || 4, 246 | decay: 0, 247 | delay: 0, 248 | sync: false, 249 | mode: "auto", 250 | startValue: "random", 251 | destroy: "none", 252 | }, 253 | }, 254 | reduceDuplicates: false, 255 | shadow: { 256 | blur: 0, 257 | color: { 258 | value: "#000", 259 | }, 260 | enable: false, 261 | offset: { 262 | x: 0, 263 | y: 0, 264 | }, 265 | }, 266 | shape: { 267 | close: true, 268 | fill: true, 269 | options: {}, 270 | type: "circle", 271 | }, 272 | size: { 273 | value: { 274 | min: minSize || 1, 275 | max: maxSize || 3, 276 | }, 277 | animation: { 278 | count: 0, 279 | enable: false, 280 | speed: 5, 281 | decay: 0, 282 | delay: 0, 283 | sync: false, 284 | mode: "auto", 285 | startValue: "random", 286 | destroy: "none", 287 | }, 288 | }, 289 | stroke: { 290 | width: 0, 291 | }, 292 | zIndex: { 293 | value: 0, 294 | opacityRate: 1, 295 | sizeRate: 1, 296 | velocityRate: 1, 297 | }, 298 | destroy: { 299 | bounds: {}, 300 | mode: "none", 301 | split: { 302 | count: 1, 303 | factor: { 304 | value: 3, 305 | }, 306 | rate: { 307 | value: { 308 | min: 4, 309 | max: 9, 310 | }, 311 | }, 312 | sizeOffset: true, 313 | }, 314 | }, 315 | roll: { 316 | darken: { 317 | enable: false, 318 | value: 0, 319 | }, 320 | enable: false, 321 | enlighten: { 322 | enable: false, 323 | value: 0, 324 | }, 325 | mode: "vertical", 326 | speed: 25, 327 | }, 328 | tilt: { 329 | value: 0, 330 | animation: { 331 | enable: false, 332 | speed: 0, 333 | decay: 0, 334 | sync: false, 335 | }, 336 | direction: "clockwise", 337 | enable: false, 338 | }, 339 | twinkle: { 340 | lines: { 341 | enable: false, 342 | frequency: 0.05, 343 | opacity: 1, 344 | }, 345 | particles: { 346 | enable: false, 347 | frequency: 0.05, 348 | opacity: 1, 349 | }, 350 | }, 351 | wobble: { 352 | distance: 5, 353 | enable: false, 354 | speed: { 355 | angle: 50, 356 | move: 10, 357 | }, 358 | }, 359 | life: { 360 | count: 0, 361 | delay: { 362 | value: 0, 363 | sync: false, 364 | }, 365 | duration: { 366 | value: 0, 367 | sync: false, 368 | }, 369 | }, 370 | rotate: { 371 | value: 0, 372 | animation: { 373 | enable: false, 374 | speed: 0, 375 | decay: 0, 376 | sync: false, 377 | }, 378 | direction: "clockwise", 379 | path: false, 380 | }, 381 | orbit: { 382 | animation: { 383 | count: 0, 384 | enable: false, 385 | speed: 1, 386 | decay: 0, 387 | delay: 0, 388 | sync: false, 389 | }, 390 | enable: false, 391 | opacity: 1, 392 | rotation: { 393 | value: 45, 394 | }, 395 | width: 1, 396 | }, 397 | links: { 398 | blink: false, 399 | color: { 400 | value: "#fff", 401 | }, 402 | consent: false, 403 | distance: 100, 404 | enable: false, 405 | frequency: 1, 406 | opacity: 1, 407 | shadow: { 408 | blur: 5, 409 | color: { 410 | value: "#000", 411 | }, 412 | enable: false, 413 | }, 414 | triangles: { 415 | enable: false, 416 | frequency: 1, 417 | }, 418 | width: 1, 419 | warp: false, 420 | }, 421 | repulse: { 422 | value: 0, 423 | enabled: false, 424 | distance: 1, 425 | duration: 1, 426 | factor: 1, 427 | speed: 1, 428 | }, 429 | }, 430 | detectRetina: true, 431 | }} 432 | /> 433 | )} 434 | </motion.div> 435 | ); 436 | }; 437 | ``` -------------------------------------------------------------------------------- /docs/components/ui/code-block.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { Check, Copy } from "lucide-react"; 3 | import { 4 | ButtonHTMLAttributes, 5 | type ComponentProps, 6 | createContext, 7 | forwardRef, 8 | type HTMLAttributes, 9 | ReactElement, 10 | type ReactNode, 11 | type RefObject, 12 | useCallback, 13 | useContext, 14 | useMemo, 15 | useRef, 16 | } from "react"; 17 | import { cn } from "@/lib/utils"; 18 | import { useCopyButton } from "./use-copy-button"; 19 | import { buttonVariants } from "@/components/ui/button"; 20 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 21 | import { mergeRefs } from "@/lib/utils"; 22 | import { ScrollArea, ScrollBar, ScrollViewport } from "./scroll-area"; 23 | 24 | export interface CodeBlockProps extends ComponentProps<"figure"> { 25 | /** 26 | * Icon of code block 27 | * 28 | * When passed as a string, it assumes the value is the HTML of icon 29 | */ 30 | icon?: ReactNode; 31 | 32 | /** 33 | * Allow to copy code with copy button 34 | * 35 | * @defaultValue true 36 | */ 37 | allowCopy?: boolean; 38 | 39 | /** 40 | * Keep original background color generated by Shiki or Rehype Code 41 | * 42 | * @defaultValue false 43 | */ 44 | keepBackground?: boolean; 45 | 46 | viewportProps?: HTMLAttributes<HTMLElement>; 47 | 48 | /** 49 | * show line numbers 50 | */ 51 | "data-line-numbers"?: boolean; 52 | 53 | /** 54 | * @defaultValue 1 55 | */ 56 | "data-line-numbers-start"?: number; 57 | 58 | Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode; 59 | } 60 | 61 | const TabsContext = createContext<{ 62 | containerRef: RefObject<HTMLDivElement | null>; 63 | nested: boolean; 64 | } | null>(null); 65 | 66 | export function Pre(props: ComponentProps<"pre">) { 67 | return ( 68 | <pre 69 | {...props} 70 | className={cn("min-w-full w-max *:flex *:flex-col", props.className)} 71 | > 72 | {props.children} 73 | </pre> 74 | ); 75 | } 76 | 77 | export function CodeBlock({ 78 | ref, 79 | title, 80 | allowCopy, 81 | keepBackground = false, 82 | icon, 83 | viewportProps = {}, 84 | children, 85 | Actions = (props) => ( 86 | <div {...props} className={cn("empty:hidden", props.className)} /> 87 | ), 88 | ...props 89 | }: CodeBlockProps) { 90 | const isTab = useContext(TabsContext) !== null; 91 | const areaRef = useRef<HTMLDivElement>(null); 92 | allowCopy ??= !isTab; 93 | const bg = cn( 94 | "bg-fd-secondary", 95 | keepBackground && "bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)", 96 | ); 97 | const onCopy = useCallback(() => { 98 | const pre = areaRef.current?.getElementsByTagName("pre").item(0); 99 | if (!pre) return; 100 | const clone = pre.cloneNode(true) as HTMLElement; 101 | clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { 102 | node.remove(); 103 | }); 104 | void navigator.clipboard.writeText(clone.textContent ?? ""); 105 | }, []); 106 | return ( 107 | <figure 108 | ref={ref} 109 | dir="ltr" 110 | {...props} 111 | className={cn( 112 | isTab ? [bg, "rounded-lg"] : "my-4 rounded-lg bg-fd-card", 113 | "group shiki relative border shadow-sm outline-none not-prose overflow-hidden text-sm", 114 | props.className, 115 | )} 116 | > 117 | {title ? ( 118 | <div 119 | className={cn( 120 | "group flex text-fd-muted-foreground items-center gap-2 ps-3 h-9.5 pr-1 bg-fd-muted", 121 | isTab && "border-b", 122 | )} 123 | > 124 | {typeof icon === "string" ? ( 125 | <div 126 | className="[&_svg]:size-3.5" 127 | dangerouslySetInnerHTML={{ 128 | __html: icon, 129 | }} 130 | /> 131 | ) : ( 132 | icon 133 | )} 134 | <figcaption className="flex-1 truncate">{title}</figcaption> 135 | {Actions({ 136 | children: allowCopy && <CopyButton onCopy={onCopy} />, 137 | })} 138 | </div> 139 | ) : ( 140 | Actions({ 141 | className: "absolute top-1 right-1 z-2 text-fd-muted-foreground", 142 | children: allowCopy && <CopyButton onCopy={onCopy} />, 143 | }) 144 | )} 145 | <div 146 | ref={areaRef} 147 | {...viewportProps} 148 | className={cn( 149 | !isTab && [bg, "rounded-none border border-x-0 border-b-0"], 150 | "text-[13px] overflow-auto max-h-[600px] bg-fd-muted/50 fd-scroll-container", 151 | viewportProps.className, 152 | !title && "border-t-0", 153 | )} 154 | style={ 155 | { 156 | // space for toolbar 157 | "--padding-right": !title ? "calc(var(--spacing) * 8)" : undefined, 158 | counterSet: props["data-line-numbers"] 159 | ? `line ${Number(props["data-line-numbers-start"] ?? 1) - 1}` 160 | : undefined, 161 | ...viewportProps.style, 162 | } as object 163 | } 164 | > 165 | {children} 166 | </div> 167 | </figure> 168 | ); 169 | } 170 | function CopyButton({ 171 | className, 172 | onCopy, 173 | ...props 174 | }: ButtonHTMLAttributes<HTMLButtonElement> & { 175 | onCopy: () => void; 176 | }): ReactElement { 177 | const [checked, onClick] = useCopyButton(onCopy); 178 | 179 | return ( 180 | <button 181 | type="button" 182 | className={cn( 183 | buttonVariants({ 184 | variant: "ghost", 185 | size: "icon", 186 | }), 187 | "transition-opacity size-7 border-none group-hover:opacity-100", 188 | "opacity-0 group-hover:opacity-100", 189 | "group-hover:opacity-100", 190 | className, 191 | )} 192 | aria-label="Copy Text" 193 | onClick={onClick} 194 | {...props} 195 | > 196 | <Check 197 | className={cn("size-3.5 transition-transform", !checked && "scale-0")} 198 | /> 199 | <Copy 200 | className={cn( 201 | "absolute size-3.5 transition-transform", 202 | checked && "scale-0", 203 | )} 204 | /> 205 | </button> 206 | ); 207 | } 208 | export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) { 209 | const containerRef = useRef<HTMLDivElement>(null); 210 | const nested = useContext(TabsContext) !== null; 211 | 212 | return ( 213 | <Tabs 214 | ref={mergeRefs(containerRef, ref)} 215 | {...props} 216 | className={cn( 217 | "bg-fd-card p-1 rounded-xl border overflow-hidden", 218 | !nested && "my-4", 219 | props.className, 220 | )} 221 | > 222 | <TabsContext.Provider 223 | value={useMemo( 224 | () => ({ 225 | containerRef, 226 | nested, 227 | }), 228 | [nested], 229 | )} 230 | > 231 | {props.children} 232 | </TabsContext.Provider> 233 | </Tabs> 234 | ); 235 | } 236 | export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) { 237 | const { containerRef, nested } = useContext(TabsContext)!; 238 | 239 | return ( 240 | <TabsList 241 | {...props} 242 | className={cn( 243 | "flex flex-row overflow-x-auto px-1 -mx-1 text-fd-muted-foreground", 244 | props.className, 245 | )} 246 | > 247 | {props.children} 248 | </TabsList> 249 | ); 250 | } 251 | 252 | export function CodeBlockTabsTrigger({ 253 | children, 254 | ...props 255 | }: ComponentProps<typeof TabsTrigger>) { 256 | return ( 257 | <TabsTrigger 258 | {...props} 259 | className={cn( 260 | "relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 first:ms-1 py-1.5 hover:text-fd-accent-foreground data-[state=active]:text-fd-primary [&_svg]:size-3.5", 261 | props.className, 262 | )} 263 | > 264 | <div className="absolute inset-x-2 bottom-0 h-px group-data-[state=active]:bg-fd-primary" /> 265 | {children} 266 | </TabsTrigger> 267 | ); 268 | } 269 | 270 | // TODO: currently Vite RSC plugin has problem with adding `asChild` here, maybe revisit this in future 271 | export const CodeBlockTab = TabsContent; 272 | 273 | export const CodeBlockOld = forwardRef<HTMLElement, CodeBlockProps>( 274 | ( 275 | { 276 | title, 277 | allowCopy = true, 278 | keepBackground = false, 279 | icon, 280 | viewportProps, 281 | ...props 282 | }, 283 | ref, 284 | ) => { 285 | const areaRef = useRef<HTMLDivElement>(null); 286 | const onCopy = useCallback(() => { 287 | const pre = areaRef.current?.getElementsByTagName("pre").item(0); 288 | 289 | if (!pre) return; 290 | 291 | const clone = pre.cloneNode(true) as HTMLElement; 292 | clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { 293 | node.remove(); 294 | }); 295 | 296 | void navigator.clipboard.writeText(clone.textContent ?? ""); 297 | }, []); 298 | 299 | return ( 300 | <figure 301 | ref={ref} 302 | {...props} 303 | className={cn( 304 | "not-prose group fd-codeblock relative my-6 overflow-hidden rounded-lg border bg-fd-secondary/50 text-sm", 305 | keepBackground && 306 | "bg-[var(--shiki-light-bg)] dark:bg-[var(--shiki-dark-bg)]", 307 | props.className, 308 | )} 309 | > 310 | {title ? ( 311 | <div className="flex flex-row items-center gap-2 border-b bg-fd-muted px-4 py-1.5"> 312 | {icon ? ( 313 | <div 314 | className="text-fd-muted-foreground [&_svg]:size-3.5" 315 | dangerouslySetInnerHTML={ 316 | typeof icon === "string" 317 | ? { 318 | __html: icon, 319 | } 320 | : undefined 321 | } 322 | > 323 | {typeof icon !== "string" ? icon : null} 324 | </div> 325 | ) : null} 326 | <figcaption className="flex-1 truncate text-fd-muted-foreground"> 327 | {title} 328 | </figcaption> 329 | {allowCopy ? ( 330 | <CopyButton className="-me-2" onCopy={onCopy} /> 331 | ) : null} 332 | </div> 333 | ) : ( 334 | allowCopy && ( 335 | <CopyButton 336 | className="absolute right-2 top-2 z-[2] backdrop-blur-md" 337 | onCopy={onCopy} 338 | /> 339 | ) 340 | )} 341 | <ScrollArea ref={areaRef} dir="ltr"> 342 | <ScrollViewport 343 | {...viewportProps} 344 | className={cn("max-h-[600px]", viewportProps?.className)} 345 | > 346 | {props.children} 347 | </ScrollViewport> 348 | <ScrollBar orientation="horizontal" /> 349 | </ScrollArea> 350 | </figure> 351 | ); 352 | }, 353 | ); 354 | 355 | CodeBlockOld.displayName = "CodeBlockOld"; 356 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/organization/organization-hook.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { organization } from "."; 4 | 5 | describe("organization creation in database hooks", async () => { 6 | it("should create organization in user creation after hook within transaction", async () => { 7 | let hookCalledForTestEmail = false; 8 | let orgCreated: any = null; 9 | let errorInHook: any = null; 10 | 11 | const { auth, client, db } = await getTestInstance({ 12 | plugins: [organization()], 13 | databaseHooks: { 14 | user: { 15 | create: { 16 | after: async (user) => { 17 | // Only run for our specific test user 18 | if (user.email !== "[email protected]") { 19 | return; 20 | } 21 | hookCalledForTestEmail = true; 22 | try { 23 | // This should work now that the adapter uses getCurrentAdapter 24 | const org = await auth.api.createOrganization({ 25 | body: { 26 | name: `${user.email}'s Organization`, 27 | slug: `org-${user.id.substring(0, 8)}`, 28 | userId: user.id, 29 | }, 30 | }); 31 | orgCreated = org; 32 | } catch (error) { 33 | errorInHook = error; 34 | throw error; 35 | } 36 | }, 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | // Create a user which should trigger the hook 43 | const result = await client.signUp.email({ 44 | email: "[email protected]", 45 | password: "password123", 46 | name: "Test Hook User", 47 | }); 48 | 49 | // Verify the user was created 50 | expect(result.data).toBeDefined(); 51 | expect(result.data?.user).toBeDefined(); 52 | expect(result.data?.user?.email).toBe("[email protected]"); 53 | 54 | // Verify the hook was called 55 | expect(hookCalledForTestEmail).toBe(true); 56 | 57 | expect(errorInHook).toBeNull(); 58 | 59 | // Verify organization was created successfully 60 | expect(orgCreated).not.toBeNull(); 61 | expect(orgCreated?.name).toBe("[email protected]'s Organization"); 62 | expect(orgCreated?.slug).toMatch(/^org-/); 63 | 64 | // Verify the organization exists in the database 65 | const orgs = await db.findMany({ 66 | model: "organization", 67 | }); 68 | // Should have the test user's org from getTestInstance plus our new one 69 | expect(orgs.length).toBeGreaterThanOrEqual(1); 70 | 71 | const createdOrg = orgs.find((o: any) => o.slug?.startsWith("org-")); 72 | expect(createdOrg).toBeDefined(); 73 | expect((createdOrg as any)?.name).toBe( 74 | "[email protected]'s Organization", 75 | ); 76 | 77 | // Verify the user is a member of the organization 78 | const members = await db.findMany({ 79 | model: "member", 80 | where: [ 81 | { 82 | field: "organizationId", 83 | value: orgCreated?.id, 84 | }, 85 | ], 86 | }); 87 | expect(members).toHaveLength(1); 88 | expect(members[0]).toMatchObject({ 89 | userId: result.data?.user?.id, 90 | organizationId: orgCreated?.id, 91 | role: "owner", 92 | }); 93 | }); 94 | 95 | it("should handle errors gracefully when organization creation fails in hook", async ({ 96 | skip, 97 | }) => { 98 | let firstUserCreated = false; 99 | let errorOnSecondUser: any = null; 100 | 101 | const { auth, client, db } = await getTestInstance({ 102 | plugins: [organization()], 103 | databaseHooks: { 104 | user: { 105 | create: { 106 | after: async (user) => { 107 | // Skip test instance default user 108 | if (!user.email?.includes("-hook@")) { 109 | return; 110 | } 111 | // Try to create an org with duplicate slug (will fail on second user) 112 | await auth.api.createOrganization({ 113 | body: { 114 | name: "Test Org", 115 | slug: "duplicate-test-org", // Same slug for all users 116 | userId: user.id, 117 | }, 118 | }); 119 | if (!firstUserCreated) { 120 | firstUserCreated = true; 121 | } 122 | }, 123 | }, 124 | }, 125 | }, 126 | }); 127 | 128 | if (!db.options?.adapterConfig.transaction) { 129 | skip( 130 | "Skipping since transactions are enabled and will rollback automatically", 131 | ); 132 | } 133 | 134 | // First user should succeed 135 | const result1 = await client.signUp.email({ 136 | email: "[email protected]", 137 | password: "password123", 138 | name: "User 1", 139 | }); 140 | expect(result1.data).toBeDefined(); 141 | expect(result1.data?.user?.email).toBe("[email protected]"); 142 | expect(firstUserCreated).toBe(true); 143 | 144 | // Second user should fail due to duplicate org slug 145 | try { 146 | await client.signUp.email({ 147 | email: "[email protected]", 148 | password: "password123", 149 | name: "User 2", 150 | }); 151 | } catch (error) { 152 | errorOnSecondUser = error; 153 | } 154 | 155 | expect(errorOnSecondUser).toBeDefined(); 156 | 157 | // Verify only one organization with our test slug was created 158 | const orgs = await db.findMany({ 159 | model: "organization", 160 | where: [ 161 | { 162 | field: "slug", 163 | value: "duplicate-test-org", 164 | }, 165 | ], 166 | }); 167 | expect(orgs).toHaveLength(1); 168 | 169 | // Verify only the first user exists (transaction should have rolled back for second user) 170 | const users = await db.findMany({ 171 | model: "user", 172 | where: [ 173 | { 174 | field: "email", 175 | value: "[email protected]", 176 | }, 177 | ], 178 | }); 179 | expect(users).toHaveLength(0); 180 | }); 181 | 182 | it("should work with multiple async operations in the hook", async () => { 183 | let asyncOperationsCompleted = 0; 184 | let foundUserInTransaction = false; 185 | 186 | const { auth, client, db } = await getTestInstance({ 187 | plugins: [organization()], 188 | databaseHooks: { 189 | user: { 190 | create: { 191 | after: async (user, ctx): Promise<any> => { 192 | // Skip test instance default user 193 | if (user.email !== "[email protected]") { 194 | return; 195 | } 196 | // Simulate some async operation 197 | await new Promise((resolve) => setTimeout(resolve, 10)); 198 | asyncOperationsCompleted++; 199 | 200 | // Check if user exists in the transaction context 201 | // This should work because we're in the same transaction 202 | const foundUser = 203 | await ctx?.context?.internalAdapter?.findUserById?.(user.id); 204 | foundUserInTransaction = !!foundUser; 205 | 206 | // Create organization 207 | const org = await auth.api.createOrganization({ 208 | body: { 209 | name: `Async Org for ${user.name}`, 210 | slug: `async-${user.id.substring(0, 8)}`, 211 | userId: user.id, 212 | }, 213 | }); 214 | 215 | // Another async operation 216 | await new Promise((resolve) => setTimeout(resolve, 10)); 217 | asyncOperationsCompleted++; 218 | 219 | return org; 220 | }, 221 | }, 222 | }, 223 | }, 224 | }); 225 | 226 | const result = await client.signUp.email({ 227 | email: "[email protected]", 228 | password: "password123", 229 | name: "Async User", 230 | }); 231 | 232 | expect(result.data).toBeDefined(); 233 | expect(result.data?.user?.email).toBe("[email protected]"); 234 | 235 | // Verify async operations completed 236 | expect(asyncOperationsCompleted).toBe(2); 237 | expect(foundUserInTransaction).toBe(true); 238 | 239 | // Verify organization was created 240 | const orgs = await db.findMany({ 241 | model: "organization", 242 | where: [ 243 | { 244 | field: "slug", 245 | operator: "contains", 246 | value: "async-", 247 | }, 248 | ], 249 | }); 250 | expect(orgs.length).toBeGreaterThanOrEqual(1); 251 | 252 | const asyncOrg = orgs.find((o: any) => o.name?.includes("Async Org")); 253 | expect(asyncOrg).toBeDefined(); 254 | expect((asyncOrg as any)?.name).toBe("Async Org for Async User"); 255 | }); 256 | 257 | it("should work when creating organization from before hook", async () => { 258 | let orgId: string | null = null; 259 | 260 | const { auth, client, db } = await getTestInstance({ 261 | plugins: [organization()], 262 | databaseHooks: { 263 | user: { 264 | create: { 265 | before: async (user) => { 266 | // We can't create the org here since user doesn't have an ID yet 267 | // But we can prepare the data 268 | return { 269 | data: { 270 | ...user, 271 | image: "prepared-in-before-hook", 272 | }, 273 | }; 274 | }, 275 | after: async (user) => { 276 | // Skip test instance default user 277 | if (user.email !== "[email protected]") { 278 | return; 279 | } 280 | // Now we can create the org with the user ID 281 | const org = await auth.api.createOrganization({ 282 | body: { 283 | name: `Before-After Org`, 284 | slug: `before-after-${user.id.substring(0, 8)}`, 285 | userId: user.id, 286 | }, 287 | }); 288 | orgId = org?.id || null; 289 | }, 290 | }, 291 | }, 292 | }, 293 | }); 294 | 295 | const result = await client.signUp.email({ 296 | email: "[email protected]", 297 | password: "password123", 298 | name: "Before Hook User", 299 | }); 300 | 301 | expect(result.data).toBeDefined(); 302 | expect(result.data?.user?.image).toBe("prepared-in-before-hook"); 303 | expect(orgId).not.toBeNull(); 304 | 305 | // Verify organization was created 306 | const org = await db.findOne({ 307 | model: "organization", 308 | where: [ 309 | { 310 | field: "id", 311 | value: orgId!, 312 | }, 313 | ], 314 | }); 315 | expect(org).toBeDefined(); 316 | expect((org as any)?.name).toBe("Before-After Org"); 317 | }); 318 | }); 319 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | APIError, 3 | type Middleware, 4 | createRouter, 5 | type Endpoint, 6 | } from "better-call"; 7 | import type { BetterAuthOptions } from "@better-auth/core"; 8 | import type { UnionToIntersection } from "../types/helper"; 9 | import { originCheckMiddleware } from "./middlewares"; 10 | import { 11 | callbackOAuth, 12 | getSession, 13 | listSessions, 14 | resetPassword, 15 | revokeSession, 16 | revokeSessions, 17 | sendVerificationEmail, 18 | changeEmail, 19 | signInEmail, 20 | signInSocial, 21 | signOut, 22 | verifyEmail, 23 | linkSocialAccount, 24 | revokeOtherSessions, 25 | listUserAccounts, 26 | changePassword, 27 | deleteUser, 28 | setPassword, 29 | updateUser, 30 | deleteUserCallback, 31 | unlinkAccount, 32 | refreshToken, 33 | getAccessToken, 34 | accountInfo, 35 | requestPasswordReset, 36 | requestPasswordResetCallback, 37 | } from "./routes"; 38 | import { ok } from "./routes"; 39 | import { signUpEmail } from "./routes"; 40 | import { error } from "./routes"; 41 | import { type InternalLogger, logger } from "@better-auth/core/env"; 42 | import type { BetterAuthPlugin } from "@better-auth/core"; 43 | import { onRequestRateLimit } from "./rate-limiter"; 44 | import { toAuthEndpoints } from "./to-auth-endpoints"; 45 | import type { AuthContext } from "@better-auth/core"; 46 | 47 | export function checkEndpointConflicts( 48 | options: BetterAuthOptions, 49 | logger: InternalLogger, 50 | ) { 51 | const endpointRegistry = new Map< 52 | string, 53 | { pluginId: string; endpointKey: string; methods: string[] }[] 54 | >(); 55 | 56 | options.plugins?.forEach((plugin) => { 57 | if (plugin.endpoints) { 58 | for (const [key, endpoint] of Object.entries(plugin.endpoints)) { 59 | if (endpoint && "path" in endpoint) { 60 | const path = endpoint.path; 61 | let methods: string[] = []; 62 | if (endpoint.options && "method" in endpoint.options) { 63 | if (Array.isArray(endpoint.options.method)) { 64 | methods = endpoint.options.method; 65 | } else if (typeof endpoint.options.method === "string") { 66 | methods = [endpoint.options.method]; 67 | } 68 | } 69 | if (methods.length === 0) { 70 | methods = ["*"]; 71 | } 72 | 73 | if (!endpointRegistry.has(path)) { 74 | endpointRegistry.set(path, []); 75 | } 76 | endpointRegistry.get(path)!.push({ 77 | pluginId: plugin.id, 78 | endpointKey: key, 79 | methods, 80 | }); 81 | } 82 | } 83 | } 84 | }); 85 | 86 | const conflicts: { 87 | path: string; 88 | plugins: string[]; 89 | conflictingMethods: string[]; 90 | }[] = []; 91 | for (const [path, entries] of endpointRegistry.entries()) { 92 | if (entries.length > 1) { 93 | const methodMap = new Map<string, string[]>(); 94 | let hasConflict = false; 95 | 96 | for (const entry of entries) { 97 | for (const method of entry.methods) { 98 | if (!methodMap.has(method)) { 99 | methodMap.set(method, []); 100 | } 101 | methodMap.get(method)!.push(entry.pluginId); 102 | 103 | if (methodMap.get(method)!.length > 1) { 104 | hasConflict = true; 105 | } 106 | 107 | if (method === "*" && entries.length > 1) { 108 | hasConflict = true; 109 | } else if (method !== "*" && methodMap.has("*")) { 110 | hasConflict = true; 111 | } 112 | } 113 | } 114 | 115 | if (hasConflict) { 116 | const uniquePlugins = [...new Set(entries.map((e) => e.pluginId))]; 117 | const conflictingMethods: string[] = []; 118 | 119 | for (const [method, plugins] of methodMap.entries()) { 120 | if ( 121 | plugins.length > 1 || 122 | (method === "*" && entries.length > 1) || 123 | (method !== "*" && methodMap.has("*")) 124 | ) { 125 | conflictingMethods.push(method); 126 | } 127 | } 128 | 129 | conflicts.push({ 130 | path, 131 | plugins: uniquePlugins, 132 | conflictingMethods, 133 | }); 134 | } 135 | } 136 | } 137 | 138 | if (conflicts.length > 0) { 139 | const conflictMessages = conflicts 140 | .map( 141 | (conflict) => 142 | ` - "${conflict.path}" [${conflict.conflictingMethods.join(", ")}] used by plugins: ${conflict.plugins.join(", ")}`, 143 | ) 144 | .join("\n"); 145 | logger.error( 146 | `Endpoint path conflicts detected! Multiple plugins are trying to use the same endpoint paths with conflicting HTTP methods: 147 | ${conflictMessages} 148 | 149 | To resolve this, you can: 150 | 1. Use only one of the conflicting plugins 151 | 2. Configure the plugins to use different paths (if supported) 152 | 3. Ensure plugins use different HTTP methods for the same path 153 | `, 154 | ); 155 | } 156 | } 157 | 158 | export function getEndpoints<Option extends BetterAuthOptions>( 159 | ctx: Promise<AuthContext> | AuthContext, 160 | options: Option, 161 | ) { 162 | const pluginEndpoints = 163 | options.plugins?.reduce<Record<string, Endpoint>>((acc, plugin) => { 164 | return { 165 | ...acc, 166 | ...plugin.endpoints, 167 | }; 168 | }, {}) ?? {}; 169 | 170 | type PluginEndpoint = UnionToIntersection< 171 | Option["plugins"] extends Array<infer T> 172 | ? T extends BetterAuthPlugin 173 | ? T extends { 174 | endpoints: infer E; 175 | } 176 | ? E 177 | : {} 178 | : {} 179 | : {} 180 | >; 181 | 182 | const middlewares = 183 | options.plugins 184 | ?.map((plugin) => 185 | plugin.middlewares?.map((m) => { 186 | const middleware = (async (context: any) => { 187 | const authContext = await ctx; 188 | return m.middleware({ 189 | ...context, 190 | context: { 191 | ...authContext, 192 | ...context.context, 193 | }, 194 | }); 195 | }) as Middleware; 196 | middleware.options = m.middleware.options; 197 | return { 198 | path: m.path, 199 | middleware, 200 | }; 201 | }), 202 | ) 203 | .filter((plugin) => plugin !== undefined) 204 | .flat() || []; 205 | 206 | const baseEndpoints = { 207 | signInSocial, 208 | callbackOAuth, 209 | getSession: getSession<Option>(), 210 | signOut, 211 | signUpEmail: signUpEmail<Option>(), 212 | signInEmail, 213 | resetPassword, 214 | verifyEmail, 215 | sendVerificationEmail, 216 | changeEmail, 217 | changePassword, 218 | setPassword, 219 | updateUser: updateUser<Option>(), 220 | deleteUser, 221 | requestPasswordReset, 222 | requestPasswordResetCallback, 223 | listSessions: listSessions<Option>(), 224 | revokeSession, 225 | revokeSessions, 226 | revokeOtherSessions, 227 | linkSocialAccount, 228 | listUserAccounts, 229 | deleteUserCallback, 230 | unlinkAccount, 231 | refreshToken, 232 | getAccessToken, 233 | accountInfo, 234 | }; 235 | const endpoints = { 236 | ...baseEndpoints, 237 | ...pluginEndpoints, 238 | ok, 239 | error, 240 | } as const; 241 | const api = toAuthEndpoints(endpoints, ctx); 242 | return { 243 | api: api as typeof endpoints & PluginEndpoint, 244 | middlewares, 245 | }; 246 | } 247 | export const router = <Option extends BetterAuthOptions>( 248 | ctx: AuthContext, 249 | options: Option, 250 | ) => { 251 | const { api, middlewares } = getEndpoints(ctx, options); 252 | const basePath = new URL(ctx.baseURL).pathname; 253 | 254 | return createRouter(api, { 255 | routerContext: ctx, 256 | openapi: { 257 | disabled: true, 258 | }, 259 | basePath, 260 | routerMiddleware: [ 261 | { 262 | path: "/**", 263 | middleware: originCheckMiddleware, 264 | }, 265 | ...middlewares, 266 | ], 267 | async onRequest(req) { 268 | //handle disabled paths 269 | const disabledPaths = ctx.options.disabledPaths || []; 270 | const path = new URL(req.url).pathname.replace(basePath, ""); 271 | if (disabledPaths.includes(path)) { 272 | return new Response("Not Found", { status: 404 }); 273 | } 274 | for (const plugin of ctx.options.plugins || []) { 275 | if (plugin.onRequest) { 276 | const response = await plugin.onRequest(req, ctx); 277 | if (response && "response" in response) { 278 | return response.response; 279 | } 280 | } 281 | } 282 | return onRequestRateLimit(req, ctx); 283 | }, 284 | async onResponse(res) { 285 | for (const plugin of ctx.options.plugins || []) { 286 | if (plugin.onResponse) { 287 | const response = await plugin.onResponse(res, ctx); 288 | if (response) { 289 | return response.response; 290 | } 291 | } 292 | } 293 | return res; 294 | }, 295 | onError(e) { 296 | if (e instanceof APIError && e.status === "FOUND") { 297 | return; 298 | } 299 | if (options.onAPIError?.throw) { 300 | throw e; 301 | } 302 | if (options.onAPIError?.onError) { 303 | options.onAPIError.onError(e, ctx); 304 | return; 305 | } 306 | 307 | const optLogLevel = options.logger?.level; 308 | const log = 309 | optLogLevel === "error" || 310 | optLogLevel === "warn" || 311 | optLogLevel === "debug" 312 | ? logger 313 | : undefined; 314 | if (options.logger?.disabled !== true) { 315 | if ( 316 | e && 317 | typeof e === "object" && 318 | "message" in e && 319 | typeof e.message === "string" 320 | ) { 321 | if ( 322 | e.message.includes("no column") || 323 | e.message.includes("column") || 324 | e.message.includes("relation") || 325 | e.message.includes("table") || 326 | e.message.includes("does not exist") 327 | ) { 328 | ctx.logger?.error(e.message); 329 | return; 330 | } 331 | } 332 | 333 | if (e instanceof APIError) { 334 | if (e.status === "INTERNAL_SERVER_ERROR") { 335 | ctx.logger.error(e.status, e); 336 | } 337 | log?.error(e.message); 338 | } else { 339 | ctx.logger?.error( 340 | e && typeof e === "object" && "name" in e ? (e.name as string) : "", 341 | e, 342 | ); 343 | } 344 | } 345 | }, 346 | }); 347 | }; 348 | 349 | export * from "./routes"; 350 | export * from "./middlewares"; 351 | export { APIError } from "better-call"; 352 | export { 353 | createAuthEndpoint, 354 | createAuthMiddleware, 355 | optionsMiddleware, 356 | type AuthEndpoint, 357 | type AuthMiddleware, 358 | } from "@better-auth/core/api"; 359 | ``` -------------------------------------------------------------------------------- /docs/components/side-bar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { AsideLink } from "@/components/ui/aside-link"; 4 | import { cn } from "@/lib/utils"; 5 | import { AnimatePresence, motion, MotionConfig } from "framer-motion"; 6 | import { useSearchContext } from "fumadocs-ui/provider"; 7 | import { ChevronDownIcon, Search } from "lucide-react"; 8 | import { usePathname, useRouter } from "next/navigation"; 9 | import { Suspense, useEffect, useState } from "react"; 10 | import { contents, examples } from "./sidebar-content"; 11 | import { Badge } from "./ui/badge"; 12 | import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; 13 | 14 | export default function ArticleLayout() { 15 | const [currentOpen, setCurrentOpen] = useState<number>(0); 16 | 17 | const { setOpenSearch } = useSearchContext(); 18 | const pathname = usePathname(); 19 | 20 | function getDefaultValue() { 21 | const defaultValue = contents.findIndex((item) => 22 | item.list.some((listItem) => listItem.href === pathname), 23 | ); 24 | return defaultValue === -1 ? 0 : defaultValue; 25 | } 26 | 27 | const [group, setGroup] = useState("docs"); 28 | 29 | useEffect(() => { 30 | const grp = pathname.includes("examples") ? "examples" : "docs"; 31 | setGroup(grp); 32 | setCurrentOpen(getDefaultValue()); 33 | }, [pathname]); 34 | 35 | const cts = group === "docs" ? contents : examples; 36 | 37 | return ( 38 | <div className={cn("fixed start-0 top-0")}> 39 | <aside 40 | className={cn( 41 | "md:transition-all", 42 | "border-r border-lines top-[55px] md:flex hidden md:w-[268px] lg:w-[286px] overflow-y-auto absolute h-[calc(100dvh-55px)] pb-2 flex-col justify-between w-[var(--fd-sidebar-width)]", 43 | )} 44 | > 45 | <div> 46 | <SidebarTab group={group} setGroup={setGroup} /> 47 | <button 48 | className="flex w-full items-center gap-2 px-5 py-2.5 border-b text-muted-foreground dark:bg-zinc-950 dark:border-t-zinc-900/30 dark:border-t" 49 | onClick={() => { 50 | setOpenSearch(true); 51 | }} 52 | > 53 | <Search className="size-4 mx-0.5" /> 54 | <p className="text-sm">Search documentation...</p> 55 | </button> 56 | 57 | <MotionConfig 58 | transition={{ duration: 0.4, type: "spring", bounce: 0 }} 59 | > 60 | <div className="flex flex-col"> 61 | {cts.map((item, index) => ( 62 | <div key={item.title}> 63 | <button 64 | className="border-b w-full hover:underline border-lines text-sm px-5 py-2.5 text-left flex items-center gap-2" 65 | onClick={() => { 66 | if (currentOpen === index) { 67 | setCurrentOpen(-1); 68 | } else { 69 | setCurrentOpen(index); 70 | } 71 | }} 72 | > 73 | <item.Icon className="size-5" /> 74 | <span className="grow">{item.title}</span> 75 | {item.isNew && <NewBadge />} 76 | <motion.div 77 | animate={{ rotate: currentOpen === index ? 180 : 0 }} 78 | > 79 | <ChevronDownIcon 80 | className={cn( 81 | "h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200", 82 | )} 83 | /> 84 | </motion.div> 85 | </button> 86 | <AnimatePresence initial={false}> 87 | {currentOpen === index && ( 88 | <motion.div 89 | initial={{ opacity: 0, height: 0 }} 90 | animate={{ opacity: 1, height: "auto" }} 91 | exit={{ opacity: 0, height: 0 }} 92 | className="relative overflow-hidden" 93 | > 94 | <motion.div className="text-sm"> 95 | {item.list.map((listItem, j) => ( 96 | <div key={listItem.title}> 97 | <Suspense fallback={<>Loading...</>}> 98 | {listItem.group ? ( 99 | <div className="flex flex-row items-center gap-2 mx-5 my-1 "> 100 | <p className="text-sm text-transparent bg-gradient-to-tr dark:from-gray-100 dark:to-stone-200 bg-clip-text from-gray-900 to-stone-900"> 101 | {listItem.title} 102 | </p> 103 | <div className="flex-grow h-px bg-gradient-to-r from-stone-800/90 to-stone-800/60" /> 104 | </div> 105 | ) : ( 106 | <AsideLink 107 | href={listItem.href} 108 | startWith="/docs" 109 | title={listItem.title} 110 | className="break-words text-nowrap w-[--fd-sidebar-width] [&>div>div]:hover:!bg-fd-muted" 111 | activeClassName="[&>div>div]:!bg-fd-muted" 112 | > 113 | <div className="min-w-4"> 114 | <listItem.icon className="text-stone-950 dark:text-white" /> 115 | </div> 116 | {listItem.title} 117 | {listItem.isNew && <NewBadge />} 118 | </AsideLink> 119 | )} 120 | </Suspense> 121 | </div> 122 | ))} 123 | </motion.div> 124 | </motion.div> 125 | )} 126 | </AnimatePresence> 127 | </div> 128 | ))} 129 | </div> 130 | </MotionConfig> 131 | </div> 132 | </aside> 133 | </div> 134 | ); 135 | } 136 | 137 | function NewBadge({ isSelected }: { isSelected?: boolean }) { 138 | return ( 139 | <div className="flex items-center justify-end w-full"> 140 | <Badge 141 | className={cn( 142 | " pointer-events-none !no-underline border-dashed !decoration-transparent", 143 | isSelected && "!border-solid", 144 | )} 145 | variant={isSelected ? "default" : "outline"} 146 | > 147 | New 148 | </Badge> 149 | </div> 150 | ); 151 | } 152 | 153 | const tabs = [ 154 | { 155 | value: "docs", 156 | icon: ( 157 | <svg 158 | xmlns="http://www.w3.org/2000/svg" 159 | width="1.4em" 160 | height="1.4em" 161 | viewBox="0 0 24 24" 162 | > 163 | <path 164 | fill="currentColor" 165 | d="M4.727 2.733c.306-.308.734-.508 1.544-.618C7.105 2.002 8.209 2 9.793 2h4.414c1.584 0 2.688.002 3.522.115c.81.11 1.238.31 1.544.618c.305.308.504.74.613 1.557c.112.84.114 1.955.114 3.552V18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505A1.3 1.3 0 0 0 4 19.7V7.842c0-1.597.002-2.711.114-3.552c.109-.816.308-1.249.613-1.557" 166 | opacity=".5" 167 | ></path> 168 | <path 169 | fill="currentColor" 170 | d="M20 18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505s-.107.489-.066.78l.022.15c.11.653.31.998.616 1.244c.307.246.737.407 1.55.494c.837.09 1.946.092 3.536.092h4.43c1.59 0 2.7-.001 3.536-.092c.813-.087 1.243-.248 1.55-.494c.2-.16.354-.362.467-.664H8a.75.75 0 0 1 0-1.5h11.975c.018-.363.023-.776.025-1.25M7.25 7A.75.75 0 0 1 8 6.25h8a.75.75 0 0 1 0 1.5H8A.75.75 0 0 1 7.25 7M8 9.75a.75.75 0 0 0 0 1.5h5a.75.75 0 0 0 0-1.5z" 171 | ></path> 172 | </svg> 173 | ), 174 | title: "Docs", 175 | description: "get started, concepts, and plugins", 176 | }, 177 | { 178 | value: "examples", 179 | icon: ( 180 | <svg 181 | xmlns="http://www.w3.org/2000/svg" 182 | width="1.4em" 183 | height="1.4em" 184 | viewBox="0 0 24 24" 185 | > 186 | <path 187 | fill="currentColor" 188 | d="M12 2c4.714 0 7.071 0 8.535 1.464c1.08 1.08 1.364 2.647 1.439 5.286L22 9.5H2.026v-.75c.075-2.64.358-4.205 1.438-5.286C4.93 2 7.286 2 12 2" 189 | opacity=".5" 190 | ></path> 191 | <path 192 | fill="currentColor" 193 | d="M13 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-3 0a1 1 0 1 1-2 0a1 1 0 0 1 2 0M7 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0" 194 | ></path> 195 | <path 196 | fill="currentColor" 197 | d="M2 12c0 4.714 0 7.071 1.464 8.535c1.01 1.01 2.446 1.324 4.786 1.421L9 22V9.5H2.026l-.023.75Q2 11.066 2 12" 198 | opacity=".7" 199 | ></path> 200 | <path 201 | fill="currentColor" 202 | d="M22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22c-.819 0-2.316 0-3-.008V9.5h13l-.003.75Q22 11.066 22 12" 203 | ></path> 204 | </svg> 205 | ), 206 | title: "Examples", 207 | description: "examples and guides", 208 | }, 209 | ]; 210 | 211 | function SidebarTab({ 212 | group, 213 | setGroup, 214 | }: { 215 | group: string; 216 | setGroup: (group: string) => void; 217 | }) { 218 | const router = useRouter(); 219 | const selected = tabs.find((tab) => tab.value === group); 220 | 221 | return ( 222 | <Select 223 | value={group} 224 | onValueChange={(val) => { 225 | setGroup(val); 226 | if (val === "docs") { 227 | router.push("/docs"); 228 | } else { 229 | router.push("/docs/examples"); 230 | } 231 | }} 232 | > 233 | <SelectTrigger className="h-16 border border-b border-none rounded-none px-5"> 234 | {selected ? ( 235 | <div className="flex flex-col gap-1 items-start"> 236 | <div className="flex items-center gap-1 -ml-0.5"> 237 | {selected.icon} 238 | {selected.title} 239 | </div> 240 | <p className="text-xs text-muted-foreground"> 241 | {selected.description} 242 | </p> 243 | </div> 244 | ) : null} 245 | </SelectTrigger> 246 | <SelectContent> 247 | {tabs.map((tab) => ( 248 | <SelectItem 249 | key={tab.value} 250 | value={tab.value} 251 | className="h-12 flex flex-col items-start gap-1" 252 | > 253 | <div className="flex items-center gap-1"> 254 | {tab.icon} 255 | {tab.title} 256 | </div> 257 | <p className="text-xs text-muted-foreground">{tab.description}</p> 258 | </SelectItem> 259 | ))} 260 | </SelectContent> 261 | </Select> 262 | ); 263 | } 264 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/autumn.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Autumn Billing 3 | description: Better Auth Plugin for Autumn Billing 4 | --- 5 | 6 | import { HomeIcon } from "lucide-react"; 7 | import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; 8 | 9 | [Autumn](https://useautumn.com) is open source infrastructure to run SaaS pricing plans. It sits between your app and Stripe, and acts as the database for your customers' subscription status, usage metering and feature permissions. 10 | 11 | <Card href="https://discord.gg/STqxY92zuS" title="Get help on Autumn's Discord"> 12 | We're online to help you with any questions you have. 13 | </Card> 14 | 15 | ## Features 16 | 17 | - One function for all checkout, subscription and payment flows 18 | - No webhooks required: query Autumn for the data you need 19 | - Manages your application's free and paid plans 20 | - Usage tracking for usage billing and periodic limits 21 | - Custom plans and pricing changes through Autumn's dashboard 22 | 23 | 24 | <Steps> 25 | <Step> 26 | ### Setup Autumn Account 27 | First, create your pricing plans in Autumn's [dashboard](https://app.useautumn.com), where you define what each plan and product gets access to and how it should be billed. In this example, we're handling the free and pro plans for an AI chatbot, which comes with a number of `messages` per month. 28 | </Step> 29 | <Step> 30 | ### Install Autumn SDK 31 | 32 | ```package-install 33 | autumn-js 34 | ``` 35 | <Callout> 36 | If you're using a separate client and server setup, make sure to install the plugin in both parts of your project. 37 | </Callout> 38 | </Step> 39 | 40 | <Step> 41 | ### Add `AUTUMN_SECRET_KEY` to your environment variables 42 | 43 | You can find it in Autumn's dashboard under "[Developer](https://app.useautumn.com/sandbox/onboarding)". 44 | 45 | ```bash title=".env" 46 | AUTUMN_SECRET_KEY=am_sk_xxxxxxxxxx 47 | ``` 48 | </Step> 49 | 50 | <Step> 51 | ### Add the Autumn plugin to your `auth` config 52 | 53 | <Tabs items={["User", "Organization", "User & Organization", "Custom"]}> 54 | <Tab value="User"> 55 | 56 | ```ts title="auth.ts" 57 | import { autumn } from "autumn-js/better-auth"; 58 | 59 | export const auth = betterAuth({ 60 | // ... 61 | plugins: [autumn()], 62 | }); 63 | ``` 64 | 65 | </Tab> 66 | <Tab value="Organization"> 67 | 68 | ```ts title="auth.ts" 69 | import { autumn } from "autumn-js/better-auth"; 70 | import { organization } from "better-auth/plugins"; 71 | 72 | export const auth = betterAuth({ 73 | // ... 74 | plugins: [organization(), autumn({ customerScope: "organization" })], 75 | }); 76 | ``` 77 | 78 | </Tab> 79 | <Tab value="User & Organization"> 80 | 81 | ```ts title="auth.ts" 82 | import { autumn } from "autumn-js/better-auth"; 83 | import { organization } from "better-auth/plugins"; 84 | 85 | export const auth = betterAuth({ 86 | // ... 87 | plugins: [ 88 | organization(), 89 | autumn({ customerScope: "user_and_organization" }) 90 | ], 91 | }); 92 | ``` 93 | 94 | </Tab> 95 | <Tab value="Custom"> 96 | 97 | ```ts title="auth.ts" 98 | import { autumn } from "autumn-js/better-auth"; 99 | import { organization } from "better-auth/plugins"; 100 | 101 | export const auth = betterAuth({ 102 | // ... 103 | plugins: [ 104 | organization(), 105 | autumn({ 106 | identify: async ({ session, organization }) => { 107 | return { 108 | customerId: "your_customer_id", 109 | customerData: { 110 | name: "Customer Name", 111 | email: "[email protected]", 112 | }, 113 | }; 114 | }, 115 | }), 116 | ], 117 | }); 118 | ``` 119 | 120 | </Tab> 121 | </Tabs> 122 | 123 | <Callout> 124 | Autumn will auto-create your customers when they sign up, and assign them any 125 | default plans you created (eg your Free plan). You can choose who becomes a customer: individual users, organizations, both, or something custom like workspaces. 126 | </Callout> 127 | </Step> 128 | 129 | <Step> 130 | ### Add `<AutumnProvider />` 131 | 132 | Client side, wrap your application with the AutumnProvider component, and pass in the `baseUrl` that you define within better-auth's `authClient`. 133 | 134 | ```tsx title="app/layout.tsx" 135 | import { AutumnProvider } from "autumn-js/react"; 136 | 137 | export default function RootLayout({ 138 | children, 139 | }: { 140 | children: React.ReactNode; 141 | }) { 142 | return ( 143 | <html> 144 | <body> 145 | {/* or meta.env.BETTER_AUTH_URL for vite */} 146 | <AutumnProvider betterAuthUrl={process.env.NEXT_PUBLIC_BETTER_AUTH_URL}> 147 | {children} 148 | </AutumnProvider> 149 | </body> 150 | </html> 151 | ); 152 | } 153 | ``` 154 | </Step> 155 | </Steps> 156 | 157 | ## Usage 158 | 159 | ### Handle payments 160 | 161 | Call `attach` to redirect the customer to a Stripe checkout page when they want to purchase the Pro plan. 162 | 163 | If their payment method is already on file, `AttachDialog` will open instead to let the customer confirm their new subscription or purchase, and handle the payment. 164 | 165 | <Callout type="warn"> 166 | {" "} 167 | Make sure you've pasted in your [Stripe test secret 168 | key](https://dashboard.stripe.com/test/apikeys) in the [Autumn 169 | dashboard](https://app.useautumn.com/integrations/stripe). 170 | </Callout> 171 | 172 | ```tsx 173 | import { useCustomer, AttachDialog } from "autumn-js/react"; 174 | 175 | export default function PurchaseButton() { 176 | const { attach } = useCustomer(); 177 | 178 | return ( 179 | <button 180 | onClick={async () => { 181 | await attach({ 182 | productId: "pro", 183 | dialog: AttachDialog, 184 | }); 185 | }} 186 | > 187 | Upgrade to Pro 188 | </button> 189 | ); 190 | } 191 | ``` 192 | 193 | The AttachDialog component can be used directly from the `autumn-js/react` 194 | library (as shown in the example above), or downloaded as a [shadcn/ui component](https://docs.useautumn.com/quickstart/shadcn) to customize. 195 | 196 | ### Integrate Pricing Logic 197 | 198 | Integrate your client and server pricing tiers logic with the following functions: 199 | 200 | - `check` to see if the customer is `allowed` to send a message. 201 | - `track` a usage event in Autumn (typically done server-side) 202 | - `customer` to display any relevant billing data in your UI (subscriptions, feature balances) 203 | 204 | Server-side, you can access Autumn's functions through the `auth` object. 205 | 206 | <Tabs items={["Client", "Server"]}> 207 | <Tab value="Client"> 208 | 209 | ```jsx 210 | import { useCustomer } from "autumn-js/react"; 211 | 212 | export default function SendChatMessage() { 213 | const { customer, allowed, refetch } = useCustomer(); 214 | 215 | return ( 216 | <> 217 | <button 218 | onClick={async () => { 219 | if (allowed({ featureId: "messages" })) { 220 | //... send chatbot message server-side, then 221 | await refetch(); // refetch customer usage data 222 | alert( 223 | "Remaining messages: " + customer?.features.messages?.balance 224 | ); 225 | } else { 226 | alert("You're out of messages"); 227 | } 228 | }} 229 | > 230 | Send Message 231 | </button> 232 | </> 233 | ); 234 | } 235 | ``` 236 | 237 | </Tab> 238 | <Tab value="Server"> 239 | 240 | ```typescript Server 241 | import { auth } from "@/lib/auth"; 242 | 243 | // check on the backend if the customer can send a message 244 | const { allowed } = await auth.api.check({ 245 | headers: await headers(), // pass the request headers 246 | body: { 247 | featureId: "messages", 248 | }, 249 | }); 250 | 251 | // server-side function to send the message 252 | 253 | // then track the usage 254 | await auth.api.track({ 255 | headers: await headers(), 256 | body: { 257 | featureId: "messages", 258 | value: 2, 259 | }, 260 | }); 261 | ``` 262 | 263 | </Tab> 264 | </Tabs> 265 | 266 | ### Additional Functions 267 | 268 | #### openBillingPortal() 269 | 270 | Opens a billing portal where the customer can update their payment method or cancel their plan. 271 | 272 | ```tsx 273 | import { useCustomer } from "autumn-js/react"; 274 | 275 | export default function BillingSettings() { 276 | const { openBillingPortal } = useCustomer(); 277 | 278 | return ( 279 | <button 280 | onClick={async () => { 281 | await openBillingPortal({ 282 | returnUrl: "/settings/billing", 283 | }); 284 | }} 285 | > 286 | Manage Billing 287 | </button> 288 | ); 289 | } 290 | ``` 291 | 292 | #### cancel() 293 | 294 | Cancel a product or subscription. 295 | 296 | ```tsx 297 | import { useCustomer } from "autumn-js/react"; 298 | 299 | export default function CancelSubscription() { 300 | const { cancel } = useCustomer(); 301 | 302 | return ( 303 | <button 304 | onClick={async () => { 305 | await cancel({ productId: "pro" }); 306 | }} 307 | > 308 | Cancel Subscription 309 | </button> 310 | ); 311 | } 312 | ``` 313 | 314 | #### Get invoice history 315 | 316 | Pass in an `expand` param into `useCustomer` to get additional information. You can expand `invoices`, `trials_used`, `payment_method`, or `rewards`. 317 | 318 | ```tsx 319 | import { useCustomer } from "autumn-js/react"; 320 | 321 | export default function CustomerProfile() { 322 | const { customer } = useCustomer({ expand: ["invoices"] }); 323 | 324 | return ( 325 | <div> 326 | <h2>Customer Profile</h2> 327 | <p>Name: {customer?.name}</p> 328 | <p>Email: {customer?.email}</p> 329 | <p>Balance: {customer?.features.chat_messages?.balance}</p> 330 | </div> 331 | ); 332 | } 333 | ``` 334 | ``` -------------------------------------------------------------------------------- /docs/app/blog/_components/default-changelog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | import { useId } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import { IconLink } from "./changelog-layout"; 5 | import { BookIcon, GitHubIcon, XIcon } from "./icons"; 6 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 7 | import { StarField } from "./stat-field"; 8 | import { betterFetch } from "@better-fetch/fetch"; 9 | import Markdown from "react-markdown"; 10 | import defaultMdxComponents from "fumadocs-ui/mdx"; 11 | import rehypeHighlight from "rehype-highlight"; 12 | import "highlight.js/styles/dark.css"; 13 | 14 | export const dynamic = "force-static"; 15 | const ChangelogPage = async () => { 16 | const { data: releases } = await betterFetch< 17 | { 18 | id: number; 19 | tag_name: string; 20 | name: string; 21 | body: string; 22 | html_url: string; 23 | prerelease: boolean; 24 | published_at: string; 25 | }[] 26 | >("https://api.github.com/repos/better-auth/better-auth/releases"); 27 | 28 | const messages = releases 29 | ?.filter((release) => !release.prerelease) 30 | .map((release) => ({ 31 | tag: release.tag_name, 32 | title: release.name, 33 | content: getContent(release.body), 34 | date: new Date(release.published_at).toLocaleDateString("en-US", { 35 | year: "numeric", 36 | month: "short", 37 | day: "numeric", 38 | }), 39 | url: release.html_url, 40 | })); 41 | 42 | function getContent(content: string) { 43 | const lines = content.split("\n"); 44 | const newContext = lines.map((line) => { 45 | if (line.startsWith("- ")) { 46 | const mainContent = line.split(";")[0]; 47 | const context = line.split(";")[2]; 48 | const mentionMatches = 49 | (context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? []; 50 | if (mentionMatches.length === 0) { 51 | return (mainContent || line).replace(/ /g, ""); 52 | } 53 | const mentions = mentionMatches.map((match) => { 54 | const username = match.slice(1); 55 | const avatarUrl = `https://github.com/${username}.png`; 56 | return `[](https://github.com/${username})`; 57 | }); 58 | // Remove   59 | return ( 60 | (mainContent || line).replace(/ /g, "") + 61 | " – " + 62 | mentions.join(" ") 63 | ); 64 | } 65 | return line; 66 | }); 67 | return newContext.join("\n"); 68 | } 69 | 70 | return ( 71 | <div className="grid md:grid-cols-2 items-start"> 72 | <div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 73 | <StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" /> 74 | <Glow /> 75 | 76 | <div className="flex flex-col md:justify-center max-w-xl mx-auto h-full"> 77 | <h1 className="mt-14 font-sans font-semibold tracking-tighter text-5xl"> 78 | All of the changes made will be{" "} 79 | <span className="">available here.</span> 80 | </h1> 81 | <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> 82 | Better Auth is comprehensive authentication library for TypeScript 83 | that provides a wide range of features to make authentication easier 84 | and more secure. 85 | </p> 86 | <hr className="h-px bg-gray-300 mt-5" /> 87 | <div className="mt-8 flex flex-wrap text-gray-600 dark:text-gray-300 gap-x-1 gap-y-3 sm:gap-x-2"> 88 | <IconLink 89 | href="/docs" 90 | icon={BookIcon} 91 | className="flex-none text-gray-600 dark:text-gray-300" 92 | > 93 | Documentation 94 | </IconLink> 95 | <IconLink 96 | href="https://github.com/better-auth/better-auth" 97 | icon={GitHubIcon} 98 | className="flex-none text-gray-600 dark:text-gray-300" 99 | > 100 | GitHub 101 | </IconLink> 102 | <IconLink 103 | href="https://discord.gg/better-auth" 104 | icon={DiscordLogoIcon} 105 | className="flex-none text-gray-600 dark:text-gray-300" 106 | > 107 | Community 108 | </IconLink> 109 | </div> 110 | <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> 111 | <IconLink href="https://x.com/better_auth" icon={XIcon} compact> 112 | BETTER-AUTH. 113 | </IconLink> 114 | </p> 115 | </div> 116 | </div> 117 | <div className="px-4 relative md:px-8 pb-12 md:py-12"> 118 | <div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div> 119 | 120 | <div className="max-w-2xl relative"> 121 | <Markdown 122 | rehypePlugins={[[rehypeHighlight]]} 123 | components={{ 124 | pre: (props) => ( 125 | <defaultMdxComponents.pre 126 | {...props} 127 | className={cn(props.className, " ml-10 my-2")} 128 | /> 129 | ), 130 | h2: (props) => ( 131 | <h2 132 | id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically 133 | className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]" 134 | {...props} 135 | > 136 | <div className="sticky top-0 left-[-9.9rem] hidden md:block"> 137 | <time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal"> 138 | {props.children?.toString().includes("date=") && 139 | props.children?.toString().split("date=")[1]} 140 | 141 | <div className="w-4 h-[1px] dark:bg-white/60 bg-black" /> 142 | </time> 143 | </div> 144 | <Link 145 | href={ 146 | props.children 147 | ?.toString() 148 | .split("date=")[0] 149 | .trim() 150 | .endsWith(".00") 151 | ? `/changelogs/${props.children 152 | ?.toString() 153 | .split("date=")[0] 154 | .trim()}` 155 | : `#${props.children 156 | ?.toString() 157 | .split("date=")[0] 158 | .trim()}` 159 | } 160 | > 161 | {props.children?.toString().split("date=")[0].trim()} 162 | </Link> 163 | <p className="text-xs font-normal opacity-60 hidden"> 164 | {props.children?.toString().includes("date=") && 165 | props.children?.toString().split("date=")[1]} 166 | </p> 167 | </h2> 168 | ), 169 | h3: (props) => ( 170 | <h3 className="text-xl tracking-tighter py-1" {...props}> 171 | {props.children?.toString()?.trim()} 172 | <hr className="h-[1px] my-1 mb-2 bg-input" /> 173 | </h3> 174 | ), 175 | p: (props) => <p className="my-0 ml-10 text-sm" {...props} />, 176 | ul: (props) => ( 177 | <ul 178 | className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300" 179 | {...props} 180 | /> 181 | ), 182 | li: (props) => <li className="my-1" {...props} />, 183 | a: ({ className, ...props }: any) => ( 184 | <Link 185 | target="_blank" 186 | className={cn("font-medium underline", className)} 187 | {...props} 188 | /> 189 | ), 190 | strong: (props) => ( 191 | <strong className="font-semibold" {...props} /> 192 | ), 193 | img: (props) => ( 194 | <img 195 | className="rounded-full w-6 h-6 border opacity-70 inline-block" 196 | {...props} 197 | style={{ maxWidth: "100%" }} 198 | /> 199 | ), 200 | }} 201 | > 202 | {messages 203 | ?.map((message) => { 204 | return ` 205 | ## ${message.title} date=${message.date} 206 | 207 | ${message.content} 208 | `; 209 | }) 210 | .join("\n")} 211 | </Markdown> 212 | </div> 213 | </div> 214 | </div> 215 | ); 216 | }; 217 | 218 | export default ChangelogPage; 219 | 220 | export function Glow() { 221 | let id = useId(); 222 | 223 | return ( 224 | <div className="absolute inset-0 -z-10 overflow-hidden bg-gradient-to-tr from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 225 | <svg 226 | className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]" 227 | aria-hidden="true" 228 | > 229 | <defs> 230 | <radialGradient id={`${id}-desktop`} cx="100%"> 231 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" /> 232 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 233 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 234 | </radialGradient> 235 | <radialGradient id={`${id}-mobile`} cy="100%"> 236 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" /> 237 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 238 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 239 | </radialGradient> 240 | </defs> 241 | <rect 242 | width="100%" 243 | height="100%" 244 | fill={`url(#${id}-desktop)`} 245 | className="hidden lg:block" 246 | /> 247 | <rect 248 | width="100%" 249 | height="100%" 250 | fill={`url(#${id}-mobile)`} 251 | className="lg:hidden" 252 | /> 253 | </svg> 254 | <div className="absolute inset-x-0 bottom-0 right-0 h-px dark:bg-white/5 mix-blend-overlay lg:left-auto lg:top-0 lg:h-auto lg:w-px" /> 255 | </div> 256 | ); 257 | } 258 | ``` -------------------------------------------------------------------------------- /docs/app/changelogs/_components/default-changelog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | import { useId } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import { IconLink } from "./changelog-layout"; 5 | import { BookIcon, GitHubIcon, XIcon } from "./icons"; 6 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 7 | import { StarField } from "./stat-field"; 8 | import { betterFetch } from "@better-fetch/fetch"; 9 | import Markdown from "react-markdown"; 10 | import defaultMdxComponents from "fumadocs-ui/mdx"; 11 | import rehypeHighlight from "rehype-highlight"; 12 | import "highlight.js/styles/dark.css"; 13 | 14 | export const dynamic = "force-static"; 15 | const ChangelogPage = async () => { 16 | const { data: releases } = await betterFetch< 17 | { 18 | id: number; 19 | tag_name: string; 20 | name: string; 21 | body: string; 22 | html_url: string; 23 | prerelease: boolean; 24 | published_at: string; 25 | }[] 26 | >("https://api.github.com/repos/better-auth/better-auth/releases"); 27 | 28 | const messages = releases 29 | ?.filter((release) => !release.prerelease) 30 | .map((release) => ({ 31 | tag: release.tag_name, 32 | title: release.name, 33 | content: getContent(release.body), 34 | date: new Date(release.published_at).toLocaleDateString("en-US", { 35 | year: "numeric", 36 | month: "short", 37 | day: "numeric", 38 | }), 39 | url: release.html_url, 40 | })); 41 | 42 | function getContent(content: string) { 43 | const lines = content.split("\n"); 44 | const newContext = lines.map((line) => { 45 | if (line.trim().startsWith("- ")) { 46 | const mainContent = line.split(";")[0]; 47 | const context = line.split(";")[2]; 48 | const mentionMatches = 49 | (context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? []; 50 | if (mentionMatches.length === 0) { 51 | return (mainContent || line).replace(/ /g, ""); 52 | } 53 | const mentions = mentionMatches.map((match) => { 54 | const username = match.slice(1); 55 | const avatarUrl = `https://github.com/${username}.png`; 56 | return `[](https://github.com/${username})`; 57 | }); 58 | // Remove   59 | return ( 60 | (mainContent || line).replace(/ /g, "") + 61 | " – " + 62 | mentions.join(" ") 63 | ); 64 | } 65 | return line; 66 | }); 67 | return newContext.join("\n"); 68 | } 69 | 70 | return ( 71 | <div className="grid items-start md:grid-cols-2"> 72 | <div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 73 | <StarField className="top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" /> 74 | <Glow /> 75 | 76 | <div className="flex flex-col mx-auto max-w-xl h-full md:justify-center"> 77 | <h1 className="mt-14 font-sans text-5xl font-semibold tracking-tighter"> 78 | All of the changes made will be{" "} 79 | <span className="">available here.</span> 80 | </h1> 81 | <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> 82 | Better Auth is comprehensive authentication library for TypeScript 83 | that provides a wide range of features to make authentication easier 84 | and more secure. 85 | </p> 86 | <hr className="mt-5 h-px bg-gray-300" /> 87 | <div className="flex flex-wrap gap-x-1 gap-y-3 mt-8 text-gray-600 dark:text-gray-300 sm:gap-x-2"> 88 | <IconLink 89 | href="/docs" 90 | icon={BookIcon} 91 | className="flex-none text-gray-600 dark:text-gray-300" 92 | > 93 | Documentation 94 | </IconLink> 95 | <IconLink 96 | href="https://github.com/better-auth/better-auth" 97 | icon={GitHubIcon} 98 | className="flex-none text-gray-600 dark:text-gray-300" 99 | > 100 | GitHub 101 | </IconLink> 102 | <IconLink 103 | href="https://discord.gg/better-auth" 104 | icon={DiscordLogoIcon} 105 | className="flex-none text-gray-600 dark:text-gray-300" 106 | > 107 | Community 108 | </IconLink> 109 | </div> 110 | <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> 111 | <IconLink href="https://x.com/better_auth" icon={XIcon} compact> 112 | BETTER-AUTH. 113 | </IconLink> 114 | </p> 115 | </div> 116 | </div> 117 | <div className="relative px-4 pb-12 md:px-8 md:py-12"> 118 | <div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div> 119 | 120 | <div className="relative max-w-2xl"> 121 | <Markdown 122 | rehypePlugins={[[rehypeHighlight]]} 123 | components={{ 124 | pre: (props) => ( 125 | <defaultMdxComponents.pre 126 | {...props} 127 | className={cn(props.className, " ml-10 my-2")} 128 | /> 129 | ), 130 | h2: (props) => ( 131 | <h2 132 | id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically 133 | className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]" 134 | {...props} 135 | > 136 | <div className="sticky top-0 left-[-9.9rem] hidden md:block"> 137 | <time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal"> 138 | {props.children?.toString().includes("date=") && 139 | props.children?.toString().split("date=")[1]} 140 | 141 | <div className="w-4 h-[1px] dark:bg-white/60 bg-black" /> 142 | </time> 143 | </div> 144 | <Link 145 | href={ 146 | props.children 147 | ?.toString() 148 | .split("date=")[0] 149 | .trim() 150 | .endsWith(".00") 151 | ? `/changelogs/${props.children 152 | ?.toString() 153 | .split("date=")[0] 154 | .trim()}` 155 | : `#${props.children 156 | ?.toString() 157 | .split("date=")[0] 158 | .trim()}` 159 | } 160 | > 161 | {props.children?.toString().split("date=")[0].trim()} 162 | </Link> 163 | <p className="hidden text-xs font-normal opacity-60"> 164 | {props.children?.toString().includes("date=") && 165 | props.children?.toString().split("date=")[1]} 166 | </p> 167 | </h2> 168 | ), 169 | h3: (props) => ( 170 | <h3 className="py-1 text-xl tracking-tighter" {...props}> 171 | {props.children?.toString()?.trim()} 172 | <hr className="h-[1px] my-1 mb-2 bg-input" /> 173 | </h3> 174 | ), 175 | p: (props) => <p className="my-0 ml-10 text-sm" {...props} />, 176 | ul: (props) => ( 177 | <ul 178 | className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300" 179 | {...props} 180 | /> 181 | ), 182 | li: (props) => <li className="my-1" {...props} />, 183 | a: ({ className, ...props }: any) => ( 184 | <Link 185 | target="_blank" 186 | className={cn("font-medium underline", className)} 187 | {...props} 188 | /> 189 | ), 190 | strong: (props) => ( 191 | <strong className="font-semibold" {...props} /> 192 | ), 193 | img: (props) => ( 194 | <img 195 | className="inline-block w-6 h-6 rounded-full border opacity-70" 196 | {...props} 197 | style={{ maxWidth: "100%" }} 198 | /> 199 | ), 200 | }} 201 | > 202 | {messages 203 | ?.map((message) => { 204 | return ` 205 | ## ${message.title} date=${message.date} 206 | 207 | ${message.content} 208 | `; 209 | }) 210 | .join("\n")} 211 | </Markdown> 212 | </div> 213 | </div> 214 | </div> 215 | ); 216 | }; 217 | 218 | export default ChangelogPage; 219 | 220 | export function Glow() { 221 | let id = useId(); 222 | 223 | return ( 224 | <div className="overflow-hidden absolute inset-0 bg-gradient-to-tr from-transparent -z-10 dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 225 | <svg 226 | className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]" 227 | aria-hidden="true" 228 | > 229 | <defs> 230 | <radialGradient id={`${id}-desktop`} cx="100%"> 231 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" /> 232 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 233 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 234 | </radialGradient> 235 | <radialGradient id={`${id}-mobile`} cy="100%"> 236 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" /> 237 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 238 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 239 | </radialGradient> 240 | </defs> 241 | <rect 242 | width="100%" 243 | height="100%" 244 | fill={`url(#${id}-desktop)`} 245 | className="hidden lg:block" 246 | /> 247 | <rect 248 | width="100%" 249 | height="100%" 250 | fill={`url(#${id}-mobile)`} 251 | className="lg:hidden" 252 | /> 253 | </svg> 254 | <div className="absolute inset-x-0 right-0 bottom-0 h-px mix-blend-overlay dark:bg-white/5 lg:left-auto lg:top-0 lg:h-auto lg:w-px" /> 255 | </div> 256 | ); 257 | } 258 | ```