This is page 8 of 69. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/next-js.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthPlugin } from "@better-auth/core"; 2 | import { parseSetCookieHeader } from "../cookies"; 3 | import { createAuthMiddleware } from "@better-auth/core/api"; 4 | 5 | export function toNextJsHandler( 6 | auth: 7 | | { 8 | handler: (request: Request) => Promise<Response>; 9 | } 10 | | ((request: Request) => Promise<Response>), 11 | ) { 12 | const handler = async (request: Request) => { 13 | return "handler" in auth ? auth.handler(request) : auth(request); 14 | }; 15 | return { 16 | GET: handler, 17 | POST: handler, 18 | }; 19 | } 20 | 21 | export const nextCookies = () => { 22 | return { 23 | id: "next-cookies", 24 | hooks: { 25 | after: [ 26 | { 27 | matcher(ctx) { 28 | return true; 29 | }, 30 | handler: createAuthMiddleware(async (ctx) => { 31 | const returned = ctx.context.responseHeaders; 32 | if ("_flag" in ctx && ctx._flag === "router") { 33 | return; 34 | } 35 | if (returned instanceof Headers) { 36 | const setCookies = returned?.get("set-cookie"); 37 | if (!setCookies) return; 38 | const parsed = parseSetCookieHeader(setCookies); 39 | const { cookies } = await import("next/headers"); 40 | let cookieHelper: Awaited<ReturnType<typeof cookies>>; 41 | try { 42 | cookieHelper = await cookies(); 43 | } catch (error) { 44 | if ( 45 | error instanceof Error && 46 | error.message.startsWith( 47 | "`cookies` was called outside a request scope.", 48 | ) 49 | ) { 50 | // If error it means the `cookies` was called outside request scope. 51 | // NextJS docs on this: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context 52 | // This often gets called in a monorepo workspace (outside of NextJS), 53 | // so we will try to catch this suppress it, and ignore using next-cookies. 54 | return; 55 | } 56 | // If it's an unexpected error, throw it. 57 | throw error; 58 | } 59 | parsed.forEach((value, key) => { 60 | if (!key) return; 61 | const opts = { 62 | sameSite: value.samesite, 63 | secure: value.secure, 64 | maxAge: value["max-age"], 65 | httpOnly: value.httponly, 66 | domain: value.domain, 67 | path: value.path, 68 | } as const; 69 | try { 70 | cookieHelper.set(key, decodeURIComponent(value.value), opts); 71 | } catch (e) { 72 | // this will fail if the cookie is being set on server component 73 | } 74 | }); 75 | return; 76 | } 77 | }), 78 | }, 79 | ], 80 | }, 81 | } satisfies BetterAuthPlugin; 82 | }; 83 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/linkedin.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: LinkedIn 3 | description: LinkedIn Provider 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your LinkedIn credentials 9 | To use LinkedIn sign in, you need a client ID and client secret. You can get them from the [LinkedIn Developer Portal](https://www.linkedin.com/developers/). 10 | 11 | Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/linkedin` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly. 12 | </Step> 13 | 14 | <Callout type="info"> 15 | In the LinkedIn portal under products you need the **Sign In with LinkedIn using OpenID Connect** product. 16 | </Callout> 17 | 18 | There are some different Guides here: 19 | [Authorization Code Flow (3-legged OAuth) (Outdated)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow) 20 | [Sign In with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin%2Fconsumer%2Fcontext) 21 | 22 | <Step> 23 | ### Configure the provider 24 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 25 | 26 | ```ts title="auth.ts" 27 | import { betterAuth } from "better-auth" 28 | 29 | export const auth = betterAuth({ 30 | socialProviders: { 31 | linkedin: { // [!code highlight] 32 | clientId: process.env.LINKEDIN_CLIENT_ID as string, // [!code highlight] 33 | clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string, // [!code highlight] 34 | }, // [!code highlight] 35 | }, 36 | }) 37 | ``` 38 | </Step> 39 | <Step> 40 | ### Sign In with LinkedIn 41 | To sign in with LinkedIn, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 42 | - `provider`: The provider to use. It should be set to `linkedin`. 43 | 44 | ```ts title="auth-client.ts" 45 | import { createAuthClient } from "better-auth/client" 46 | const authClient = createAuthClient() 47 | 48 | const signIn = async () => { 49 | const data = await authClient.signIn.social({ 50 | provider: "linkedin" 51 | }) 52 | } 53 | ``` 54 | </Step> 55 | 56 | </Steps> 57 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/test-plugin.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { atom, computed } from "nanostores"; 2 | import type { BetterAuthClientPlugin } from "@better-auth/core"; 3 | import type { BetterAuthPlugin } from "@better-auth/core"; 4 | import { createAuthEndpoint } from "@better-auth/core/api"; 5 | import { useAuthQuery } from "./query"; 6 | import z from "zod"; 7 | 8 | const serverPlugin = { 9 | id: "test", 10 | endpoints: { 11 | test: createAuthEndpoint( 12 | "/test", 13 | { 14 | method: "GET", 15 | error: z.object({ 16 | code: z.number(), 17 | message: z.string(), 18 | test: z.boolean(), 19 | }), 20 | }, 21 | async (c) => { 22 | return { 23 | data: "test", 24 | }; 25 | }, 26 | ), 27 | testSignOut2: createAuthEndpoint( 28 | "/test-2/sign-out", 29 | { 30 | method: "POST", 31 | }, 32 | async (c) => { 33 | return null; 34 | }, 35 | ), 36 | }, 37 | schema: { 38 | user: { 39 | fields: { 40 | testField: { 41 | type: "string", 42 | required: false, 43 | }, 44 | testField2: { 45 | type: "number", 46 | required: false, 47 | }, 48 | testField3: { 49 | type: "string", 50 | returned: false, 51 | }, 52 | testField4: { 53 | type: "string", 54 | defaultValue: "test", 55 | }, 56 | }, 57 | }, 58 | }, 59 | } satisfies BetterAuthPlugin; 60 | 61 | export const testClientPlugin = () => { 62 | const $test = atom(false); 63 | let testValue = 0; 64 | const computedAtom = computed($test, () => { 65 | return testValue++; 66 | }); 67 | return { 68 | id: "test" as const, 69 | getActions($fetch) { 70 | return { 71 | setTestAtom(value: boolean) { 72 | $test.set(value); 73 | }, 74 | test: { 75 | signOut: async () => {}, 76 | }, 77 | }; 78 | }, 79 | getAtoms($fetch) { 80 | const $signal = atom(false); 81 | const queryAtom = useAuthQuery<any>($signal, "/test", $fetch, { 82 | method: "GET", 83 | }); 84 | return { 85 | $test, 86 | $signal, 87 | computedAtom, 88 | queryAtom, 89 | }; 90 | }, 91 | $InferServerPlugin: {} as typeof serverPlugin, 92 | atomListeners: [ 93 | { 94 | matcher: (path) => path === "/test", 95 | signal: "$test", 96 | }, 97 | { 98 | matcher: (path) => path === "/test2/sign-out", 99 | signal: "$sessionSignal", 100 | }, 101 | ], 102 | } satisfies BetterAuthClientPlugin; 103 | }; 104 | export const testClientPlugin2 = () => { 105 | const $test2 = atom(false); 106 | let testValue = 0; 107 | const anotherAtom = computed($test2, () => { 108 | return testValue++; 109 | }); 110 | return { 111 | id: "test", 112 | getAtoms($fetch) { 113 | return { 114 | $test2, 115 | anotherAtom, 116 | }; 117 | }, 118 | atomListeners: [ 119 | { 120 | matcher: (path) => path === "/test", 121 | signal: "$test", 122 | }, 123 | { 124 | matcher: (path) => path === "/test2/sign-out", 125 | signal: "$sessionSignal", 126 | }, 127 | ], 128 | } satisfies BetterAuthClientPlugin; 129 | }; 130 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/line.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: LINE 3 | description: LINE provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your LINE credentials 9 | 10 | 1. Create a channel in the LINE Developers Console. 11 | 2. Note your Channel ID (client_id) and Channel secret (client_secret). 12 | 3. In the channel settings, add your Redirect URI, e.g. `http://localhost:3000/api/auth/callback/line` for local development. 13 | 4. Enable required scopes (at least `openid`; add `profile`, `email` if you need name, avatar, email). 14 | 15 | See LINE Login v2.1 reference for details: [`https://developers.line.biz/en/reference/line-login/#issue-access-token`] 16 | </Step> 17 | 18 | <Step> 19 | ### Configure the provider 20 | 21 | Add your LINE credentials to `socialProviders.line` in your auth configuration. 22 | 23 | ```ts title="auth.ts" 24 | import { betterAuth } from "better-auth"; 25 | 26 | export const auth = betterAuth({ 27 | socialProviders: { 28 | line: { 29 | clientId: process.env.LINE_CLIENT_ID as string, 30 | clientSecret: process.env.LINE_CLIENT_SECRET as string, 31 | // Optional: override redirect if needed 32 | // redirectURI: "https://your.app/api/auth/callback/line", 33 | // scopes are prefilled: ["openid","profile","email"]. Append if needed 34 | }, 35 | }, 36 | }); 37 | ``` 38 | </Step> 39 | </Steps> 40 | 41 | ## Usage 42 | 43 | ### Sign In with LINE 44 | 45 | Use the client `signIn.social` with `provider: "line"`. 46 | 47 | ```ts title="auth-client.ts" 48 | import { createAuthClient } from "better-auth/client"; 49 | const authClient = createAuthClient(); 50 | 51 | async function signInWithLINE() { 52 | const res = await authClient.signIn.social({ provider: "line" }); 53 | } 54 | ``` 55 | 56 | ### Sign In with LINE using ID Token (optional) 57 | 58 | If you obtain the LINE ID token on the client, you can sign in directly without redirection. 59 | 60 | ```ts title="auth-client.ts" 61 | await authClient.signIn.social({ 62 | provider: "line", 63 | idToken: { 64 | token: "<LINE_ID_TOKEN>", 65 | accessToken: "<LINE_ACCESS_TOKEN>", 66 | }, 67 | }); 68 | ``` 69 | 70 | ### Notes 71 | 72 | - Default scopes include `openid profile email`. Adjust as needed via provider options. 73 | - Verify redirect URI exactly matches the value configured in LINE Developers Console. 74 | - LINE ID token verification uses the official endpoint and checks audience and optional nonce per spec. 75 | 76 | Designing a login button? Follow LINE's button [guidelines](https://developers.line.biz/en/docs/line-login/login-button/). 77 | 78 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/to-zod.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import type { ZodType } from "zod"; 3 | import type { DBFieldAttribute } from "@better-auth/core/db"; 4 | 5 | export function toZodSchema< 6 | Fields extends Record<string, DBFieldAttribute | never>, 7 | IsClientSide extends boolean, 8 | >({ 9 | fields, 10 | isClientSide, 11 | }: { 12 | fields: Fields; 13 | /** 14 | * If true, then any fields that have `input: false` will be removed from the schema to prevent user input. 15 | */ 16 | isClientSide: IsClientSide; 17 | }) { 18 | const zodFields = Object.keys(fields).reduce((acc, key) => { 19 | const field = fields[key]; 20 | if (!field) { 21 | return acc; 22 | } 23 | if (isClientSide && field.input === false) { 24 | return acc; 25 | } 26 | 27 | let schema: ZodType; 28 | if (field.type === "json") { 29 | schema = (z as any).json ? (z as any).json() : z.any(); 30 | } else if (field.type === "string[]" || field.type === "number[]") { 31 | schema = z.array(field.type === "string[]" ? z.string() : z.number()); 32 | } else if (Array.isArray(field.type)) { 33 | schema = z.any(); 34 | } else { 35 | schema = z[field.type](); 36 | } 37 | 38 | if (field?.required === false) { 39 | schema = schema.optional(); 40 | } 41 | if (field?.returned === false) { 42 | return acc; 43 | } 44 | return { 45 | ...acc, 46 | [key]: schema, 47 | }; 48 | }, {}); 49 | const schema = z.object(zodFields); 50 | return schema as z.ZodObject< 51 | RemoveNeverProps<{ 52 | [key in keyof Fields]: FieldAttributeToSchema<Fields[key], IsClientSide>; 53 | }>, 54 | z.core.$strip 55 | >; 56 | } 57 | 58 | export type FieldAttributeToSchema< 59 | Field extends DBFieldAttribute | Record<string, never>, 60 | // if it's client side, then field attributes of `input` that are false should be removed 61 | isClientSide extends boolean = false, 62 | > = Field extends { type: any } 63 | ? GetInput<isClientSide, Field, GetRequired<Field, GetType<Field>>> 64 | : Record<string, never>; 65 | 66 | type GetType<F extends DBFieldAttribute> = F extends { 67 | type: "string"; 68 | } 69 | ? z.ZodString 70 | : F extends { type: "number" } 71 | ? z.ZodNumber 72 | : F extends { type: "boolean" } 73 | ? z.ZodBoolean 74 | : F extends { type: "date" } 75 | ? z.ZodDate 76 | : z.ZodAny; 77 | 78 | type GetRequired< 79 | F extends DBFieldAttribute, 80 | Schema extends z.core.SomeType, 81 | > = F extends { 82 | required: true; 83 | } 84 | ? Schema 85 | : z.ZodOptional<Schema>; 86 | 87 | type GetInput< 88 | isClientSide extends boolean, 89 | Field extends DBFieldAttribute, 90 | Schema extends z.core.SomeType, 91 | > = Field extends { 92 | input: false; 93 | } 94 | ? isClientSide extends true 95 | ? never 96 | : Schema 97 | : Schema; 98 | 99 | type RemoveNeverProps<T> = { 100 | [K in keyof T as [T[K]] extends [never] ? never : K]: T[K]; 101 | }; 102 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/utils/url.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { env } from "@better-auth/core/env"; 2 | import { BetterAuthError } from "@better-auth/core/error"; 3 | 4 | function checkHasPath(url: string): boolean { 5 | try { 6 | const parsedUrl = new URL(url); 7 | const pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/"; 8 | return pathname !== "/"; 9 | } catch (error) { 10 | throw new BetterAuthError( 11 | `Invalid base URL: ${url}. Please provide a valid base URL.`, 12 | ); 13 | } 14 | } 15 | 16 | function withPath(url: string, path = "/api/auth") { 17 | const hasPath = checkHasPath(url); 18 | if (hasPath) { 19 | return url; 20 | } 21 | 22 | const trimmedUrl = url.replace(/\/+$/, ""); 23 | 24 | if (!path || path === "/") { 25 | return trimmedUrl; 26 | } 27 | 28 | path = path.startsWith("/") ? path : `/${path}`; 29 | return `${trimmedUrl}${path}`; 30 | } 31 | 32 | export function getBaseURL( 33 | url?: string, 34 | path?: string, 35 | request?: Request, 36 | loadEnv?: boolean, 37 | ) { 38 | if (url) { 39 | return withPath(url, path); 40 | } 41 | 42 | if (loadEnv !== false) { 43 | const fromEnv = 44 | env.BETTER_AUTH_URL || 45 | env.NEXT_PUBLIC_BETTER_AUTH_URL || 46 | env.PUBLIC_BETTER_AUTH_URL || 47 | env.NUXT_PUBLIC_BETTER_AUTH_URL || 48 | env.NUXT_PUBLIC_AUTH_URL || 49 | (env.BASE_URL !== "/" ? env.BASE_URL : undefined); 50 | 51 | if (fromEnv) { 52 | return withPath(fromEnv, path); 53 | } 54 | } 55 | 56 | const fromRequest = request?.headers.get("x-forwarded-host"); 57 | const fromRequestProto = request?.headers.get("x-forwarded-proto"); 58 | if (fromRequest && fromRequestProto) { 59 | return withPath(`${fromRequestProto}://${fromRequest}`, path); 60 | } 61 | 62 | if (request) { 63 | const url = getOrigin(request.url); 64 | if (!url) { 65 | throw new BetterAuthError( 66 | "Could not get origin from request. Please provide a valid base URL.", 67 | ); 68 | } 69 | return withPath(url, path); 70 | } 71 | 72 | if (typeof window !== "undefined" && window.location) { 73 | return withPath(window.location.origin, path); 74 | } 75 | return undefined; 76 | } 77 | 78 | export function getOrigin(url: string) { 79 | try { 80 | const parsedUrl = new URL(url); 81 | // For custom URL schemes (like exp://), the origin property returns the string "null" 82 | // instead of null. We need to handle this case and return null so the fallback logic works. 83 | return parsedUrl.origin === "null" ? null : parsedUrl.origin; 84 | } catch (error) { 85 | return null; 86 | } 87 | } 88 | 89 | export function getProtocol(url: string) { 90 | try { 91 | const parsedUrl = new URL(url); 92 | return parsedUrl.protocol; 93 | } catch (error) { 94 | return null; 95 | } 96 | } 97 | 98 | export function getHost(url: string) { 99 | try { 100 | const parsedUrl = new URL(url); 101 | return parsedUrl.host; 102 | } catch (error) { 103 | return url; 104 | } 105 | } 106 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/calendar.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { DayPicker } from "react-day-picker"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | export type CalendarProps = React.ComponentProps<typeof DayPicker>; 10 | 11 | function Calendar({ 12 | className, 13 | classNames, 14 | showOutsideDays = true, 15 | ...props 16 | }: CalendarProps) { 17 | return ( 18 | <DayPicker 19 | showOutsideDays={showOutsideDays} 20 | className={cn("p-3", className)} 21 | classNames={{ 22 | months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", 23 | month: "space-y-4", 24 | caption: "flex justify-center pt-1 relative items-center", 25 | caption_label: "text-sm font-medium", 26 | nav: "space-x-1 flex items-center", 27 | nav_button: cn( 28 | buttonVariants({ variant: "outline" }), 29 | "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", 30 | ), 31 | nav_button_previous: "absolute left-1", 32 | nav_button_next: "absolute right-1", 33 | table: "w-full border-collapse space-y-1", 34 | head_row: "flex", 35 | head_cell: 36 | "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 37 | row: "flex w-full mt-2", 38 | cell: cn( 39 | "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", 40 | props.mode === "range" 41 | ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 42 | : "[&:has([aria-selected])]:rounded-md", 43 | ), 44 | day: cn( 45 | buttonVariants({ variant: "ghost" }), 46 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100", 47 | ), 48 | day_range_start: "day-range-start", 49 | day_range_end: "day-range-end", 50 | day_selected: 51 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 52 | day_today: "bg-accent text-accent-foreground", 53 | day_outside: 54 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 55 | day_disabled: "text-muted-foreground opacity-50", 56 | day_range_middle: 57 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 58 | day_hidden: "invisible", 59 | ...classNames, 60 | }} 61 | {...props} 62 | /> 63 | ); 64 | } 65 | Calendar.displayName = "Calendar"; 66 | 67 | export { Calendar }; 68 | ``` -------------------------------------------------------------------------------- /demo/nextjs/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Breadcrumb = ({ ref, ...props }) => ( 8 | <nav ref={ref} aria-label="breadcrumb" {...props} /> 9 | ); 10 | Breadcrumb.displayName = "Breadcrumb"; 11 | 12 | const BreadcrumbList = ({ 13 | ref, 14 | className, 15 | ...props 16 | }: React.ComponentPropsWithoutRef<"ol"> & { 17 | ref: React.RefObject<HTMLOListElement>; 18 | }) => ( 19 | <ol 20 | ref={ref} 21 | className={cn( 22 | "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", 23 | className, 24 | )} 25 | {...props} 26 | /> 27 | ); 28 | BreadcrumbList.displayName = "BreadcrumbList"; 29 | 30 | const BreadcrumbItem = ({ 31 | ref, 32 | className, 33 | ...props 34 | }: React.ComponentPropsWithoutRef<"li"> & { 35 | ref: React.RefObject<HTMLLIElement>; 36 | }) => ( 37 | <li 38 | ref={ref} 39 | className={cn("inline-flex items-center gap-1.5", className)} 40 | {...props} 41 | /> 42 | ); 43 | BreadcrumbItem.displayName = "BreadcrumbItem"; 44 | 45 | const BreadcrumbLink = ({ ref, asChild, className, ...props }) => { 46 | const Comp = asChild ? Slot : "a"; 47 | 48 | return ( 49 | <Comp 50 | ref={ref} 51 | className={cn("transition-colors hover:text-foreground", className)} 52 | {...props} 53 | /> 54 | ); 55 | }; 56 | BreadcrumbLink.displayName = "BreadcrumbLink"; 57 | 58 | const BreadcrumbPage = ({ 59 | ref, 60 | className, 61 | ...props 62 | }: React.ComponentPropsWithoutRef<"span"> & { 63 | ref: React.RefObject<HTMLSpanElement>; 64 | }) => ( 65 | <span 66 | ref={ref} 67 | role="link" 68 | aria-disabled="true" 69 | aria-current="page" 70 | className={cn("font-normal text-foreground", className)} 71 | {...props} 72 | /> 73 | ); 74 | BreadcrumbPage.displayName = "BreadcrumbPage"; 75 | 76 | const BreadcrumbSeparator = ({ 77 | children, 78 | className, 79 | ...props 80 | }: React.ComponentProps<"li">) => ( 81 | <li 82 | role="presentation" 83 | aria-hidden="true" 84 | className={cn("[&>svg]:size-3.5", className)} 85 | {...props} 86 | > 87 | {children ?? <ChevronRightIcon />} 88 | </li> 89 | ); 90 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"; 91 | 92 | const BreadcrumbEllipsis = ({ 93 | className, 94 | ...props 95 | }: React.ComponentProps<"span">) => ( 96 | <span 97 | role="presentation" 98 | aria-hidden="true" 99 | className={cn("flex h-9 w-9 items-center justify-center", className)} 100 | {...props} 101 | > 102 | <DotsHorizontalIcon className="h-4 w-4" /> 103 | <span className="sr-only">More</span> 104 | </span> 105 | ); 106 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"; 107 | 108 | export { 109 | Breadcrumb, 110 | BreadcrumbList, 111 | BreadcrumbItem, 112 | BreadcrumbLink, 113 | BreadcrumbPage, 114 | BreadcrumbSeparator, 115 | BreadcrumbEllipsis, 116 | }; 117 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/app/sign-up.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { Button } from "@/components/ui/button"; 2 | import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { Input } from "@/components/ui/input"; 4 | import { Text } from "@/components/ui/text"; 5 | import { authClient } from "@/lib/auth-client"; 6 | import { KeyboardAvoidingView, View } from "react-native"; 7 | import { Image } from "react-native"; 8 | import { useRouter } from "expo-router"; 9 | import { useState } from "react"; 10 | 11 | export default function SignUp() { 12 | const router = useRouter(); 13 | const [email, setEmail] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | const [name, setName] = useState(""); 16 | return ( 17 | <Card className="z-50 mx-6"> 18 | <CardHeader className="flex items-center justify-center gap-8"> 19 | <Image 20 | source={require("../../assets/images/logo.png")} 21 | style={{ 22 | width: 40, 23 | height: 40, 24 | }} 25 | /> 26 | <CardTitle>Create new Account</CardTitle> 27 | </CardHeader> 28 | <View className="px-6"> 29 | <KeyboardAvoidingView> 30 | <Input 31 | placeholder="Name" 32 | className="rounded-b-none border-b-0" 33 | value={name} 34 | onChangeText={(text) => { 35 | setName(text); 36 | }} 37 | /> 38 | </KeyboardAvoidingView> 39 | <KeyboardAvoidingView> 40 | <Input 41 | placeholder="Email" 42 | className="rounded-b-none border-b-0" 43 | value={email} 44 | onChangeText={(text) => { 45 | setEmail(text); 46 | }} 47 | autoCapitalize="none" 48 | /> 49 | </KeyboardAvoidingView> 50 | 51 | <KeyboardAvoidingView> 52 | <Input 53 | placeholder="Password" 54 | secureTextEntry 55 | className="rounded-t-none" 56 | value={password} 57 | onChangeText={(text) => { 58 | setPassword(text); 59 | }} 60 | /> 61 | </KeyboardAvoidingView> 62 | </View> 63 | <CardFooter> 64 | <View className="w-full mt-2"> 65 | <Button 66 | onPress={async () => { 67 | const res = await authClient.signUp.email( 68 | { 69 | email, 70 | password, 71 | name, 72 | }, 73 | { 74 | onError: (ctx) => { 75 | alert(ctx.error.message); 76 | }, 77 | onSuccess: (ctx) => { 78 | router.push("/dashboard"); 79 | }, 80 | }, 81 | ); 82 | console.log(res); 83 | }} 84 | > 85 | <Text>Sign Up</Text> 86 | </Button> 87 | <Text className="text-center mt-2"> 88 | Already have an account?{" "} 89 | <Text 90 | className="underline" 91 | onPress={() => { 92 | router.push("/"); 93 | }} 94 | > 95 | Sign In 96 | </Text> 97 | </Text> 98 | </View> 99 | </CardFooter> 100 | </Card> 101 | ); 102 | } 103 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/figma.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Figma 3 | description: Figma provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Credentials 9 | 1. Sign in to your Figma account and go to the [Developer Apps page](https://www.figma.com/developers/apps) 10 | 2. Click "Create new app" 11 | 3. Fill out the app details (name, description, etc.) 12 | 4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/figma`) 13 | 5. Note your Client ID and Client Secret 14 | 15 | <Callout type="info"> 16 | - The default scope is `file_read`. For additional scopes like `file_write`, refer to the [Figma OAuth documentation](https://www.figma.com/developers/api#oauth2). 17 | </Callout> 18 | 19 | Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly. 20 | </Step> 21 | 22 | <Step> 23 | ### Configure the provider 24 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 25 | 26 | ```ts title="auth.ts" 27 | import { betterAuth } from "better-auth" 28 | 29 | export const auth = betterAuth({ 30 | socialProviders: { 31 | figma: { // [!code highlight] 32 | clientId: process.env.FIGMA_CLIENT_ID as string, // [!code highlight] 33 | clientSecret: process.env.FIGMA_CLIENT_SECRET as string, // [!code highlight] 34 | clientKey: process.env.FIGMA_CLIENT_KEY as string, // [!code highlight] 35 | }, // [!code highlight] 36 | }, 37 | }) 38 | ``` 39 | </Step> 40 | <Step> 41 | ### Sign In with Figma 42 | To sign in with Figma, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 43 | - `provider`: The provider to use. It should be set to `figma`. 44 | 45 | ```ts title="auth-client.ts" 46 | import { createAuthClient } from "better-auth/client" 47 | const authClient = createAuthClient() 48 | 49 | const signIn = async () => { 50 | const data = await authClient.signIn.social({ 51 | provider: "figma" 52 | }) 53 | } 54 | ``` 55 | <Callout type="info"> 56 | For more information about Figma's OAuth scopes and API capabilities, refer to the [official Figma API documentation](https://www.figma.com/developers/api). 57 | </Callout> 58 | </Step> 59 | 60 | </Steps> 61 | ``` -------------------------------------------------------------------------------- /docs/components/ui/dynamic-code-block.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import { 3 | CodeBlock, 4 | type CodeBlockProps, 5 | Pre, 6 | } from "@/components/ui/code-block"; 7 | import type { 8 | HighlightOptions, 9 | HighlightOptionsCommon, 10 | HighlightOptionsThemes, 11 | } from "fumadocs-core/highlight"; 12 | import { useShiki } from "fumadocs-core/highlight/client"; 13 | import { cn } from "@/lib/utils"; 14 | import { 15 | type ComponentProps, 16 | createContext, 17 | type FC, 18 | Suspense, 19 | use, 20 | } from "react"; 21 | 22 | export interface DynamicCodeblockProps { 23 | lang: string; 24 | code: string; 25 | /** 26 | * Extra props for the underlying `<CodeBlock />` component. 27 | * 28 | * Ignored if you defined your own `pre` component in `options.components`. 29 | */ 30 | codeblock?: CodeBlockProps; 31 | /** 32 | * Wrap in React `<Suspense />` and provide a fallback. 33 | * 34 | * @defaultValue true 35 | */ 36 | wrapInSuspense?: boolean; 37 | /** 38 | * Allow to copy code with copy button 39 | * 40 | * @defaultValue true 41 | */ 42 | allowCopy?: boolean; 43 | options?: Omit<HighlightOptionsCommon, "lang"> & HighlightOptionsThemes; 44 | } 45 | 46 | const PropsContext = createContext<CodeBlockProps | undefined>(undefined); 47 | 48 | function DefaultPre(props: ComponentProps<"pre">) { 49 | const extraProps = use(PropsContext); 50 | 51 | return ( 52 | <CodeBlock 53 | {...props} 54 | {...extraProps} 55 | className={cn( 56 | "my-0 border-t-0 rounded-none", 57 | props.className, 58 | extraProps?.className, 59 | )} 60 | > 61 | <Pre className="py-2">{props.children}</Pre> 62 | </CodeBlock> 63 | ); 64 | } 65 | 66 | export function DynamicCodeBlock({ 67 | lang, 68 | code, 69 | codeblock, 70 | options, 71 | wrapInSuspense = true, 72 | allowCopy = true, 73 | }: DynamicCodeblockProps) { 74 | const shikiOptions = { 75 | lang, 76 | ...options, 77 | components: { 78 | pre: DefaultPre, 79 | ...options?.components, 80 | }, 81 | } satisfies HighlightOptions; 82 | let children = <Internal code={code} options={shikiOptions} />; 83 | 84 | if (wrapInSuspense) 85 | children = ( 86 | <Suspense 87 | fallback={ 88 | <Placeholder code={code} components={shikiOptions.components} /> 89 | } 90 | > 91 | {children} 92 | </Suspense> 93 | ); 94 | 95 | return ( 96 | <PropsContext value={{ ...codeblock, allowCopy }}>{children}</PropsContext> 97 | ); 98 | } 99 | 100 | function Placeholder({ 101 | code, 102 | components = {}, 103 | }: { 104 | code: string; 105 | components: HighlightOptions["components"]; 106 | }) { 107 | const { pre: Pre = "pre", code: Code = "code" } = components as Record< 108 | string, 109 | FC 110 | >; 111 | 112 | return ( 113 | <Pre> 114 | <Code> 115 | {code.split("\n").map((line, i) => ( 116 | <span key={i} className="line"> 117 | {line} 118 | </span> 119 | ))} 120 | </Code> 121 | </Pre> 122 | ); 123 | } 124 | 125 | function Internal({ 126 | code, 127 | options, 128 | }: { 129 | code: string; 130 | options: HighlightOptions; 131 | }) { 132 | return useShiki(code, options); 133 | } 134 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/drizzle-adapter/test/generate-schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthOptions } from "@better-auth/core"; 2 | import type { DBAdapter } from "@better-auth/core/db/adapter"; 3 | import { drizzleAdapter } from "../drizzle-adapter"; 4 | import fs from "fs/promises"; 5 | import { join } from "path"; 6 | 7 | let generationCount = 0; 8 | 9 | const schemaCache = new Map<string, { count: number; schema: any }>(); 10 | 11 | /** 12 | * generates a drizzle schema based on BetterAuthOptions & a given dialect. 13 | * 14 | * Useful for testing the Drizzle adapter. 15 | */ 16 | export const generateDrizzleSchema = async ( 17 | db: any, 18 | options: BetterAuthOptions, 19 | dialect: "sqlite" | "mysql" | "pg", 20 | ) => { 21 | const cacheKey = `${dialect}-${JSON.stringify(options)}`; 22 | if (schemaCache.has(cacheKey)) { 23 | const { count, schema } = schemaCache.get(cacheKey)!; 24 | return { 25 | schema, 26 | fileName: `./.tmp/generated-${dialect}-schema-${count}`, 27 | }; 28 | } 29 | generationCount++; 30 | let thisCount = generationCount; 31 | const i = async (x: string) => { 32 | // Clear the Node.js module cache for the generated schema file to ensure fresh import 33 | try { 34 | const resolvedPath = 35 | require?.resolve?.(x) || 36 | (import.meta && new URL(x, import.meta.url).pathname); 37 | if (resolvedPath && typeof resolvedPath === "string" && require?.cache) { 38 | delete require.cache[resolvedPath]; 39 | } 40 | } catch (error) {} 41 | return await import(x); 42 | }; 43 | 44 | const { generateSchema } = (await i( 45 | "./../../../../../cli/src/generators/index", 46 | )) as { 47 | generateSchema: (opts: { 48 | adapter: DBAdapter<BetterAuthOptions>; 49 | file?: string; 50 | options: BetterAuthOptions; 51 | }) => Promise<{ 52 | code: string | undefined; 53 | fileName: string; 54 | overwrite: boolean | undefined; 55 | }>; 56 | }; 57 | 58 | const exists = await fs 59 | .access(join(import.meta.dirname, `/.tmp`)) 60 | .then(() => true) 61 | .catch(() => false); 62 | if (!exists) { 63 | await fs.mkdir(join(import.meta.dirname, `/.tmp`), { recursive: true }); 64 | } 65 | 66 | let adapter = drizzleAdapter(db, { provider: dialect })(options); 67 | 68 | let { code } = await generateSchema({ 69 | adapter, 70 | options, 71 | }); 72 | 73 | await fs.writeFile( 74 | join( 75 | import.meta.dirname, 76 | `/.tmp/generated-${dialect}-schema-${thisCount}.ts`, 77 | ), 78 | code || "", 79 | "utf-8", 80 | ); 81 | 82 | const res = await i(`./.tmp/generated-${dialect}-schema-${thisCount}`); 83 | schemaCache.set(cacheKey, { 84 | count: thisCount, 85 | schema: res, 86 | }); 87 | return { 88 | schema: res, 89 | fileName: `./.tmp/generated-${dialect}-schema-${thisCount}`, 90 | }; 91 | }; 92 | 93 | export const clearSchemaCache = () => { 94 | schemaCache.clear(); 95 | }; 96 | 97 | export const resetGenerationCount = () => { 98 | generationCount = 0; 99 | }; 100 | ``` -------------------------------------------------------------------------------- /docs/content/docs/authentication/atlassian.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Atlassian 3 | description: Atlassian provider setup and usage. 4 | --- 5 | 6 | <Steps> 7 | <Step> 8 | ### Get your Credentials 9 | 1. Sign in to your Atlassian account and go to the [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/) 10 | 2. Click "Create new app" 11 | 3. Fill out the app details 12 | 4. Configure your redirect URI (e.g., `https://yourdomain.com/api/auth/callback/atlassian`) 13 | 5. Note your Client ID and Client Secret 14 | 15 | <Callout type="info"> 16 | - The default scope is `read:jira-user` and `offline_access`. For additional scopes, refer to the [Atlassian OAuth documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/). 17 | </Callout> 18 | 19 | Make sure to set the redirect URI to match your application's callback URL. If you change the base path of the auth routes, you should update the redirect URI accordingly. 20 | </Step> 21 | 22 | <Step> 23 | ### Configure the provider 24 | To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. 25 | 26 | ```ts title="auth.ts" 27 | import { betterAuth } from "better-auth" 28 | 29 | export const auth = betterAuth({ 30 | socialProviders: { 31 | atlassian: { // [!code highlight] 32 | clientId: process.env.ATLASSIAN_CLIENT_ID as string, // [!code highlight] 33 | clientSecret: process.env.ATLASSIAN_CLIENT_SECRET as string, // [!code highlight] 34 | }, // [!code highlight] 35 | }, 36 | }) 37 | ``` 38 | </Step> 39 | <Step> 40 | ### Sign In with Atlassian 41 | To sign in with Atlassian, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: 42 | - `provider`: The provider to use. It should be set to `atlassian`. 43 | 44 | ```ts title="auth-client.ts" 45 | import { createAuthClient } from "better-auth/client" 46 | const authClient = createAuthClient() 47 | 48 | const signIn = async () => { 49 | const data = await authClient.signIn.social({ 50 | provider: "atlassian" 51 | }) 52 | } 53 | ``` 54 | <Callout type="info"> 55 | For more information about Atlassian's OAuth scopes and API capabilities, refer to the [official Atlassian OAuth 2.0 (3LO) apps documentation](https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/). 56 | </Callout> 57 | </Step> 58 | 59 | </Steps> 60 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-mysql-enum.txt: -------------------------------------------------------------------------------- ``` 1 | import { 2 | mysqlTable, 3 | varchar, 4 | text, 5 | timestamp, 6 | boolean, 7 | mysqlEnum, 8 | } from "drizzle-orm/mysql-core"; 9 | 10 | export const user = mysqlTable("user", { 11 | id: varchar("id", { length: 36 }).primaryKey(), 12 | name: text("name").notNull(), 13 | email: varchar("email", { length: 255 }).notNull().unique(), 14 | emailVerified: boolean("email_verified").default(false).notNull(), 15 | image: text("image"), 16 | createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), 17 | updatedAt: timestamp("updated_at", { fsp: 3 }) 18 | .defaultNow() 19 | .$onUpdate(() => /* @__PURE__ */ new Date()) 20 | .notNull(), 21 | status: mysqlEnum(["active", "inactive", "pending"]), 22 | }); 23 | 24 | export const session = mysqlTable("session", { 25 | id: varchar("id", { length: 36 }).primaryKey(), 26 | expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(), 27 | token: varchar("token", { length: 255 }).notNull().unique(), 28 | createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), 29 | updatedAt: timestamp("updated_at", { fsp: 3 }) 30 | .$onUpdate(() => /* @__PURE__ */ new Date()) 31 | .notNull(), 32 | ipAddress: text("ip_address"), 33 | userAgent: text("user_agent"), 34 | userId: varchar("user_id", { length: 36 }) 35 | .notNull() 36 | .references(() => user.id, { onDelete: "cascade" }), 37 | }); 38 | 39 | export const account = mysqlTable("account", { 40 | id: varchar("id", { length: 36 }).primaryKey(), 41 | accountId: text("account_id").notNull(), 42 | providerId: text("provider_id").notNull(), 43 | userId: varchar("user_id", { length: 36 }) 44 | .notNull() 45 | .references(() => user.id, { onDelete: "cascade" }), 46 | accessToken: text("access_token"), 47 | refreshToken: text("refresh_token"), 48 | idToken: text("id_token"), 49 | accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }), 50 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }), 51 | scope: text("scope"), 52 | password: text("password"), 53 | createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), 54 | updatedAt: timestamp("updated_at", { fsp: 3 }) 55 | .$onUpdate(() => /* @__PURE__ */ new Date()) 56 | .notNull(), 57 | }); 58 | 59 | export const verification = mysqlTable("verification", { 60 | id: varchar("id", { length: 36 }).primaryKey(), 61 | identifier: text("identifier").notNull(), 62 | value: text("value").notNull(), 63 | expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(), 64 | createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(), 65 | updatedAt: timestamp("updated_at", { fsp: 3 }) 66 | .defaultNow() 67 | .$onUpdate(() => /* @__PURE__ */ new Date()) 68 | .notNull(), 69 | }); 70 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/linkedin.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | validateAuthorizationCode, 6 | refreshAccessToken, 7 | } from "../oauth2"; 8 | 9 | export interface LinkedInProfile { 10 | sub: string; 11 | name: string; 12 | given_name: string; 13 | family_name: string; 14 | picture: string; 15 | locale: { 16 | country: string; 17 | language: string; 18 | }; 19 | email: string; 20 | email_verified: boolean; 21 | } 22 | 23 | export interface LinkedInOptions extends ProviderOptions<LinkedInProfile> { 24 | clientId: string; 25 | } 26 | 27 | export const linkedin = (options: LinkedInOptions) => { 28 | const authorizationEndpoint = 29 | "https://www.linkedin.com/oauth/v2/authorization"; 30 | const tokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken"; 31 | 32 | return { 33 | id: "linkedin", 34 | name: "Linkedin", 35 | createAuthorizationURL: async ({ 36 | state, 37 | scopes, 38 | redirectURI, 39 | loginHint, 40 | }) => { 41 | const _scopes = options.disableDefaultScope 42 | ? [] 43 | : ["profile", "email", "openid"]; 44 | options.scope && _scopes.push(...options.scope); 45 | scopes && _scopes.push(...scopes); 46 | return await createAuthorizationURL({ 47 | id: "linkedin", 48 | options, 49 | authorizationEndpoint, 50 | scopes: _scopes, 51 | state, 52 | loginHint, 53 | redirectURI, 54 | }); 55 | }, 56 | validateAuthorizationCode: async ({ code, redirectURI }) => { 57 | return await validateAuthorizationCode({ 58 | code, 59 | redirectURI, 60 | options, 61 | tokenEndpoint, 62 | }); 63 | }, 64 | refreshAccessToken: options.refreshAccessToken 65 | ? options.refreshAccessToken 66 | : async (refreshToken) => { 67 | return refreshAccessToken({ 68 | refreshToken, 69 | options: { 70 | clientId: options.clientId, 71 | clientKey: options.clientKey, 72 | clientSecret: options.clientSecret, 73 | }, 74 | tokenEndpoint, 75 | }); 76 | }, 77 | async getUserInfo(token) { 78 | if (options.getUserInfo) { 79 | return options.getUserInfo(token); 80 | } 81 | const { data: profile, error } = await betterFetch<LinkedInProfile>( 82 | "https://api.linkedin.com/v2/userinfo", 83 | { 84 | method: "GET", 85 | headers: { 86 | Authorization: `Bearer ${token.accessToken}`, 87 | }, 88 | }, 89 | ); 90 | 91 | if (error) { 92 | return null; 93 | } 94 | 95 | const userMap = await options.mapProfileToUser?.(profile); 96 | return { 97 | user: { 98 | id: profile.sub, 99 | name: profile.name, 100 | email: profile.email, 101 | emailVerified: profile.email_verified || false, 102 | image: profile.picture, 103 | ...userMap, 104 | }, 105 | data: profile, 106 | }; 107 | }, 108 | options, 109 | } satisfies OAuthProvider<LinkedInProfile>; 110 | }; 111 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/svelte/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getClientConfig } from "../config"; 2 | import { capitalizeFirstLetter } from "../../utils/misc"; 3 | import type { 4 | InferActions, 5 | InferClientAPI, 6 | InferErrorCodes, 7 | IsSignal, 8 | } from "../types"; 9 | import type { 10 | BetterAuthClientPlugin, 11 | BetterAuthClientOptions, 12 | } from "@better-auth/core"; 13 | import { createDynamicPathProxy } from "../proxy"; 14 | import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; 15 | import type { Atom } from "nanostores"; 16 | import type { 17 | BetterFetchError, 18 | BetterFetchResponse, 19 | } from "@better-fetch/fetch"; 20 | import type { BASE_ERROR_CODES } from "@better-auth/core/error"; 21 | 22 | type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { 23 | plugins: Array<infer Plugin>; 24 | } 25 | ? UnionToIntersection< 26 | Plugin extends BetterAuthClientPlugin 27 | ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms 28 | ? Atoms extends Record<string, any> 29 | ? { 30 | [key in keyof Atoms as IsSignal<key> extends true 31 | ? never 32 | : key extends string 33 | ? `use${Capitalize<key>}` 34 | : never]: () => Atoms[key]; 35 | } 36 | : {} 37 | : {} 38 | : {} 39 | > 40 | : {}; 41 | 42 | export function createAuthClient<Option extends BetterAuthClientOptions>( 43 | options?: Option, 44 | ) { 45 | const { 46 | pluginPathMethods, 47 | pluginsActions, 48 | pluginsAtoms, 49 | $fetch, 50 | atomListeners, 51 | $store, 52 | } = getClientConfig(options); 53 | let resolvedHooks: Record<string, any> = {}; 54 | for (const [key, value] of Object.entries(pluginsAtoms)) { 55 | resolvedHooks[`use${capitalizeFirstLetter(key)}`] = () => value; 56 | } 57 | const routes = { 58 | ...pluginsActions, 59 | ...resolvedHooks, 60 | $fetch, 61 | $store, 62 | }; 63 | const proxy = createDynamicPathProxy( 64 | routes, 65 | $fetch, 66 | pluginPathMethods, 67 | pluginsAtoms, 68 | atomListeners, 69 | ); 70 | type ClientAPI = InferClientAPI<Option>; 71 | type Session = ClientAPI extends { 72 | getSession: () => Promise<infer Res>; 73 | } 74 | ? Res extends BetterFetchResponse<infer S> 75 | ? S 76 | : Res extends Record<string, any> 77 | ? Res 78 | : never 79 | : never; 80 | return proxy as UnionToIntersection<InferResolvedHooks<Option>> & 81 | InferClientAPI<Option> & 82 | InferActions<Option> & { 83 | useSession: () => Atom<{ 84 | data: Session; 85 | error: BetterFetchError | null; 86 | isPending: boolean; 87 | isRefetching: boolean; 88 | }>; 89 | $fetch: typeof $fetch; 90 | $store: typeof $store; 91 | $Infer: { 92 | Session: NonNullable<Session>; 93 | }; 94 | $ERROR_CODES: PrettifyDeep< 95 | InferErrorCodes<Option> & typeof BASE_ERROR_CODES 96 | >; 97 | }; 98 | } 99 | 100 | export type * from "@better-fetch/fetch"; 101 | export type * from "nanostores"; 102 | ``` -------------------------------------------------------------------------------- /docs/components/ui/pagination.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import * as React from "react"; 2 | import { 3 | ChevronLeftIcon, 4 | ChevronRightIcon, 5 | MoreHorizontalIcon, 6 | } from "lucide-react"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button, buttonVariants } from "@/components/ui/button"; 10 | 11 | function Pagination({ className, ...props }: React.ComponentProps<"nav">) { 12 | return ( 13 | <nav 14 | role="navigation" 15 | aria-label="pagination" 16 | data-slot="pagination" 17 | className={cn("mx-auto flex w-full justify-center", className)} 18 | {...props} 19 | /> 20 | ); 21 | } 22 | 23 | function PaginationContent({ 24 | className, 25 | ...props 26 | }: React.ComponentProps<"ul">) { 27 | return ( 28 | <ul 29 | data-slot="pagination-content" 30 | className={cn("flex flex-row items-center gap-1", className)} 31 | {...props} 32 | /> 33 | ); 34 | } 35 | 36 | function PaginationItem({ ...props }: React.ComponentProps<"li">) { 37 | return <li data-slot="pagination-item" {...props} />; 38 | } 39 | 40 | type PaginationLinkProps = { 41 | isActive?: boolean; 42 | } & Pick<React.ComponentProps<typeof Button>, "size"> & 43 | React.ComponentProps<"a">; 44 | 45 | function PaginationLink({ 46 | className, 47 | isActive, 48 | size = "icon", 49 | ...props 50 | }: PaginationLinkProps) { 51 | return ( 52 | <a 53 | aria-current={isActive ? "page" : undefined} 54 | data-slot="pagination-link" 55 | data-active={isActive} 56 | className={cn( 57 | buttonVariants({ 58 | variant: isActive ? "outline" : "ghost", 59 | size, 60 | }), 61 | className, 62 | )} 63 | {...props} 64 | /> 65 | ); 66 | } 67 | 68 | function PaginationPrevious({ 69 | className, 70 | ...props 71 | }: React.ComponentProps<typeof PaginationLink>) { 72 | return ( 73 | <PaginationLink 74 | aria-label="Go to previous page" 75 | size="default" 76 | className={cn("gap-1 px-2.5 sm:pl-2.5", className)} 77 | {...props} 78 | > 79 | <ChevronLeftIcon /> 80 | <span className="hidden sm:block">Previous</span> 81 | </PaginationLink> 82 | ); 83 | } 84 | 85 | function PaginationNext({ 86 | className, 87 | ...props 88 | }: React.ComponentProps<typeof PaginationLink>) { 89 | return ( 90 | <PaginationLink 91 | aria-label="Go to next page" 92 | size="default" 93 | className={cn("gap-1 px-2.5 sm:pr-2.5", className)} 94 | {...props} 95 | > 96 | <span className="hidden sm:block">Next</span> 97 | <ChevronRightIcon /> 98 | </PaginationLink> 99 | ); 100 | } 101 | 102 | function PaginationEllipsis({ 103 | className, 104 | ...props 105 | }: React.ComponentProps<"span">) { 106 | return ( 107 | <span 108 | aria-hidden 109 | data-slot="pagination-ellipsis" 110 | className={cn("flex size-9 items-center justify-center", className)} 111 | {...props} 112 | > 113 | <MoreHorizontalIcon className="size-4" /> 114 | <span className="sr-only">More pages</span> 115 | </span> 116 | ); 117 | } 118 | 119 | export { 120 | Pagination, 121 | PaginationContent, 122 | PaginationLink, 123 | PaginationItem, 124 | PaginationPrevious, 125 | PaginationNext, 126 | PaginationEllipsis, 127 | }; 128 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/postgresql.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: PostgreSQL 3 | description: Integrate Better Auth with PostgreSQL. 4 | --- 5 | 6 | PostgreSQL is a powerful, open-source relational database management system known for its advanced features, extensibility, and support for complex queries and large datasets. 7 | Read more [here](https://www.postgresql.org/). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have PostgreSQL installed and configured. 12 | Then, you can connect it straight into Better Auth. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { Pool } from "pg"; 17 | 18 | export const auth = betterAuth({ 19 | database: new Pool({ 20 | connectionString: "postgres://user:password@localhost:5432/database", 21 | }), 22 | }); 23 | ``` 24 | 25 | <Callout> 26 | For more information, read Kysely's documentation to the 27 | [PostgresDialect](https://kysely-org.github.io/kysely-apidoc/classes/PostgresDialect.html). 28 | </Callout> 29 | 30 | ## Schema generation & migration 31 | 32 | The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate 33 | your database schema based on your Better Auth configuration and plugins. 34 | 35 | <table> 36 | <thead> 37 | <tr className="border-b"> 38 | <th> 39 | <p className="font-bold text-[16px] mb-1">PostgreSQL Schema Generation</p> 40 | </th> 41 | <th> 42 | <p className="font-bold text-[16px] mb-1">PostgreSQL Schema Migration</p> 43 | </th> 44 | </tr> 45 | </thead> 46 | <tbody> 47 | <tr className="h-10"> 48 | <td>✅ Supported</td> 49 | <td>✅ Supported</td> 50 | </tr> 51 | </tbody> 52 | </table> 53 | 54 | ```bash title="Schema Generation" 55 | npx @better-auth/cli@latest generate 56 | ``` 57 | 58 | ```bash title="Schema Migration" 59 | npx @better-auth/cli@latest migrate 60 | ``` 61 | 62 | ## Use a non-default schema 63 | 64 | In most cases, the default schema is `public`. To have Better Auth use a 65 | non-default schema (e.g., `auth`) for its tables, 66 | set the PostgreSQL user's default schema before generating or migrating: 67 | 68 | ```sql 69 | ALTER USER authuser SET SEARCH_PATH TO auth; 70 | ``` 71 | 72 | alternatively, append the option to your connection URI, for example: 73 | 74 | ``` 75 | postgres://<DATABASE_URL>?option=-c search_path=auth 76 | ``` 77 | URL-encode if needed: `?option=-c%20search_path%3Dauth`. 78 | 79 | Ensure the target schema exists and the database user has the required permissions. 80 | 81 | ## Additional Information 82 | 83 | PostgreSQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (<Link href="/docs/adapters/other-relational-databases">Read more here</Link>) 84 | 85 | If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. 86 | 87 | ``` -------------------------------------------------------------------------------- /packages/telemetry/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ENV, getBooleanEnvVar, isTest } from "@better-auth/core/env"; 2 | import { getProjectId } from "./project-id"; 3 | import type { BetterAuthOptions } from "@better-auth/core"; 4 | import { detectEnvironment, detectRuntime } from "./detectors/detect-runtime"; 5 | import { detectDatabase } from "./detectors/detect-database"; 6 | import { detectFramework } from "./detectors/detect-framework"; 7 | import { detectSystemInfo } from "./detectors/detect-system-info"; 8 | import { detectPackageManager } from "./detectors/detect-project-info"; 9 | import { betterFetch } from "@better-fetch/fetch"; 10 | import type { TelemetryContext, TelemetryEvent } from "./types"; 11 | import { logger } from "@better-auth/core/env"; 12 | import { getTelemetryAuthConfig } from "./detectors/detect-auth-config"; 13 | export { getTelemetryAuthConfig }; 14 | export type { TelemetryEvent } from "./types"; 15 | export async function createTelemetry( 16 | options: BetterAuthOptions, 17 | context?: TelemetryContext, 18 | ) { 19 | const debugEnabled = 20 | options.telemetry?.debug || 21 | getBooleanEnvVar("BETTER_AUTH_TELEMETRY_DEBUG", false); 22 | 23 | const TELEMETRY_ENDPOINT = ENV.BETTER_AUTH_TELEMETRY_ENDPOINT; 24 | const track = async (event: TelemetryEvent) => { 25 | if (context?.customTrack) { 26 | await context.customTrack(event).catch(logger.error); 27 | } else { 28 | if (debugEnabled) { 29 | logger.info("telemetry event", JSON.stringify(event, null, 2)); 30 | } else { 31 | await betterFetch(TELEMETRY_ENDPOINT, { 32 | method: "POST", 33 | body: event, 34 | }).catch(logger.error); 35 | } 36 | } 37 | }; 38 | 39 | const isEnabled = async () => { 40 | const telemetryEnabled = 41 | options.telemetry?.enabled !== undefined 42 | ? options.telemetry.enabled 43 | : false; 44 | const envEnabled = getBooleanEnvVar("BETTER_AUTH_TELEMETRY", false); 45 | return ( 46 | (envEnabled || telemetryEnabled) && (context?.skipTestCheck || !isTest()) 47 | ); 48 | }; 49 | 50 | const enabled = await isEnabled(); 51 | let anonymousId: string | undefined; 52 | 53 | if (enabled) { 54 | anonymousId = await getProjectId(options.baseURL); 55 | 56 | const payload = { 57 | config: getTelemetryAuthConfig(options), 58 | runtime: detectRuntime(), 59 | database: await detectDatabase(), 60 | framework: await detectFramework(), 61 | environment: detectEnvironment(), 62 | systemInfo: await detectSystemInfo(), 63 | packageManager: detectPackageManager(), 64 | }; 65 | 66 | void track({ type: "init", payload, anonymousId }); 67 | } 68 | 69 | return { 70 | publish: async (event: TelemetryEvent) => { 71 | if (!enabled) return; 72 | if (!anonymousId) { 73 | anonymousId = await getProjectId(options.baseURL); 74 | } 75 | await track({ 76 | type: event.type, 77 | payload: event.payload, 78 | anonymousId, 79 | }); 80 | }, 81 | }; 82 | } 83 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/integrations/svelte-kit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { BetterAuthOptions } from "../types"; 2 | import type { BetterAuthPlugin } from "../types"; 3 | import { createAuthMiddleware } from "@better-auth/core/api"; 4 | import { parseSetCookieHeader } from "../cookies"; 5 | import type { RequestEvent } from "@sveltejs/kit"; 6 | 7 | export const toSvelteKitHandler = (auth: { 8 | handler: (request: Request) => Response | Promise<Response>; 9 | options: BetterAuthOptions; 10 | }) => { 11 | return (event: { request: Request }) => auth.handler(event.request); 12 | }; 13 | 14 | export const svelteKitHandler = async ({ 15 | auth, 16 | event, 17 | resolve, 18 | building, 19 | }: { 20 | auth: { 21 | handler: (request: Request) => Response | Promise<Response>; 22 | options: BetterAuthOptions; 23 | }; 24 | event: RequestEvent; 25 | resolve: (event: RequestEvent) => Response | Promise<Response>; 26 | building: boolean; 27 | }) => { 28 | if (building) { 29 | return resolve(event); 30 | } 31 | const { request, url } = event; 32 | if (isAuthPath(url.toString(), auth.options)) { 33 | return auth.handler(request); 34 | } 35 | return resolve(event); 36 | }; 37 | 38 | export function isAuthPath(url: string, options: BetterAuthOptions) { 39 | const _url = new URL(url); 40 | const baseURL = new URL( 41 | `${options.baseURL || _url.origin}${options.basePath || "/api/auth"}`, 42 | ); 43 | if (_url.origin !== baseURL.origin) return false; 44 | if ( 45 | !_url.pathname.startsWith( 46 | baseURL.pathname.endsWith("/") 47 | ? baseURL.pathname 48 | : `${baseURL.pathname}/`, 49 | ) 50 | ) 51 | return false; 52 | return true; 53 | } 54 | 55 | export const sveltekitCookies = ( 56 | getRequestEvent: () => RequestEvent<any, any>, 57 | ) => { 58 | return { 59 | id: "sveltekit-cookies", 60 | hooks: { 61 | after: [ 62 | { 63 | matcher() { 64 | return true; 65 | }, 66 | handler: createAuthMiddleware(async (ctx) => { 67 | const returned = ctx.context.responseHeaders; 68 | if ("_flag" in ctx && ctx._flag === "router") { 69 | return; 70 | } 71 | if (returned instanceof Headers) { 72 | const setCookies = returned?.get("set-cookie"); 73 | if (!setCookies) return; 74 | const event = getRequestEvent(); 75 | if (!event) return; 76 | const parsed = parseSetCookieHeader(setCookies); 77 | 78 | for (const [name, { value, ...ops }] of parsed) { 79 | try { 80 | event.cookies.set(name, decodeURIComponent(value), { 81 | sameSite: ops.samesite, 82 | path: ops.path || "/", 83 | expires: ops.expires, 84 | secure: ops.secure, 85 | httpOnly: ops.httponly, 86 | domain: ops.domain, 87 | maxAge: ops["max-age"], 88 | }); 89 | } catch (e) { 90 | // this will avoid any issue related to already streamed response 91 | } 92 | } 93 | } 94 | }), 95 | }, 96 | ], 97 | }, 98 | } satisfies BetterAuthPlugin; 99 | }; 100 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema.txt: -------------------------------------------------------------------------------- ``` 1 | import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; 2 | 3 | export const custom_user = pgTable("custom_user", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | email: text("email").notNull().unique(), 7 | emailVerified: boolean("email_verified").default(false).notNull(), 8 | image: text("image"), 9 | createdAt: timestamp("created_at").defaultNow().notNull(), 10 | updatedAt: timestamp("updated_at") 11 | .defaultNow() 12 | .$onUpdate(() => /* @__PURE__ */ new Date()) 13 | .notNull(), 14 | twoFactorEnabled: boolean("two_factor_enabled").default(false), 15 | username: text("username").unique(), 16 | displayUsername: text("display_username"), 17 | }); 18 | 19 | export const custom_session = pgTable("custom_session", { 20 | id: text("id").primaryKey(), 21 | expiresAt: timestamp("expires_at").notNull(), 22 | token: text("token").notNull().unique(), 23 | createdAt: timestamp("created_at").defaultNow().notNull(), 24 | updatedAt: timestamp("updated_at") 25 | .$onUpdate(() => /* @__PURE__ */ new Date()) 26 | .notNull(), 27 | ipAddress: text("ip_address"), 28 | userAgent: text("user_agent"), 29 | userId: text("user_id") 30 | .notNull() 31 | .references(() => custom_user.id, { onDelete: "cascade" }), 32 | }); 33 | 34 | export const custom_account = pgTable("custom_account", { 35 | id: text("id").primaryKey(), 36 | accountId: text("account_id").notNull(), 37 | providerId: text("provider_id").notNull(), 38 | userId: text("user_id") 39 | .notNull() 40 | .references(() => custom_user.id, { onDelete: "cascade" }), 41 | accessToken: text("access_token"), 42 | refreshToken: text("refresh_token"), 43 | idToken: text("id_token"), 44 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 45 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 46 | scope: text("scope"), 47 | password: text("password"), 48 | createdAt: timestamp("created_at").defaultNow().notNull(), 49 | updatedAt: timestamp("updated_at") 50 | .$onUpdate(() => /* @__PURE__ */ new Date()) 51 | .notNull(), 52 | }); 53 | 54 | export const custom_verification = pgTable("custom_verification", { 55 | id: text("id").primaryKey(), 56 | identifier: text("identifier").notNull(), 57 | value: text("value").notNull(), 58 | expiresAt: timestamp("expires_at").notNull(), 59 | createdAt: timestamp("created_at").defaultNow().notNull(), 60 | updatedAt: timestamp("updated_at") 61 | .defaultNow() 62 | .$onUpdate(() => /* @__PURE__ */ new Date()) 63 | .notNull(), 64 | }); 65 | 66 | export const twoFactor = pgTable("two_factor", { 67 | id: text("id").primaryKey(), 68 | secret: text("secret").notNull(), 69 | backupCodes: text("backup_codes").notNull(), 70 | userId: text("user_id") 71 | .notNull() 72 | .references(() => custom_user.id, { onDelete: "cascade" }), 73 | }); 74 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/(auth)/two-factor/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { Input } from "@/components/ui/input"; 13 | import { Label } from "@/components/ui/label"; 14 | import { client } from "@/lib/auth-client"; 15 | import { AlertCircle, CheckCircle2 } from "lucide-react"; 16 | import Link from "next/link"; 17 | import { useState } from "react"; 18 | 19 | export default function Component() { 20 | const [totpCode, setTotpCode] = useState(""); 21 | const [error, setError] = useState(""); 22 | const [success, setSuccess] = useState(false); 23 | 24 | const handleSubmit = (e: React.FormEvent) => { 25 | e.preventDefault(); 26 | if (totpCode.length !== 6 || !/^\d+$/.test(totpCode)) { 27 | setError("TOTP code must be 6 digits"); 28 | return; 29 | } 30 | client.twoFactor 31 | .verifyTotp({ 32 | code: totpCode, 33 | }) 34 | .then((res) => { 35 | if (res.data?.token) { 36 | setSuccess(true); 37 | setError(""); 38 | } else { 39 | setError("Invalid TOTP code"); 40 | } 41 | }); 42 | }; 43 | 44 | return ( 45 | <main className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]"> 46 | <Card className="w-[350px]"> 47 | <CardHeader> 48 | <CardTitle>TOTP Verification</CardTitle> 49 | <CardDescription> 50 | Enter your 6-digit TOTP code to authenticate 51 | </CardDescription> 52 | </CardHeader> 53 | <CardContent> 54 | {!success ? ( 55 | <form onSubmit={handleSubmit}> 56 | <div className="space-y-2"> 57 | <Label htmlFor="totp">TOTP Code</Label> 58 | <Input 59 | id="totp" 60 | type="text" 61 | inputMode="numeric" 62 | pattern="\d{6}" 63 | maxLength={6} 64 | value={totpCode} 65 | onChange={(e) => setTotpCode(e.target.value)} 66 | placeholder="Enter 6-digit code" 67 | required 68 | /> 69 | </div> 70 | {error && ( 71 | <div className="flex items-center mt-2 text-red-500"> 72 | <AlertCircle className="w-4 h-4 mr-2" /> 73 | <span className="text-sm">{error}</span> 74 | </div> 75 | )} 76 | <Button type="submit" className="w-full mt-4"> 77 | Verify 78 | </Button> 79 | </form> 80 | ) : ( 81 | <div className="flex flex-col items-center justify-center space-y-2"> 82 | <CheckCircle2 className="w-12 h-12 text-green-500" /> 83 | <p className="text-lg font-semibold">Verification Successful</p> 84 | </div> 85 | )} 86 | </CardContent> 87 | <CardFooter className="text-sm text-muted-foreground gap-2"> 88 | <Link href="/two-factor/otp"> 89 | <Button variant="link" size="sm"> 90 | Switch to Email Verification 91 | </Button> 92 | </Link> 93 | </CardFooter> 94 | </Card> 95 | </main> 96 | ); 97 | } 98 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/db/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getAuthTables } from "."; 2 | import { BetterAuthError } from "@better-auth/core/error"; 3 | import type { BetterAuthOptions } from "@better-auth/core"; 4 | import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect"; 5 | import { kyselyAdapter } from "../adapters/kysely-adapter"; 6 | import { memoryAdapter, type MemoryDB } from "../adapters/memory-adapter"; 7 | import { logger } from "@better-auth/core/env"; 8 | import type { DBFieldAttribute } from "@better-auth/core/db"; 9 | import type { DBAdapter } from "@better-auth/core/db/adapter"; 10 | 11 | export async function getAdapter( 12 | options: BetterAuthOptions, 13 | ): Promise<DBAdapter<BetterAuthOptions>> { 14 | let adapter: DBAdapter<BetterAuthOptions>; 15 | if (!options.database) { 16 | const tables = getAuthTables(options); 17 | const memoryDB = Object.keys(tables).reduce<MemoryDB>((acc, key) => { 18 | acc[key] = []; 19 | return acc; 20 | }, {}); 21 | logger.warn( 22 | "No database configuration provided. Using memory adapter in development", 23 | ); 24 | adapter = memoryAdapter(memoryDB)(options); 25 | } else if (typeof options.database === "function") { 26 | adapter = options.database(options); 27 | } else { 28 | const { kysely, databaseType, transaction } = 29 | await createKyselyAdapter(options); 30 | if (!kysely) { 31 | throw new BetterAuthError("Failed to initialize database adapter"); 32 | } 33 | adapter = kyselyAdapter(kysely, { 34 | type: databaseType || "sqlite", 35 | debugLogs: 36 | "debugLogs" in options.database ? options.database.debugLogs : false, 37 | transaction: transaction, 38 | })(options); 39 | } 40 | // patch for 1.3.x to ensure we have a transaction function in the adapter 41 | if (!adapter.transaction) { 42 | logger.warn( 43 | "Adapter does not correctly implement transaction function, patching it automatically. Please update your adapter implementation.", 44 | ); 45 | adapter.transaction = async (cb) => { 46 | return cb(adapter); 47 | }; 48 | } 49 | return adapter; 50 | } 51 | 52 | export function convertToDB<T extends Record<string, any>>( 53 | fields: Record<string, DBFieldAttribute>, 54 | values: T, 55 | ) { 56 | let result: Record<string, any> = values.id 57 | ? { 58 | id: values.id, 59 | } 60 | : {}; 61 | for (const key in fields) { 62 | const field = fields[key]!; 63 | const value = values[key]; 64 | if (value === undefined) { 65 | continue; 66 | } 67 | result[field.fieldName || key] = value; 68 | } 69 | return result as T; 70 | } 71 | 72 | export function convertFromDB<T extends Record<string, any>>( 73 | fields: Record<string, DBFieldAttribute>, 74 | values: T | null, 75 | ) { 76 | if (!values) { 77 | return null; 78 | } 79 | let result: Record<string, any> = { 80 | id: values.id, 81 | }; 82 | for (const [key, value] of Object.entries(fields)) { 83 | result[key] = values[value.fieldName || key]; 84 | } 85 | return result as T; 86 | } 87 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/client/solid/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getClientConfig } from "../config"; 2 | import { createDynamicPathProxy } from "../proxy"; 3 | import { capitalizeFirstLetter } from "../../utils/misc"; 4 | import type { 5 | InferActions, 6 | InferClientAPI, 7 | InferErrorCodes, 8 | IsSignal, 9 | } from "../types"; 10 | import type { 11 | BetterAuthClientPlugin, 12 | BetterAuthClientOptions, 13 | } from "@better-auth/core"; 14 | 15 | import type { Accessor } from "solid-js"; 16 | import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; 17 | import type { 18 | BetterFetchError, 19 | BetterFetchResponse, 20 | } from "@better-fetch/fetch"; 21 | import { useStore } from "./solid-store"; 22 | import type { BASE_ERROR_CODES } from "@better-auth/core/error"; 23 | 24 | function getAtomKey(str: string) { 25 | return `use${capitalizeFirstLetter(str)}`; 26 | } 27 | 28 | type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends { 29 | plugins: Array<infer Plugin>; 30 | } 31 | ? UnionToIntersection< 32 | Plugin extends BetterAuthClientPlugin 33 | ? Plugin["getAtoms"] extends (fetch: any) => infer Atoms 34 | ? Atoms extends Record<string, any> 35 | ? { 36 | [key in keyof Atoms as IsSignal<key> extends true 37 | ? never 38 | : key extends string 39 | ? `use${Capitalize<key>}` 40 | : never]: () => Accessor<ReturnType<Atoms[key]["get"]>>; 41 | } 42 | : {} 43 | : {} 44 | : {} 45 | > 46 | : {}; 47 | 48 | export function createAuthClient<Option extends BetterAuthClientOptions>( 49 | options?: Option, 50 | ) { 51 | const { 52 | pluginPathMethods, 53 | pluginsActions, 54 | pluginsAtoms, 55 | $fetch, 56 | atomListeners, 57 | } = getClientConfig(options); 58 | let resolvedHooks: Record<string, any> = {}; 59 | for (const [key, value] of Object.entries(pluginsAtoms)) { 60 | resolvedHooks[getAtomKey(key)] = () => useStore(value); 61 | } 62 | const routes = { 63 | ...pluginsActions, 64 | ...resolvedHooks, 65 | }; 66 | const proxy = createDynamicPathProxy( 67 | routes, 68 | $fetch, 69 | pluginPathMethods, 70 | pluginsAtoms, 71 | atomListeners, 72 | ); 73 | type ClientAPI = InferClientAPI<Option>; 74 | type Session = ClientAPI extends { 75 | getSession: () => Promise<infer Res>; 76 | } 77 | ? Res extends BetterFetchResponse<infer S> 78 | ? S 79 | : Res extends Record<string, any> 80 | ? Res 81 | : never 82 | : never; 83 | return proxy as UnionToIntersection<InferResolvedHooks<Option>> & 84 | InferClientAPI<Option> & 85 | InferActions<Option> & { 86 | useSession: () => Accessor<{ 87 | data: Session; 88 | isPending: boolean; 89 | isRefetching: boolean; 90 | error: BetterFetchError | null; 91 | }>; 92 | $Infer: { 93 | Session: NonNullable<Session>; 94 | }; 95 | $fetch: typeof $fetch; 96 | $ERROR_CODES: PrettifyDeep< 97 | InferErrorCodes<Option> & typeof BASE_ERROR_CODES 98 | >; 99 | }; 100 | } 101 | 102 | export type * from "@better-fetch/fetch"; 103 | export type * from "nanostores"; 104 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/notion.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { betterFetch } from "@better-fetch/fetch"; 2 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 3 | import { 4 | createAuthorizationURL, 5 | refreshAccessToken, 6 | validateAuthorizationCode, 7 | } from "../oauth2"; 8 | 9 | export interface NotionProfile { 10 | object: "user"; 11 | id: string; 12 | type: "person" | "bot"; 13 | name?: string; 14 | avatar_url?: string; 15 | person?: { 16 | email?: string; 17 | }; 18 | } 19 | 20 | export interface NotionOptions extends ProviderOptions<NotionProfile> { 21 | clientId: string; 22 | } 23 | 24 | export const notion = (options: NotionOptions) => { 25 | const tokenEndpoint = "https://api.notion.com/v1/oauth/token"; 26 | return { 27 | id: "notion", 28 | name: "Notion", 29 | createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { 30 | const _scopes: string[] = options.disableDefaultScope ? [] : []; 31 | options.scope && _scopes.push(...options.scope); 32 | scopes && _scopes.push(...scopes); 33 | return createAuthorizationURL({ 34 | id: "notion", 35 | options, 36 | authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize", 37 | scopes: _scopes, 38 | state, 39 | redirectURI, 40 | loginHint, 41 | additionalParams: { 42 | owner: "user", 43 | }, 44 | }); 45 | }, 46 | validateAuthorizationCode: async ({ code, redirectURI }) => { 47 | return validateAuthorizationCode({ 48 | code, 49 | redirectURI, 50 | options, 51 | tokenEndpoint, 52 | authentication: "basic", 53 | }); 54 | }, 55 | refreshAccessToken: options.refreshAccessToken 56 | ? options.refreshAccessToken 57 | : async (refreshToken) => { 58 | return refreshAccessToken({ 59 | refreshToken, 60 | options: { 61 | clientId: options.clientId, 62 | clientKey: options.clientKey, 63 | clientSecret: options.clientSecret, 64 | }, 65 | tokenEndpoint, 66 | }); 67 | }, 68 | async getUserInfo(token) { 69 | if (options.getUserInfo) { 70 | return options.getUserInfo(token); 71 | } 72 | const { data: profile, error } = await betterFetch<{ 73 | bot: { 74 | owner: { 75 | user: NotionProfile; 76 | }; 77 | }; 78 | }>("https://api.notion.com/v1/users/me", { 79 | headers: { 80 | Authorization: `Bearer ${token.accessToken}`, 81 | "Notion-Version": "2022-06-28", 82 | }, 83 | }); 84 | if (error || !profile) { 85 | return null; 86 | } 87 | const userProfile = profile.bot?.owner?.user; 88 | if (!userProfile) { 89 | return null; 90 | } 91 | const userMap = await options.mapProfileToUser?.(userProfile); 92 | return { 93 | user: { 94 | id: userProfile.id, 95 | name: userProfile.name || "Notion User", 96 | email: userProfile.person?.email || null, 97 | image: userProfile.avatar_url, 98 | emailVerified: !!userProfile.person?.email, 99 | ...userMap, 100 | }, 101 | data: userProfile, 102 | }; 103 | }, 104 | options, 105 | } satisfies OAuthProvider<NotionProfile>; 106 | }; 107 | ``` -------------------------------------------------------------------------------- /docs/content/docs/adapters/mssql.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: MS SQL 3 | description: Integrate Better Auth with MS SQL. 4 | --- 5 | 6 | Microsoft SQL Server is a relational database management system developed by Microsoft, designed for enterprise-level data storage, management, and analytics with robust security and scalability features. 7 | Read more [here](https://en.wikipedia.org/wiki/Microsoft_SQL_Server). 8 | 9 | ## Example Usage 10 | 11 | Make sure you have MS SQL installed and configured. 12 | Then, you can connect it straight into Better Auth. 13 | 14 | ```ts title="auth.ts" 15 | import { betterAuth } from "better-auth"; 16 | import { MssqlDialect } from "kysely"; 17 | import * as Tedious from 'tedious' 18 | import * as Tarn from 'tarn' 19 | 20 | const dialect = new MssqlDialect({ 21 | tarn: { 22 | ...Tarn, 23 | options: { 24 | min: 0, 25 | max: 10, 26 | }, 27 | }, 28 | tedious: { 29 | ...Tedious, 30 | connectionFactory: () => new Tedious.Connection({ 31 | authentication: { 32 | options: { 33 | password: 'password', 34 | userName: 'username', 35 | }, 36 | type: 'default', 37 | }, 38 | options: { 39 | database: 'some_db', 40 | port: 1433, 41 | trustServerCertificate: true, 42 | }, 43 | server: 'localhost', 44 | }), 45 | }, 46 | TYPES: { 47 | ...Tedious.TYPES, 48 | DateTime: Tedious.TYPES.DateTime2, 49 | }, 50 | }) 51 | 52 | export const auth = betterAuth({ 53 | database: { 54 | dialect, 55 | type: "mssql" 56 | } 57 | }); 58 | 59 | 60 | ``` 61 | <Callout> 62 | For more information, read Kysely's documentation to the [MssqlDialect](https://kysely-org.github.io/kysely-apidoc/classes/MssqlDialect.html). 63 | </Callout> 64 | 65 | ## Schema generation & migration 66 | 67 | The [Better Auth CLI](/docs/concepts/cli) allows you to generate or migrate 68 | your database schema based on your Better Auth configuration and plugins. 69 | 70 | <table> 71 | <thead> 72 | <tr className="border-b"> 73 | <th> 74 | <p className="font-bold text-[16px] mb-1">MS SQL Schema Generation</p> 75 | </th> 76 | <th> 77 | <p className="font-bold text-[16px] mb-1">MS SQL Schema Migration</p> 78 | </th> 79 | </tr> 80 | </thead> 81 | <tbody> 82 | <tr className="h-10"> 83 | <td>✅ Supported</td> 84 | <td>✅ Supported</td> 85 | </tr> 86 | </tbody> 87 | </table> 88 | 89 | ```bash title="Schema Generation" 90 | npx @better-auth/cli@latest generate 91 | ``` 92 | 93 | ```bash title="Schema Migration" 94 | npx @better-auth/cli@latest migrate 95 | ``` 96 | 97 | ## Additional Information 98 | 99 | MS SQL is supported under the hood via the [Kysely](https://kysely.dev/) adapter, any database supported by Kysely would also be supported. (<Link href="/docs/adapters/other-relational-databases">Read more here</Link>) 100 | 101 | If you're looking for performance improvements or tips, take a look at our guide to <Link href="/docs/guides/optimizing-for-performance">performance optimizations</Link>. 102 | ``` -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/auth-schema-number-id.txt: -------------------------------------------------------------------------------- ``` 1 | import { 2 | pgTable, 3 | text, 4 | timestamp, 5 | boolean, 6 | integer, 7 | serial, 8 | } from "drizzle-orm/pg-core"; 9 | 10 | export const custom_user = pgTable("custom_user", { 11 | id: serial("id").primaryKey(), 12 | name: text("name").notNull(), 13 | email: text("email").notNull().unique(), 14 | emailVerified: boolean("email_verified").default(false).notNull(), 15 | image: text("image"), 16 | createdAt: timestamp("created_at").defaultNow().notNull(), 17 | updatedAt: timestamp("updated_at") 18 | .defaultNow() 19 | .$onUpdate(() => /* @__PURE__ */ new Date()) 20 | .notNull(), 21 | twoFactorEnabled: boolean("two_factor_enabled").default(false), 22 | username: text("username").unique(), 23 | displayUsername: text("display_username"), 24 | }); 25 | 26 | export const custom_session = pgTable("custom_session", { 27 | id: serial("id").primaryKey(), 28 | expiresAt: timestamp("expires_at").notNull(), 29 | token: text("token").notNull().unique(), 30 | createdAt: timestamp("created_at").defaultNow().notNull(), 31 | updatedAt: timestamp("updated_at") 32 | .$onUpdate(() => /* @__PURE__ */ new Date()) 33 | .notNull(), 34 | ipAddress: text("ip_address"), 35 | userAgent: text("user_agent"), 36 | userId: integer("user_id") 37 | .notNull() 38 | .references(() => custom_user.id, { onDelete: "cascade" }), 39 | }); 40 | 41 | export const custom_account = pgTable("custom_account", { 42 | id: serial("id").primaryKey(), 43 | accountId: text("account_id").notNull(), 44 | providerId: text("provider_id").notNull(), 45 | userId: integer("user_id") 46 | .notNull() 47 | .references(() => custom_user.id, { onDelete: "cascade" }), 48 | accessToken: text("access_token"), 49 | refreshToken: text("refresh_token"), 50 | idToken: text("id_token"), 51 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 52 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 53 | scope: text("scope"), 54 | password: text("password"), 55 | createdAt: timestamp("created_at").defaultNow().notNull(), 56 | updatedAt: timestamp("updated_at") 57 | .$onUpdate(() => /* @__PURE__ */ new Date()) 58 | .notNull(), 59 | }); 60 | 61 | export const custom_verification = pgTable("custom_verification", { 62 | id: serial("id").primaryKey(), 63 | identifier: text("identifier").notNull(), 64 | value: text("value").notNull(), 65 | expiresAt: timestamp("expires_at").notNull(), 66 | createdAt: timestamp("created_at").defaultNow().notNull(), 67 | updatedAt: timestamp("updated_at") 68 | .defaultNow() 69 | .$onUpdate(() => /* @__PURE__ */ new Date()) 70 | .notNull(), 71 | }); 72 | 73 | export const twoFactor = pgTable("two_factor", { 74 | id: serial("id").primaryKey(), 75 | secret: text("secret").notNull(), 76 | backupCodes: text("backup_codes").notNull(), 77 | userId: integer("user_id") 78 | .notNull() 79 | .references(() => custom_user.id, { onDelete: "cascade" }), 80 | }); 81 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/auth.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getEndpoints, router } from "./api"; 2 | import { init } from "./init"; 3 | import type { BetterAuthOptions } from "@better-auth/core"; 4 | import type { 5 | InferPluginErrorCodes, 6 | InferPluginTypes, 7 | InferSession, 8 | InferUser, 9 | InferAPI, 10 | } from "./types"; 11 | import type { PrettifyDeep, Expand } from "./types/helper"; 12 | import { getBaseURL, getOrigin } from "./utils/url"; 13 | import { BASE_ERROR_CODES } from "@better-auth/core/error"; 14 | import { BetterAuthError } from "@better-auth/core/error"; 15 | import { runWithAdapter } from "@better-auth/core/context"; 16 | import type { AuthContext } from "@better-auth/core"; 17 | 18 | export type WithJsDoc<T, D> = Expand<T & D>; 19 | 20 | export const betterAuth = <Options extends BetterAuthOptions>( 21 | options: Options & 22 | // fixme(alex): do we need Record<never, never> here? 23 | Record<never, never>, 24 | ): Auth<Options> => { 25 | const authContext = init(options); 26 | const { api } = getEndpoints(authContext, options); 27 | const errorCodes = options.plugins?.reduce((acc, plugin) => { 28 | if (plugin.$ERROR_CODES) { 29 | return { 30 | ...acc, 31 | ...plugin.$ERROR_CODES, 32 | }; 33 | } 34 | return acc; 35 | }, {}); 36 | return { 37 | handler: async (request: Request) => { 38 | const ctx = await authContext; 39 | const basePath = ctx.options.basePath || "/api/auth"; 40 | if (!ctx.options.baseURL) { 41 | const baseURL = getBaseURL(undefined, basePath, request); 42 | if (baseURL) { 43 | ctx.baseURL = baseURL; 44 | ctx.options.baseURL = getOrigin(ctx.baseURL) || undefined; 45 | } else { 46 | throw new BetterAuthError( 47 | "Could not get base URL from request. Please provide a valid base URL.", 48 | ); 49 | } 50 | } 51 | ctx.trustedOrigins = [ 52 | ...(options.trustedOrigins 53 | ? Array.isArray(options.trustedOrigins) 54 | ? options.trustedOrigins 55 | : await options.trustedOrigins(request) 56 | : []), 57 | ctx.options.baseURL!, 58 | ]; 59 | const { handler } = router(ctx, options); 60 | return runWithAdapter(ctx.adapter, () => handler(request)); 61 | }, 62 | api, 63 | options: options, 64 | $context: authContext, 65 | $ERROR_CODES: { 66 | ...errorCodes, 67 | ...BASE_ERROR_CODES, 68 | }, 69 | } as any; 70 | }; 71 | 72 | export type Auth<Options extends BetterAuthOptions = BetterAuthOptions> = { 73 | handler: (request: Request) => Promise<Response>; 74 | api: InferAPI<ReturnType<typeof router<Options>>["endpoints"]>; 75 | options: Options; 76 | $ERROR_CODES: InferPluginErrorCodes<Options> & typeof BASE_ERROR_CODES; 77 | $context: Promise<AuthContext>; 78 | /** 79 | * Share types 80 | */ 81 | $Infer: InferPluginTypes<Options> extends { 82 | Session: any; 83 | } 84 | ? InferPluginTypes<Options> 85 | : { 86 | Session: { 87 | session: PrettifyDeep<InferSession<Options>>; 88 | user: PrettifyDeep<InferUser<Options>>; 89 | }; 90 | } & InferPluginTypes<Options>; 91 | }; 92 | ``` -------------------------------------------------------------------------------- /demo/nextjs/app/device/page.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import { useState, useTransition } from "react"; 4 | import { useRouter, useSearchParams } from "next/navigation"; 5 | import { client } from "@/lib/auth-client"; 6 | import { Card } from "@/components/ui/card"; 7 | import { Input } from "@/components/ui/input"; 8 | import { Button } from "@/components/ui/button"; 9 | import { Label } from "@/components/ui/label"; 10 | import { Alert, AlertDescription } from "@/components/ui/alert"; 11 | import { Loader2 } from "lucide-react"; 12 | 13 | export default function DeviceAuthorizationPage() { 14 | const router = useRouter(); 15 | const params = useSearchParams(); 16 | const user_code = params.get("user_code"); 17 | const [userCode, setUserCode] = useState<string>(user_code ? user_code : ""); 18 | const [isPending, startTransition] = useTransition(); 19 | const [error, setError] = useState<string | null>(null); 20 | 21 | const handleSubmit = (e: React.FormEvent) => { 22 | e.preventDefault(); 23 | setError(null); 24 | 25 | startTransition(async () => { 26 | try { 27 | const finalCode = userCode.trim().replaceAll(/-/g, "").toUpperCase(); 28 | // Get the device authorization status 29 | const response = await client.device({ 30 | query: { 31 | user_code: finalCode, 32 | }, 33 | }); 34 | 35 | if (response.data) { 36 | router.push(`/device/approve?user_code=${finalCode}`); 37 | } 38 | } catch (err: any) { 39 | setError( 40 | err.error?.message || "Invalid code. Please check and try again.", 41 | ); 42 | } 43 | }); 44 | }; 45 | 46 | return ( 47 | <div className="flex min-h-screen items-center justify-center p-4"> 48 | <Card className="w-full max-w-md p-6"> 49 | <div className="space-y-4"> 50 | <div className="text-center"> 51 | <h1 className="text-2xl font-bold">Device Authorization</h1> 52 | <p className="text-muted-foreground mt-2"> 53 | Enter the code displayed on your device 54 | </p> 55 | </div> 56 | 57 | <form onSubmit={handleSubmit} className="space-y-4"> 58 | <div className="space-y-2"> 59 | <Label htmlFor="userCode">Device Code</Label> 60 | <Input 61 | id="userCode" 62 | type="text" 63 | placeholder="XXXX-XXXX" 64 | value={userCode} 65 | onChange={(e) => setUserCode(e.target.value)} 66 | className="text-center text-lg font-mono uppercase" 67 | maxLength={9} 68 | disabled={isPending} 69 | required 70 | /> 71 | </div> 72 | 73 | {error && ( 74 | <Alert variant="destructive"> 75 | <AlertDescription>{error}</AlertDescription> 76 | </Alert> 77 | )} 78 | 79 | <Button type="submit" className="w-full" disabled={isPending}> 80 | {isPending ? ( 81 | <> 82 | <Loader2 className="mr-2 h-4 w-4 animate-spin" /> 83 | Verifying... 84 | </> 85 | ) : ( 86 | "Continue" 87 | )} 88 | </Button> 89 | </form> 90 | </div> 91 | </Card> 92 | </div> 93 | ); 94 | } 95 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/routes/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { apiKeySchema } from "../schema"; 2 | import type { ApiKey, ApiKeyOptions } from "../types"; 3 | import { createApiKey } from "./create-api-key"; 4 | import { deleteApiKey } from "./delete-api-key"; 5 | import { getApiKey } from "./get-api-key"; 6 | import { updateApiKey } from "./update-api-key"; 7 | import { verifyApiKey } from "./verify-api-key"; 8 | import { listApiKeys } from "./list-api-keys"; 9 | import { deleteAllExpiredApiKeysEndpoint } from "./delete-all-expired-api-keys"; 10 | import { API_KEY_TABLE_NAME } from ".."; 11 | import type { AuthContext } from "@better-auth/core"; 12 | 13 | export type PredefinedApiKeyOptions = ApiKeyOptions & 14 | Required< 15 | Pick< 16 | ApiKeyOptions, 17 | | "apiKeyHeaders" 18 | | "defaultKeyLength" 19 | | "keyExpiration" 20 | | "rateLimit" 21 | | "maximumPrefixLength" 22 | | "minimumPrefixLength" 23 | | "maximumNameLength" 24 | | "disableKeyHashing" 25 | | "minimumNameLength" 26 | | "requireName" 27 | | "enableMetadata" 28 | | "enableSessionForAPIKeys" 29 | | "startingCharactersConfig" 30 | > 31 | > & { 32 | keyExpiration: Required<ApiKeyOptions["keyExpiration"]>; 33 | startingCharactersConfig: Required< 34 | ApiKeyOptions["startingCharactersConfig"] 35 | >; 36 | }; 37 | 38 | let lastChecked: Date | null = null; 39 | 40 | export async function deleteAllExpiredApiKeys( 41 | ctx: AuthContext, 42 | byPassLastCheckTime = false, 43 | ): Promise<void> { 44 | if (lastChecked && !byPassLastCheckTime) { 45 | const now = new Date(); 46 | const diff = now.getTime() - lastChecked.getTime(); 47 | if (diff < 10000) { 48 | return; 49 | } 50 | } 51 | lastChecked = new Date(); 52 | await ctx.adapter 53 | .deleteMany({ 54 | model: API_KEY_TABLE_NAME, 55 | where: [ 56 | { 57 | field: "expiresAt" satisfies keyof ApiKey, 58 | operator: "lt", 59 | value: new Date(), 60 | }, 61 | { 62 | field: "expiresAt", 63 | operator: "ne", 64 | value: null, 65 | }, 66 | ], 67 | }) 68 | .catch((error) => { 69 | ctx.logger.error(`Failed to delete expired API keys:`, error); 70 | }); 71 | } 72 | 73 | export function createApiKeyRoutes({ 74 | keyGenerator, 75 | opts, 76 | schema, 77 | }: { 78 | keyGenerator: (options: { 79 | length: number; 80 | prefix: string | undefined; 81 | }) => Promise<string> | string; 82 | opts: PredefinedApiKeyOptions; 83 | schema: ReturnType<typeof apiKeySchema>; 84 | }) { 85 | return { 86 | createApiKey: createApiKey({ 87 | keyGenerator, 88 | opts, 89 | schema, 90 | deleteAllExpiredApiKeys, 91 | }), 92 | verifyApiKey: verifyApiKey({ opts, schema, deleteAllExpiredApiKeys }), 93 | getApiKey: getApiKey({ opts, schema, deleteAllExpiredApiKeys }), 94 | updateApiKey: updateApiKey({ opts, schema, deleteAllExpiredApiKeys }), 95 | deleteApiKey: deleteApiKey({ opts, schema, deleteAllExpiredApiKeys }), 96 | listApiKeys: listApiKeys({ opts, schema, deleteAllExpiredApiKeys }), 97 | deleteAllExpiredApiKeys: deleteAllExpiredApiKeysEndpoint({ 98 | deleteAllExpiredApiKeys, 99 | }), 100 | }; 101 | } 102 | ``` -------------------------------------------------------------------------------- /packages/core/src/social-providers/twitch.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { OAuthProvider, ProviderOptions } from "../oauth2"; 2 | import { logger } from "../env"; 3 | import { 4 | createAuthorizationURL, 5 | validateAuthorizationCode, 6 | refreshAccessToken, 7 | } from "../oauth2"; 8 | import { decodeJwt } from "jose"; 9 | 10 | /** 11 | * @see https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#requesting-claims 12 | */ 13 | export interface TwitchProfile { 14 | /** 15 | * The sub of the user 16 | */ 17 | sub: string; 18 | /** 19 | * The preferred username of the user 20 | */ 21 | preferred_username: string; 22 | /** 23 | * The email of the user 24 | */ 25 | email: string; 26 | /** 27 | * Indicate if this user has a verified email. 28 | */ 29 | email_verified: boolean; 30 | /** 31 | * The picture of the user 32 | */ 33 | picture: string; 34 | } 35 | 36 | export interface TwitchOptions extends ProviderOptions<TwitchProfile> { 37 | clientId: string; 38 | claims?: string[]; 39 | } 40 | export const twitch = (options: TwitchOptions) => { 41 | return { 42 | id: "twitch", 43 | name: "Twitch", 44 | createAuthorizationURL({ state, scopes, redirectURI }) { 45 | const _scopes = options.disableDefaultScope 46 | ? [] 47 | : ["user:read:email", "openid"]; 48 | options.scope && _scopes.push(...options.scope); 49 | scopes && _scopes.push(...scopes); 50 | return createAuthorizationURL({ 51 | id: "twitch", 52 | redirectURI, 53 | options, 54 | authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize", 55 | scopes: _scopes, 56 | state, 57 | claims: options.claims || [ 58 | "email", 59 | "email_verified", 60 | "preferred_username", 61 | "picture", 62 | ], 63 | }); 64 | }, 65 | validateAuthorizationCode: async ({ code, redirectURI }) => { 66 | return validateAuthorizationCode({ 67 | code, 68 | redirectURI, 69 | options, 70 | tokenEndpoint: "https://id.twitch.tv/oauth2/token", 71 | }); 72 | }, 73 | refreshAccessToken: options.refreshAccessToken 74 | ? options.refreshAccessToken 75 | : async (refreshToken) => { 76 | return refreshAccessToken({ 77 | refreshToken, 78 | options: { 79 | clientId: options.clientId, 80 | clientKey: options.clientKey, 81 | clientSecret: options.clientSecret, 82 | }, 83 | tokenEndpoint: "https://id.twitch.tv/oauth2/token", 84 | }); 85 | }, 86 | async getUserInfo(token) { 87 | if (options.getUserInfo) { 88 | return options.getUserInfo(token); 89 | } 90 | const idToken = token.idToken; 91 | if (!idToken) { 92 | logger.error("No idToken found in token"); 93 | return null; 94 | } 95 | const profile = decodeJwt(idToken) as TwitchProfile; 96 | const userMap = await options.mapProfileToUser?.(profile); 97 | return { 98 | user: { 99 | id: profile.sub, 100 | name: profile.preferred_username, 101 | email: profile.email, 102 | image: profile.picture, 103 | emailVerified: profile.email_verified, 104 | ...userMap, 105 | }, 106 | data: profile, 107 | }; 108 | }, 109 | options, 110 | } satisfies OAuthProvider<TwitchProfile>; 111 | }; 112 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/api-key/routes/delete-api-key.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { APIError, sessionMiddleware } from "../../../api"; 3 | import { ERROR_CODES } from ".."; 4 | import type { apiKeySchema } from "../schema"; 5 | import type { ApiKey } from "../types"; 6 | import type { PredefinedApiKeyOptions } from "."; 7 | import { API_KEY_TABLE_NAME } from ".."; 8 | import type { AuthContext } from "@better-auth/core"; 9 | import { createAuthEndpoint } from "@better-auth/core/api"; 10 | export function deleteApiKey({ 11 | opts, 12 | schema, 13 | deleteAllExpiredApiKeys, 14 | }: { 15 | opts: PredefinedApiKeyOptions; 16 | schema: ReturnType<typeof apiKeySchema>; 17 | deleteAllExpiredApiKeys( 18 | ctx: AuthContext, 19 | byPassLastCheckTime?: boolean, 20 | ): void; 21 | }) { 22 | return createAuthEndpoint( 23 | "/api-key/delete", 24 | { 25 | method: "POST", 26 | body: z.object({ 27 | keyId: z.string().meta({ 28 | description: "The id of the Api Key", 29 | }), 30 | }), 31 | use: [sessionMiddleware], 32 | metadata: { 33 | openapi: { 34 | description: "Delete an existing API key", 35 | requestBody: { 36 | content: { 37 | "application/json": { 38 | schema: { 39 | type: "object", 40 | properties: { 41 | keyId: { 42 | type: "string", 43 | description: "The id of the API key to delete", 44 | }, 45 | }, 46 | required: ["keyId"], 47 | }, 48 | }, 49 | }, 50 | }, 51 | responses: { 52 | "200": { 53 | description: "API key deleted successfully", 54 | content: { 55 | "application/json": { 56 | schema: { 57 | type: "object", 58 | properties: { 59 | success: { 60 | type: "boolean", 61 | description: 62 | "Indicates if the API key was successfully deleted", 63 | }, 64 | }, 65 | required: ["success"], 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | async (ctx) => { 75 | const { keyId } = ctx.body; 76 | const session = ctx.context.session; 77 | if (session.user.banned === true) { 78 | throw new APIError("UNAUTHORIZED", { 79 | message: ERROR_CODES.USER_BANNED, 80 | }); 81 | } 82 | const apiKey = await ctx.context.adapter.findOne<ApiKey>({ 83 | model: API_KEY_TABLE_NAME, 84 | where: [ 85 | { 86 | field: "id", 87 | value: keyId, 88 | }, 89 | ], 90 | }); 91 | 92 | if (!apiKey || apiKey.userId !== session.user.id) { 93 | throw new APIError("NOT_FOUND", { 94 | message: ERROR_CODES.KEY_NOT_FOUND, 95 | }); 96 | } 97 | 98 | try { 99 | await ctx.context.adapter.delete<ApiKey>({ 100 | model: API_KEY_TABLE_NAME, 101 | where: [ 102 | { 103 | field: "id", 104 | value: apiKey.id, 105 | }, 106 | ], 107 | }); 108 | } catch (error: any) { 109 | throw new APIError("INTERNAL_SERVER_ERROR", { 110 | message: error?.message, 111 | }); 112 | } 113 | deleteAllExpiredApiKeys(ctx.context); 114 | return ctx.json({ 115 | success: true, 116 | }); 117 | }, 118 | ); 119 | } 120 | ``` -------------------------------------------------------------------------------- /demo/expo-example/src/components/ui/button.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { Pressable } from "react-native"; 4 | import { cn } from "@/lib/utils"; 5 | import { TextClassContext } from "@/components/ui/text"; 6 | 7 | const buttonVariants = cva( 8 | "group flex items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary web:hover:opacity-90 active:opacity-90", 13 | destructive: "bg-destructive web:hover:opacity-90 active:opacity-90", 14 | outline: 15 | "border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent", 16 | secondary: "bg-secondary web:hover:opacity-80 active:opacity-80", 17 | ghost: 18 | "web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent", 19 | link: "web:underline-offset-4 web:hover:underline web:focus:underline ", 20 | }, 21 | size: { 22 | default: "h-10 px-4 py-2 native:h-12 native:px-5 native:py-3", 23 | sm: "h-9 rounded-md px-3", 24 | lg: "h-11 rounded-md px-8 native:h-14", 25 | icon: "h-10 w-10", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | }, 33 | ); 34 | 35 | const buttonTextVariants = cva( 36 | "web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors", 37 | { 38 | variants: { 39 | variant: { 40 | default: "text-primary-foreground", 41 | destructive: "text-destructive-foreground", 42 | outline: "group-active:text-accent-foreground", 43 | secondary: 44 | "text-secondary-foreground group-active:text-secondary-foreground", 45 | ghost: "group-active:text-accent-foreground", 46 | link: "text-primary group-active:underline", 47 | }, 48 | size: { 49 | default: "", 50 | sm: "", 51 | lg: "native:text-lg", 52 | icon: "", 53 | }, 54 | }, 55 | defaultVariants: { 56 | variant: "default", 57 | size: "default", 58 | }, 59 | }, 60 | ); 61 | 62 | type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> & 63 | VariantProps<typeof buttonVariants>; 64 | 65 | const Button = React.forwardRef< 66 | React.ElementRef<typeof Pressable>, 67 | ButtonProps 68 | >(({ className, variant, size, ...props }, ref) => { 69 | return ( 70 | <TextClassContext.Provider 71 | value={buttonTextVariants({ 72 | variant, 73 | size, 74 | className: "web:pointer-events-none", 75 | })} 76 | > 77 | <Pressable 78 | className={cn( 79 | props.disabled && "opacity-50 web:pointer-events-none", 80 | buttonVariants({ variant, size, className }), 81 | )} 82 | ref={ref} 83 | role="button" 84 | {...props} 85 | /> 86 | </TextClassContext.Provider> 87 | ); 88 | }); 89 | Button.displayName = "Button"; 90 | 91 | export { Button, buttonTextVariants, buttonVariants }; 92 | export type { ButtonProps }; 93 | ```