#
tokens: 43319/50000 3/1091 files (page 50/67)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 50 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   └── nextjs
│       ├── .env.example
│       ├── .gitignore
│       ├── app
│       │   ├── (auth)
│       │   │   ├── forget-password
│       │   │   │   └── page.tsx
│       │   │   ├── reset-password
│       │   │   │   └── page.tsx
│       │   │   ├── sign-in
│       │   │   │   ├── loading.tsx
│       │   │   │   └── page.tsx
│       │   │   └── two-factor
│       │   │       ├── otp
│       │   │       │   └── page.tsx
│       │   │       └── page.tsx
│       │   ├── accept-invitation
│       │   │   └── [id]
│       │   │       ├── invitation-error.tsx
│       │   │       └── page.tsx
│       │   ├── admin
│       │   │   └── page.tsx
│       │   ├── api
│       │   │   └── auth
│       │   │       └── [...all]
│       │   │           └── route.ts
│       │   ├── apps
│       │   │   └── register
│       │   │       └── page.tsx
│       │   ├── client-test
│       │   │   └── page.tsx
│       │   ├── dashboard
│       │   │   ├── change-plan.tsx
│       │   │   ├── client.tsx
│       │   │   ├── organization-card.tsx
│       │   │   ├── page.tsx
│       │   │   ├── upgrade-button.tsx
│       │   │   └── user-card.tsx
│       │   ├── device
│       │   │   ├── approve
│       │   │   │   └── page.tsx
│       │   │   ├── denied
│       │   │   │   └── page.tsx
│       │   │   ├── layout.tsx
│       │   │   ├── page.tsx
│       │   │   └── success
│       │   │       └── page.tsx
│       │   ├── favicon.ico
│       │   ├── features.tsx
│       │   ├── fonts
│       │   │   ├── GeistMonoVF.woff
│       │   │   └── GeistVF.woff
│       │   ├── globals.css
│       │   ├── layout.tsx
│       │   ├── oauth
│       │   │   └── authorize
│       │   │       ├── concet-buttons.tsx
│       │   │       └── page.tsx
│       │   ├── page.tsx
│       │   └── pricing
│       │       └── page.tsx
│       ├── components
│       │   ├── account-switch.tsx
│       │   ├── blocks
│       │   │   └── pricing.tsx
│       │   ├── logo.tsx
│       │   ├── one-tap.tsx
│       │   ├── sign-in-btn.tsx
│       │   ├── sign-in.tsx
│       │   ├── sign-up.tsx
│       │   ├── theme-provider.tsx
│       │   ├── theme-toggle.tsx
│       │   ├── tier-labels.tsx
│       │   ├── ui
│       │   │   ├── accordion.tsx
│       │   │   ├── alert-dialog.tsx
│       │   │   ├── alert.tsx
│       │   │   ├── aspect-ratio.tsx
│       │   │   ├── avatar.tsx
│       │   │   ├── badge.tsx
│       │   │   ├── breadcrumb.tsx
│       │   │   ├── button.tsx
│       │   │   ├── calendar.tsx
│       │   │   ├── card.tsx
│       │   │   ├── carousel.tsx
│       │   │   ├── chart.tsx
│       │   │   ├── checkbox.tsx
│       │   │   ├── collapsible.tsx
│       │   │   ├── command.tsx
│       │   │   ├── context-menu.tsx
│       │   │   ├── copy-button.tsx
│       │   │   ├── dialog.tsx
│       │   │   ├── drawer.tsx
│       │   │   ├── dropdown-menu.tsx
│       │   │   ├── form.tsx
│       │   │   ├── hover-card.tsx
│       │   │   ├── input-otp.tsx
│       │   │   ├── input.tsx
│       │   │   ├── label.tsx
│       │   │   ├── menubar.tsx
│       │   │   ├── navigation-menu.tsx
│       │   │   ├── pagination.tsx
│       │   │   ├── password-input.tsx
│       │   │   ├── popover.tsx
│       │   │   ├── progress.tsx
│       │   │   ├── radio-group.tsx
│       │   │   ├── resizable.tsx
│       │   │   ├── scroll-area.tsx
│       │   │   ├── select.tsx
│       │   │   ├── separator.tsx
│       │   │   ├── sheet.tsx
│       │   │   ├── skeleton.tsx
│       │   │   ├── slider.tsx
│       │   │   ├── sonner.tsx
│       │   │   ├── switch.tsx
│       │   │   ├── table.tsx
│       │   │   ├── tabs.tsx
│       │   │   ├── tabs2.tsx
│       │   │   ├── textarea.tsx
│       │   │   ├── toast.tsx
│       │   │   ├── toaster.tsx
│       │   │   ├── toggle-group.tsx
│       │   │   ├── toggle.tsx
│       │   │   └── tooltip.tsx
│       │   └── wrapper.tsx
│       ├── components.json
│       ├── hooks
│       │   └── use-toast.ts
│       ├── lib
│       │   ├── auth-client.ts
│       │   ├── auth-types.ts
│       │   ├── auth.ts
│       │   ├── email
│       │   │   ├── invitation.tsx
│       │   │   ├── resend.ts
│       │   │   └── reset-password.tsx
│       │   ├── metadata.ts
│       │   ├── shared.ts
│       │   └── utils.ts
│       ├── middleware.ts
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── public
│       │   ├── __og.png
│       │   ├── _og.png
│       │   ├── favicon
│       │   │   ├── android-chrome-192x192.png
│       │   │   ├── android-chrome-512x512.png
│       │   │   ├── apple-touch-icon.png
│       │   │   ├── favicon-16x16.png
│       │   │   ├── favicon-32x32.png
│       │   │   ├── favicon.ico
│       │   │   ├── light
│       │   │   │   ├── android-chrome-192x192.png
│       │   │   │   ├── android-chrome-512x512.png
│       │   │   │   ├── apple-touch-icon.png
│       │   │   │   ├── favicon-16x16.png
│       │   │   │   ├── favicon-32x32.png
│       │   │   │   ├── favicon.ico
│       │   │   │   └── site.webmanifest
│       │   │   └── site.webmanifest
│       │   ├── logo.svg
│       │   └── og.png
│       ├── README.md
│       ├── tailwind.config.ts
│       ├── tsconfig.json
│       └── turbo.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── user-additional-fields.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── sso
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── sso.test.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── 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
│   │   │   ├── middleware
│   │   │   │   └── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/docs/content/docs/plugins/sso.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Single Sign-On (SSO)
  3 | description: Integrate Single Sign-On (SSO) with your application.
  4 | ---
  5 | 
  6 | `OIDC` `OAuth2` `SSO` `SAML`
  7 | 
  8 | Single Sign-On (SSO) allows users to authenticate with multiple applications using a single set of credentials. This plugin supports OpenID Connect (OIDC), OAuth2 providers, and SAML 2.0.
  9 | 
 10 | ## Installation
 11 | 
 12 | <Steps>
 13 |     <Step>
 14 |         ### Install the plugin
 15 | 
 16 |         ```bash
 17 |         npm install @better-auth/sso
 18 |         ```
 19 |     </Step>
 20 |     <Step>
 21 |         ### Add Plugin to the server
 22 | 
 23 |         ```ts title="auth.ts"
 24 |         import { betterAuth } from "better-auth"
 25 |         import { sso } from "@better-auth/sso";
 26 | 
 27 |         const auth = betterAuth({
 28 |             plugins: [ // [!code highlight]
 29 |                 sso() // [!code highlight]
 30 |             ] // [!code highlight]
 31 |         })
 32 |         ```
 33 |     </Step>
 34 |     <Step>
 35 |         ### Migrate the database
 36 | 
 37 |         Run the migration or generate the schema to add the necessary fields and tables to the database.
 38 | 
 39 |         <Tabs items={["migrate", "generate"]}>
 40 |             <Tab value="migrate">
 41 |             ```bash
 42 |             npx @better-auth/cli migrate
 43 |             ```
 44 |             </Tab>
 45 |             <Tab value="generate">
 46 |             ```bash
 47 |             npx @better-auth/cli generate
 48 |             ```
 49 |             </Tab>
 50 |         </Tabs>
 51 |         See the [Schema](#schema) section to add the fields manually.
 52 |     </Step>
 53 |     <Step>
 54 |         ### Add the client plugin
 55 | 
 56 |         ```ts title="auth-client.ts"
 57 |         import { createAuthClient } from "better-auth/client"
 58 |         import { ssoClient } from "@better-auth/sso/client"
 59 | 
 60 |         const authClient = createAuthClient({
 61 |             plugins: [ // [!code highlight]
 62 |                 ssoClient() // [!code highlight]
 63 |             ] // [!code highlight]
 64 |         })
 65 |         ```
 66 |     </Step>
 67 | </Steps>
 68 | 
 69 | ## Usage
 70 | 
 71 | ### Register an OIDC Provider
 72 | 
 73 | To register an OIDC provider, use the `registerSSOProvider` endpoint and provide the necessary configuration details for the provider.
 74 | 
 75 | A redirect URL will be automatically generated using the provider ID. For instance, if the provider ID is `hydra`, the redirect URL would be `{baseURL}/api/auth/sso/callback/hydra`. Note that `/api/auth` may vary depending on your base path configuration.
 76 | 
 77 | #### Example
 78 | 
 79 | <Tabs items={["client", "server"]}>
 80 |     <Tab value="client">
 81 | ```ts title="register-oidc-provider.ts"
 82 | import { authClient } from "@/lib/auth-client";
 83 | 
 84 | // Register with OIDC configuration
 85 | await authClient.sso.register({
 86 |     providerId: "example-provider",
 87 |     issuer: "https://idp.example.com",
 88 |     domain: "example.com",
 89 |     oidcConfig: {
 90 |         clientId: "client-id",
 91 |         clientSecret: "client-secret",
 92 |         authorizationEndpoint: "https://idp.example.com/authorize",
 93 |         tokenEndpoint: "https://idp.example.com/token",
 94 |         jwksEndpoint: "https://idp.example.com/jwks",
 95 |         discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
 96 |         scopes: ["openid", "email", "profile"],
 97 |         pkce: true,
 98 |         mapping: {
 99 |             id: "sub",
100 |             email: "email",
101 |             emailVerified: "email_verified",
102 |             name: "name",
103 |             image: "picture",
104 |             extraFields: {
105 |                 department: "department",
106 |                 role: "role"
107 |             }
108 |         }
109 |     }
110 | });
111 | ```
112 |     </Tab>
113 | 
114 |     <Tab value="server">
115 | ```ts title="register-oidc-provider.ts"
116 | const { headers } = await signInWithTestUser();
117 | await auth.api.registerSSOProvider({
118 |     body: {
119 |         providerId: "example-provider",
120 |         issuer: "https://idp.example.com",
121 |         domain: "example.com",
122 |         oidcConfig: {
123 |             clientId: "your-client-id",
124 |             clientSecret: "your-client-secret",
125 |             authorizationEndpoint: "https://idp.example.com/authorize",
126 |             tokenEndpoint: "https://idp.example.com/token",
127 |             jwksEndpoint: "https://idp.example.com/jwks",
128 |             discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
129 |             scopes: ["openid", "email", "profile"],
130 |             pkce: true,
131 |             mapping: {
132 |                 id: "sub",
133 |                 email: "email",
134 |                 emailVerified: "email_verified",
135 |                 name: "name",
136 |                 image: "picture",
137 |                 extraFields: {
138 |                     department: "department",
139 |                     role: "role"
140 |                 }
141 |             }
142 |         }
143 |     },
144 |     headers,
145 | });
146 | ```
147 |     </Tab>
148 | </Tabs>
149 | 
150 | 
151 | ### Register a SAML Provider
152 | 
153 | To register a SAML provider, use the `registerSSOProvider` endpoint with SAML configuration details. The provider will act as a Service Provider (SP) and integrate with your Identity Provider (IdP).
154 | 
155 | <Tabs items={["client", "server"]}>
156 |     <Tab value="client">
157 | ```ts title="register-saml-provider.ts"
158 | import { authClient } from "@/lib/auth-client";
159 | 
160 | await authClient.sso.register({
161 |     providerId: "saml-provider",
162 |     issuer: "https://idp.example.com",
163 |     domain: "example.com",
164 |     samlConfig: {
165 |         entryPoint: "https://idp.example.com/sso",
166 |         cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
167 |         callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
168 |         audience: "https://yourapp.com",
169 |         wantAssertionsSigned: true,
170 |         signatureAlgorithm: "sha256",
171 |         digestAlgorithm: "sha256",
172 |         identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
173 |         idpMetadata: {
174 |             metadata: "<!-- IdP Metadata XML -->",
175 |             privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
176 |             privateKeyPass: "your-private-key-password",
177 |             isAssertionEncrypted: true,
178 |             encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
179 |             encPrivateKeyPass: "your-encryption-key-password"
180 |         },
181 |         spMetadata: {
182 |             metadata: "<!-- SP Metadata XML -->",
183 |             binding: "post",
184 |             privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
185 |             privateKeyPass: "your-sp-private-key-password",
186 |             isAssertionEncrypted: true,
187 |             encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
188 |             encPrivateKeyPass: "your-sp-encryption-key-password"
189 |         },
190 |         mapping: {
191 |             id: "nameID",
192 |             email: "email",
193 |             name: "displayName",
194 |             firstName: "givenName",
195 |             lastName: "surname",
196 |             emailVerified: "email_verified",
197 |             extraFields: {
198 |                 department: "department",
199 |                 role: "role"
200 |             }
201 |         }
202 |     }
203 | });
204 | ```
205 |     </Tab>
206 | 
207 |     <Tab value="server">
208 | ```ts title="register-saml-provider.ts"
209 | const { headers } = await signInWithTestUser();
210 | await auth.api.registerSSOProvider({
211 |     body: {
212 |         providerId: "saml-provider",
213 |         issuer: "https://idp.example.com",
214 |         domain: "example.com",
215 |         samlConfig: {
216 |             entryPoint: "https://idp.example.com/sso",
217 |             cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
218 |             callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
219 |             audience: "https://yourapp.com",
220 |             wantAssertionsSigned: true,
221 |             signatureAlgorithm: "sha256",
222 |             digestAlgorithm: "sha256",
223 |             identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
224 |             idpMetadata: {
225 |                 metadata: "<!-- IdP Metadata XML -->",
226 |                 privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
227 |                 privateKeyPass: "your-private-key-password",
228 |                 isAssertionEncrypted: true,
229 |                 encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
230 |                 encPrivateKeyPass: "your-encryption-key-password"
231 |             },
232 |             spMetadata: {
233 |                 metadata: "<!-- SP Metadata XML -->",
234 |                 binding: "post",
235 |                 privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
236 |                 privateKeyPass: "your-sp-private-key-password",
237 |                 isAssertionEncrypted: true,
238 |                 encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
239 |                 encPrivateKeyPass: "your-sp-encryption-key-password"
240 |             },
241 |             mapping: {
242 |                 id: "nameID",
243 |                 email: "email",
244 |                 name: "displayName",
245 |                 firstName: "givenName",
246 |                 lastName: "surname",
247 |                 emailVerified: "email_verified",
248 |                 extraFields: {
249 |                     department: "department",
250 |                     role: "role"
251 |                 }
252 |             }
253 |         }
254 |     },
255 |     headers,
256 | });
257 | ```
258 |     </Tab>
259 | </Tabs>
260 | 
261 | ### Get Service Provider Metadata
262 | 
263 | For SAML providers, you can retrieve the Service Provider metadata XML that needs to be configured in your Identity Provider:
264 | 
265 | ```ts title="get-sp-metadata.ts"
266 | const response = await auth.api.spMetadata({
267 |     query: {
268 |         providerId: "saml-provider",
269 |         format: "xml" // or "json"
270 |     }
271 | });
272 | 
273 | const metadataXML = await response.text();
274 | console.log(metadataXML);
275 | ```
276 | 
277 | ### Sign In with SSO
278 | 
279 | To sign in with an SSO provider, you can call `signIn.sso`
280 | 
281 | You can sign in using the email with domain matching:
282 | 
283 | ```ts title="sign-in.ts"
284 | const res = await authClient.signIn.sso({
285 |     email: "[email protected]",
286 |     callbackURL: "/dashboard",
287 | });
288 | ```
289 | 
290 | or you can specify the domain:
291 | 
292 | ```ts title="sign-in-domain.ts"
293 | const res = await authClient.signIn.sso({
294 |     domain: "example.com",
295 |     callbackURL: "/dashboard",
296 | });
297 | ```
298 | 
299 | You can also sign in using the organization slug if a provider is associated with an organization:
300 | 
301 | ```ts title="sign-in-org.ts"
302 | const res = await authClient.signIn.sso({
303 |     organizationSlug: "example-org",
304 |     callbackURL: "/dashboard",
305 | });
306 | ```
307 | 
308 | Alternatively, you can sign in using the provider's ID:
309 | 
310 | ```ts title="sign-in-provider-id.ts"
311 | const res = await authClient.signIn.sso({
312 |     providerId: "example-provider-id",
313 |     callbackURL: "/dashboard",
314 | });
315 | ```
316 | 
317 | To use the server API you can use `signInSSO`
318 | 
319 | ```ts title="sign-in-org.ts"
320 | const res = await auth.api.signInSSO({
321 |     body: {
322 |         organizationSlug: "example-org",
323 |         callbackURL: "/dashboard",
324 |     }
325 | });
326 | ```
327 | 
328 | #### Full method
329 | 
330 | <APIMethod path="/sign-in/sso" method="POST">
331 | ```ts
332 | type signInSSO = {
333 |     /**
334 |      * The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided. 
335 |      */
336 |     email?: string = "[email protected]"
337 |     /**
338 |      * The slug of the organization to sign in with. 
339 |      */
340 |     organizationSlug?: string = "example-org"
341 |     /**
342 |      * The ID of the provider to sign in with. This can be provided instead of email or issuer. 
343 |      */
344 |     providerId?: string = "example-provider"
345 |     /**
346 |      * The domain of the provider. 
347 |      */
348 |     domain?: string = "example.com"
349 |     /**
350 |      * The URL to redirect to after login. 
351 |      */
352 |     callbackURL: string = "https://example.com/callback"
353 |     /**
354 |      * The URL to redirect to after login. 
355 |      */
356 |     errorCallbackURL?: string = "https://example.com/callback"
357 |     /**
358 |      * The URL to redirect to after login if the user is new. 
359 |      */
360 |     newUserCallbackURL?: string = "https://example.com/new-user"
361 |     /**
362 |      * Scopes to request from the provider. 
363 |      */
364 |     scopes?: string[] = ["openid", "email", "profile", "offline_access"]
365 |     /**
366 |      * Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. 
367 |      */
368 |     requestSignUp?: boolean = true
369 | }
370 | ```
371 | </APIMethod>
372 | 
373 | When a user is authenticated, if the user does not exist, the user will be provisioned using the `provisionUser` function. If the organization provisioning is enabled and a provider is associated with an organization, the user will be added to the organization.
374 | 
375 | ```ts title="auth.ts"
376 | const auth = betterAuth({
377 |     plugins: [
378 |         sso({
379 |             provisionUser: async (user) => {
380 |                 // provision user
381 |             },
382 |             organizationProvisioning: {
383 |                 disabled: false,
384 |                 defaultRole: "member",
385 |                 getRole: async (user) => {
386 |                     // get role if needed
387 |                 },
388 |             },
389 |         }),
390 |     ],
391 | });
392 | ```
393 | 
394 | ## Provisioning
395 | 
396 | The SSO plugin provides powerful provisioning capabilities to automatically set up users and manage their organization memberships when they sign in through SSO providers.
397 | 
398 | ### User Provisioning
399 | 
400 | User provisioning allows you to run custom logic whenever a user signs in through an SSO provider. This is useful for:
401 | 
402 | - Setting up user profiles with additional data from the SSO provider
403 | - Synchronizing user attributes with external systems
404 | - Creating user-specific resources
405 | - Logging SSO sign-ins
406 | - Updating user information from the SSO provider
407 | 
408 | ```ts title="auth.ts"
409 | const auth = betterAuth({
410 |     plugins: [
411 |         sso({
412 |             provisionUser: async ({ user, userInfo, token, provider }) => {
413 |                 // Update user profile with SSO data
414 |                 await updateUserProfile(user.id, {
415 |                     department: userInfo.attributes?.department,
416 |                     jobTitle: userInfo.attributes?.jobTitle,
417 |                     manager: userInfo.attributes?.manager,
418 |                     lastSSOLogin: new Date(),
419 |                 });
420 | 
421 |                 // Create user-specific resources
422 |                 await createUserWorkspace(user.id);
423 | 
424 |                 // Sync with external systems
425 |                 await syncUserWithCRM(user.id, userInfo);
426 | 
427 |                 // Log the SSO sign-in
428 |                 await auditLog.create({
429 |                     userId: user.id,
430 |                     action: 'sso_signin',
431 |                     provider: provider.providerId,
432 |                     metadata: {
433 |                         email: userInfo.email,
434 |                         ssoProvider: provider.issuer,
435 |                     },
436 |                 });
437 |             },
438 |         }),
439 |     ],
440 | });
441 | ```
442 | 
443 | The `provisionUser` function receives:
444 | - **user**: The user object from the database
445 | - **userInfo**: User information from the SSO provider (includes attributes, email, name, etc.)
446 | - **token**: OAuth2 tokens (for OIDC providers) - may be undefined for SAML
447 | - **provider**: The SSO provider configuration
448 | 
449 | ### Organization Provisioning
450 | 
451 | Organization provisioning automatically manages user memberships in organizations when SSO providers are linked to specific organizations. This is particularly useful for:
452 | 
453 | - Enterprise SSO where each company/domain maps to an organization
454 | - Automatic role assignment based on SSO attributes
455 | - Managing team memberships through SSO
456 | 
457 | #### Basic Organization Provisioning
458 | 
459 | ```ts title="auth.ts"
460 | const auth = betterAuth({
461 |     plugins: [
462 |         sso({
463 |             organizationProvisioning: {
464 |                 disabled: false,           // Enable org provisioning
465 |                 defaultRole: "member",     // Default role for new members
466 |             },
467 |         }),
468 |     ],
469 | });
470 | ```
471 | 
472 | #### Advanced Organization Provisioning with Custom Roles
473 | 
474 | ```ts title="auth.ts"
475 | const auth = betterAuth({
476 |     plugins: [
477 |         sso({
478 |             organizationProvisioning: {
479 |                 disabled: false,
480 |                 defaultRole: "member",
481 |                 getRole: async ({ user, userInfo, provider }) => {
482 |                     // Assign roles based on SSO attributes
483 |                     const department = userInfo.attributes?.department;
484 |                     const jobTitle = userInfo.attributes?.jobTitle;
485 |                     
486 |                     // Admins based on job title
487 |                     if (jobTitle?.toLowerCase().includes('manager') || 
488 |                         jobTitle?.toLowerCase().includes('director') ||
489 |                         jobTitle?.toLowerCase().includes('vp')) {
490 |                         return "admin";
491 |                     }
492 |                     
493 |                     // Special roles for IT department
494 |                     if (department?.toLowerCase() === 'it') {
495 |                         return "admin";
496 |                     }
497 |                     
498 |                     // Default to member for everyone else
499 |                     return "member";
500 |                 },
501 |             },
502 |         }),
503 |     ],
504 | });
505 | ```
506 | 
507 | #### Linking SSO Providers to Organizations
508 | 
509 | When registering an SSO provider, you can link it to a specific organization:
510 | 
511 | ```ts title="register-org-provider.ts"
512 | await auth.api.registerSSOProvider({
513 |     body: {
514 |         providerId: "acme-corp-saml",
515 |         issuer: "https://acme-corp.okta.com",
516 |         domain: "acmecorp.com",
517 |         organizationId: "org_acme_corp_id", // Link to organization
518 |         samlConfig: {
519 |             // SAML configuration...
520 |         },
521 |     },
522 |     headers,
523 | });
524 | ```
525 | 
526 | Now when users from `acmecorp.com` sign in through this provider, they'll automatically be added to the "Acme Corp" organization with the appropriate role.
527 | 
528 | #### Multiple Organizations Example
529 | 
530 | You can set up multiple SSO providers for different organizations:
531 | 
532 | ```ts title="multi-org-setup.ts"
533 | // Acme Corp SAML provider
534 | await auth.api.registerSSOProvider({
535 |     body: {
536 |         providerId: "acme-corp",
537 |         issuer: "https://acme.okta.com",
538 |         domain: "acmecorp.com",
539 |         organizationId: "org_acme_id",
540 |         samlConfig: { /* ... */ },
541 |     },
542 |     headers,
543 | });
544 | 
545 | // TechStart OIDC provider
546 | await auth.api.registerSSOProvider({
547 |     body: {
548 |         providerId: "techstart-google",
549 |         issuer: "https://accounts.google.com",
550 |         domain: "techstart.io",
551 |         organizationId: "org_techstart_id",
552 |         oidcConfig: { /* ... */ },
553 |     },
554 |     headers,
555 | });
556 | ```
557 | 
558 | #### Organization Provisioning Flow
559 | 
560 | 1. **User signs in** through an SSO provider linked to an organization
561 | 2. **User is authenticated** and either found or created in the database
562 | 3. **Organization membership is checked** - if the user isn't already a member of the linked organization
563 | 4. **Role is determined** using either the `defaultRole` or `getRole` function
564 | 5. **User is added** to the organization with the determined role
565 | 6. **User provisioning runs** (if configured) for additional setup
566 | 
567 | ### Provisioning Best Practices
568 | 
569 | #### 1. Idempotent Operations
570 | Make sure your provisioning functions can be safely run multiple times:
571 | 
572 | ```ts
573 | provisionUser: async ({ user, userInfo }) => {
574 |     // Check if already provisioned
575 |     const existingProfile = await getUserProfile(user.id);
576 |     if (!existingProfile.ssoProvisioned) {
577 |         await createUserResources(user.id);
578 |         await markAsProvisioned(user.id);
579 |     }
580 |     
581 |     // Always update attributes (they might change)
582 |     await updateUserAttributes(user.id, userInfo.attributes);
583 | },
584 | ```
585 | 
586 | #### 2. Error Handling
587 | Handle errors gracefully to avoid blocking user sign-in:
588 | 
589 | ```ts
590 | provisionUser: async ({ user, userInfo }) => {
591 |     try {
592 |         await syncWithExternalSystem(user, userInfo);
593 |     } catch (error) {
594 |         // Log error but don't throw - user can still sign in
595 |         console.error('Failed to sync user with external system:', error);
596 |         await logProvisioningError(user.id, error);
597 |     }
598 | },
599 | ```
600 | 
601 | #### 3. Conditional Provisioning
602 | Only run certain provisioning steps when needed:
603 | 
604 | ```ts
605 | organizationProvisioning: {
606 |     disabled: false,
607 |     getRole: async ({ user, userInfo, provider }) => {
608 |         // Only process role assignment for certain providers
609 |         if (provider.providerId.includes('enterprise')) {
610 |             return determineEnterpriseRole(userInfo);
611 |         }
612 |         return "member";
613 |     },
614 | },
615 | ```
616 | 
617 | ## SAML Configuration
618 | 
619 | 
620 | ### Default SSO Provider
621 | 
622 | ```ts title="auth.ts"
623 | const auth = betterAuth({
624 |     plugins: [
625 |         sso({
626 |             defaultSSO: {
627 |                 providerId: "default-saml", // Provider ID for the default provider
628 |                 samlConfig: {
629 |                     issuer: "https://your-app.com",
630 |                     entryPoint: "https://idp.example.com/sso",
631 |                     cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
632 |                     callbackUrl: "http://localhost:3000/api/auth/sso/saml2/sp/acs",
633 |                     spMetadata: {
634 |                         entityID: "http://localhost:3000/api/auth/sso/saml2/sp/metadata",
635 |                         metadata: "<!-- Your SP Metadata XML -->",
636 |                     }
637 |                 }
638 |             }
639 |         })
640 |     ]
641 | });
642 | ```
643 | 
644 | The defaultSSO provider will be used when:
645 | 1. No matching provider is found in the database
646 | 
647 | This allows you to test SAML authentication without setting up providers in the database. The defaultSSO provider supports all the same configuration options as regular SAML providers.
648 | 
649 | ### Service Provider Configuration
650 | 
651 | When registering a SAML provider, you need to provide Service Provider (SP) metadata configuration:
652 | 
653 | - **metadata**: XML metadata for the Service Provider
654 | - **binding**: The binding method, typically "post" or "redirect"
655 | - **privateKey**: Private key for signing (optional)
656 | - **privateKeyPass**: Password for the private key (if encrypted)
657 | - **isAssertionEncrypted**: Whether assertions should be encrypted
658 | - **encPrivateKey**: Private key for decryption (if encryption is enabled)
659 | - **encPrivateKeyPass**: Password for the encryption private key
660 | 
661 | ### Identity Provider Configuration
662 | 
663 | You also need to provide Identity Provider (IdP) configuration:
664 | 
665 | - **metadata**: XML metadata from your Identity Provider
666 | - **privateKey**: Private key for the IdP communication (optional)
667 | - **privateKeyPass**: Password for the IdP private key (if encrypted)
668 | - **isAssertionEncrypted**: Whether assertions from IdP are encrypted
669 | - **encPrivateKey**: Private key for IdP assertion decryption
670 | - **encPrivateKeyPass**: Password for the IdP decryption key
671 | 
672 | ### SAML Attribute Mapping
673 | 
674 | Configure how SAML attributes map to user fields:
675 | 
676 | ```ts
677 | mapping: {
678 |     id: "nameID",           // Default: "nameID"
679 |     email: "email",         // Default: "email" or "nameID"
680 |     name: "displayName",    // Default: "displayName"
681 |     firstName: "givenName", // Default: "givenName"
682 |     lastName: "surname",    // Default: "surname"
683 |     extraFields: {
684 |         department: "department",
685 |         role: "jobTitle",
686 |         phone: "telephoneNumber"
687 |     }
688 | }
689 | ```
690 | 
691 | ### SAML Endpoints
692 | 
693 | The plugin automatically creates the following SAML endpoints:
694 | 
695 | - **SP Metadata**: `/api/auth/sso/saml2/sp/metadata?providerId={providerId}`
696 | - **SAML Callback**: `/api/auth/sso/saml2/callback/{providerId}`
697 | 
698 | ## Schema
699 | 
700 | The plugin requires additional fields in the `ssoProvider` table to store the provider's configuration.
701 | 
702 | <DatabaseTable
703 |     fields={[
704 |         {
705 |             name: "id", type: "string", description: "A database identifier", isRequired: true, isPrimaryKey: true,
706 |         },
707 |         { name: "issuer", type: "string", description: "The issuer identifier", isRequired: true },
708 |         { name: "domain", type: "string", description: "The domain of the provider", isRequired: true },
709 |         { name: "oidcConfig", type: "string", description: "The OIDC configuration (JSON string)", isRequired: false },
710 |         { name: "samlConfig", type: "string", description: "The SAML configuration (JSON string)", isRequired: false },
711 |         { name: "userId", type: "string", description: "The user ID", isRequired: true, references: { model: "user", field: "id" } },
712 |         { name: "providerId", type: "string", description: "The provider ID. Used to identify a provider and to generate a redirect URL.", isRequired: true, isUnique: true },
713 |         { name: "organizationId", type: "string", description: "The organization Id. If provider is linked to an organization.", isRequired: false },
714 |     ]}
715 | />
716 | 
717 | For a detailed guide on setting up SAML SSO with examples for Okta and testing with DummyIDP, see our [SAML SSO with Okta](/docs/guides/saml-sso-with-okta).
718 | 
719 | ## Options
720 | 
721 | ### Server
722 | 
723 | **provisionUser**: A custom function to provision a user when they sign in with an SSO provider.
724 | 
725 | **organizationProvisioning**: Options for provisioning users to an organization.
726 | 
727 | **defaultOverrideUserInfo**: Override user info with the provider info by default.
728 | 
729 | **disableImplicitSignUp**: Disable implicit sign up for new users.
730 | 
731 | **trustEmailVerified**: Trust the email verified flag from the provider.
732 | 
733 | <TypeTable
734 |   type={{
735 |     provisionUser: {
736 |         description: "A custom function to provision a user when they sign in with an SSO provider.",
737 |         type: "function",
738 |     },
739 |     organizationProvisioning: {
740 |         description: "Options for provisioning users to an organization.",
741 |         type: "object",
742 |         properties: {
743 |             disabled: {
744 |                 description: "Disable organization provisioning.",
745 |                 type: "boolean",
746 |                 default: false,
747 |             },
748 |             defaultRole: {
749 |                 description: "The default role for new users.",
750 |                 type: "string",
751 |                 enum: ["member", "admin"],
752 |                 default: "member",
753 |             },
754 |             getRole: {
755 |                 description: "A custom function to determine the role for new users.",
756 |                 type: "function",
757 |             },
758 |         },
759 |     },
760 |     defaultOverrideUserInfo: {
761 |         description: "Override user info with the provider info by default.",
762 |         type: "boolean",
763 |         default: false,
764 |     },
765 |     disableImplicitSignUp: {
766 |         description: "Disable implicit sign up for new users. When set to true, sign-in needs to be called with requestSignUp as true to create new users.",
767 |         type: "boolean",
768 |         default: false,
769 |     },
770 |     providersLimit: {
771 |         description: "Configure the maximum number of SSO providers a user can register. Set to 0 to disable SSO provider registration.",
772 |         type: "number | function",
773 |         default: 10,
774 |     },
775 |     defaultSSO: {
776 |         description: "Configure a default SSO provider for testing and development. This provider will be used when no matching provider is found in the database.",
777 |         type: "object",
778 |         properties: {
779 |             domain: {
780 |                 description: "The domain to match for this default provider.",
781 |                 type: "string",
782 |                 required: true,
783 |             },
784 |             providerId: {
785 |                 description: "The provider ID to use for the default provider.",
786 |                 type: "string",
787 |                 required: true,
788 |             },
789 |             samlConfig: {
790 |                 description: "SAML configuration for the default provider.",
791 |                 type: "SAMLConfig",
792 |                 required: false,
793 |             },
794 |             oidcConfig: {
795 |                 description: "OIDC configuration for the default provider.",
796 |                 type: "OIDCConfig",
797 |                 required: false,
798 |             },
799 |         },
800 |     },
801 |   }}
802 | />
803 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/stripe.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Stripe
  3 | description: Stripe plugin for Better Auth to manage subscriptions and payments.
  4 | ---
  5 | 
  6 | The Stripe plugin integrates Stripe's payment and subscription functionality with Better Auth. Since payment and authentication are often tightly coupled, this plugin simplifies the integration of Stripe into your application, handling customer creation, subscription management, and webhook processing.
  7 | 
  8 | ## Features
  9 | 
 10 | - Create Stripe Customers automatically when users sign up
 11 | - Manage subscription plans and pricing
 12 | - Process subscription lifecycle events (creation, updates, cancellations)
 13 | - Handle Stripe webhooks securely with signature verification
 14 | - Expose subscription data to your application
 15 | - Support for trial periods and subscription upgrades
 16 | - **Automatic trial abuse prevention** - Users can only get one trial per account across all plans
 17 | - Flexible reference system to associate subscriptions with users or organizations
 18 | - Team subscription support with seats management
 19 | 
 20 | ## Installation
 21 | 
 22 | <Steps>
 23 |     <Step>
 24 |         ### Install the plugin
 25 | 
 26 |         First, install the plugin:
 27 | 
 28 |         ```package-install
 29 |         @better-auth/stripe
 30 |         ```
 31 |         <Callout>
 32 |         If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
 33 |         </Callout>
 34 |     </Step>
 35 |     <Step>
 36 |         ### Install the Stripe SDK
 37 | 
 38 |         Next, install the Stripe SDK on your server:
 39 | 
 40 |         ```package-install
 41 |         stripe@^18.0.0
 42 |         ```
 43 |     </Step>
 44 |     <Step>
 45 |         ### Add the plugin to your auth config
 46 | 
 47 |         ```ts title="auth.ts"
 48 |         import { betterAuth } from "better-auth"
 49 |         import { stripe } from "@better-auth/stripe"
 50 |         import Stripe from "stripe"
 51 | 
 52 |         const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
 53 |             apiVersion: "2025-02-24.acacia",
 54 |         })
 55 | 
 56 |         export const auth = betterAuth({
 57 |             // ... your existing config
 58 |             plugins: [
 59 |                 stripe({
 60 |                     stripeClient,
 61 |                     stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
 62 |                     createCustomerOnSignUp: true,
 63 |                 })
 64 |             ]
 65 |         })
 66 |         ```
 67 |     </Step>
 68 |     <Step>
 69 |         ### Add the client plugin
 70 | 
 71 |         ```ts title="auth-client.ts"
 72 |         import { createAuthClient } from "better-auth/client"
 73 |         import { stripeClient } from "@better-auth/stripe/client"
 74 | 
 75 |         export const client = createAuthClient({
 76 |             // ... your existing config
 77 |             plugins: [
 78 |                 stripeClient({
 79 |                     subscription: true //if you want to enable subscription management
 80 |                 })
 81 |             ]
 82 |         })
 83 |         ```
 84 |     </Step>
 85 |     <Step>
 86 |         ### Migrate the database
 87 | 
 88 |         Run the migration or generate the schema to add the necessary tables to the database.
 89 | 
 90 |         <Tabs items={["migrate", "generate"]}>
 91 |             <Tab value="migrate">
 92 |             ```bash
 93 |             npx @better-auth/cli migrate
 94 |             ```
 95 |             </Tab>
 96 |             <Tab value="generate">
 97 |             ```bash
 98 |             npx @better-auth/cli generate
 99 |             ```
100 |             </Tab>
101 |         </Tabs>
102 |         See the [Schema](#schema) section to add the tables manually.
103 |     </Step>
104 |     <Step>
105 |         ### Set up Stripe webhooks
106 | 
107 |         Create a webhook endpoint in your Stripe dashboard pointing to:
108 | 
109 |         ```
110 |         https://your-domain.com/api/auth/stripe/webhook
111 |         ```
112 |         `/api/auth` is the default path for the auth server.
113 |         
114 |         Make sure to select at least these events:
115 |         - `checkout.session.completed`
116 |         - `customer.subscription.updated`
117 |         - `customer.subscription.deleted`
118 | 
119 |         Save the webhook signing secret provided by Stripe and add it to your environment variables as `STRIPE_WEBHOOK_SECRET`.
120 |     </Step>
121 | </Steps>
122 | 
123 | ## Usage
124 | 
125 | ### Customer Management
126 | 
127 | You can use this plugin solely for customer management without enabling subscriptions. This is useful if you just want to link Stripe customers to your users.
128 | 
129 | By default, when a user signs up, a Stripe customer is automatically created if you set `createCustomerOnSignUp: true`. This customer is linked to the user in your database.
130 | You can customize the customer creation process:
131 | 
132 | ```ts title="auth.ts"
133 | stripe({
134 |     // ... other options
135 |     createCustomerOnSignUp: true,
136 |     onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
137 |         // Do something with the newly created customer
138 |         console.log(`Customer ${customer.id} created for user ${user.id}`);
139 |     },
140 |     getCustomerCreateParams: async ({ user, session }, request) => {
141 |         // Customize the Stripe customer creation parameters
142 |         return {
143 |             metadata: {
144 |                 referralSource: user.metadata?.referralSource
145 |             }
146 |         };
147 |     }
148 | })
149 | ```
150 | 
151 | ### Subscription Management
152 | 
153 | #### Defining Plans
154 | 
155 | You can define your subscription plans either statically or dynamically:
156 | 
157 | ```ts title="auth.ts"
158 | // Static plans
159 | subscription: {
160 |     enabled: true,
161 |     plans: [
162 |         {
163 |             name: "basic", // the name of the plan, it'll be automatically lower cased when stored in the database
164 |             priceId: "price_1234567890", // the price ID from stripe
165 |             annualDiscountPriceId: "price_1234567890", // (optional) the price ID for annual billing with a discount
166 |             limits: {
167 |                 projects: 5,
168 |                 storage: 10
169 |             }
170 |         },
171 |         {
172 |             name: "pro",
173 |             priceId: "price_0987654321",
174 |             limits: {
175 |                 projects: 20,
176 |                 storage: 50
177 |             },
178 |             freeTrial: {
179 |                 days: 14,
180 |             }
181 |         }
182 |     ]
183 | }
184 | 
185 | // Dynamic plans (fetched from database or API)
186 | subscription: {
187 |     enabled: true,
188 |     plans: async () => {
189 |         const plans = await db.query("SELECT * FROM plans");
190 |         return plans.map(plan => ({
191 |             name: plan.name,
192 |             priceId: plan.stripe_price_id,
193 |             limits: JSON.parse(plan.limits)
194 |         }));
195 |     }
196 | }
197 | ```
198 | 
199 | see [plan configuration](#plan-configuration) for more.
200 | 
201 | #### Creating a Subscription
202 | 
203 | To create a subscription, use the `subscription.upgrade` method:
204 | 
205 | <APIMethod
206 |   path="/subscription/upgrade"
207 |   method="POST"
208 |   requireSession
209 | >
210 | ```ts
211 | type upgradeSubscription = {
212 |     /**
213 |      * The name of the plan to upgrade to. 
214 |      */
215 |     plan: string = "pro"
216 |     /**
217 |      * Whether to upgrade to an annual plan. 
218 |      */
219 |     annual?: boolean = true
220 |     /**
221 |      * Reference id of the subscription to upgrade. 
222 |      */
223 |     referenceId?: string = "123"
224 |     /**
225 |      * The id of the subscription to upgrade. 
226 |      */
227 |     subscriptionId?: string = "sub_123"
228 |     metadata?: Record<string, any>
229 |     /**
230 |      * Number of seats to upgrade to (if applicable). 
231 |      */
232 |     seats?: number = 1
233 |     /**
234 |      * Callback URL to redirect back after successful subscription. 
235 |      */
236 |     successUrl: string
237 |     /**
238 |      * If set, checkout shows a back button and customers will be directed here if they cancel payment.
239 |      */
240 |     cancelUrl: string 
241 |     /**
242 |      * URL to take customers to when they click on the billing portal’s link to return to your website.
243 |      */
244 |     returnUrl?: string
245 |     /**
246 |      * Disable redirect after successful subscription. 
247 |      */
248 |     disableRedirect: boolean = true
249 | }
250 | ```
251 | </APIMethod>
252 | 
253 | **Simple Example:**
254 | 
255 | ```ts title="client.ts"
256 | await client.subscription.upgrade({
257 |     plan: "pro",
258 |     successUrl: "/dashboard",
259 |     cancelUrl: "/pricing",
260 |     annual: true, // Optional: upgrade to an annual plan
261 |     referenceId: "org_123", // Optional: defaults to the current logged in user ID
262 |     seats: 5 // Optional: for team plans
263 | });
264 | ```
265 | 
266 | This will create a Checkout Session and redirect the user to the Stripe Checkout page.
267 | 
268 | <Callout type="warn">
269 | If the user already has an active subscription, you *must* provide the `subscriptionId` parameter. Otherwise, the user will be subscribed to (and pay for) both plans.
270 | </Callout>
271 | 
272 | > **Important:** The `successUrl` parameter will be internally modified to handle race conditions between checkout completion and webhook processing. The plugin creates an intermediate redirect that ensures subscription status is properly updated before redirecting to your success page.
273 | 
274 | ```ts
275 | const { error } = await client.subscription.upgrade({
276 |     plan: "pro",
277 |     successUrl: "/dashboard",
278 |     cancelUrl: "/pricing",
279 | });
280 | if(error) {
281 |     alert(error.message);
282 | }
283 | ```
284 | 
285 | <Callout type="warn">
286 | For each reference ID (user or organization), only one active or trialing subscription is supported at a time. The plugin doesn't currently support multiple concurrent active subscriptions for the same reference ID.
287 | </Callout>
288 | 
289 | #### Switching Plans
290 | 
291 | To switch a subscription to a different plan, use the `subscription.upgrade` method:
292 | ```ts title="client.ts"
293 | await client.subscription.upgrade({
294 |     plan: "pro",
295 |     successUrl: "/dashboard",
296 |     cancelUrl: "/pricing",
297 |     subscriptionId: "sub_123", // the Stripe subscription ID of the user's current plan
298 | });
299 | ```
300 | This ensures that the user only pays for the new plan, and not both.
301 | 
302 | #### Listing Active Subscriptions
303 | 
304 | To get the user's active subscriptions:
305 | 
306 | <APIMethod
307 |   path="/subscription/list"
308 |   method="GET"
309 |   requireSession
310 |   resultVariable="subscriptions"
311 | >
312 | ```ts
313 | type listActiveSubscriptions = {
314 |     /**
315 |      * Reference id of the subscription to list. 
316 |      */
317 |     referenceId?: string = '123'
318 | }
319 | 
320 | // get the active subscription
321 | const activeSubscription = subscriptions.find(
322 |     sub => sub.status === "active" || sub.status === "trialing"
323 | );
324 | 
325 | // Check subscription limits
326 | const projectLimit = subscriptions?.limits?.projects || 0;
327 | ```
328 | </APIMethod>
329 | 
330 | #### Canceling a Subscription
331 | 
332 | To cancel a subscription:
333 | 
334 | <APIMethod
335 |   path="/subscription/cancel"
336 |   method="POST"
337 |   requireSession
338 | >
339 | ```ts
340 | type cancelSubscription = {
341 |     /**
342 |      * Reference id of the subscription to cancel. Defaults to the userId.
343 |      */
344 |     referenceId?: string = 'org_123'
345 |     /**
346 |      * The id of the subscription to cancel. 
347 |      */
348 |     subscriptionId?: string = 'sub_123'
349 |     /**
350 |      * URL to take customers to when they click on the billing portal’s link to return to your website.
351 |      */
352 |     returnUrl: string = '/account'
353 | }
354 | ```
355 | </APIMethod>
356 | 
357 | This will redirect the user to the Stripe Billing Portal where they can cancel their subscription.
358 | 
359 | #### Restoring a Canceled Subscription
360 | 
361 | If a user changes their mind after canceling a subscription (but before the subscription period ends), you can restore the subscription:
362 | 
363 | 
364 | <APIMethod
365 |   path="/subscription/restore"
366 |   method="POST"
367 |   requireSession
368 | >
369 | ```ts
370 | type restoreSubscription = {
371 |     /**
372 |      * Reference id of the subscription to restore. Defaults to the userId.
373 |      */
374 |     referenceId?: string = '123'
375 |     /**
376 |      * The id of the subscription to restore. 
377 |      */
378 |     subscriptionId?: string = 'sub_123'
379 | }
380 | ```
381 | </APIMethod>
382 | 
383 | 
384 | This will reactivate a subscription that was previously set to cancel at the end of the billing period (`cancelAtPeriodEnd: true`). The subscription will continue to renew automatically.
385 | 
386 | > **Note:** This only works for subscriptions that are still active but marked to cancel at the end of the period. It cannot restore subscriptions that have already ended.
387 | 
388 | #### Creating Billing Portal Sessions
389 | 
390 | To create a [Stripe billing portal session](https://docs.stripe.com/api/customer_portal/sessions/create) where customers can manage their subscriptions, update payment methods, and view billing history:
391 | 
392 | <APIMethod
393 |   path="/subscription/billing-portal"
394 |   method="POST"
395 |   requireSession
396 | >
397 | ```ts
398 | type createBillingPortal = {
399 |     /**
400 |     * The IETF language tag of the locale customer portal is displayed in. If blank or auto, browser's locale is used.
401 |     */
402 |     locale?: string
403 |     /**
404 |      * Reference id of the subscription to upgrade. 
405 |      */
406 |     referenceId?: string = "123"
407 |     /**
408 |      * Return URL to redirect back after successful subscription. 
409 |      */
410 |     returnUrl?: string
411 | }
412 | ```
413 | </APIMethod>
414 | <Callout type="info" >
415 | For supported locales, see the [IETF language tag documentation](https://docs.stripe.com/js/appendix/supported_locales).
416 | </Callout>
417 |         
418 | This endpoint creates a Stripe billing portal session and returns a URL in the response as `data.url`. You can redirect users to this URL to allow them to manage their subscription, payment methods, and billing history.
419 | 
420 | ### Reference System
421 | 
422 | By default, subscriptions are associated with the user ID. However, you can use a custom reference ID to associate subscriptions with other entities, such as organizations:
423 | 
424 | ```ts title="client.ts"
425 | // Create a subscription for an organization
426 | await client.subscription.upgrade({
427 |     plan: "pro",
428 |     referenceId: "org_123456",
429 |     successUrl: "/dashboard",
430 |     cancelUrl: "/pricing",
431 |     seats: 5 // Number of seats for team plans
432 | });
433 | 
434 | // List subscriptions for an organization
435 | const { data: subscriptions } = await client.subscription.list({
436 |     query: {
437 |         referenceId: "org_123456"
438 |     }
439 | });
440 | ```
441 | 
442 | #### Team Subscriptions with Seats
443 | 
444 | For team or organization plans, you can specify the number of seats:
445 | 
446 | ```ts
447 | await client.subscription.upgrade({
448 |     plan: "team",
449 |     referenceId: "org_123456",
450 |     seats: 10, // 10 team members
451 |     successUrl: "/org/billing/success",
452 |     cancelUrl: "/org/billing"
453 | });
454 | ```
455 | 
456 | The `seats` parameter is passed to Stripe as the quantity for the subscription item. You can use this value in your application logic to limit the number of members in a team or organization.
457 | 
458 | To authorize reference IDs, implement the `authorizeReference` function:
459 | 
460 | ```ts title="auth.ts"
461 | subscription: {
462 |     // ... other options
463 |     authorizeReference: async ({ user, session, referenceId, action }) => {
464 |         // Check if the user has permission to manage subscriptions for this reference
465 |         if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
466 |             const org = await db.member.findFirst({
467 |                 where: {
468 |                     organizationId: referenceId,
469 |                     userId: user.id
470 |                 }   
471 |             });
472 |             return org?.role === "owner"
473 |         }
474 |         return true;
475 |     }
476 | }
477 | ```
478 | 
479 | ### Webhook Handling
480 | 
481 | The plugin automatically handles common webhook events:
482 | 
483 | - `checkout.session.completed`: Updates subscription status after checkout
484 | - `customer.subscription.updated`: Updates subscription details when changed
485 | - `customer.subscription.deleted`: Marks subscription as canceled
486 | 
487 | You can also handle custom events:
488 | 
489 | ```ts title="auth.ts"
490 | stripe({
491 |     // ... other options
492 |     onEvent: async (event) => {
493 |         // Handle any Stripe event
494 |         switch (event.type) {
495 |             case "invoice.paid":
496 |                 // Handle paid invoice
497 |                 break;
498 |             case "payment_intent.succeeded":
499 |                 // Handle successful payment
500 |                 break;
501 |         }
502 |     }
503 | })
504 | ```
505 | 
506 | ### Subscription Lifecycle Hooks
507 | 
508 | You can hook into various subscription lifecycle events:
509 | 
510 | ```ts title="auth.ts"
511 | subscription: {
512 |     // ... other options
513 |     onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
514 |         // Called when a subscription is successfully created
515 |         await sendWelcomeEmail(subscription.referenceId, plan.name);
516 |     },
517 |     onSubscriptionUpdate: async ({ event, subscription }) => {
518 |         // Called when a subscription is updated
519 |         console.log(`Subscription ${subscription.id} updated`);
520 |     },
521 |     onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
522 |         // Called when a subscription is canceled
523 |         await sendCancellationEmail(subscription.referenceId);
524 |     },
525 |     onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
526 |         // Called when a subscription is deleted
527 |         console.log(`Subscription ${subscription.id} deleted`);
528 |     }
529 | }
530 | ```
531 | 
532 | ### Trial Periods
533 | 
534 | You can configure trial periods for your plans:
535 | 
536 | ```ts title="auth.ts"
537 | {
538 |     name: "pro",
539 |     priceId: "price_0987654321",
540 |     freeTrial: {
541 |         days: 14,
542 |         onTrialStart: async (subscription) => {
543 |             // Called when a trial starts
544 |             await sendTrialStartEmail(subscription.referenceId);
545 |         },
546 |         onTrialEnd: async ({ subscription, user }, request) => {
547 |             // Called when a trial ends
548 |             await sendTrialEndEmail(user.email);
549 |         },
550 |         onTrialExpired: async (subscription) => {
551 |             // Called when a trial expires without conversion
552 |             await sendTrialExpiredEmail(subscription.referenceId);
553 |         }
554 |     }
555 | }
556 | ```
557 | 
558 | ## Schema
559 | 
560 | The Stripe plugin adds the following tables to your database:
561 | 
562 | 
563 | ### User
564 | 
565 | Table Name: `user`
566 | 
567 | <DatabaseTable
568 |   fields={[
569 |     { 
570 |       name: "stripeCustomerId", 
571 |       type: "string", 
572 |       description: "The Stripe customer ID",
573 |       isOptional: true
574 |     },
575 |   ]}
576 | />
577 | 
578 | ### Subscription
579 | 
580 | Table Name: `subscription`
581 | 
582 | <DatabaseTable
583 |   fields={[
584 |     { 
585 |       name: "id", 
586 |       type: "string", 
587 |       description: "Unique identifier for each subscription",
588 |       isPrimaryKey: true
589 |     },
590 |     { 
591 |       name: "plan", 
592 |       type: "string", 
593 |       description: "The name of the subscription plan" 
594 |     },
595 |     { 
596 |       name: "referenceId", 
597 |       type: "string", 
598 |       description: "The ID this subscription is associated with (user ID by default)",
599 |       isUnique: true
600 |     },
601 |     { 
602 |       name: "stripeCustomerId", 
603 |       type: "string", 
604 |       description: "The Stripe customer ID",
605 |       isOptional: true
606 |     },
607 |     { 
608 |       name: "stripeSubscriptionId", 
609 |       type: "string", 
610 |       description: "The Stripe subscription ID",
611 |       isOptional: true
612 |     },
613 |     { 
614 |       name: "status", 
615 |       type: "string", 
616 |       description: "The status of the subscription (active, canceled, etc.)",
617 |       defaultValue: "incomplete"
618 |     },
619 |     { 
620 |       name: "periodStart", 
621 |       type: "Date", 
622 |       description: "Start date of the current billing period",
623 |       isOptional: true
624 |     },
625 |     { 
626 |       name: "periodEnd", 
627 |       type: "Date", 
628 |       description: "End date of the current billing period",
629 |       isOptional: true
630 |     },
631 |     { 
632 |       name: "cancelAtPeriodEnd", 
633 |       type: "boolean", 
634 |       description: "Whether the subscription will be canceled at the end of the period",
635 |       defaultValue: false,
636 |       isOptional: true
637 |     },
638 |     { 
639 |       name: "seats", 
640 |       type: "number", 
641 |       description: "Number of seats for team plans",
642 |       isOptional: true
643 |     },
644 |     { 
645 |       name: "trialStart", 
646 |       type: "Date", 
647 |       description: "Start date of the trial period",
648 |       isOptional: true
649 |     },
650 |     { 
651 |       name: "trialEnd", 
652 |       type: "Date", 
653 |       description: "End date of the trial period",
654 |       isOptional: true
655 |     }
656 |   ]}
657 | />
658 | 
659 | ### Customizing the Schema
660 | 
661 | To change the schema table names or fields, you can pass a `schema` option to the Stripe plugin:
662 | 
663 | ```ts title="auth.ts"
664 | stripe({
665 |     // ... other options
666 |     schema: {
667 |         subscription: {
668 |             modelName: "stripeSubscriptions", // map the subscription table to stripeSubscriptions
669 |             fields: {
670 |                 plan: "planName" // map the plan field to planName
671 |             }
672 |         }
673 |     }
674 | })
675 | ```
676 | 
677 | ## Options
678 | 
679 | ### Main Options
680 | 
681 | **stripeClient**: `Stripe` - The Stripe client instance. Required.
682 | 
683 | **stripeWebhookSecret**: `string` - The webhook signing secret from Stripe. Required.
684 | 
685 | **createCustomerOnSignUp**: `boolean` - Whether to automatically create a Stripe customer when a user signs up. Default: `false`.
686 | 
687 | **onCustomerCreate**: `(data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise<void>` - A function called after a customer is created.
688 | 
689 | **getCustomerCreateParams**: `(data: { user: User, session: Session }, request?: Request) => Promise<{}>` - A function to customize the Stripe customer creation parameters.
690 | 
691 | **onEvent**: `(event: Stripe.Event) => Promise<void>` - A function called for any Stripe webhook event.
692 | 
693 | ### Subscription Options
694 | 
695 | **enabled**: `boolean` - Whether to enable subscription functionality. Required.
696 | 
697 | **plans**: `Plan[] | (() => Promise<Plan[]>)` - An array of subscription plans or a function that returns plans. Required if subscriptions are enabled.
698 | 
699 | **requireEmailVerification**: `boolean` - Whether to require email verification before allowing subscription upgrades. Default: `false`.
700 | 
701 | **authorizeReference**: `(data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, request?: Request) => Promise<boolean>` - A function to authorize reference IDs.
702 | 
703 | ### Plan Configuration
704 | 
705 | Each plan can have the following properties:
706 | 
707 | **name**: `string` - The name of the plan. Required.
708 | 
709 | **priceId**: `string` - The Stripe price ID. Required unless using `lookupKey`.
710 | 
711 | **lookupKey**: `string` - The Stripe price lookup key. Alternative to `priceId`.
712 | 
713 | **annualDiscountPriceId**: `string` - A price ID for annual billing.
714 | 
715 | **annualDiscountLookupKey**: `string` - The Stripe price lookup key for annual billing. Alternative to `annualDiscountPriceId`.
716 | 
717 | **limits**: `Record<string, number>` - Limits associated with the plan (e.g., `{ projects: 10, storage: 5 }`).
718 | 
719 | **group**: `string` - A group name for the plan, useful for categorizing plans.
720 | 
721 | **freeTrial**: Object containing trial configuration:
722 |   - **days**: `number` - Number of trial days.
723 |   - **onTrialStart**: `(subscription: Subscription) => Promise<void>` - Called when a trial starts.
724 |   - **onTrialEnd**: `(data: { subscription: Subscription, user: User }, request?: Request) => Promise<void>` - Called when a trial ends.
725 |   - **onTrialExpired**: `(subscription: Subscription) => Promise<void>` - Called when a trial expires without conversion.
726 | 
727 | ## Advanced Usage
728 | 
729 | ### Using with Organizations
730 | 
731 | The Stripe plugin works well with the organization plugin. You can associate subscriptions with organizations instead of individual users:
732 | 
733 | ```ts title="client.ts"
734 | // Get the active organization
735 | const { data: activeOrg } = client.useActiveOrganization();
736 | 
737 | // Create a subscription for the organization
738 | await client.subscription.upgrade({
739 |     plan: "team",
740 |     referenceId: activeOrg.id,
741 |     seats: 10,
742 |     annual: true, // upgrade to an annual plan (optional)
743 |     successUrl: "/org/billing/success",
744 |     cancelUrl: "/org/billing"
745 | });
746 | ```
747 | 
748 | Make sure to implement the `authorizeReference` function to verify that the user has permission to manage subscriptions for the organization:
749 | 
750 | ```ts title="auth.ts"
751 | authorizeReference: async ({ user, referenceId, action }) => {
752 |     const member = await db.members.findFirst({
753 |         where: {
754 |             userId: user.id,
755 |             organizationId: referenceId
756 |         }
757 |     });
758 |     
759 |     return member?.role === "owner" || member?.role === "admin";
760 | }
761 | ```
762 | 
763 | ### Custom Checkout Session Parameters
764 | 
765 | You can customize the Stripe Checkout session with additional parameters:
766 | 
767 | ```ts title="auth.ts"
768 | getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
769 |     return {
770 |         params: {
771 |             allow_promotion_codes: true,
772 |             tax_id_collection: {
773 |                 enabled: true
774 |             },
775 |             billing_address_collection: "required",
776 |             custom_text: {
777 |                 submit: {
778 |                     message: "We'll start your subscription right away"
779 |                 }
780 |             },
781 |             metadata: {
782 |                 planType: "business",
783 |                 referralCode: user.metadata?.referralCode
784 |             }
785 |         },
786 |         options: {
787 |             idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
788 |         }
789 |     };
790 | }
791 | ```
792 | 
793 | ### Tax Collection
794 | 
795 | To collect tax IDs from the customer, set `tax_id_collection` to true:
796 | 
797 | ```ts title="auth.ts"
798 | subscription: {
799 |     // ... other options
800 |     getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
801 |         return {
802 |             params: {
803 |                 tax_id_collection: {
804 |                     enabled: true
805 |                 }
806 |             }
807 |         };
808 |     }
809 | }
810 | ```
811 | 
812 | ### Automatic Tax Calculation
813 | 
814 | To enable automatic tax calculation using the customer's location, set `automatic_tax` to true. Enabling this parameter causes Checkout to collect any billing address information necessary for tax calculation. You need to have tax registration setup and configured in the Stripe dashboard first for this to work.
815 | 
816 | ```ts title="auth.ts"
817 | subscription: {
818 |     // ... other options
819 |     getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
820 |         return {
821 |             params: {
822 |                 automatic_tax: {
823 |                     enabled: true
824 |                 }
825 |             }
826 |         };
827 |     }
828 | }
829 | ```
830 | 
831 | ### Trial Period Management
832 | 
833 | The Stripe plugin automatically prevents users from getting multiple free trials. Once a user has used a trial period (regardless of which plan), they will not be eligible for additional trials on any plan.
834 | 
835 | **How it works:**
836 | - The system tracks trial usage across all plans for each user
837 | - When a user subscribes to a plan with a trial, the system checks their subscription history
838 | - If the user has ever had a trial (indicated by `trialStart`/`trialEnd` fields or `trialing` status), no new trial will be offered
839 | - This prevents abuse where users cancel subscriptions and resubscribe to get multiple free trials
840 | 
841 | **Example scenario:**
842 | 1. User subscribes to "Starter" plan with 7-day trial
843 | 2. User cancels the subscription after the trial
844 | 3. User tries to subscribe to "Premium" plan - no trial will be offered
845 | 4. User will be charged immediately for the Premium plan
846 | 
847 | This behavior is automatic and requires no additional configuration. The trial eligibility is determined at the time of subscription creation and cannot be overridden through configuration.
848 | 
849 | ## Troubleshooting
850 | 
851 | ### Webhook Issues
852 | 
853 | If webhooks aren't being processed correctly:
854 | 
855 | 1. Check that your webhook URL is correctly configured in the Stripe dashboard
856 | 2. Verify that the webhook signing secret is correct
857 | 3. Ensure you've selected all the necessary events in the Stripe dashboard
858 | 4. Check your server logs for any errors during webhook processing
859 | 
860 | ### Subscription Status Issues
861 | 
862 | If subscription statuses aren't updating correctly:
863 | 
864 | 1. Make sure the webhook events are being received and processed
865 | 2. Check that the `stripeCustomerId` and `stripeSubscriptionId` fields are correctly populated
866 | 3. Verify that the reference IDs match between your application and Stripe
867 | 
868 | ### Testing Webhooks Locally
869 | 
870 | For local development, you can use the Stripe CLI to forward webhooks to your local environment:
871 | 
872 | ```bash
873 | stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
874 | ```
875 | 
876 | This will provide you with a webhook signing secret that you can use in your local environment.
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/routes/crud-invites.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as z from "zod";
   2 | import { createAuthEndpoint } from "@better-auth/core/middleware";
   3 | import { getSessionFromCtx } from "../../../api/routes";
   4 | import { getOrgAdapter } from "../adapter";
   5 | import { orgMiddleware, orgSessionMiddleware } from "../call";
   6 | import {
   7 | 	type InferOrganizationRolesFromOption,
   8 | 	type Invitation,
   9 | } from "../schema";
  10 | import { APIError } from "better-call";
  11 | import { parseRoles } from "../organization";
  12 | import { type OrganizationOptions } from "../types";
  13 | import { ORGANIZATION_ERROR_CODES } from "../error-codes";
  14 | import { hasPermission } from "../has-permission";
  15 | import { setSessionCookie } from "../../../cookies";
  16 | import {
  17 | 	toZodSchema,
  18 | 	type InferAdditionalFieldsFromPluginOptions,
  19 | } from "../../../db";
  20 | import { getDate } from "../../../utils/date";
  21 | 
  22 | export const createInvitation = <O extends OrganizationOptions>(option: O) => {
  23 | 	const additionalFieldsSchema = toZodSchema({
  24 | 		fields: option?.schema?.invitation?.additionalFields || {},
  25 | 		isClientSide: true,
  26 | 	});
  27 | 
  28 | 	const baseSchema = z.object({
  29 | 		email: z.string().meta({
  30 | 			description: "The email address of the user to invite",
  31 | 		}),
  32 | 		role: z
  33 | 			.union([
  34 | 				z.string().meta({
  35 | 					description: "The role to assign to the user",
  36 | 				}),
  37 | 				z.array(
  38 | 					z.string().meta({
  39 | 						description: "The roles to assign to the user",
  40 | 					}),
  41 | 				),
  42 | 			])
  43 | 			.meta({
  44 | 				description:
  45 | 					'The role(s) to assign to the user. It can be `admin`, `member`, or `guest`. Eg: "member"',
  46 | 			}),
  47 | 		organizationId: z
  48 | 			.string()
  49 | 			.meta({
  50 | 				description: "The organization ID to invite the user to",
  51 | 			})
  52 | 			.optional(),
  53 | 		resend: z
  54 | 			.boolean()
  55 | 			.meta({
  56 | 				description:
  57 | 					"Resend the invitation email, if the user is already invited. Eg: true",
  58 | 			})
  59 | 			.optional(),
  60 | 		teamId: z.union([
  61 | 			z
  62 | 				.string()
  63 | 				.meta({
  64 | 					description: "The team ID to invite the user to",
  65 | 				})
  66 | 				.optional(),
  67 | 			z
  68 | 				.array(z.string())
  69 | 				.meta({
  70 | 					description: "The team IDs to invite the user to",
  71 | 				})
  72 | 				.optional(),
  73 | 		]),
  74 | 	});
  75 | 
  76 | 	return createAuthEndpoint(
  77 | 		"/organization/invite-member",
  78 | 		{
  79 | 			method: "POST",
  80 | 			use: [orgMiddleware, orgSessionMiddleware],
  81 | 			body: z.object({
  82 | 				...baseSchema.shape,
  83 | 				...additionalFieldsSchema.shape,
  84 | 			}),
  85 | 			metadata: {
  86 | 				$Infer: {
  87 | 					body: {} as {
  88 | 						/**
  89 | 						 * The email address of the user
  90 | 						 * to invite
  91 | 						 */
  92 | 						email: string;
  93 | 						/**
  94 | 						 * The role to assign to the user
  95 | 						 */
  96 | 						role:
  97 | 							| InferOrganizationRolesFromOption<O>
  98 | 							| InferOrganizationRolesFromOption<O>[];
  99 | 						/**
 100 | 						 * The organization ID to invite
 101 | 						 * the user to
 102 | 						 */
 103 | 						organizationId?: string | undefined;
 104 | 						/**
 105 | 						 * Resend the invitation email, if
 106 | 						 * the user is already invited
 107 | 						 */
 108 | 						resend?: boolean;
 109 | 					} & (O extends { teams: { enabled: true } }
 110 | 						? {
 111 | 								/**
 112 | 								 * The team the user is
 113 | 								 * being invited to.
 114 | 								 */
 115 | 								teamId?: string | string[];
 116 | 							}
 117 | 						: {}) &
 118 | 						InferAdditionalFieldsFromPluginOptions<"invitation", O, false>,
 119 | 				},
 120 | 				openapi: {
 121 | 					description: "Invite a user to an organization",
 122 | 					responses: {
 123 | 						"200": {
 124 | 							description: "Success",
 125 | 							content: {
 126 | 								"application/json": {
 127 | 									schema: {
 128 | 										type: "object",
 129 | 										properties: {
 130 | 											id: {
 131 | 												type: "string",
 132 | 											},
 133 | 											email: {
 134 | 												type: "string",
 135 | 											},
 136 | 											role: {
 137 | 												type: "string",
 138 | 											},
 139 | 											organizationId: {
 140 | 												type: "string",
 141 | 											},
 142 | 											inviterId: {
 143 | 												type: "string",
 144 | 											},
 145 | 											status: {
 146 | 												type: "string",
 147 | 											},
 148 | 											expiresAt: {
 149 | 												type: "string",
 150 | 											},
 151 | 											createdAt: {
 152 | 												type: "string",
 153 | 											},
 154 | 										},
 155 | 										required: [
 156 | 											"id",
 157 | 											"email",
 158 | 											"role",
 159 | 											"organizationId",
 160 | 											"inviterId",
 161 | 											"status",
 162 | 											"expiresAt",
 163 | 											"createdAt",
 164 | 										],
 165 | 									},
 166 | 								},
 167 | 							},
 168 | 						},
 169 | 					},
 170 | 				},
 171 | 			},
 172 | 		},
 173 | 		async (ctx) => {
 174 | 			const session = ctx.context.session;
 175 | 			const organizationId =
 176 | 				ctx.body.organizationId || session.session.activeOrganizationId;
 177 | 			if (!organizationId) {
 178 | 				throw new APIError("BAD_REQUEST", {
 179 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 180 | 				});
 181 | 			}
 182 | 			const adapter = getOrgAdapter<O>(ctx.context, option as O);
 183 | 			const member = await adapter.findMemberByOrgId({
 184 | 				userId: session.user.id,
 185 | 				organizationId: organizationId,
 186 | 			});
 187 | 			if (!member) {
 188 | 				throw new APIError("BAD_REQUEST", {
 189 | 					message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
 190 | 				});
 191 | 			}
 192 | 			const canInvite = await hasPermission(
 193 | 				{
 194 | 					role: member.role,
 195 | 					options: ctx.context.orgOptions,
 196 | 					permissions: {
 197 | 						invitation: ["create"],
 198 | 					},
 199 | 					organizationId,
 200 | 				},
 201 | 				ctx,
 202 | 			);
 203 | 
 204 | 			if (!canInvite) {
 205 | 				throw new APIError("FORBIDDEN", {
 206 | 					message:
 207 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
 208 | 				});
 209 | 			}
 210 | 
 211 | 			const creatorRole = ctx.context.orgOptions.creatorRole || "owner";
 212 | 
 213 | 			const roles = parseRoles(ctx.body.role as string | string[]);
 214 | 
 215 | 			if (
 216 | 				member.role !== creatorRole &&
 217 | 				roles.split(",").includes(creatorRole)
 218 | 			) {
 219 | 				throw new APIError("FORBIDDEN", {
 220 | 					message:
 221 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE,
 222 | 				});
 223 | 			}
 224 | 
 225 | 			const alreadyMember = await adapter.findMemberByEmail({
 226 | 				email: ctx.body.email,
 227 | 				organizationId: organizationId,
 228 | 			});
 229 | 			if (alreadyMember) {
 230 | 				throw new APIError("BAD_REQUEST", {
 231 | 					message:
 232 | 						ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION,
 233 | 				});
 234 | 			}
 235 | 			const alreadyInvited = await adapter.findPendingInvitation({
 236 | 				email: ctx.body.email,
 237 | 				organizationId: organizationId,
 238 | 			});
 239 | 			if (alreadyInvited.length && !ctx.body.resend) {
 240 | 				throw new APIError("BAD_REQUEST", {
 241 | 					message:
 242 | 						ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION,
 243 | 				});
 244 | 			}
 245 | 
 246 | 			const organization = await adapter.findOrganizationById(organizationId);
 247 | 			if (!organization) {
 248 | 				throw new APIError("BAD_REQUEST", {
 249 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 250 | 				});
 251 | 			}
 252 | 
 253 | 			// If resend is true and there's an existing invitation, reuse it
 254 | 			if (alreadyInvited.length && ctx.body.resend) {
 255 | 				const existingInvitation = alreadyInvited[0];
 256 | 
 257 | 				// Update the invitation's expiration date using the same logic as createInvitation
 258 | 				const defaultExpiration = 60 * 60 * 48; // 48 hours in seconds
 259 | 				const newExpiresAt = getDate(
 260 | 					ctx.context.orgOptions.invitationExpiresIn || defaultExpiration,
 261 | 					"sec",
 262 | 				);
 263 | 
 264 | 				await ctx.context.adapter.update({
 265 | 					model: "invitation",
 266 | 					where: [
 267 | 						{
 268 | 							field: "id",
 269 | 							value: existingInvitation!.id,
 270 | 						},
 271 | 					],
 272 | 					update: {
 273 | 						expiresAt: newExpiresAt,
 274 | 					},
 275 | 				});
 276 | 
 277 | 				const updatedInvitation = {
 278 | 					...existingInvitation,
 279 | 					expiresAt: newExpiresAt,
 280 | 				};
 281 | 
 282 | 				await ctx.context.orgOptions.sendInvitationEmail?.(
 283 | 					{
 284 | 						id: updatedInvitation.id!,
 285 | 						role: updatedInvitation.role! as string,
 286 | 						email: updatedInvitation.email!.toLowerCase(),
 287 | 						organization: organization,
 288 | 						inviter: {
 289 | 							...member,
 290 | 							user: session.user,
 291 | 						},
 292 | 						invitation: updatedInvitation as unknown as Invitation,
 293 | 					},
 294 | 					ctx.request,
 295 | 				);
 296 | 
 297 | 				return ctx.json(updatedInvitation);
 298 | 			}
 299 | 
 300 | 			if (
 301 | 				alreadyInvited.length &&
 302 | 				ctx.context.orgOptions.cancelPendingInvitationsOnReInvite
 303 | 			) {
 304 | 				await adapter.updateInvitation({
 305 | 					invitationId: alreadyInvited[0]!.id,
 306 | 					status: "canceled",
 307 | 				});
 308 | 			}
 309 | 
 310 | 			const invitationLimit =
 311 | 				typeof ctx.context.orgOptions.invitationLimit === "function"
 312 | 					? await ctx.context.orgOptions.invitationLimit(
 313 | 							{
 314 | 								user: session.user,
 315 | 								organization,
 316 | 								member: member,
 317 | 							},
 318 | 							ctx.context,
 319 | 						)
 320 | 					: (ctx.context.orgOptions.invitationLimit ?? 100);
 321 | 
 322 | 			const pendingInvitations = await adapter.findPendingInvitations({
 323 | 				organizationId: organizationId,
 324 | 			});
 325 | 
 326 | 			if (pendingInvitations.length >= invitationLimit) {
 327 | 				throw new APIError("FORBIDDEN", {
 328 | 					message: ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED,
 329 | 				});
 330 | 			}
 331 | 
 332 | 			if (
 333 | 				ctx.context.orgOptions.teams &&
 334 | 				ctx.context.orgOptions.teams.enabled &&
 335 | 				typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !==
 336 | 					"undefined" &&
 337 | 				"teamId" in ctx.body &&
 338 | 				ctx.body.teamId
 339 | 			) {
 340 | 				const teamIds =
 341 | 					typeof ctx.body.teamId === "string"
 342 | 						? [ctx.body.teamId as string]
 343 | 						: (ctx.body.teamId as string[]);
 344 | 
 345 | 				for (const teamId of teamIds) {
 346 | 					const team = await adapter.findTeamById({
 347 | 						teamId,
 348 | 						organizationId: organizationId,
 349 | 						includeTeamMembers: true,
 350 | 					});
 351 | 
 352 | 					if (!team) {
 353 | 						throw new APIError("BAD_REQUEST", {
 354 | 							message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
 355 | 						});
 356 | 					}
 357 | 
 358 | 					const maximumMembersPerTeam =
 359 | 						typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
 360 | 						"function"
 361 | 							? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
 362 | 									teamId,
 363 | 									session: session,
 364 | 									organizationId: organizationId,
 365 | 								})
 366 | 							: ctx.context.orgOptions.teams.maximumMembersPerTeam;
 367 | 					if (team.members.length >= maximumMembersPerTeam) {
 368 | 						throw new APIError("FORBIDDEN", {
 369 | 							message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
 370 | 						});
 371 | 					}
 372 | 				}
 373 | 			}
 374 | 
 375 | 			const teamIds: string[] =
 376 | 				"teamId" in ctx.body
 377 | 					? typeof ctx.body.teamId === "string"
 378 | 						? [ctx.body.teamId as string]
 379 | 						: ((ctx.body.teamId as string[]) ?? [])
 380 | 					: [];
 381 | 
 382 | 			const {
 383 | 				email: _,
 384 | 				role: __,
 385 | 				organizationId: ___,
 386 | 				resend: ____,
 387 | 				...additionalFields
 388 | 			} = ctx.body;
 389 | 
 390 | 			let invitationData = {
 391 | 				role: roles,
 392 | 				email: ctx.body.email.toLowerCase(),
 393 | 				organizationId: organizationId,
 394 | 				teamIds,
 395 | 				...(additionalFields ? additionalFields : {}),
 396 | 			};
 397 | 
 398 | 			// Run beforeCreateInvitation hook
 399 | 			if (option?.organizationHooks?.beforeCreateInvitation) {
 400 | 				const response = await option?.organizationHooks.beforeCreateInvitation(
 401 | 					{
 402 | 						invitation: {
 403 | 							...invitationData,
 404 | 							inviterId: session.user.id,
 405 | 							teamId: teamIds.length > 0 ? teamIds[0] : undefined,
 406 | 						},
 407 | 						inviter: session.user,
 408 | 						organization,
 409 | 					},
 410 | 				);
 411 | 				if (response && typeof response === "object" && "data" in response) {
 412 | 					invitationData = {
 413 | 						...invitationData,
 414 | 						...response.data,
 415 | 					};
 416 | 				}
 417 | 			}
 418 | 
 419 | 			const invitation = await adapter.createInvitation({
 420 | 				invitation: invitationData,
 421 | 				user: session.user,
 422 | 			});
 423 | 
 424 | 			await ctx.context.orgOptions.sendInvitationEmail?.(
 425 | 				{
 426 | 					id: invitation.id,
 427 | 					role: invitation.role as string,
 428 | 					email: invitation.email.toLowerCase(),
 429 | 					organization: organization,
 430 | 					inviter: {
 431 | 						...member,
 432 | 						user: session.user,
 433 | 					},
 434 | 					//@ts-expect-error
 435 | 					invitation,
 436 | 				},
 437 | 				ctx.request,
 438 | 			);
 439 | 
 440 | 			// Run afterCreateInvitation hook
 441 | 			if (option?.organizationHooks?.afterCreateInvitation) {
 442 | 				await option?.organizationHooks.afterCreateInvitation({
 443 | 					invitation: invitation as unknown as Invitation,
 444 | 					inviter: session.user,
 445 | 					organization,
 446 | 				});
 447 | 			}
 448 | 
 449 | 			return ctx.json(invitation);
 450 | 		},
 451 | 	);
 452 | };
 453 | 
 454 | export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
 455 | 	createAuthEndpoint(
 456 | 		"/organization/accept-invitation",
 457 | 		{
 458 | 			method: "POST",
 459 | 			body: z.object({
 460 | 				invitationId: z.string().meta({
 461 | 					description: "The ID of the invitation to accept",
 462 | 				}),
 463 | 			}),
 464 | 			use: [orgMiddleware, orgSessionMiddleware],
 465 | 			metadata: {
 466 | 				openapi: {
 467 | 					description: "Accept an invitation to an organization",
 468 | 					responses: {
 469 | 						"200": {
 470 | 							description: "Success",
 471 | 							content: {
 472 | 								"application/json": {
 473 | 									schema: {
 474 | 										type: "object",
 475 | 										properties: {
 476 | 											invitation: {
 477 | 												type: "object",
 478 | 											},
 479 | 											member: {
 480 | 												type: "object",
 481 | 											},
 482 | 										},
 483 | 									},
 484 | 								},
 485 | 							},
 486 | 						},
 487 | 					},
 488 | 				},
 489 | 			},
 490 | 		},
 491 | 		async (ctx) => {
 492 | 			const session = ctx.context.session;
 493 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 494 | 			const invitation = await adapter.findInvitationById(
 495 | 				ctx.body.invitationId,
 496 | 			);
 497 | 
 498 | 			if (
 499 | 				!invitation ||
 500 | 				invitation.expiresAt < new Date() ||
 501 | 				invitation.status !== "pending"
 502 | 			) {
 503 | 				throw new APIError("BAD_REQUEST", {
 504 | 					message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
 505 | 				});
 506 | 			}
 507 | 
 508 | 			if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
 509 | 				throw new APIError("FORBIDDEN", {
 510 | 					message:
 511 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
 512 | 				});
 513 | 			}
 514 | 
 515 | 			if (
 516 | 				ctx.context.orgOptions.requireEmailVerificationOnInvitation &&
 517 | 				!session.user.emailVerified
 518 | 			) {
 519 | 				throw new APIError("FORBIDDEN", {
 520 | 					message:
 521 | 						ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
 522 | 				});
 523 | 			}
 524 | 
 525 | 			const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
 526 | 			const membersCount = await adapter.countMembers({
 527 | 				organizationId: invitation.organizationId,
 528 | 			});
 529 | 
 530 | 			if (membersCount >= membershipLimit) {
 531 | 				throw new APIError("FORBIDDEN", {
 532 | 					message:
 533 | 						ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
 534 | 				});
 535 | 			}
 536 | 
 537 | 			const organization = await adapter.findOrganizationById(
 538 | 				invitation.organizationId,
 539 | 			);
 540 | 			if (!organization) {
 541 | 				throw new APIError("BAD_REQUEST", {
 542 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 543 | 				});
 544 | 			}
 545 | 
 546 | 			// Run beforeAcceptInvitation hook
 547 | 			if (options?.organizationHooks?.beforeAcceptInvitation) {
 548 | 				await options?.organizationHooks.beforeAcceptInvitation({
 549 | 					invitation: invitation as unknown as Invitation,
 550 | 					user: session.user,
 551 | 					organization,
 552 | 				});
 553 | 			}
 554 | 
 555 | 			const acceptedI = await adapter.updateInvitation({
 556 | 				invitationId: ctx.body.invitationId,
 557 | 				status: "accepted",
 558 | 			});
 559 | 			if (!acceptedI) {
 560 | 				throw new APIError("BAD_REQUEST", {
 561 | 					message: ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION,
 562 | 				});
 563 | 			}
 564 | 			if (
 565 | 				ctx.context.orgOptions.teams &&
 566 | 				ctx.context.orgOptions.teams.enabled &&
 567 | 				"teamId" in acceptedI &&
 568 | 				acceptedI.teamId
 569 | 			) {
 570 | 				const teamIds = (acceptedI.teamId as string).split(",");
 571 | 				const onlyOne = teamIds.length === 1;
 572 | 
 573 | 				for (const teamId of teamIds) {
 574 | 					await adapter.findOrCreateTeamMember({
 575 | 						teamId: teamId,
 576 | 						userId: session.user.id,
 577 | 					});
 578 | 
 579 | 					if (
 580 | 						typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !==
 581 | 						"undefined"
 582 | 					) {
 583 | 						const members = await adapter.countTeamMembers({ teamId });
 584 | 
 585 | 						const maximumMembersPerTeam =
 586 | 							typeof ctx.context.orgOptions.teams.maximumMembersPerTeam ===
 587 | 							"function"
 588 | 								? await ctx.context.orgOptions.teams.maximumMembersPerTeam({
 589 | 										teamId,
 590 | 										session: session,
 591 | 										organizationId: invitation.organizationId,
 592 | 									})
 593 | 								: ctx.context.orgOptions.teams.maximumMembersPerTeam;
 594 | 
 595 | 						if (members >= maximumMembersPerTeam) {
 596 | 							throw new APIError("FORBIDDEN", {
 597 | 								message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED,
 598 | 							});
 599 | 						}
 600 | 					}
 601 | 				}
 602 | 
 603 | 				if (onlyOne) {
 604 | 					const teamId = teamIds[0]!;
 605 | 					const updatedSession = await adapter.setActiveTeam(
 606 | 						session.session.token,
 607 | 						teamId,
 608 | 						ctx,
 609 | 					);
 610 | 
 611 | 					await setSessionCookie(ctx, {
 612 | 						session: updatedSession,
 613 | 						user: session.user,
 614 | 					});
 615 | 				}
 616 | 			}
 617 | 
 618 | 			const member = await adapter.createMember({
 619 | 				organizationId: invitation.organizationId,
 620 | 				userId: session.user.id,
 621 | 				role: invitation.role as string,
 622 | 				createdAt: new Date(),
 623 | 			});
 624 | 
 625 | 			await adapter.setActiveOrganization(
 626 | 				session.session.token,
 627 | 				invitation.organizationId,
 628 | 				ctx,
 629 | 			);
 630 | 			if (!acceptedI) {
 631 | 				return ctx.json(null, {
 632 | 					status: 400,
 633 | 					body: {
 634 | 						message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
 635 | 					},
 636 | 				});
 637 | 			}
 638 | 			if (options?.organizationHooks?.afterAcceptInvitation) {
 639 | 				await options?.organizationHooks.afterAcceptInvitation({
 640 | 					invitation: acceptedI as unknown as Invitation,
 641 | 					member,
 642 | 					user: session.user,
 643 | 					organization,
 644 | 				});
 645 | 			}
 646 | 			return ctx.json({
 647 | 				invitation: acceptedI,
 648 | 				member,
 649 | 			});
 650 | 		},
 651 | 	);
 652 | 
 653 | export const rejectInvitation = <O extends OrganizationOptions>(options: O) =>
 654 | 	createAuthEndpoint(
 655 | 		"/organization/reject-invitation",
 656 | 		{
 657 | 			method: "POST",
 658 | 			body: z.object({
 659 | 				invitationId: z.string().meta({
 660 | 					description: "The ID of the invitation to reject",
 661 | 				}),
 662 | 			}),
 663 | 			use: [orgMiddleware, orgSessionMiddleware],
 664 | 			metadata: {
 665 | 				openapi: {
 666 | 					description: "Reject an invitation to an organization",
 667 | 					responses: {
 668 | 						"200": {
 669 | 							description: "Success",
 670 | 							content: {
 671 | 								"application/json": {
 672 | 									schema: {
 673 | 										type: "object",
 674 | 										properties: {
 675 | 											invitation: {
 676 | 												type: "object",
 677 | 											},
 678 | 											member: {
 679 | 												type: "null",
 680 | 											},
 681 | 										},
 682 | 									},
 683 | 								},
 684 | 							},
 685 | 						},
 686 | 					},
 687 | 				},
 688 | 			},
 689 | 		},
 690 | 		async (ctx) => {
 691 | 			const session = ctx.context.session;
 692 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
 693 | 			const invitation = await adapter.findInvitationById(
 694 | 				ctx.body.invitationId,
 695 | 			);
 696 | 			if (
 697 | 				!invitation ||
 698 | 				invitation.expiresAt < new Date() ||
 699 | 				invitation.status !== "pending"
 700 | 			) {
 701 | 				throw new APIError("BAD_REQUEST", {
 702 | 					message: "Invitation not found!",
 703 | 				});
 704 | 			}
 705 | 			if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
 706 | 				throw new APIError("FORBIDDEN", {
 707 | 					message:
 708 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
 709 | 				});
 710 | 			}
 711 | 
 712 | 			if (
 713 | 				ctx.context.orgOptions.requireEmailVerificationOnInvitation &&
 714 | 				!session.user.emailVerified
 715 | 			) {
 716 | 				throw new APIError("FORBIDDEN", {
 717 | 					message:
 718 | 						ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION,
 719 | 				});
 720 | 			}
 721 | 
 722 | 			const organization = await adapter.findOrganizationById(
 723 | 				invitation.organizationId,
 724 | 			);
 725 | 			if (!organization) {
 726 | 				throw new APIError("BAD_REQUEST", {
 727 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 728 | 				});
 729 | 			}
 730 | 
 731 | 			// Run beforeRejectInvitation hook
 732 | 			if (options?.organizationHooks?.beforeRejectInvitation) {
 733 | 				await options?.organizationHooks.beforeRejectInvitation({
 734 | 					invitation: invitation as unknown as Invitation,
 735 | 					user: session.user,
 736 | 					organization,
 737 | 				});
 738 | 			}
 739 | 
 740 | 			const rejectedI = await adapter.updateInvitation({
 741 | 				invitationId: ctx.body.invitationId,
 742 | 				status: "rejected",
 743 | 			});
 744 | 
 745 | 			// Run afterRejectInvitation hook
 746 | 			if (options?.organizationHooks?.afterRejectInvitation) {
 747 | 				await options?.organizationHooks.afterRejectInvitation({
 748 | 					invitation: rejectedI || (invitation as unknown as Invitation),
 749 | 					user: session.user,
 750 | 					organization,
 751 | 				});
 752 | 			}
 753 | 
 754 | 			return ctx.json({
 755 | 				invitation: rejectedI,
 756 | 				member: null,
 757 | 			});
 758 | 		},
 759 | 	);
 760 | 
 761 | export const cancelInvitation = <O extends OrganizationOptions>(options: O) =>
 762 | 	createAuthEndpoint(
 763 | 		"/organization/cancel-invitation",
 764 | 		{
 765 | 			method: "POST",
 766 | 			body: z.object({
 767 | 				invitationId: z.string().meta({
 768 | 					description: "The ID of the invitation to cancel",
 769 | 				}),
 770 | 			}),
 771 | 			use: [orgMiddleware, orgSessionMiddleware],
 772 | 			openapi: {
 773 | 				description: "Cancel an invitation to an organization",
 774 | 				responses: {
 775 | 					"200": {
 776 | 						description: "Success",
 777 | 						content: {
 778 | 							"application/json": {
 779 | 								schema: {
 780 | 									type: "object",
 781 | 									properties: {
 782 | 										invitation: {
 783 | 											type: "object",
 784 | 										},
 785 | 									},
 786 | 								},
 787 | 							},
 788 | 						},
 789 | 					},
 790 | 				},
 791 | 			},
 792 | 		},
 793 | 		async (ctx) => {
 794 | 			const session = ctx.context.session;
 795 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 796 | 			const invitation = await adapter.findInvitationById(
 797 | 				ctx.body.invitationId,
 798 | 			);
 799 | 			if (!invitation) {
 800 | 				throw new APIError("BAD_REQUEST", {
 801 | 					message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
 802 | 				});
 803 | 			}
 804 | 			const member = await adapter.findMemberByOrgId({
 805 | 				userId: session.user.id,
 806 | 				organizationId: invitation.organizationId,
 807 | 			});
 808 | 			if (!member) {
 809 | 				throw new APIError("BAD_REQUEST", {
 810 | 					message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
 811 | 				});
 812 | 			}
 813 | 			const canCancel = await hasPermission(
 814 | 				{
 815 | 					role: member.role,
 816 | 					options: ctx.context.orgOptions,
 817 | 					permissions: {
 818 | 						invitation: ["cancel"],
 819 | 					},
 820 | 					organizationId: invitation.organizationId,
 821 | 				},
 822 | 				ctx,
 823 | 			);
 824 | 
 825 | 			if (!canCancel) {
 826 | 				throw new APIError("FORBIDDEN", {
 827 | 					message:
 828 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
 829 | 				});
 830 | 			}
 831 | 
 832 | 			const organization = await adapter.findOrganizationById(
 833 | 				invitation.organizationId,
 834 | 			);
 835 | 			if (!organization) {
 836 | 				throw new APIError("BAD_REQUEST", {
 837 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 838 | 				});
 839 | 			}
 840 | 
 841 | 			// Run beforeCancelInvitation hook
 842 | 			if (options?.organizationHooks?.beforeCancelInvitation) {
 843 | 				await options?.organizationHooks.beforeCancelInvitation({
 844 | 					invitation: invitation as unknown as Invitation,
 845 | 					cancelledBy: session.user,
 846 | 					organization,
 847 | 				});
 848 | 			}
 849 | 
 850 | 			const canceledI = await adapter.updateInvitation({
 851 | 				invitationId: ctx.body.invitationId,
 852 | 				status: "canceled",
 853 | 			});
 854 | 
 855 | 			// Run afterCancelInvitation hook
 856 | 			if (options?.organizationHooks?.afterCancelInvitation) {
 857 | 				await options?.organizationHooks.afterCancelInvitation({
 858 | 					invitation: (canceledI as unknown as Invitation) || invitation,
 859 | 					cancelledBy: session.user,
 860 | 					organization,
 861 | 				});
 862 | 			}
 863 | 
 864 | 			return ctx.json(canceledI);
 865 | 		},
 866 | 	);
 867 | 
 868 | export const getInvitation = <O extends OrganizationOptions>(options: O) =>
 869 | 	createAuthEndpoint(
 870 | 		"/organization/get-invitation",
 871 | 		{
 872 | 			method: "GET",
 873 | 			use: [orgMiddleware],
 874 | 			requireHeaders: true,
 875 | 			query: z.object({
 876 | 				id: z.string().meta({
 877 | 					description: "The ID of the invitation to get",
 878 | 				}),
 879 | 			}),
 880 | 			metadata: {
 881 | 				openapi: {
 882 | 					description: "Get an invitation by ID",
 883 | 					responses: {
 884 | 						"200": {
 885 | 							description: "Success",
 886 | 							content: {
 887 | 								"application/json": {
 888 | 									schema: {
 889 | 										type: "object",
 890 | 										properties: {
 891 | 											id: {
 892 | 												type: "string",
 893 | 											},
 894 | 											email: {
 895 | 												type: "string",
 896 | 											},
 897 | 											role: {
 898 | 												type: "string",
 899 | 											},
 900 | 											organizationId: {
 901 | 												type: "string",
 902 | 											},
 903 | 											inviterId: {
 904 | 												type: "string",
 905 | 											},
 906 | 											status: {
 907 | 												type: "string",
 908 | 											},
 909 | 											expiresAt: {
 910 | 												type: "string",
 911 | 											},
 912 | 											organizationName: {
 913 | 												type: "string",
 914 | 											},
 915 | 											organizationSlug: {
 916 | 												type: "string",
 917 | 											},
 918 | 											inviterEmail: {
 919 | 												type: "string",
 920 | 											},
 921 | 										},
 922 | 										required: [
 923 | 											"id",
 924 | 											"email",
 925 | 											"role",
 926 | 											"organizationId",
 927 | 											"inviterId",
 928 | 											"status",
 929 | 											"expiresAt",
 930 | 											"organizationName",
 931 | 											"organizationSlug",
 932 | 											"inviterEmail",
 933 | 										],
 934 | 									},
 935 | 								},
 936 | 							},
 937 | 						},
 938 | 					},
 939 | 				},
 940 | 			},
 941 | 		},
 942 | 		async (ctx) => {
 943 | 			const session = await getSessionFromCtx(ctx);
 944 | 			if (!session) {
 945 | 				throw new APIError("UNAUTHORIZED", {
 946 | 					message: "Not authenticated",
 947 | 				});
 948 | 			}
 949 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 950 | 			const invitation = await adapter.findInvitationById(ctx.query.id);
 951 | 			if (
 952 | 				!invitation ||
 953 | 				invitation.status !== "pending" ||
 954 | 				invitation.expiresAt < new Date()
 955 | 			) {
 956 | 				throw new APIError("BAD_REQUEST", {
 957 | 					message: "Invitation not found!",
 958 | 				});
 959 | 			}
 960 | 			if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
 961 | 				throw new APIError("FORBIDDEN", {
 962 | 					message:
 963 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
 964 | 				});
 965 | 			}
 966 | 			const organization = await adapter.findOrganizationById(
 967 | 				invitation.organizationId,
 968 | 			);
 969 | 			if (!organization) {
 970 | 				throw new APIError("BAD_REQUEST", {
 971 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 972 | 				});
 973 | 			}
 974 | 			const member = await adapter.findMemberByOrgId({
 975 | 				userId: invitation.inviterId,
 976 | 				organizationId: invitation.organizationId,
 977 | 			});
 978 | 			if (!member) {
 979 | 				throw new APIError("BAD_REQUEST", {
 980 | 					message:
 981 | 						ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION,
 982 | 				});
 983 | 			}
 984 | 
 985 | 			return ctx.json({
 986 | 				...invitation,
 987 | 				organizationName: organization.name,
 988 | 				organizationSlug: organization.slug,
 989 | 				inviterEmail: member.user.email,
 990 | 			});
 991 | 		},
 992 | 	);
 993 | 
 994 | export const listInvitations = <O extends OrganizationOptions>(options: O) =>
 995 | 	createAuthEndpoint(
 996 | 		"/organization/list-invitations",
 997 | 		{
 998 | 			method: "GET",
 999 | 			use: [orgMiddleware, orgSessionMiddleware],
1000 | 			query: z
1001 | 				.object({
1002 | 					organizationId: z
1003 | 						.string()
1004 | 						.meta({
1005 | 							description: "The ID of the organization to list invitations for",
1006 | 						})
1007 | 						.optional(),
1008 | 				})
1009 | 				.optional(),
1010 | 		},
1011 | 		async (ctx) => {
1012 | 			const session = await getSessionFromCtx(ctx);
1013 | 			if (!session) {
1014 | 				throw new APIError("UNAUTHORIZED", {
1015 | 					message: "Not authenticated",
1016 | 				});
1017 | 			}
1018 | 			const orgId =
1019 | 				ctx.query?.organizationId || session.session.activeOrganizationId;
1020 | 			if (!orgId) {
1021 | 				throw new APIError("BAD_REQUEST", {
1022 | 					message: "Organization ID is required",
1023 | 				});
1024 | 			}
1025 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
1026 | 			const isMember = await adapter.findMemberByOrgId({
1027 | 				userId: session.user.id,
1028 | 				organizationId: orgId,
1029 | 			});
1030 | 			if (!isMember) {
1031 | 				throw new APIError("FORBIDDEN", {
1032 | 					message: "You are not a member of this organization",
1033 | 				});
1034 | 			}
1035 | 			const invitations = await adapter.listInvitations({
1036 | 				organizationId: orgId,
1037 | 			});
1038 | 			return ctx.json(invitations);
1039 | 		},
1040 | 	);
1041 | 
1042 | /**
1043 |  * List all invitations a user has received
1044 |  */
1045 | export const listUserInvitations = <O extends OrganizationOptions>(
1046 | 	options: O,
1047 | ) =>
1048 | 	createAuthEndpoint(
1049 | 		"/organization/list-user-invitations",
1050 | 		{
1051 | 			method: "GET",
1052 | 			use: [orgMiddleware],
1053 | 			query: z
1054 | 				.object({
1055 | 					email: z
1056 | 						.string()
1057 | 						.meta({
1058 | 							description:
1059 | 								"The email of the user to list invitations for. This only works for server side API calls.",
1060 | 						})
1061 | 						.optional(),
1062 | 				})
1063 | 				.optional(),
1064 | 		},
1065 | 		async (ctx) => {
1066 | 			const session = await getSessionFromCtx(ctx);
1067 | 
1068 | 			if (ctx.request && ctx.query?.email) {
1069 | 				throw new APIError("BAD_REQUEST", {
1070 | 					message: "User email cannot be passed for client side API calls.",
1071 | 				});
1072 | 			}
1073 | 
1074 | 			const userEmail = session?.user.email || ctx.query?.email;
1075 | 			if (!userEmail) {
1076 | 				throw new APIError("BAD_REQUEST", {
1077 | 					message: "Missing session headers, or email query parameter.",
1078 | 				});
1079 | 			}
1080 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
1081 | 
1082 | 			const invitations = await adapter.listUserInvitations(userEmail);
1083 | 			return ctx.json(invitations);
1084 | 		},
1085 | 	);
1086 | 
```
Page 50/67FirstPrevNextLast