This is page 36 of 54. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── renovate.json5
│ └── workflows
│ ├── ci.yml
│ ├── e2e.yml
│ ├── preview.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│ └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│ ├── expo-example
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── app.config.ts
│ │ ├── assets
│ │ │ ├── bg-image.jpeg
│ │ │ ├── fonts
│ │ │ │ └── SpaceMono-Regular.ttf
│ │ │ ├── icon.png
│ │ │ └── images
│ │ │ ├── adaptive-icon.png
│ │ │ ├── favicon.png
│ │ │ ├── logo.png
│ │ │ ├── partial-react-logo.png
│ │ │ ├── react-logo.png
│ │ │ ├── [email protected]
│ │ │ ├── [email protected]
│ │ │ └── splash.png
│ │ ├── babel.config.js
│ │ ├── components.json
│ │ ├── expo-env.d.ts
│ │ ├── index.ts
│ │ ├── metro.config.js
│ │ ├── nativewind-env.d.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── app
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── api
│ │ │ │ │ └── auth
│ │ │ │ │ └── [...route]+api.ts
│ │ │ │ ├── dashboard.tsx
│ │ │ │ ├── forget-password.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── sign-up.tsx
│ │ │ ├── components
│ │ │ │ ├── icons
│ │ │ │ │ └── google.tsx
│ │ │ │ └── ui
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ └── text.tsx
│ │ │ ├── global.css
│ │ │ └── lib
│ │ │ ├── auth-client.ts
│ │ │ ├── auth.ts
│ │ │ ├── icons
│ │ │ │ ├── iconWithClassName.ts
│ │ │ │ └── X.tsx
│ │ │ └── utils.ts
│ │ ├── tailwind.config.js
│ │ └── tsconfig.json
│ ├── nextjs
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── app
│ │ │ ├── (auth)
│ │ │ │ ├── forget-password
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── reset-password
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── sign-in
│ │ │ │ │ ├── loading.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── two-factor
│ │ │ │ ├── otp
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── accept-invitation
│ │ │ │ └── [id]
│ │ │ │ ├── invitation-error.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── admin
│ │ │ │ └── page.tsx
│ │ │ ├── api
│ │ │ │ └── auth
│ │ │ │ └── [...all]
│ │ │ │ └── route.ts
│ │ │ ├── apps
│ │ │ │ └── register
│ │ │ │ └── page.tsx
│ │ │ ├── client-test
│ │ │ │ └── page.tsx
│ │ │ ├── dashboard
│ │ │ │ ├── change-plan.tsx
│ │ │ │ ├── client.tsx
│ │ │ │ ├── organization-card.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ ├── upgrade-button.tsx
│ │ │ │ └── user-card.tsx
│ │ │ ├── device
│ │ │ │ ├── approve
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── denied
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── success
│ │ │ │ └── page.tsx
│ │ │ ├── favicon.ico
│ │ │ ├── features.tsx
│ │ │ ├── fonts
│ │ │ │ ├── GeistMonoVF.woff
│ │ │ │ └── GeistVF.woff
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── oauth
│ │ │ │ └── authorize
│ │ │ │ ├── concet-buttons.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── pricing
│ │ │ └── page.tsx
│ │ ├── components
│ │ │ ├── account-switch.tsx
│ │ │ ├── blocks
│ │ │ │ └── pricing.tsx
│ │ │ ├── logo.tsx
│ │ │ ├── one-tap.tsx
│ │ │ ├── sign-in-btn.tsx
│ │ │ ├── sign-in.tsx
│ │ │ ├── sign-up.tsx
│ │ │ ├── theme-provider.tsx
│ │ │ ├── theme-toggle.tsx
│ │ │ ├── tier-labels.tsx
│ │ │ ├── ui
│ │ │ │ ├── accordion.tsx
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── aspect-ratio.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── calendar.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── carousel.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── context-menu.tsx
│ │ │ │ ├── copy-button.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── drawer.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── form.tsx
│ │ │ │ ├── hover-card.tsx
│ │ │ │ ├── input-otp.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── menubar.tsx
│ │ │ │ ├── navigation-menu.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── password-input.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── resizable.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── tabs2.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── toaster.tsx
│ │ │ │ ├── toggle-group.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ └── wrapper.tsx
│ │ ├── components.json
│ │ ├── hooks
│ │ │ └── use-toast.ts
│ │ ├── lib
│ │ │ ├── auth-client.ts
│ │ │ ├── auth-types.ts
│ │ │ ├── auth.ts
│ │ │ ├── email
│ │ │ │ ├── invitation.tsx
│ │ │ │ ├── resend.ts
│ │ │ │ └── reset-password.tsx
│ │ │ ├── metadata.ts
│ │ │ ├── shared.ts
│ │ │ └── utils.ts
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── proxy.ts
│ │ ├── public
│ │ │ ├── __og.png
│ │ │ ├── _og.png
│ │ │ ├── favicon
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── light
│ │ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ │ ├── apple-touch-icon.png
│ │ │ │ │ ├── favicon-16x16.png
│ │ │ │ │ ├── favicon-32x32.png
│ │ │ │ │ ├── favicon.ico
│ │ │ │ │ └── site.webmanifest
│ │ │ │ └── site.webmanifest
│ │ │ ├── logo.svg
│ │ │ └── og.png
│ │ ├── README.md
│ │ ├── tailwind.config.ts
│ │ ├── tsconfig.json
│ │ └── turbo.json
│ └── stateless
│ ├── .env.example
│ ├── .gitignore
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── src
│ │ ├── app
│ │ │ ├── api
│ │ │ │ ├── auth
│ │ │ │ │ └── [...all]
│ │ │ │ │ └── route.ts
│ │ │ │ └── user
│ │ │ │ └── route.ts
│ │ │ ├── dashboard
│ │ │ │ └── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ └── lib
│ │ ├── auth-client.ts
│ │ └── auth.ts
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── docker-compose.yml
├── docs
│ ├── .env.example
│ ├── .gitignore
│ ├── app
│ │ ├── api
│ │ │ ├── ai-chat
│ │ │ │ └── route.ts
│ │ │ ├── analytics
│ │ │ │ ├── conversation
│ │ │ │ │ └── route.ts
│ │ │ │ ├── event
│ │ │ │ │ └── route.ts
│ │ │ │ └── feedback
│ │ │ │ └── route.ts
│ │ │ ├── chat
│ │ │ │ └── route.ts
│ │ │ ├── og
│ │ │ │ └── route.tsx
│ │ │ ├── og-release
│ │ │ │ └── route.tsx
│ │ │ ├── search
│ │ │ │ └── route.ts
│ │ │ └── support
│ │ │ └── route.ts
│ │ ├── blog
│ │ │ ├── _components
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── blog-list.tsx
│ │ │ │ ├── changelog-layout.tsx
│ │ │ │ ├── default-changelog.tsx
│ │ │ │ ├── fmt-dates.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ ├── stat-field.tsx
│ │ │ │ └── support.tsx
│ │ │ ├── [[...slug]]
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── changelogs
│ │ │ ├── _components
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── changelog-layout.tsx
│ │ │ │ ├── default-changelog.tsx
│ │ │ │ ├── fmt-dates.tsx
│ │ │ │ ├── grid-pattern.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ └── stat-field.tsx
│ │ │ ├── [[...slug]]
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── community
│ │ │ ├── _components
│ │ │ │ ├── header.tsx
│ │ │ │ └── stats.tsx
│ │ │ └── page.tsx
│ │ ├── docs
│ │ │ ├── [[...slug]]
│ │ │ │ ├── page.client.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── lib
│ │ │ └── get-llm-text.ts
│ │ ├── global.css
│ │ ├── layout.config.tsx
│ │ ├── layout.tsx
│ │ ├── llms.txt
│ │ │ ├── [...slug]
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── not-found.tsx
│ │ ├── page.tsx
│ │ ├── reference
│ │ │ └── route.ts
│ │ ├── sitemap.xml
│ │ ├── static.json
│ │ │ └── route.ts
│ │ └── v1
│ │ ├── _components
│ │ │ └── v1-text.tsx
│ │ ├── bg-line.tsx
│ │ └── page.tsx
│ ├── assets
│ │ ├── Geist.ttf
│ │ └── GeistMono.ttf
│ ├── components
│ │ ├── ai-chat-modal.tsx
│ │ ├── anchor-scroll-fix.tsx
│ │ ├── api-method-tabs.tsx
│ │ ├── api-method.tsx
│ │ ├── banner.tsx
│ │ ├── blocks
│ │ │ └── features.tsx
│ │ ├── builder
│ │ │ ├── beam.tsx
│ │ │ ├── code-tabs
│ │ │ │ ├── code-editor.tsx
│ │ │ │ ├── code-tabs.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── tab-bar.tsx
│ │ │ │ └── theme.ts
│ │ │ ├── index.tsx
│ │ │ ├── sign-in.tsx
│ │ │ ├── sign-up.tsx
│ │ │ ├── social-provider.tsx
│ │ │ ├── store.ts
│ │ │ └── tabs.tsx
│ │ ├── display-techstack.tsx
│ │ ├── divider-text.tsx
│ │ ├── docs
│ │ │ ├── docs.client.tsx
│ │ │ ├── docs.tsx
│ │ │ ├── layout
│ │ │ │ ├── nav.tsx
│ │ │ │ ├── theme-toggle.tsx
│ │ │ │ ├── toc-thumb.tsx
│ │ │ │ └── toc.tsx
│ │ │ ├── page.client.tsx
│ │ │ ├── page.tsx
│ │ │ ├── shared.tsx
│ │ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── popover.tsx
│ │ │ └── scroll-area.tsx
│ │ ├── endpoint.tsx
│ │ ├── features.tsx
│ │ ├── floating-ai-search.tsx
│ │ ├── fork-button.tsx
│ │ ├── generate-apple-jwt.tsx
│ │ ├── generate-secret.tsx
│ │ ├── github-stat.tsx
│ │ ├── halloween
│ │ │ ├── halloween-logo-dark.json
│ │ │ ├── halloween-logo-light.json
│ │ │ └── logo.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
│ │ │ ├── polar.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
│ ├── 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
│ │ │ │ ├── polar.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/components/floating-ai-search.tsx:
--------------------------------------------------------------------------------
```typescript
"use client";
import { type UIMessage, type UseChatHelpers, useChat } from "@ai-sdk/react";
import { Presence } from "@radix-ui/react-presence";
import { DefaultChatTransport } from "ai";
import Link from "fumadocs-core/link";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { InfoIcon, Loader2, SearchIcon, Send, Trash2, X } from "lucide-react";
import {
type ComponentProps,
createContext,
SVGProps,
type SyntheticEvent,
use,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { RemoveScroll } from "react-remove-scroll";
import type { z } from "zod";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useIsMobile } from "@/hooks/use-mobile";
import type { ProvideLinksToolSchema } from "@/lib/chat/inkeep-qa-schema";
import { cn } from "@/lib/utils";
import { Markdown } from "./markdown";
import { MessageFeedback } from "./message-feedback";
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<UIMessage>;
} | null>(null);
function useChatContext() {
return use(Context)!.chat;
}
// function SearchAIActions() {
// const { messages, status, setMessages, stop } = useChatContext();
// const isGenerating = status === "streaming" || status === "submitted";
// if (messages.length === 0) return null;
// return (
// <>
// <button
// type="button"
// className={cn(
// buttonVariants({
// color: "secondary",
// size: "sm",
// className: "rounded-none",
// }),
// )}
// onClick={isGenerating ? stop : () => setMessages([])}
// >
// {isGenerating ? "Cancel" : "Clear Chat"}
// </button>
// </>
// );
// }
const suggestions = [
"How to configure Sqlite database?",
"How to require email verification?",
"How to change session expiry?",
"How to share cookies across subdomains?",
];
function SearchAIInput(props: ComponentProps<"form"> & { isMobile?: boolean }) {
const { status, sendMessage, stop, messages, setMessages } = useChatContext();
const [input, setInput] = useState("");
const isLoading = status === "streaming" || status === "submitted";
const showSuggestions = messages.length === 0 && !isLoading;
const onStart = (e?: SyntheticEvent) => {
e?.preventDefault();
void sendMessage({ text: input });
setInput("");
};
const handleSuggestionClick = (suggestion: string) => {
void sendMessage({ text: suggestion });
};
const handleClear = () => {
setMessages([]);
setInput("");
};
useEffect(() => {
if (isLoading) document.getElementById("nd-ai-input")?.focus();
}, [isLoading]);
return (
<div
className={cn(
"flex flex-col relative bg-fd-background m-[1px] border border-fd-border rounded-lg shadow-2xl shadow-fd-background",
isLoading ? "opacity-50" : "",
)}
>
<form
{...props}
className={cn("flex items-start pe-2", props.className)}
onSubmit={onStart}
>
<Input
value={input}
placeholder={isLoading ? "answering..." : "Ask BA Bot"}
autoFocus
className={cn("p-4", "sm:text-sm")}
disabled={status === "streaming" || status === "submitted"}
onChange={(e) => {
setInput(e.target.value);
}}
onKeyDown={(event) => {
if (!event.shiftKey && event.key === "Enter") {
onStart(event);
}
}}
/>
{isLoading ? (
<button
key="bn"
type="button"
className={cn(
buttonVariants({
color: "secondary",
className: "transition-all rounded-full mt-2 gap-2",
}),
)}
onClick={stop}
>
<Loader2 className="size-4 animate-spin text-fd-muted-foreground" />
</button>
) : (
<button
key="bn"
type="submit"
className={cn(
buttonVariants({
color: "secondary",
className: "transition-all rounded-full mt-2",
}),
)}
disabled={input.length === 0}
>
<Send className="size-4" />
</button>
)}
</form>
{showSuggestions && (
<div className={cn("mt-3", props.isMobile ? "px-3" : "px-4")}>
<p className="text-xs font-medium text-fd-muted-foreground mb-2">
Try asking:
</p>
<div
className={cn(
"flex gap-2",
props.isMobile
? "overflow-x-auto pb-2 -mx-3 px-3 [mask-image:linear-gradient(to_right,transparent_0%,black_1rem,black_calc(100%-1rem),transparent_100%)] [-webkit-mask-image:linear-gradient(to_right,transparent_0%,black_1rem,black_calc(100%-1rem),transparent_100%)]"
: "flex-wrap",
)}
>
{suggestions.slice(0, 4).map((suggestion, i) => (
<button
key={i}
onClick={() => handleSuggestionClick(suggestion)}
className={cn(
"bg-fd-muted/30 hover:bg-fd-muted/50 text-fd-muted-foreground hover:text-fd-foreground rounded-full border border-fd-border/50 hover:border-fd-border transition-all duration-200 text-left",
props.isMobile
? "text-xs px-2.5 py-1 whitespace-nowrap flex-shrink-0"
: "text-xs px-3 py-1.5",
)}
>
{suggestion}
</button>
))}
</div>
</div>
)}
{showSuggestions && (
<div className="border-t px-4 text-xs text-fd-muted-foreground bg-fd-accent/40 flex items-center gap-1 mt-2 py-2 relative">
<div className="flex items-center gap-1 flex-1">
Powered by{" "}
<Link
href="https://inkeep.com"
target="_blank"
rel="noopener noreferrer"
className="text-fd-primary hover:text-fd-primary/80 hover:underline"
>
Inkeep.
</Link>
<span className="hidden sm:inline">
AI can be inaccurate, please verify the information.
</span>
</div>
<Popover>
<PopoverTrigger asChild>
<button
className="sm:hidden hover:bg-fd-accent/50 rounded transition-colors"
aria-label="Show information"
>
<InfoIcon className="size-3.5" />
</button>
</PopoverTrigger>
<PopoverContent
side="top"
align="end"
className="w-auto max-w-44 p-2 text-xs text-pretty"
>
AI can be inaccurate, please verify the information.
</PopoverContent>
</Popover>
</div>
)}
{!showSuggestions && (
<div className="border-t px-4 text-xs text-fd-muted-foreground cursor-pointer bg-fd-accent/40 flex items-center gap-1 mt-2 py-2">
<div
className="flex items-center gap-1 empty:hidden hover:text-fd-foreground transition-all duration-200 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
role="button"
aria-disabled={isLoading}
tabIndex={0}
onClick={() => {
if (!isLoading) {
handleClear();
}
}}
>
<Trash2 className="size-3" />
<p>Clear</p>
</div>
</div>
)}
</div>
);
}
function List(
props: Omit<ComponentProps<"div">, "dir"> & { messageCount: number },
) {
const containerRef = useRef<HTMLDivElement>(null);
const isUserScrollingRef = useRef(false);
const prevMessageCountRef = useRef(props.messageCount);
// Scroll to bottom when new message is submitted
useEffect(() => {
if (props.messageCount > prevMessageCountRef.current) {
// New message submitted, reset scroll lock and scroll to bottom
isUserScrollingRef.current = false;
if (containerRef.current) {
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior: "smooth",
});
}
}
prevMessageCountRef.current = props.messageCount;
}, [props.messageCount]);
useEffect(() => {
if (!containerRef.current) return;
function callback() {
const container = containerRef.current;
if (!container) return;
// Only auto-scroll if user hasn't manually scrolled up
if (!isUserScrollingRef.current) {
container.scrollTo({
top: container.scrollHeight,
behavior: "instant",
});
}
}
const observer = new ResizeObserver(callback);
callback();
const element = containerRef.current?.firstElementChild;
if (element) {
observer.observe(element);
}
return () => {
observer.disconnect();
};
}, []);
// Track when user manually scrolls
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = container;
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
// If user is near bottom, enable auto-scroll, otherwise disable it
isUserScrollingRef.current = !isNearBottom;
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, []);
return (
<div
ref={containerRef}
{...props}
className={cn(
"fd-scroll-container overflow-y-auto min-w-0 flex flex-col",
props.className,
)}
>
{props.children}
</div>
);
}
function Input(props: ComponentProps<"textarea">) {
const ref = useRef<HTMLDivElement>(null);
const shared = cn("col-start-1 row-start-1", props.className);
return (
<div className="grid flex-1">
<textarea
id="nd-ai-input"
{...props}
className={cn(
"resize-none bg-transparent placeholder:text-fd-muted-foreground focus-visible:outline-none",
shared,
)}
/>
<div ref={ref} className={cn(shared, "break-all invisible")}>
{`${props.value?.toString() ?? ""}\n`}
</div>
</div>
);
}
const roleName: Record<string, string> = {
user: "you",
assistant: "BA bot",
};
function ThinkingIndicator() {
return (
<div className="flex flex-col">
<p className="mb-1 text-sm font-medium text-fd-muted-foreground">
BA bot
</p>
<div className="flex items-end gap-1 text-sm text-fd-muted-foreground">
<div className="flex items-center gap-1 opacity-70">
<span className="inline-block size-1 bg-fd-primary rounded-full animate-bounce [animation-delay:0ms]" />
<span className="inline-block size-1 opacity-80 bg-fd-primary rounded-full animate-bounce [animation-delay:150ms]" />
<span className="inline-block size-1 bg-fd-primary rounded-full animate-bounce [animation-delay:300ms]" />
</div>
</div>
</div>
);
}
function Message({
message,
messages,
messageIndex,
isStreaming,
...props
}: {
message: UIMessage;
messages?: UIMessage[];
messageIndex?: number;
isStreaming?: boolean;
} & ComponentProps<"div">) {
let markdown = "";
let links: z.infer<typeof ProvideLinksToolSchema>["links"] = [];
for (const part of message.parts ?? []) {
if (part.type === "text") {
const textWithCitations = part.text.replace(/\((\d+)\)/g, "");
markdown += textWithCitations;
continue;
}
if (part.type === "tool-provideLinks" && part.input) {
links = (part.input as z.infer<typeof ProvideLinksToolSchema>).links;
}
}
// Fix incomplete code blocks for better rendering during streaming
const codeBlockCount = (markdown.match(/```/g) || []).length;
if (codeBlockCount % 2 !== 0) {
// Odd number of ``` means there's an unclosed code block
markdown += "\n```";
}
// Ensure proper spacing around code blocks
markdown = markdown
.replace(/```(\w+)?\n/g, "\n```$1\n")
.replace(/\n```\n/g, "\n```\n\n");
return (
<div {...props}>
<p
className={cn(
"mb-1 text-sm font-medium text-fd-muted-foreground",
message.role === "assistant" && "text-fd-primary",
)}
>
{roleName[message.role] ?? "unknown"}
</p>
<div className="prose text-sm">
<Markdown text={markdown} />
</div>
{links && links.length > 0 && (
<div className="mt-3 flex flex-col gap-2">
<p className="text-xs font-medium text-fd-muted-foreground">
References:
</p>
<div className="flex flex-col gap-1">
{links.map((item, i) => (
<Link
key={i}
href={item.url}
className="flex items-center gap-2 text-xs rounded-lg border p-2 hover:bg-fd-accent hover:text-fd-accent-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<span className="truncate">{item.title || item.label}</span>
</Link>
))}
</div>
</div>
)}
{message.role === "assistant" && message.id && !isStreaming && (
<MessageFeedback
messageId={message.id}
userMessageId={
messages && messageIndex !== undefined && messageIndex > 0
? messages[messageIndex - 1]?.id
: undefined
}
content={markdown}
className="opacity-100 transition-opacity"
/>
)}
</div>
);
}
const InKeepLogo = (props: SVGProps<any>) => {
return (
<svg
className={props.className}
width="2rem"
height="2rem"
viewBox="0 0 897 175"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.7678 81.0084C10.0534 79.7709 10.0537 78.2461 10.768 77.0086L46.6654 14.8312C47.3799 13.5938 48.7006 12.8314 50.1295 12.8312L121.925 12.8318C123.354 12.8319 124.675 13.594 125.389 14.8316L161.287 77.0078C162.001 78.2454 162.002 79.7709 161.287 81.0085L125.389 143.185C124.675 144.422 123.354 145.185 121.925 145.185L50.1298 145.185C48.7007 145.185 47.3798 144.422 46.6653 143.185L10.7678 81.0084ZM38.005 148.185C40.5059 152.516 45.1281 155.185 50.1297 155.185L121.925 155.185C126.927 155.185 131.549 152.516 134.049 148.185L169.947 86.0084C172.448 81.6768 172.448 76.3394 169.947 72.0078L134.05 9.8316C131.549 5.50004 126.927 2.83196 121.925 2.83189L50.1295 2.83131C45.128 2.83143 40.506 5.49981 38.0052 9.83131L2.10775 72.0086C-0.392861 76.3402 -0.393192 81.6769 2.10758 86.0084L38.005 148.185Z"
fill="currentColor"
/>
<path
d="M97.4628 113.697C80.91 88.4351 86.665 52.1106 111.925 34.1427L111.931 34.1388C122.704 26.4965 136.454 16.3423 151.082 9.16902C165.646 2.02694 181.956 -2.61365 197.791 1.7364L197.792 1.73542C203.599 3.22962 208.168 5.26954 211.8 8.12898C215.476 11.0241 217.894 14.5262 219.731 18.4678C221.515 22.2962 222.814 26.6945 224.158 31.3585C225.529 36.1177 227.006 41.3803 229.174 47.4141C230.371 50.747 232.778 54.4086 235.929 59.3302C238.935 64.0265 242.394 69.5657 244.829 75.9512C247.292 82.4111 248.729 89.7772 247.749 98.2022C246.772 106.606 243.425 115.763 236.765 125.876C232.346 132.586 223.573 139.318 214.181 144.73C204.656 150.217 193.716 154.789 184.313 156.809L184.307 156.81L184.299 156.812C163.919 161.121 147.298 158.432 133.07 150.322C119.023 142.315 107.752 129.273 97.4726 113.712L97.4677 113.704L97.4628 113.697ZM117.6 42.1319C96.8344 56.9074 91.8642 87.2556 105.649 108.312L105.65 108.311C115.598 123.368 125.84 134.921 137.924 141.809C149.824 148.591 163.947 151.096 182.256 147.227L183.026 147.056C191.04 145.216 200.675 141.199 209.288 136.237C218.311 131.039 225.449 125.239 228.58 120.485C234.568 111.392 237.247 103.673 238.015 97.0704C238.78 90.4888 237.683 84.7163 235.672 79.4425C233.633 74.0943 230.679 69.3045 227.676 64.6134C224.817 60.1475 221.616 55.3629 219.951 50.7276C217.657 44.3415 216.092 38.7622 214.741 34.0723C213.362 29.2875 212.257 25.6292 210.849 22.6075C209.493 19.699 207.931 17.5565 205.736 15.8282C203.496 14.0643 200.302 12.5011 195.35 11.2266L195.307 11.2159L195.264 11.2032C182.837 7.76335 169.185 11.2077 155.397 17.9688C141.647 24.7118 128.64 34.3005 117.601 42.1319L117.6 42.1319Z"
fill="currentColor"
/>
<path d="M289 25.2163H302.391V133.916H289V25.2163Z" />
<path
d="M321.323 25.2165H334.715V45.0725C340.533 34.6366 358.634 23 378.398 23C385.601 23 392.713 24.0159 398.254 26.14C403.703 28.2642 408.413 31.4042 412.199 35.5601C415.986 39.716 418.849 44.9802 420.88 51.3525C422.82 57.6326 423.836 64.9285 423.836 73.148V133.917H410.444V74.6256C410.444 67.9762 409.706 62.2503 408.228 57.3555C406.75 52.4608 404.534 48.3972 401.671 45.0725C398.808 41.7478 395.206 39.3466 390.865 37.7766C386.617 36.2066 381.722 35.4677 376.181 35.4677C369.809 35.4677 364.083 36.3913 359.004 38.3307C353.924 40.2701 349.583 42.9484 345.982 46.4578C342.38 49.9672 339.609 54.1232 337.762 58.8332C335.823 63.5432 334.899 68.8997 334.899 74.718V134.009H321.508V25.2165H321.323Z"
fill="currentColor"
/>
<path
d="M542.475 56.6167C545.245 49.6902 549.032 43.6872 553.927 38.6078C558.821 33.6207 564.64 29.7418 571.381 27.0636C578.123 24.3853 585.604 23 593.731 23C601.858 23 609.246 24.3853 615.988 27.1559C622.73 29.9265 628.548 33.8054 633.443 38.7001C638.43 43.6872 642.217 49.5978 644.895 56.6167C647.573 63.6356 648.958 71.3933 648.958 79.8898V84.1381H549.771V72.3168H638.523L635.66 79.7975C635.66 73.148 634.644 67.0527 632.52 61.6038C630.395 56.1549 627.532 51.4449 623.931 47.4737C620.237 43.5025 615.896 40.5472 610.724 38.423C605.552 36.2989 599.919 35.283 593.823 35.283C587.728 35.283 581.91 36.3913 576.738 38.5154C571.566 40.6395 567.133 43.6872 563.439 47.5661C559.745 51.4449 556.79 56.0626 554.665 61.5114C552.541 66.9603 551.525 72.8709 551.525 79.428C551.525 85.9851 552.634 92.0805 554.758 97.5293C556.882 102.978 559.93 107.596 563.716 111.475C567.503 115.354 572.028 118.309 577.2 120.525C582.372 122.65 588.097 123.758 594.378 123.758C604.167 123.758 612.479 121.264 619.405 116.369C626.332 111.475 631.227 104.548 634.274 95.7746L647.296 100.208C643.51 111.29 636.953 120.064 627.81 126.528C618.574 132.993 607.584 136.226 594.655 136.226C586.435 136.226 578.77 134.84 571.936 132.162C565.009 129.484 559.098 125.605 554.111 120.618C549.124 115.631 545.245 109.628 542.475 102.701C539.704 95.7746 538.227 88.1093 538.227 79.5204C538.227 70.9315 539.612 63.6356 542.382 56.6167H542.475Z"
fill="currentColor"
/>
<path
d="M664.383 56.6167C667.154 49.6902 670.94 43.6872 675.835 38.6078C680.73 33.5283 686.548 29.7418 693.29 27.0636C700.032 24.3853 707.512 23 715.639 23C723.766 23 731.155 24.3853 737.896 27.1559C744.638 29.9265 750.457 33.8054 755.351 38.7001C760.338 43.6872 764.125 49.5978 766.803 56.6167C769.481 63.6356 770.867 71.3933 770.867 79.8898V84.1381H671.679V72.3168H760.431L757.568 79.7975C757.568 73.148 756.552 67.0527 754.428 61.6038C752.304 56.1549 749.441 51.4449 745.747 47.4737C742.052 43.5025 737.712 40.5472 732.54 38.423C727.368 36.2989 721.735 35.283 715.639 35.283C709.544 35.283 703.726 36.3913 698.554 38.5154C693.382 40.6395 688.949 43.6872 685.255 47.5661C681.561 51.4449 678.605 56.0626 676.481 61.5114C674.357 66.9603 673.341 72.8709 673.341 79.428C673.341 85.9851 674.45 92.0805 676.574 97.5293C678.698 102.978 681.746 107.596 685.532 111.475C689.318 115.354 693.844 118.309 699.016 120.525C704.187 122.65 709.913 123.758 716.193 123.758C725.983 123.758 734.295 121.264 741.221 116.369C748.148 111.475 753.042 104.548 756.09 95.7746L769.112 100.208C765.325 111.29 758.861 120.064 749.625 126.528C740.39 132.993 729.4 136.226 716.47 136.226C708.251 136.226 700.586 134.84 693.751 132.162C686.825 129.484 680.914 125.605 676.02 120.618C671.032 115.631 667.154 109.628 664.383 102.701C661.52 95.7746 660.135 88.1093 660.135 79.5204C660.135 70.9315 661.52 63.6356 664.291 56.6167H664.383Z"
fill="currentColor"
/>
<path
d="M785.371 25.2165H798.762V49.7825C803.288 41.7478 808.829 36.1142 816.864 30.8501C824.806 25.6783 834.226 23 845.124 23C852.882 23 859.808 24.293 866.18 26.9712C872.461 29.6495 877.817 33.3436 882.25 38.146C886.683 42.9484 890.008 48.859 892.409 55.8779C894.81 62.8967 896.011 70.8392 896.011 79.6127C896.011 88.3863 894.81 96.3287 892.409 103.255C890.008 110.274 886.683 116.185 882.25 120.987C877.817 125.882 872.553 129.576 866.273 132.162C860.085 134.748 853.066 136.041 845.309 136.041C834.503 136.041 824.991 133.363 816.956 128.098C808.921 122.834 803.288 116.185 798.762 109.258V174.368H785.371V25.2165ZM801.995 97.7141C804.119 103.163 807.167 107.873 811.045 111.752C814.924 115.631 819.542 118.586 824.991 120.71C830.44 122.834 836.258 123.85 842.446 123.85C848.633 123.85 854.082 122.834 859.069 120.895C864.056 118.955 868.305 116.092 871.814 112.214C875.324 108.335 877.909 103.809 879.849 98.3605C881.788 92.9117 882.712 86.8163 882.712 79.8898C882.712 72.9633 881.696 66.5909 879.756 61.0497C877.817 55.6008 875.046 50.8908 871.629 47.0119C868.212 43.1331 863.964 40.2701 858.885 38.2383C853.897 36.2989 848.356 35.283 842.261 35.283C836.166 35.283 830.07 36.2989 824.806 38.423C819.542 40.5472 814.924 43.5025 811.045 47.3813C807.167 51.2602 804.211 55.8779 801.995 61.3267C799.778 66.7756 798.762 72.8709 798.762 79.6127C798.762 86.3546 799.871 92.0805 801.995 97.6217V97.7141Z"
fill="currentColor"
/>
<path
d="M541.309 23H521.73L453.204 83.6763V23H440.274V133.917H453.204V100.669L453.389 100.854L476.754 80.1669L528.195 133.917H546.666L486.451 71.578L541.309 23Z"
fill="currentColor"
/>
</svg>
);
};
export function AISearchTrigger() {
const [open, setOpen] = useState(false);
const isMobile = useIsMobile();
const chat = useChat({
id: "search",
transport: new DefaultChatTransport({
api: "/api/chat",
}),
});
const showSuggestions =
chat.messages.length === 0 && chat.status !== "streaming";
const onKeyPress = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
setOpen(false);
e.preventDefault();
}
if (e.key === "/" && (e.metaKey || e.ctrlKey) && !open) {
setOpen(true);
e.preventDefault();
}
};
const onKeyPressRef = useRef(onKeyPress);
onKeyPressRef.current = onKeyPress;
useEffect(() => {
const listener = (e: KeyboardEvent) => onKeyPressRef.current(e);
window.addEventListener("keydown", listener);
return () => window.removeEventListener("keydown", listener);
}, []);
return (
<Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>
<RemoveScroll enabled={open}>
<Presence present={open}>
<div
className={cn(
"fixed inset-0 flex flex-col items-center bg-fd-background/80 backdrop-blur-sm z-30",
isMobile
? "p-4 pb-40"
: "p-2 right-(--removed-body-scroll-bar-size,0) pb-[8.375rem]",
open ? "animate-fd-fade-in" : "animate-fd-fade-out",
)}
onClick={(e) => {
if (e.target === e.currentTarget) {
setOpen(false);
e.preventDefault();
}
}}
>
<div
className={cn(
"sticky top-0 flex gap-2 items-center py-2",
isMobile ? "w-full" : "w-[min(800px,90vw)]",
)}
>
<div className="flex justify-end w-full items-center">
<button
aria-label="Close"
tabIndex={-1}
className={cn(
buttonVariants({
size: isMobile ? "icon" : "icon-sm",
color: "secondary",
className: "rounded-full",
}),
)}
onClick={() => setOpen(false)}
>
<X />
</button>
</div>
</div>
<List
messageCount={chat.messages.length}
className={cn(
"overscroll-contain",
isMobile
? "pt-6 pb-28 px-2 w-full"
: "py-10 pr-2 w-[min(800px,90vw)]",
)}
style={{
maskImage: isMobile
? "linear-gradient(to bottom, transparent, white 2rem, white calc(100% - 12rem), transparent 100%)"
: "linear-gradient(to bottom, transparent, white 4rem, white calc(100% - 2rem), transparent 100%)",
}}
>
<div className="flex flex-col gap-4">
{chat.messages
.filter((msg: UIMessage) => msg.role !== "system")
.map((item: UIMessage, index: number) => {
const filteredMessages = chat.messages.filter(
(msg: UIMessage) => msg.role !== "system",
);
const isLastMessage = index === filteredMessages.length - 1;
const isCurrentlyStreaming =
(chat.status === "streaming" ||
chat.status === "submitted") &&
item.role === "assistant" &&
isLastMessage;
return (
<Message
key={item.id}
message={item}
messages={filteredMessages}
messageIndex={index}
isStreaming={isCurrentlyStreaming}
/>
);
})}
{chat.status === "submitted" && <ThinkingIndicator />}
</div>
</List>
</div>
</Presence>
<div
className={cn(
"fixed bg-transparent transition-[width,height] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] -translate-x-1/2 shadow-xl z-30",
isMobile ? "bottom-4" : "bottom-4",
open
? isMobile
? `w-[calc(100vw-2rem)] bg-fd-accent/30 overflow-visible h-auto`
: `w-[min(800px,90vw)] bg-fd-accent/30 overflow-visible h-auto`
: isMobile
? "w-32 h-12 bg-fd-secondary text-fd-secondary-foreground shadow-fd-background rounded-2xl overflow-hidden"
: "w-40 h-10 bg-fd-secondary text-fd-secondary-foreground shadow-fd-background rounded-2xl overflow-hidden",
)}
style={{
left: "calc(50% - var(--removed-body-scroll-bar-size,0px)/2)",
}}
>
{!open && (
<button
className={cn(
"absolute inset-0 text-center transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground rounded-2xl",
isMobile ? "p-3 text-xs" : "p-2 text-sm",
"text-fd-muted-foreground",
)}
onClick={() => setOpen(true)}
>
<SearchIcon
className={cn(
"absolute top-1/2 -translate-y-1/2",
isMobile ? "left-2 size-4" : "size-4.5",
)}
/>
<span className={cn(isMobile ? "ml-6" : "")}>Ask AI</span>
</button>
)}
{open && (
<div className="flex flex-col">
<SearchAIInput isMobile={isMobile} />
</div>
)}
</div>
</RemoveScroll>
</Context>
);
}
```
--------------------------------------------------------------------------------
/packages/better-auth/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "better-auth",
"version": "1.4.0-beta.13",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/better-auth/better-auth",
"directory": "packages/better-auth"
},
"keywords": [
"auth",
"oauth",
"oidc",
"2fa",
"social",
"security",
"typescript",
"nextjs"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsdown",
"test": "vitest",
"prepare": "prisma generate --schema ./src/adapters/prisma-adapter/test/base.prisma",
"typecheck": "tsc --project tsconfig.json"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./social-providers": {
"better-auth-dev-source": "./src/social-providers/index.ts",
"import": {
"types": "./dist/social-providers/index.d.ts",
"default": "./dist/social-providers/index.js"
},
"require": {
"types": "./dist/social-providers/index.d.cts",
"default": "./dist/social-providers/index.cjs"
}
},
"./client": {
"better-auth-dev-source": "./src/client/index.ts",
"import": {
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.js"
},
"require": {
"types": "./dist/client/index.d.cts",
"default": "./dist/client/index.cjs"
}
},
"./client/plugins": {
"better-auth-dev-source": "./src/client/plugins/index.ts",
"import": {
"types": "./dist/client/plugins/index.d.ts",
"default": "./dist/client/plugins/index.js"
},
"require": {
"types": "./dist/client/plugins/index.d.cts",
"default": "./dist/client/plugins/index.cjs"
}
},
"./types": {
"better-auth-dev-source": "./src/types/index.ts",
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"require": {
"types": "./dist/types/index.d.cts",
"default": "./dist/types/index.cjs"
}
},
"./crypto": {
"better-auth-dev-source": "./src/crypto/index.ts",
"import": {
"types": "./dist/crypto/index.d.ts",
"default": "./dist/crypto/index.js"
},
"require": {
"types": "./dist/crypto/index.d.cts",
"default": "./dist/crypto/index.cjs"
}
},
"./cookies": {
"better-auth-dev-source": "./src/cookies/index.ts",
"import": {
"types": "./dist/cookies/index.d.ts",
"default": "./dist/cookies/index.js"
},
"require": {
"types": "./dist/cookies/index.d.cts",
"default": "./dist/cookies/index.cjs"
}
},
"./oauth2": {
"better-auth-dev-source": "./src/oauth2/index.ts",
"import": {
"types": "./dist/oauth2/index.d.ts",
"default": "./dist/oauth2/index.js"
},
"require": {
"types": "./dist/oauth2/index.d.cts",
"default": "./dist/oauth2/index.cjs"
}
},
"./react": {
"better-auth-dev-source": "./src/client/react/index.ts",
"import": {
"types": "./dist/client/react/index.d.ts",
"default": "./dist/client/react/index.js"
},
"require": {
"types": "./dist/client/react/index.d.cts",
"default": "./dist/client/react/index.cjs"
}
},
"./solid": {
"better-auth-dev-source": "./src/client/solid/index.ts",
"import": {
"types": "./dist/client/solid/index.d.ts",
"default": "./dist/client/solid/index.js"
},
"require": {
"types": "./dist/client/solid/index.d.cts",
"default": "./dist/client/solid/index.cjs"
}
},
"./lynx": {
"better-auth-dev-source": "./src/client/lynx/index.ts",
"import": {
"types": "./dist/client/lynx/index.d.ts",
"default": "./dist/client/lynx/index.js"
},
"require": {
"types": "./dist/client/lynx/index.d.cts",
"default": "./dist/client/lynx/index.cjs"
}
},
"./test": {
"better-auth-dev-source": "./src/test-utils/index.ts",
"import": {
"types": "./dist/test-utils/index.d.ts",
"default": "./dist/test-utils/index.js"
},
"require": {
"types": "./dist/test-utils/index.d.cts",
"default": "./dist/test-utils/index.cjs"
}
},
"./api": {
"better-auth-dev-source": "./src/api/index.ts",
"import": {
"types": "./dist/api/index.d.ts",
"default": "./dist/api/index.js"
},
"require": {
"types": "./dist/api/index.d.cts",
"default": "./dist/api/index.cjs"
}
},
"./db": {
"better-auth-dev-source": "./src/db/index.ts",
"import": {
"types": "./dist/db/index.d.ts",
"default": "./dist/db/index.js"
},
"require": {
"types": "./dist/db/index.d.cts",
"default": "./dist/db/index.cjs"
}
},
"./vue": {
"better-auth-dev-source": "./src/client/vue/index.ts",
"import": {
"types": "./dist/client/vue/index.d.ts",
"default": "./dist/client/vue/index.js"
},
"require": {
"types": "./dist/client/vue/index.d.cts",
"default": "./dist/client/vue/index.cjs"
}
},
"./plugins": {
"better-auth-dev-source": "./src/plugins/index.ts",
"import": {
"types": "./dist/plugins/index.d.ts",
"default": "./dist/plugins/index.js"
},
"require": {
"types": "./dist/plugins/index.d.cts",
"default": "./dist/plugins/index.cjs"
}
},
"./svelte-kit": {
"better-auth-dev-source": "./src/integrations/svelte-kit.ts",
"import": {
"types": "./dist/integrations/svelte-kit.d.ts",
"default": "./dist/integrations/svelte-kit.js"
},
"require": {
"types": "./dist/integrations/svelte-kit.d.cts",
"default": "./dist/integrations/svelte-kit.cjs"
}
},
"./solid-start": {
"better-auth-dev-source": "./src/integrations/solid-start.ts",
"import": {
"types": "./dist/integrations/solid-start.d.ts",
"default": "./dist/integrations/solid-start.js"
},
"require": {
"types": "./dist/integrations/solid-start.d.cts",
"default": "./dist/integrations/solid-start.cjs"
}
},
"./svelte": {
"better-auth-dev-source": "./src/client/svelte/index.ts",
"import": {
"types": "./dist/client/svelte/index.d.ts",
"default": "./dist/client/svelte/index.js"
},
"require": {
"types": "./dist/client/svelte/index.d.cts",
"default": "./dist/client/svelte/index.cjs"
}
},
"./next-js": {
"better-auth-dev-source": "./src/integrations/next-js.ts",
"import": {
"types": "./dist/integrations/next-js.d.ts",
"default": "./dist/integrations/next-js.js"
},
"require": {
"types": "./dist/integrations/next-js.d.cts",
"default": "./dist/integrations/next-js.cjs"
}
},
"./react-start": {
"better-auth-dev-source": "./src/integrations/react-start.ts",
"import": {
"types": "./dist/integrations/react-start.d.ts",
"default": "./dist/integrations/react-start.js"
},
"require": {
"types": "./dist/integrations/react-start.d.cts",
"default": "./dist/integrations/react-start.cjs"
}
},
"./node": {
"better-auth-dev-source": "./src/integrations/node.ts",
"import": {
"types": "./dist/integrations/node.d.ts",
"default": "./dist/integrations/node.js"
},
"require": {
"types": "./dist/integrations/node.d.cts",
"default": "./dist/integrations/node.cjs"
}
},
"./adapters/prisma": {
"better-auth-dev-source": "./src/adapters/prisma-adapter/index.ts",
"import": {
"types": "./dist/adapters/prisma-adapter/index.d.ts",
"default": "./dist/adapters/prisma-adapter/index.js"
},
"require": {
"types": "./dist/adapters/prisma-adapter/index.d.cts",
"default": "./dist/adapters/prisma-adapter/index.cjs"
}
},
"./adapters/drizzle": {
"better-auth-dev-source": "./src/adapters/drizzle-adapter/index.ts",
"import": {
"types": "./dist/adapters/drizzle-adapter/index.d.ts",
"default": "./dist/adapters/drizzle-adapter/index.js"
},
"require": {
"types": "./dist/adapters/drizzle-adapter/index.d.cts",
"default": "./dist/adapters/drizzle-adapter/index.cjs"
}
},
"./adapters/mongodb": {
"better-auth-dev-source": "./src/adapters/mongodb-adapter/index.ts",
"import": {
"types": "./dist/adapters/mongodb-adapter/index.d.ts",
"default": "./dist/adapters/mongodb-adapter/index.js"
},
"require": {
"types": "./dist/adapters/mongodb-adapter/index.d.cts",
"default": "./dist/adapters/mongodb-adapter/index.cjs"
}
},
"./adapters/memory": {
"better-auth-dev-source": "./src/adapters/memory-adapter/index.ts",
"import": {
"types": "./dist/adapters/memory-adapter/index.d.ts",
"default": "./dist/adapters/memory-adapter/index.js"
},
"require": {
"types": "./dist/adapters/memory-adapter/index.d.cts",
"default": "./dist/adapters/memory-adapter/index.cjs"
}
},
"./adapters/test": {
"better-auth-dev-source": "./src/adapters/test.ts",
"import": {
"types": "./dist/adapters/test.d.ts",
"default": "./dist/adapters/test.js"
},
"require": {
"types": "./dist/adapters/test.d.cts",
"default": "./dist/adapters/test.cjs"
}
},
"./adapters": {
"better-auth-dev-source": "./src/adapters/index.ts",
"import": {
"types": "./dist/adapters/index.d.ts",
"default": "./dist/adapters/index.js"
},
"require": {
"types": "./dist/adapters/index.d.cts",
"default": "./dist/adapters/index.cjs"
}
},
"./plugins/access": {
"better-auth-dev-source": "./src/plugins/access/index.ts",
"import": {
"types": "./dist/plugins/access/index.d.ts",
"default": "./dist/plugins/access/index.js"
},
"require": {
"types": "./dist/plugins/access/index.d.cts",
"default": "./dist/plugins/access/index.cjs"
}
},
"./plugins/admin": {
"better-auth-dev-source": "./src/plugins/admin/index.ts",
"import": {
"types": "./dist/plugins/admin/index.d.ts",
"default": "./dist/plugins/admin/index.js"
},
"require": {
"types": "./dist/plugins/admin/index.d.cts",
"default": "./dist/plugins/admin/index.cjs"
}
},
"./plugins/admin/access": {
"better-auth-dev-source": "./src/plugins/admin/access/index.ts",
"import": {
"types": "./dist/plugins/admin/access/index.d.ts",
"default": "./dist/plugins/admin/access/index.js"
},
"require": {
"types": "./dist/plugins/admin/access/index.d.cts",
"default": "./dist/plugins/admin/access/index.cjs"
}
},
"./plugins/anonymous": {
"better-auth-dev-source": "./src/plugins/anonymous/index.ts",
"import": {
"types": "./dist/plugins/anonymous/index.d.ts",
"default": "./dist/plugins/anonymous/index.js"
},
"require": {
"types": "./dist/plugins/anonymous/index.d.cts",
"default": "./dist/plugins/anonymous/index.cjs"
}
},
"./plugins/bearer": {
"better-auth-dev-source": "./src/plugins/bearer/index.ts",
"import": {
"types": "./dist/plugins/bearer/index.d.ts",
"default": "./dist/plugins/bearer/index.js"
},
"require": {
"types": "./dist/plugins/bearer/index.d.cts",
"default": "./dist/plugins/bearer/index.cjs"
}
},
"./plugins/custom-session": {
"better-auth-dev-source": "./src/plugins/custom-session/index.ts",
"import": {
"types": "./dist/plugins/custom-session/index.d.ts",
"default": "./dist/plugins/custom-session/index.js"
},
"require": {
"types": "./dist/plugins/custom-session/index.d.cts",
"default": "./dist/plugins/custom-session/index.cjs"
}
},
"./plugins/email-otp": {
"better-auth-dev-source": "./src/plugins/email-otp/index.ts",
"import": {
"types": "./dist/plugins/email-otp/index.d.ts",
"default": "./dist/plugins/email-otp/index.js"
},
"require": {
"types": "./dist/plugins/email-otp/index.d.cts",
"default": "./dist/plugins/email-otp/index.cjs"
}
},
"./plugins/generic-oauth": {
"better-auth-dev-source": "./src/plugins/generic-oauth/index.ts",
"import": {
"types": "./dist/plugins/generic-oauth/index.d.ts",
"default": "./dist/plugins/generic-oauth/index.js"
},
"require": {
"types": "./dist/plugins/generic-oauth/index.d.cts",
"default": "./dist/plugins/generic-oauth/index.cjs"
}
},
"./plugins/jwt": {
"better-auth-dev-source": "./src/plugins/jwt/index.ts",
"import": {
"types": "./dist/plugins/jwt/index.d.ts",
"default": "./dist/plugins/jwt/index.js"
},
"require": {
"types": "./dist/plugins/jwt/index.d.cts",
"default": "./dist/plugins/jwt/index.cjs"
}
},
"./plugins/haveibeenpwned": {
"better-auth-dev-source": "./src/plugins/haveibeenpwned/index.ts",
"import": {
"types": "./dist/plugins/haveibeenpwned/index.d.ts",
"default": "./dist/plugins/haveibeenpwned/index.js"
},
"require": {
"types": "./dist/plugins/haveibeenpwned/index.d.cts",
"default": "./dist/plugins/haveibeenpwned/index.cjs"
}
},
"./plugins/oidc-provider": {
"better-auth-dev-source": "./src/plugins/oidc-provider/index.ts",
"import": {
"types": "./dist/plugins/oidc-provider/index.d.ts",
"default": "./dist/plugins/oidc-provider/index.js"
},
"require": {
"types": "./dist/plugins/oidc-provider/index.d.cts",
"default": "./dist/plugins/oidc-provider/index.cjs"
}
},
"./plugins/magic-link": {
"better-auth-dev-source": "./src/plugins/magic-link/index.ts",
"import": {
"types": "./dist/plugins/magic-link/index.d.ts",
"default": "./dist/plugins/magic-link/index.js"
},
"require": {
"types": "./dist/plugins/magic-link/index.d.cts",
"default": "./dist/plugins/magic-link/index.cjs"
}
},
"./plugins/multi-session": {
"better-auth-dev-source": "./src/plugins/multi-session/index.ts",
"import": {
"types": "./dist/plugins/multi-session/index.d.ts",
"default": "./dist/plugins/multi-session/index.js"
},
"require": {
"types": "./dist/plugins/multi-session/index.d.cts",
"default": "./dist/plugins/multi-session/index.cjs"
}
},
"./plugins/oauth-proxy": {
"better-auth-dev-source": "./src/plugins/oauth-proxy/index.ts",
"import": {
"types": "./dist/plugins/oauth-proxy/index.d.ts",
"default": "./dist/plugins/oauth-proxy/index.js"
},
"require": {
"types": "./dist/plugins/oauth-proxy/index.d.cts",
"default": "./dist/plugins/oauth-proxy/index.cjs"
}
},
"./plugins/organization": {
"better-auth-dev-source": "./src/plugins/organization/index.ts",
"import": {
"types": "./dist/plugins/organization/index.d.ts",
"default": "./dist/plugins/organization/index.js"
},
"require": {
"types": "./dist/plugins/organization/index.d.cts",
"default": "./dist/plugins/organization/index.cjs"
}
},
"./plugins/organization/access": {
"better-auth-dev-source": "./src/plugins/organization/access/index.ts",
"import": {
"types": "./dist/plugins/organization/access/index.d.ts",
"default": "./dist/plugins/organization/access/index.js"
},
"require": {
"types": "./dist/plugins/organization/access/index.d.cts",
"default": "./dist/plugins/organization/access/index.cjs"
}
},
"./plugins/one-time-token": {
"better-auth-dev-source": "./src/plugins/one-time-token/index.ts",
"import": {
"types": "./dist/plugins/one-time-token/index.d.ts",
"default": "./dist/plugins/one-time-token/index.js"
},
"require": {
"types": "./dist/plugins/one-time-token/index.d.cts",
"default": "./dist/plugins/one-time-token/index.cjs"
}
},
"./plugins/passkey": {
"better-auth-dev-source": "./src/plugins/passkey/index.ts",
"import": {
"types": "./dist/plugins/passkey/index.d.ts",
"default": "./dist/plugins/passkey/index.js"
},
"require": {
"types": "./dist/plugins/passkey/index.d.cts",
"default": "./dist/plugins/passkey/index.cjs"
}
},
"./plugins/phone-number": {
"better-auth-dev-source": "./src/plugins/phone-number/index.ts",
"import": {
"types": "./dist/plugins/phone-number/index.d.ts",
"default": "./dist/plugins/phone-number/index.js"
},
"require": {
"types": "./dist/plugins/phone-number/index.d.cts",
"default": "./dist/plugins/phone-number/index.cjs"
}
},
"./plugins/two-factor": {
"better-auth-dev-source": "./src/plugins/two-factor/index.ts",
"import": {
"types": "./dist/plugins/two-factor/index.d.ts",
"default": "./dist/plugins/two-factor/index.js"
},
"require": {
"types": "./dist/plugins/two-factor/index.d.cts",
"default": "./dist/plugins/two-factor/index.cjs"
}
},
"./plugins/username": {
"better-auth-dev-source": "./src/plugins/username/index.ts",
"import": {
"types": "./dist/plugins/username/index.d.ts",
"default": "./dist/plugins/username/index.js"
},
"require": {
"types": "./dist/plugins/username/index.d.cts",
"default": "./dist/plugins/username/index.cjs"
}
},
"./plugins/siwe": {
"better-auth-dev-source": "./src/plugins/siwe/index.ts",
"import": {
"types": "./dist/plugins/siwe/index.d.ts",
"default": "./dist/plugins/siwe/index.js"
},
"require": {
"types": "./dist/plugins/siwe/index.d.cts",
"default": "./dist/plugins/siwe/index.cjs"
}
},
"./plugins/device-authorization": {
"better-auth-dev-source": "./src/plugins/device-authorization/index.ts",
"import": {
"types": "./dist/plugins/device-authorization/index.d.ts",
"default": "./dist/plugins/device-authorization/index.js"
},
"require": {
"types": "./dist/plugins/device-authorization/index.d.cts",
"default": "./dist/plugins/device-authorization/index.cjs"
}
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
],
"node": [
"./dist/integrations/node.d.ts"
],
"react": [
"./dist/client/react/index.d.ts"
],
"vue": [
"./dist/client/vue/index.d.ts"
],
"svelte": [
"./dist/client/svelte/index.d.ts"
],
"social-providers": [
"./dist/social-providers/index.d.ts"
],
"client": [
"./dist/client/index.d.ts"
],
"client/plugins": [
"./dist/client/plugins/index.d.ts"
],
"types": [
"./dist/types/index.d.ts"
],
"crypto": [
"./dist/crypto/index.d.ts"
],
"cookies": [
"./dist/cookies/index.d.ts"
],
"oauth2": [
"./dist/oauth2/index.d.ts"
],
"solid": [
"./dist/client/solid/index.d.ts"
],
"lynx": [
"./dist/client/lynx/index.d.ts"
],
"api": [
"./dist/api/index.d.ts"
],
"db": [
"./dist/db/index.d.ts"
],
"svelte-kit": [
"./dist/integrations/svelte-kit.d.ts"
],
"solid-start": [
"./dist/integrations/solid-start.d.ts"
],
"next-js": [
"./dist/integrations/next-js.d.ts"
],
"react-start": [
"./dist/integrations/react-start.d.ts"
],
"adapters": [
"./dist/adapters/index.d.ts"
],
"adapters/prisma": [
"./dist/adapters/prisma-adapter/index.d.ts"
],
"adapters/drizzle": [
"./dist/adapters/drizzle-adapter/index.d.ts"
],
"adapters/mongodb": [
"./dist/adapters/mongodb-adapter/index.d.ts"
],
"adapters/memory": [
"./dist/adapters/memory-adapter/index.d.ts"
],
"plugins": [
"./dist/plugins/index.d.ts"
],
"plugins/access": [
"./dist/plugins/access/index.d.ts"
],
"plugins/admin": [
"./dist/plugins/admin/index.d.ts"
],
"plugins/admin/access": [
"./dist/plugins/admin/access/index.d.ts"
],
"plugins/anonymous": [
"./dist/plugins/anonymous/index.d.ts"
],
"plugins/bearer": [
"./dist/plugins/bearer/index.d.ts"
],
"plugins/custom-session": [
"./dist/plugins/custom-session/index.d.ts"
],
"plugins/email-otp": [
"./dist/plugins/email-otp/index.d.ts"
],
"plugins/generic-oauth": [
"./dist/plugins/generic-oauth/index.d.ts"
],
"plugins/haveibeenpwned": [
"./dist/plugins/haveibeenpwned/index.d.ts"
],
"plugins/oauth-proxy": [
"./dist/plugins/oauth-proxy/index.d.ts"
],
"plugins/one-time-token": [
"./dist/plugins/one-time-token/index.d.ts"
],
"plugins/oidc-provider": [
"./dist/plugins/oidc-provider/index.d.ts"
],
"plugins/jwt": [
"./dist/plugins/jwt/index.d.ts"
],
"plugins/magic-link": [
"./dist/plugins/magic-link/index.d.ts"
],
"plugins/organization": [
"./dist/plugins/organization/index.d.ts"
],
"plugins/organization/access": [
"./dist/plugins/organization/access/index.d.ts"
],
"plugins/passkey": [
"./dist/plugins/passkey/index.d.ts"
],
"plugins/phone-number": [
"./dist/plugins/phone-number/index.d.ts"
],
"plugins/two-factor": [
"./dist/plugins/two-factor/index.d.ts"
],
"plugins/username": [
"./dist/plugins/username/index.d.ts"
],
"plugins/siwe": [
"./dist/plugins/siwe/index.d.ts"
],
"plugins/device-authorization": [
"./dist/plugins/device-authorization/index.d.ts"
]
}
},
"dependencies": {
"@better-auth/core": "workspace:*",
"@better-auth/telemetry": "workspace:*",
"@better-auth/utils": "0.3.0",
"@better-fetch/fetch": "catalog:",
"@noble/ciphers": "^2.0.0",
"@noble/hashes": "^2.0.0",
"@simplewebauthn/browser": "^13.1.2",
"@simplewebauthn/server": "^13.1.2",
"better-call": "catalog:",
"defu": "^6.1.4",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1",
"zod": "^4.1.5"
},
"peerDependenciesOptional": {
"@lynx-js/react": "*",
"@sveltejs/kit": "^2.0.0",
"next": "^14.0.0 || ^15.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"next": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
},
"devDependencies": {
"@lynx-js/react": "^0.114.0",
"@prisma/client": "^5.22.0",
"@sveltejs/kit": "^2.37.1",
"@tanstack/react-start": "^1.131.3",
"@tanstack/start-server-core": "^1.131.36",
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.3.0",
"@types/keccak": "^3.0.5",
"@types/pg": "^8.15.5",
"@types/prompts": "^2.4.9",
"@types/react": "^19.2.2",
"better-sqlite3": "^12.2.0",
"concurrently": "^9.2.1",
"deepmerge": "^4.3.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.38.2",
"happy-dom": "^20.0.0",
"hono": "^4.9.7",
"listhen": "^1.9.0",
"mongodb": "^6.18.0",
"ms": "4.0.0-nightly.202508271359",
"msw": "^2.11.5",
"mysql2": "^3.14.4",
"next": "^15.5.4",
"oauth2-mock-server": "^7.2.1",
"pg": "^8.16.3",
"prisma": "^5.22.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-native": "~0.80.2",
"solid-js": "^1.9.8",
"tarn": "^3.0.2",
"tedious": "^18.6.1",
"tsdown": "catalog:",
"type-fest": "^4.41.0",
"typescript": "catalog:",
"vue": "^3.5.18"
},
"files": [
"dist"
]
}
```
--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/routes/crud-members.ts:
--------------------------------------------------------------------------------
```typescript
import { createAuthEndpoint } from "@better-auth/core/api";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
import { APIError } from "better-call";
import * as z from "zod";
import { getSessionFromCtx, sessionMiddleware } from "../../../api";
import type { InferAdditionalFieldsFromPluginOptions } from "../../../db";
import { toZodSchema } from "../../../db/to-zod";
import type { LiteralString } from "../../../types/helper";
import { getOrgAdapter } from "../adapter";
import { orgMiddleware, orgSessionMiddleware } from "../call";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { hasPermission } from "../has-permission";
import { parseRoles } from "../organization";
import type { InferOrganizationRolesFromOption, Member } from "../schema";
import type { OrganizationOptions } from "../types";
export const addMember = <O extends OrganizationOptions>(option: O) => {
const additionalFieldsSchema = toZodSchema({
fields: option?.schema?.member?.additionalFields || {},
isClientSide: true,
});
const baseSchema = z.object({
userId: z.coerce.string().meta({
description:
'The user Id which represents the user to be added as a member. If `null` is provided, then it\'s expected to provide session headers. Eg: "user-id"',
}),
role: z.union([z.string(), z.array(z.string())]).meta({
description:
'The role(s) to assign to the new member. Eg: ["admin", "sale"]',
}),
organizationId: z
.string()
.meta({
description:
'An optional organization ID to pass. If not provided, will default to the user\'s active organization. Eg: "org-id"',
})
.optional(),
teamId: z
.string()
.meta({
description: 'An optional team ID to add the member to. Eg: "team-id"',
})
.optional(),
});
return createAuthEndpoint(
"/organization/add-member",
{
method: "POST",
body: z.object({
...baseSchema.shape,
...additionalFieldsSchema.shape,
}),
use: [orgMiddleware],
metadata: {
SERVER_ONLY: true,
$Infer: {
body: {} as {
userId: string;
role:
| InferOrganizationRolesFromOption<O>
| InferOrganizationRolesFromOption<O>[];
organizationId?: string | undefined;
} & (O extends { teams: { enabled: true } }
? { teamId?: string | undefined }
: {}) &
InferAdditionalFieldsFromPluginOptions<"member", O>,
},
},
},
async (ctx) => {
const session = ctx.body.userId
? await getSessionFromCtx<{
session: {
activeOrganizationId?: string;
};
}>(ctx).catch((e) => null)
: null;
const orgId =
ctx.body.organizationId || session?.session.activeOrganizationId;
if (!orgId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
const teamId =
"teamId" in ctx.body ? (ctx.body.teamId as string) : undefined;
if (teamId && !ctx.context.orgOptions.teams?.enabled) {
ctx.context.logger.error("Teams are not enabled");
throw new APIError("BAD_REQUEST", {
message: "Teams are not enabled",
});
}
const adapter = getOrgAdapter<O>(ctx.context, option);
const user = await ctx.context.internalAdapter.findUserById(
ctx.body.userId,
);
if (!user) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.USER_NOT_FOUND,
});
}
const alreadyMember = await adapter.findMemberByEmail({
email: user.email,
organizationId: orgId,
});
if (alreadyMember) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
});
}
if (teamId) {
const team = await adapter.findTeamById({
teamId,
organizationId: orgId,
});
if (!team || team.organizationId !== orgId) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
});
}
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const count = await adapter.countMembers({ organizationId: orgId });
if (count >= membershipLimit) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
});
}
const {
role: _,
userId: __,
organizationId: ___,
...additionalFields
} = ctx.body;
const organization = await adapter.findOrganizationById(orgId);
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
let memberData = {
organizationId: orgId,
userId: user.id,
role: parseRoles(ctx.body.role as string | string[]),
createdAt: new Date(),
...(additionalFields ? additionalFields : {}),
};
// Run beforeAddMember hook
if (option?.organizationHooks?.beforeAddMember) {
const response = await option?.organizationHooks.beforeAddMember({
member: {
userId: user.id,
organizationId: orgId,
role: parseRoles(ctx.body.role as string | string[]),
...additionalFields,
},
user,
organization,
});
if (response && typeof response === "object" && "data" in response) {
memberData = {
...memberData,
...response.data,
};
}
}
const createdMember = await adapter.createMember(memberData);
if (teamId) {
await adapter.findOrCreateTeamMember({
userId: user.id,
teamId,
});
}
// Run afterAddMember hook
if (option?.organizationHooks?.afterAddMember) {
await option?.organizationHooks.afterAddMember({
member: createdMember,
user,
organization,
});
}
return ctx.json(createdMember);
},
);
};
export const removeMember = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/remove-member",
{
method: "POST",
body: z.object({
memberIdOrEmail: z.string().meta({
description: "The ID or email of the member to remove",
}),
/**
* If not provided, the active organization will be used
*/
organizationId: z
.string()
.meta({
description:
'The ID of the organization to remove the member from. If not provided, the active organization will be used. Eg: "org-id"',
})
.optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Remove a member from an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
required: ["member"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
const adapter = getOrgAdapter<O>(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: organizationId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
let toBeRemovedMember: Member | null = null;
if (ctx.body.memberIdOrEmail.includes("@")) {
toBeRemovedMember = await adapter.findMemberByEmail({
email: ctx.body.memberIdOrEmail,
organizationId: organizationId,
});
} else {
toBeRemovedMember = await adapter.findMemberById(
ctx.body.memberIdOrEmail,
);
}
if (!toBeRemovedMember) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const roles = toBeRemovedMember.role.split(",");
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const isOwner = roles.includes(creatorRole);
if (isOwner) {
if (member.role !== creatorRole) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
});
}
const { members } = await adapter.listMembers({
organizationId: organizationId,
});
const owners = members.filter((member: Member) => {
const roles = member.role.split(",");
return roles.includes(creatorRole);
});
if (owners.length <= 1) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
});
}
}
const canDeleteMember = await hasPermission(
{
role: member.role,
options: ctx.context.orgOptions,
permissions: {
member: ["delete"],
},
organizationId,
},
ctx,
);
if (!canDeleteMember) {
throw new APIError("UNAUTHORIZED", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER,
});
}
if (toBeRemovedMember?.organizationId !== organizationId) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
toBeRemovedMember.userId,
);
if (!userBeingRemoved) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
});
}
// Run beforeRemoveMember hook
if (options?.organizationHooks?.beforeRemoveMember) {
await options?.organizationHooks.beforeRemoveMember({
member: toBeRemovedMember,
user: userBeingRemoved,
organization,
});
}
await adapter.deleteMember(toBeRemovedMember.id);
if (
session.user.id === toBeRemovedMember.userId &&
session.session.activeOrganizationId ===
toBeRemovedMember.organizationId
) {
await adapter.setActiveOrganization(session.session.token, null, ctx);
}
// Run afterRemoveMember hook
if (options?.organizationHooks?.afterRemoveMember) {
await options?.organizationHooks.afterRemoveMember({
member: toBeRemovedMember,
user: userBeingRemoved,
organization,
});
}
return ctx.json({
member: toBeRemovedMember,
});
},
);
export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
createAuthEndpoint(
"/organization/update-member-role",
{
method: "POST",
body: z.object({
role: z.union([z.string(), z.array(z.string())]).meta({
description:
'The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]',
}),
memberId: z.string().meta({
description:
'The member id to apply the role update to. Eg: "member-id"',
}),
organizationId: z
.string()
.meta({
description:
'An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: "organization-id"',
})
.optional(),
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
$Infer: {
body: {} as {
role:
| InferOrganizationRolesFromOption<O>
| InferOrganizationRolesFromOption<O>[]
| LiteralString
| LiteralString[];
memberId: string;
/**
* If not provided, the active organization will be used
*/
organizationId?: string;
},
},
openapi: {
description: "Update the role of a member in an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
required: ["member"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
if (!ctx.body.role) {
throw new APIError("BAD_REQUEST");
}
const organizationId =
ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const roleToSet: string[] = Array.isArray(ctx.body.role)
? (ctx.body.role as string[])
: ctx.body.role
? [ctx.body.role as string]
: [];
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: organizationId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const toBeUpdatedMember =
member.id !== ctx.body.memberId
? await adapter.findMemberById(ctx.body.memberId)
: member;
if (!toBeUpdatedMember) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const memberBelongsToOrganization =
toBeUpdatedMember.organizationId === organizationId;
if (!memberBelongsToOrganization) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
});
}
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const updatingMemberRoles = member.role.split(",");
const toBeUpdatedMemberRoles = toBeUpdatedMember.role.split(",");
const isUpdatingCreator = toBeUpdatedMemberRoles.includes(creatorRole);
const updaterIsCreator = updatingMemberRoles.includes(creatorRole);
const isSettingCreatorRole = roleToSet.includes(creatorRole);
const memberIsUpdatingThemselves = member.id === toBeUpdatedMember.id;
if (
(isUpdatingCreator && !updaterIsCreator) ||
(isSettingCreatorRole && !updaterIsCreator)
) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
});
}
if (updaterIsCreator && memberIsUpdatingThemselves) {
const members = await ctx.context.adapter.findMany<Member>({
model: "member",
where: [
{
field: "organizationId",
value: organizationId,
},
],
});
const owners = members.filter((member: Member) => {
const roles = member.role.split(",");
return roles.includes(creatorRole);
});
if (owners.length <= 1 && !isSettingCreatorRole) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER,
});
}
}
const canUpdateMember = await hasPermission(
{
role: member.role,
options: ctx.context.orgOptions,
permissions: {
member: ["update"],
},
allowCreatorAllPermissions: true,
organizationId,
},
ctx,
);
if (!canUpdateMember) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
});
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
});
}
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(
toBeUpdatedMember.userId,
);
if (!userBeingUpdated) {
throw new APIError("BAD_REQUEST", {
message: "User not found",
});
}
const previousRole = toBeUpdatedMember.role;
const newRole = parseRoles(ctx.body.role as string | string[]);
// Run beforeUpdateMemberRole hook
if (option?.organizationHooks?.beforeUpdateMemberRole) {
const response = await option?.organizationHooks.beforeUpdateMemberRole(
{
member: toBeUpdatedMember,
newRole,
user: userBeingUpdated,
organization,
},
);
if (response && typeof response === "object" && "data" in response) {
// Allow the hook to modify the role
const updatedMember = await adapter.updateMember(
ctx.body.memberId,
response.data.role || newRole,
);
if (!updatedMember) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
// Run afterUpdateMemberRole hook
if (option?.organizationHooks?.afterUpdateMemberRole) {
await option?.organizationHooks.afterUpdateMemberRole({
member: updatedMember,
previousRole,
user: userBeingUpdated,
organization,
});
}
return ctx.json(updatedMember);
}
}
const updatedMember = await adapter.updateMember(
ctx.body.memberId,
newRole,
);
if (!updatedMember) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
// Run afterUpdateMemberRole hook
if (option?.organizationHooks?.afterUpdateMemberRole) {
await option?.organizationHooks.afterUpdateMemberRole({
member: updatedMember,
previousRole,
user: userBeingUpdated,
organization,
});
}
return ctx.json(updatedMember);
},
);
export const getActiveMember = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/get-active-member",
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
requireHeaders: true,
metadata: {
openapi: {
description: "Get the member details of the active organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string",
},
userId: {
type: "string",
},
organizationId: {
type: "string",
},
role: {
type: "string",
},
},
required: ["id", "userId", "organizationId", "role"],
},
},
},
},
},
},
},
},
async (ctx) => {
const session = ctx.context.session;
const organizationId = session.session.activeOrganizationId;
if (!organizationId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
},
});
}
const adapter = getOrgAdapter<O>(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: organizationId,
});
if (!member) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
},
});
}
return ctx.json(member);
},
);
export const leaveOrganization = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/leave",
{
method: "POST",
body: z.object({
organizationId: z.string().meta({
description:
'The organization Id for the member to leave. Eg: "organization-id"',
}),
}),
requireHeaders: true,
use: [sessionMiddleware, orgMiddleware],
},
async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter<O>(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: ctx.body.organizationId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const isOwnerLeaving = member.role.split(",").includes(creatorRole);
if (isOwnerLeaving) {
const members = await ctx.context.adapter.findMany<Member>({
model: "member",
where: [
{
field: "organizationId",
value: ctx.body.organizationId,
},
],
});
const owners = members.filter((member) =>
member.role.split(",").includes(creatorRole),
);
if (owners.length <= 1) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
});
}
}
await adapter.deleteMember(member.id);
if (session.session.activeOrganizationId === ctx.body.organizationId) {
await adapter.setActiveOrganization(session.session.token, null, ctx);
}
return ctx.json(member);
},
);
export const listMembers = <O extends OrganizationOptions>(options: O) =>
createAuthEndpoint(
"/organization/list-members",
{
method: "GET",
query: z
.object({
limit: z
.string()
.meta({
description: "The number of users to return",
})
.or(z.number())
.optional(),
offset: z
.string()
.meta({
description: "The offset to start from",
})
.or(z.number())
.optional(),
sortBy: z
.string()
.meta({
description: "The field to sort by",
})
.optional(),
sortDirection: z
.enum(["asc", "desc"])
.meta({
description: "The direction to sort by",
})
.optional(),
filterField: z
.string()
.meta({
description: "The field to filter by",
})
.optional(),
filterValue: z
.string()
.meta({
description: "The value to filter by",
})
.or(z.number())
.or(z.boolean())
.optional(),
filterOperator: z
.enum(["eq", "ne", "lt", "lte", "gt", "gte", "contains"])
.meta({
description: "The operator to use for the filter",
})
.optional(),
organizationId: z
.string()
.meta({
description:
'The organization ID to list members for. If not provided, will default to the user\'s active organization. Eg: "organization-id"',
})
.optional(),
})
.optional(),
use: [orgMiddleware, orgSessionMiddleware],
},
async (ctx) => {
const session = ctx.context.session;
const organizationId =
ctx.query?.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
});
}
const adapter = getOrgAdapter<O>(ctx.context, options);
const isMember = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId,
});
if (!isMember) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
});
}
const { members, total } = await adapter.listMembers({
organizationId,
limit: ctx.query?.limit ? Number(ctx.query.limit) : undefined,
offset: ctx.query?.offset ? Number(ctx.query.offset) : undefined,
sortBy: ctx.query?.sortBy,
sortOrder: ctx.query?.sortDirection,
filter: ctx.query?.filterField
? {
field: ctx.query?.filterField,
operator: ctx.query.filterOperator,
value: ctx.query.filterValue,
}
: undefined,
});
return ctx.json({
members,
total,
});
},
);
export const getActiveMemberRole = <O extends OrganizationOptions>(
options: O,
) =>
createAuthEndpoint(
"/organization/get-active-member-role",
{
method: "GET",
query: z
.object({
userId: z
.string()
.meta({
description:
"The user ID to get the role for. If not provided, will default to the current user's",
})
.optional(),
organizationId: z
.string()
.meta({
description:
'The organization ID to list members for. If not provided, will default to the user\'s active organization. Eg: "organization-id"',
})
.optional(),
})
.optional(),
use: [orgMiddleware, orgSessionMiddleware],
},
async (ctx) => {
const session = ctx.context.session;
const organizationId =
ctx.query?.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
});
}
const userId = ctx.query?.userId || session.user.id;
const adapter = getOrgAdapter<O>(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId,
organizationId,
});
if (!member) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
});
}
return ctx.json({
role: member?.role,
});
},
);
```
--------------------------------------------------------------------------------
/docs/content/docs/plugins/api-key.mdx:
--------------------------------------------------------------------------------
```markdown
---
title: API Key
description: API Key plugin for Better Auth.
---
The API Key plugin allows you to create and manage API keys for your application. It provides a way to authenticate and authorize API requests by verifying API keys.
## Features
- Create, manage, and verify API keys
- [Built-in rate limiting](/docs/plugins/api-key#rate-limiting)
- [Custom expiration times, remaining count, and refill systems](/docs/plugins/api-key#remaining-refill-and-expiration)
- [metadata for API keys](/docs/plugins/api-key#metadata)
- Custom prefix
- [Sessions from API keys](/docs/plugins/api-key#sessions-from-api-keys)
## Installation
<Steps>
<Step>
### Add Plugin to the server
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [ // [!code highlight]
apiKey() // [!code highlight]
] // [!code highlight]
})
```
</Step>
<Step>
### Migrate the database
Run the migration or generate the schema to add the necessary fields and tables to the database.
<Tabs items={["migrate", "generate"]}>
<Tab value="migrate">
```bash
npx @better-auth/cli migrate
```
</Tab>
<Tab value="generate">
```bash
npx @better-auth/cli generate
```
</Tab>
</Tabs>
See the [Schema](#schema) section to add the fields manually.
</Step>
<Step>
### Add the client plugin
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { apiKeyClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
apiKeyClient() // [!code highlight]
] // [!code highlight]
})
```
</Step>
</Steps>
## Usage
You can view the list of API Key plugin options [here](/docs/plugins/api-key#api-key-plugin-options).
### Create an API key
<APIMethod
path="/api-key/create"
method="POST"
serverOnlyNote="If you're creating an API key on the server, without access to headers, you must pass the `userId` property. This is the ID of the user that the API key is associated with."
clientOnlyNote="You can adjust more specific API key configurations by using the server method instead."
>
```ts
type createApiKey = {
/**
* Name of the Api Key.
*/
name?: string = 'project-api-key'
/**
* Expiration time of the Api Key in seconds.
*/
expiresIn?: number = 60 * 60 * 24 * 7
/**
* User Id of the user that the Api Key belongs to. server-only.
* @serverOnly
*/
userId?: string = "user-id"
/**
* Prefix of the Api Key.
*/
prefix?: string = 'project-api-key'
/**
* Remaining number of requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* Metadata of the Api Key.
*/
metadata?: any | null = { someKey: 'someValue' }
/**
* Amount to refill the remaining count of the Api Key. server-only.
* @serverOnly
*/
refillAmount?: number = 100
/**
* Interval to refill the Api Key in milliseconds. server-only.
* @serverOnly
*/
refillInterval?: number = 1000
/**
* The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitTimeWindow?: number = 1000
/**
* Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitMax?: number = 100
/**
* Whether the key has rate limiting enabled. server-only.
* @serverOnly
*/
rateLimitEnabled?: boolean = true
/**
* Permissions of the Api Key.
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
<Callout>API keys are assigned to a user.</Callout>
#### Result
It'll return the `ApiKey` object which includes the `key` value for you to use.
Otherwise if it throws, it will throw an `APIError`.
---
### Verify an API key
<APIMethod
path="/api-key/verify"
method="POST"
isServerOnly
>
```ts
const permissions = { // Permissions to check are optional.
projects: ["read", "read-write"],
}
type verifyApiKey = {
/**
* The key to verify.
*/
key: string = "your_api_key_here"
/**
* The permissions to verify. Optional.
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
#### Result
```ts
type Result = {
valid: boolean;
error: { message: string; code: string } | null;
key: Omit<ApiKey, "key"> | null;
};
```
---
### Get an API key
<APIMethod
path="/api-key/get"
method="GET"
requireSession
>
```ts
type getApiKey = {
/**
* The id of the Api Key.
*/
id: string = "some-api-key-id"
}
```
</APIMethod>
#### Result
You'll receive everything about the API key details, except for the `key` value itself.
If it fails, it will throw an `APIError`.
```ts
type Result = Omit<ApiKey, "key">;
```
---
### Update an API key
<APIMethod path="/api-key/update" method="POST">
```ts
type updateApiKey = {
/**
* The id of the Api Key to update.
*/
keyId: string = "some-api-key-id"
/**
* The id of the user which the api key belongs to. server-only.
* @serverOnly
*/
userId?: string = "some-user-id"
/**
* The name of the key.
*/
name?: string = "some-api-key-name"
/**
* Whether the Api Key is enabled or not. server-only.
* @serverOnly
*/
enabled?: boolean = true
/**
* The number of remaining requests. server-only.
* @serverOnly
*/
remaining?: number = 100
/**
* The refill amount. server-only.
* @serverOnly
*/
refillAmount?: number = 100
/**
* The refill interval in milliseconds. server-only.
* @serverOnly
*/
refillInterval?: number = 1000
/**
* The metadata of the Api Key. server-only.
* @serverOnly
*/
metadata?: any | null = { "key": "value" }
/**
* Expiration time of the Api Key in seconds. server-only.
* @serverOnly
*/
expiresIn?: number = 60 * 60 * 24 * 7
/**
* Whether the key has rate limiting enabled. server-only.
* @serverOnly
*/
rateLimitEnabled?: boolean = true
/**
* The duration in milliseconds where each request is counted. server-only.
* @serverOnly
*/
rateLimitTimeWindow?: number = 1000
/**
* Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only.
* @serverOnly
*/
rateLimitMax?: number = 100
/**
* Update the permissions on the API Key. server-only.
* @serverOnly
*/
permissions?: Record<string, string[]>
}
```
</APIMethod>
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive the API Key details, except for the `key` value itself.
---
### Delete an API Key
<APIMethod
path="/api-key/delete"
method="POST"
requireSession
note="This endpoint is attempting to delete the API key from the perspective of the user. It will check if the user's ID matches the key owner to be able to delete it. If you want to delete a key without these checks, we recommend you use an ORM to directly mutate your DB instead."
>
```ts
type deleteApiKey = {
/**
* The id of the Api Key to delete.
*/
keyId: string = "some-api-key-id"
}
```
</APIMethod>
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive:
```ts
type Result = {
success: boolean;
};
```
---
### List API keys
<APIMethod
path="/api-key/list"
method="GET"
requireSession
>
```ts
type listApiKeys = {
}
```
</APIMethod>
#### Result
If fails, throws `APIError`.
Otherwise, you'll receive:
```ts
type Result = ApiKey[];
```
---
### Delete all expired API keys
This function will delete all API keys that have an expired expiration date.
<APIMethod
path="/api-key/delete-all-expired-api-keys"
method="POST"
isServerOnly
>
```ts
type deleteAllExpiredApiKeys = {
}
```
</APIMethod>
<Callout>
We automatically delete expired API keys every time any apiKey plugin
endpoints were called, however they are rate-limited to a 10 second cool down
each call to prevent multiple calls to the database.
</Callout>
---
## Sessions from API keys
Any time an endpoint in Better Auth is called that has a valid API key in the headers, you can automatically create a mock session to represent the user by enabling `sessionForAPIKeys` option.
<Callout type="warn">
This is generally not recommended, as it can lead to security issues if not used carefully. A leaked api key can be used to impersonate a user.
</Callout>
```ts
export const auth = betterAuth({
plugins: [
apiKey({
enableSessionForAPIKeys: true,
}),
],
});
```
<Tabs items={['Server']}>
<Tab value="Server">
```ts
const session = await auth.api.getSession({
headers: new Headers({
'x-api-key': apiKey,
}),
});
```
</Tab>
</Tabs>
The default header key is `x-api-key`, but this can be changed by setting the `apiKeyHeaders` option in the plugin options.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
apiKeyHeaders: ["x-api-key", "xyz-api-key"], // or you can pass just a string, eg: "x-api-key"
}),
],
});
```
Or optionally, you can pass an `apiKeyGetter` function to the plugin options, which will be called with the `GenericEndpointContext`, and from there, you should return the API key, or `null` if the request is invalid.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
apiKeyGetter: (ctx) => {
const has = ctx.request.headers.has("x-api-key");
if (!has) return null;
return ctx.request.headers.get("x-api-key");
},
}),
],
});
```
## Rate Limiting
Every API key can have its own rate limit settings, however, the built-in rate-limiting only applies to the verification process for a given API key.
For every other endpoint/method, you should utilize Better Auth's [built-in rate-limiting](/docs/concepts/rate-limit).
You can refer to the rate-limit default configurations below in the API Key plugin options.
An example default value:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
rateLimit: {
enabled: true,
timeWindow: 1000 * 60 * 60 * 24, // 1 day
maxRequests: 10, // 10 requests per day
},
}),
],
});
```
For each API key, you can customize the rate-limit options on create.
<Callout>
You can only customize the rate-limit options on the server auth instance.
</Callout>
```ts
const apiKey = await auth.api.createApiKey({
body: {
rateLimitEnabled: true,
rateLimitTimeWindow: 1000 * 60 * 60 * 24, // 1 day
rateLimitMax: 10, // 10 requests per day
},
headers: user_headers,
});
```
### How does it work?
For each request, a counter (internally called `requestCount`) is incremented.
If the `rateLimitMax` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
## Remaining, refill, and expiration
The remaining count is the number of requests left before the API key is disabled.
The refill interval is the interval in milliseconds where the `remaining` count is refilled by day.
The expiration time is the expiration date of the API key.
### How does it work?
#### Remaining:
Whenever an API key is used, the `remaining` count is updated.
If the `remaining` count is `null`, then there is no cap to key usage.
Otherwise, the `remaining` count is decremented by 1.
If the `remaining` count is 0, then the API key is disabled & removed.
#### refillInterval & refillAmount:
Whenever an API key is created, the `refillInterval` and `refillAmount` are set to `null`.
This means that the API key will not be refilled automatically.
However, if `refillInterval` & `refillAmount` are set, then the API key will be refilled accordingly.
#### Expiration:
Whenever an API key is created, the `expiresAt` is set to `null`.
This means that the API key will never expire.
However, if the `expiresIn` is set, then the API key will expire after the `expiresIn` time.
## Custom Key generation & verification
You can customize the key generation and verification process straight from the plugin options.
Here's an example:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: (options: {
length: number;
prefix: string | undefined;
}) => {
const apiKey = mySuperSecretApiKeyGenerator(
options.length,
options.prefix
);
return apiKey;
},
customAPIKeyValidator: async ({ ctx, key }) => {
const res = await keyService.verify(key)
return res.valid
},
}),
],
});
```
<Callout>
If you're **not** using the `length` property provided by `customKeyGenerator`, you **must** set the `defaultKeyLength` property to how long generated keys will be.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: () => {
return crypto.randomUUID();
},
defaultKeyLength: 36, // Or whatever the length is
}),
],
});
```
</Callout>
If an API key is validated from your `customAPIKeyValidator`, we still must match that against the database's key.
However, by providing this custom function, you can improve the performance of the API key verification process,
as all failed keys can be invalidated without having to query your database.
## Metadata
We allow you to store metadata alongside your API keys. This is useful for storing information about the key, such as a subscription plan for example.
To store metadata, make sure you haven't disabled the metadata feature in the plugin options.
```ts
export const auth = betterAuth({
plugins: [
apiKey({
enableMetadata: true,
}),
],
});
```
Then, you can store metadata in the `metadata` field of the API key object.
```ts
const apiKey = await auth.api.createApiKey({
body: {
metadata: {
plan: "premium",
},
},
});
```
You can then retrieve the metadata from the API key object.
```ts
const apiKey = await auth.api.getApiKey({
body: {
keyId: "your_api_key_id_here",
},
});
console.log(apiKey.metadata.plan); // "premium"
```
## API Key plugin options
`apiKeyHeaders` <span className="opacity-70">`string | string[];`</span>
The header name to check for API key. Default is `x-api-key`.
`customAPIKeyGetter` <span className="opacity-70">`(ctx: GenericEndpointContext) => string | null`</span>
A custom function to get the API key from the context.
`customAPIKeyValidator` <span className="opacity-70">`(options: { ctx: GenericEndpointContext; key: string; }) => boolean | Promise<boolean>`</span>
A custom function to validate the API key.
`customKeyGenerator` <span className="opacity-70">`(options: { length: number; prefix: string | undefined; }) => string | Promise<string>`</span>
A custom function to generate the API key.
`startingCharactersConfig` <span className="opacity-70">`{ shouldStore?: boolean; charactersLength?: number; }`</span>
Customize the starting characters configuration.
<Accordions>
<Accordion title="startingCharactersConfig Options">
`shouldStore` <span className="opacity-70">`boolean`</span>
Whether to store the starting characters in the database.
If false, we will set `start` to `null`.
Default is `true`.
`charactersLength` <span className="opacity-70">`number`</span>
The length of the starting characters to store in the database.
This includes the prefix length.
Default is `6`.
</Accordion>
</Accordions>
`defaultKeyLength` <span className="opacity-70">`number`</span>
The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length)
`defaultPrefix` <span className="opacity-70">`string`</span>
The prefix of the API key.
Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg `hello_`)
`maximumPrefixLength` <span className="opacity-70">`number`</span>
The maximum length of the prefix.
`minimumPrefixLength` <span className="opacity-70">`number`</span>
The minimum length of the prefix.
`requireName` <span className="opacity-70">`boolean`</span>
Whether to require a name for the API key. Default is `false`.
`maximumNameLength` <span className="opacity-70">`number`</span>
The maximum length of the name.
`minimumNameLength` <span className="opacity-70">`number`</span>
The minimum length of the name.
`enableMetadata` <span className="opacity-70">`boolean`</span>
Whether to enable metadata for an API key.
`keyExpiration` <span className="opacity-70">`{ defaultExpiresIn?: number | null; disableCustomExpiresTime?: boolean; minExpiresIn?: number; maxExpiresIn?: number; }`</span>
Customize the key expiration.
<Accordions>
<Accordion title="keyExpiration options">
`defaultExpiresIn` <span className="opacity-70">`number | null`</span>
The default expires time in milliseconds.
If `null`, then there will be no expiration time.
Default is `null`.
`disableCustomExpiresTime` <span className="opacity-70">`boolean`</span>
Whether to disable the expires time passed from the client.
If `true`, the expires time will be based on the default values.
Default is `false`.
`minExpiresIn` <span className="opacity-70">`number`</span>
The minimum expiresIn value allowed to be set from the client. in days.
Default is `1`.
`maxExpiresIn` <span className="opacity-70">`number`</span>
The maximum expiresIn value allowed to be set from the client. in days.
Default is `365`.
</Accordion>
</Accordions>
`rateLimit` <span className="opacity-70">`{ enabled?: boolean; timeWindow?: number; maxRequests?: number; }`</span>
Customize the rate-limiting.
<Accordions>
<Accordion title="rateLimit options">
`enabled` <span className="opacity-70">`boolean`</span>
Whether to enable rate limiting. (Default true)
`timeWindow` <span className="opacity-70">`number`</span>
The duration in milliseconds where each request is counted.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
`maxRequests` <span className="opacity-70">`number`</span>
Maximum amount of requests allowed within a window.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
</Accordion>
</Accordions>
`schema` <span className="opacity-70">`InferOptionSchema<ReturnType<typeof apiKeySchema>>`</span>
Custom schema for the API key plugin.
`enableSessionForAPIKeys` <span className="opacity-70">`boolean`</span>
An API Key can represent a valid session, so we can mock a session for the user if we find a valid API key in the request headers. Default is `false`.
`permissions` <span className="opacity-70">`{ defaultPermissions?: Statements | ((userId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>) }`</span>
Permissions for the API key.
Read more about permissions [here](/docs/plugins/api-key#permissions).
<Accordions>
<Accordion title="permissions Options">
`defaultPermissions` <span className="opacity-70">`Statements | ((userId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>)`</span>
The default permissions for the API key.
</Accordion>
</Accordions>
`disableKeyHashing` <span className="opacity-70">`boolean`</span>
Disable hashing of the API key.
⚠️ Security Warning: It's strongly recommended to not disable hashing.
Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys.
---
## Permissions
API keys can have permissions associated with them, allowing you to control access at a granular level. Permissions are structured as a record of resource types to arrays of allowed actions.
### Setting Default Permissions
You can configure default permissions that will be applied to all newly created API keys:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
permissions: {
defaultPermissions: {
files: ["read"],
users: ["read"],
},
},
}),
],
});
```
You can also provide a function that returns permissions dynamically:
```ts
export const auth = betterAuth({
plugins: [
apiKey({
permissions: {
defaultPermissions: async (userId, ctx) => {
// Fetch user role or other data to determine permissions
return {
files: ["read"],
users: ["read"],
};
},
},
}),
],
});
```
### Creating API Keys with Permissions
When creating an API key, you can specify custom permissions:
```ts
const apiKey = await auth.api.createApiKey({
body: {
name: "My API Key",
permissions: {
files: ["read", "write"],
users: ["read"],
},
userId: "userId",
},
});
```
### Verifying API Keys with Required Permissions
When verifying an API key, you can check if it has the required permissions:
```ts
const result = await auth.api.verifyApiKey({
body: {
key: "your_api_key_here",
permissions: {
files: ["read"],
},
},
});
if (result.valid) {
// API key is valid and has the required permissions
} else {
// API key is invalid or doesn't have the required permissions
}
```
### Updating API Key Permissions
You can update the permissions of an existing API key:
```ts
const apiKey = await auth.api.updateApiKey({
body: {
keyId: existingApiKeyId,
permissions: {
files: ["read", "write", "delete"],
users: ["read", "write"],
},
},
headers: user_headers,
});
```
### Permissions Structure
Permissions follow a resource-based structure:
```ts
type Permissions = {
[resourceType: string]: string[];
};
// Example:
const permissions = {
files: ["read", "write", "delete"],
users: ["read"],
projects: ["read", "write"],
};
```
When verifying an API key, all required permissions must be present in the API key's permissions for validation to succeed.
## Schema
Table: `apiKey`
<DatabaseTable
fields={[
{
name: "id",
type: "string",
description: "The ID of the API key.",
isUnique: true,
isPrimaryKey: true,
},
{
name: "name",
type: "string",
description: "The name of the API key.",
isOptional: true,
},
{
name: "start",
type: "string",
description:
"The starting characters of the API key. Useful for showing the first few characters of the API key in the UI for the users to easily identify.",
isOptional: true,
},
{
name: "prefix",
type: "string",
description: "The API Key prefix. Stored as plain text.",
isOptional: true,
},
{
name: "key",
type: "string",
description: "The hashed API key itself.",
},
{
name: "userId",
type: "string",
description: "The ID of the user associated with the API key.",
isForeignKey: true,
},
{
name: "refillInterval",
type: "number",
description: "The interval to refill the key in milliseconds.",
isOptional: true,
},
{
name: "refillAmount",
type: "number",
description: "The amount to refill the remaining count of the key.",
isOptional: true,
},
{
name: "lastRefillAt",
type: "Date",
description: "The date and time when the key was last refilled.",
isOptional: true,
},
{
name: "enabled",
type: "boolean",
description: "Whether the API key is enabled.",
},
{
name: "rateLimitEnabled",
type: "boolean",
description: "Whether the API key has rate limiting enabled.",
},
{
name: "rateLimitTimeWindow",
type: "number",
description: "The time window in milliseconds for the rate limit.",
isOptional: true,
},
{
name: "rateLimitMax",
type: "number",
description:
"The maximum number of requests allowed within the `rateLimitTimeWindow`.",
isOptional: true,
},
{
name: "requestCount",
type: "number",
description:
"The number of requests made within the rate limit time window.",
},
{
name: "remaining",
type: "number",
description: "The number of requests remaining.",
isOptional: true,
},
{
name: "lastRequest",
type: "Date",
description: "The date and time of the last request made to the key.",
isOptional: true,
},
{
name: "expiresAt",
type: "Date",
description: "The date and time when the key will expire.",
isOptional: true,
},
{
name: "createdAt",
type: "Date",
description: "The date and time the API key was created.",
},
{
name: "updatedAt",
type: "Date",
description: "The date and time the API key was updated.",
},
{
name: "permissions",
type: "string",
description: "The permissions of the key.",
isOptional: true,
},
{
name: "metadata",
type: "Object",
isOptional: true,
description: "Any additional metadata you want to store with the key.",
},
]}
/>
```