├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── tanstack-circle-logo.png ├── manifest.json └── better-auth.svg ├── bun.lockb ├── src ├── lib │ ├── resend.ts │ ├── auth │ │ ├── queries.ts │ │ ├── middleware.ts │ │ ├── auth-client.ts │ │ └── functions.ts │ ├── utils.ts │ ├── users │ │ ├── queries.ts │ │ ├── functions.ts │ │ └── mutations.ts │ └── organization │ │ ├── queries.ts │ │ ├── mutations.ts │ │ └── functions.ts ├── env │ ├── client.ts │ └── server.ts ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── spinner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── kbd.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── radio-group.tsx │ │ ├── hover-card.tsx │ │ ├── toggle.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── alert.tsx │ │ ├── scroll-area.tsx │ │ ├── tooltip.tsx │ │ ├── resizable.tsx │ │ ├── tabs.tsx │ │ ├── slider.tsx │ │ ├── accordion.tsx │ │ ├── card.tsx │ │ ├── input-otp.tsx │ │ ├── button-group.tsx │ │ ├── toggle-group.tsx │ │ ├── button.tsx │ │ ├── breadcrumb.tsx │ │ ├── empty.tsx │ │ ├── table.tsx │ │ ├── text-ellipsis.tsx │ │ └── pagination.tsx │ ├── sidebar │ │ ├── nav-secondary.tsx │ │ ├── app-sidebar.tsx │ │ ├── create-organization-modal.tsx │ │ ├── nav-platform.tsx │ │ ├── switcher-item.tsx │ │ ├── organization-settings.tsx │ │ └── organization-switcher.tsx │ ├── default-not-found.tsx │ ├── dev-tools.tsx │ ├── signout-button.tsx │ ├── auth │ │ ├── github.tsx │ │ ├── passkey.tsx │ │ ├── google.tsx │ │ ├── magic-link-form.tsx │ │ ├── email-otp-form.tsx │ │ ├── reset-password-form.tsx │ │ └── verify-2fa-form.tsx │ ├── default-catch-boundary.tsx │ ├── theme-toggle.tsx │ ├── main-layout.tsx │ └── theme-provider.tsx ├── routes │ ├── api │ │ └── auth │ │ │ └── $.ts │ ├── (app) │ │ ├── organizations │ │ │ ├── _pathlessLayout │ │ │ │ ├── $id │ │ │ │ │ ├── dashboard.tsx │ │ │ │ │ ├── route.tsx │ │ │ │ │ ├── members │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── -components │ │ │ │ │ │ │ └── invitation-actions.tsx │ │ │ │ │ └── settings │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── -components │ │ │ │ │ │ └── update-organization-name.tsx │ │ │ │ └── route.tsx │ │ │ ├── create.tsx │ │ │ └── -components │ │ │ │ └── organization-card.tsx │ │ ├── route.tsx │ │ ├── profile │ │ │ └── _pathlessLayout │ │ │ │ ├── security.tsx │ │ │ │ ├── api-keys.tsx │ │ │ │ └── -components │ │ │ │ ├── enable-two-factor-dialog.tsx │ │ │ │ ├── enable-two-factor.tsx │ │ │ │ ├── passkey.tsx │ │ │ │ └── password-validation.tsx │ │ └── onboarding.tsx │ ├── (auth) │ │ ├── signup.tsx │ │ ├── signin.tsx │ │ ├── verify-2fa.tsx │ │ ├── magic-link.tsx │ │ ├── signin-otp.tsx │ │ ├── forgot-password.tsx │ │ ├── verify-otp.tsx │ │ ├── reset-password.tsx │ │ └── route.tsx │ └── __root.tsx ├── hooks │ └── use-mobile.ts ├── db │ └── index.ts ├── types │ └── organizations.ts ├── constants │ └── config.ts ├── router.tsx └── emails │ ├── 2fa-otp-verification.tsx │ ├── magic-link-login.tsx │ ├── signin-otp-verification.tsx │ ├── reset-password-otp.tsx │ ├── email-verification.tsx │ ├── organization-invitation.tsx │ ├── change-email-verification.tsx │ └── reset-password.tsx ├── .cursor └── hooks.json ├── .gitignore ├── .cursorrules ├── .cta.json ├── drizzle.config.ts ├── drizzle-prod.config.ts ├── components.json ├── vite.config.ts ├── tsconfig.json ├── .vscode └── settings.json └── biome.json /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevinodpatidar/tanstack-better-auth-boilerplate/HEAD/bun.lockb -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevinodpatidar/tanstack-better-auth-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevinodpatidar/tanstack-better-auth-boilerplate/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevinodpatidar/tanstack-better-auth-boilerplate/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/tanstack-circle-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevinodpatidar/tanstack-better-auth-boilerplate/HEAD/public/tanstack-circle-logo.png -------------------------------------------------------------------------------- /src/lib/resend.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | import { env } from "@/env/server"; 3 | 4 | export const resend = new Resend(env.RESEND_API_KEY); -------------------------------------------------------------------------------- /.cursor/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "hooks": { 4 | "afterFileEdit": [ 5 | { 6 | "command": "npx ultracite fix" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | count.txt 7 | .env 8 | .nitro 9 | .tanstack 10 | .wrangler 11 | .output 12 | .vinxi 13 | todos.json 14 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # shadcn instructions 2 | 3 | Use the latest version of Shadcn to install new components, like this command to add a button component: 4 | 5 | ```bash 6 | pnpx shadcn@latest add button 7 | ``` 8 | -------------------------------------------------------------------------------- /.cta.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "tanstack-better-auth-boilerplate", 3 | "mode": "file-router", 4 | "typescript": true, 5 | "tailwind": true, 6 | "packageManager": "bun", 7 | "addOnOptions": {}, 8 | "git": true, 9 | "version": 1, 10 | "framework": "react-cra", 11 | "chosenAddOns": ["biome", "start", "shadcn"] 12 | } 13 | -------------------------------------------------------------------------------- /src/env/client.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | clientPrefix: "VITE_", 6 | client: { 7 | VITE_BASE_URL: z.string().default("http://localhost:3000"), 8 | }, 9 | 10 | runtimeEnv: import.meta.env, 11 | 12 | emptyStringAsUndefined: true, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | export { AspectRatio } 12 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /src/lib/auth/queries.ts: -------------------------------------------------------------------------------- 1 | import { queryOptions } from "@tanstack/react-query"; 2 | import { $getUser } from "@/lib/auth/functions"; 3 | 4 | export const authQueryOptions = () => 5 | queryOptions({ 6 | queryKey: ["user"], 7 | queryFn: ({ signal }) => $getUser({ signal }), 8 | }); 9 | 10 | export type AuthQueryResult = Awaited>; 11 | -------------------------------------------------------------------------------- /src/routes/api/auth/$.ts: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { auth } from "@/lib/auth/auth"; 3 | 4 | export const Route = createFileRoute("/api/auth/$")({ 5 | server: { 6 | handlers: { 7 | GET: ({ request }) => auth.handler(request), 8 | POST: ({ request }) => auth.handler(request), 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env.local", override: true }); 5 | 6 | export default defineConfig({ 7 | schema: "./src/db/schema.ts", 8 | out: "./src/db/drizzle", 9 | dialect: "postgresql", 10 | dbCredentials: { 11 | url: process.env.DATABASE_URL as string, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /drizzle-prod.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env.production.local", override: true }); 5 | 6 | export default defineConfig({ 7 | schema: "./src/db/schema.ts", 8 | out: "./src/db/drizzle", 9 | dialect: "postgresql", 10 | dbCredentials: { 11 | url: process.env.DATABASE_URL as string, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon } from "lucide-react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 | return ( 7 | 13 | ) 14 | } 15 | 16 | export { Spinner } 17 | -------------------------------------------------------------------------------- /src/routes/(app)/organizations/_pathlessLayout/$id/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { MainLayout } from "@/components/main-layout"; 3 | 4 | export const Route = createFileRoute( 5 | "/(app)/organizations/_pathlessLayout/$id/dashboard" 6 | )({ 7 | component: RouteComponent, 8 | }); 9 | 10 | function RouteComponent() { 11 | return Hello "/(app)/organizations/$id/dashboard"!; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/signup.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/signup")({ 5 | head: () => ({ 6 | meta: [{ title: "Sign Up - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /src/routes/(auth)/signin.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/signin")({ 5 | head: () => ({ 6 | meta: [{ title: "Sign In - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /src/routes/(auth)/verify-2fa.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/verify-2fa")({ 5 | head: () => ({ 6 | meta: [{ title: "Verify 2FA - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/(auth)/magic-link.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/magic-link")({ 5 | head: () => ({ 6 | meta: [{ title: "Magic Link - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /src/routes/(auth)/signin-otp.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/signin-otp")({ 5 | head: () => ({ 6 | meta: [{ title: "Sign In with OTP - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/auth/middleware.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@tanstack/react-router"; 2 | import { createMiddleware } from "@tanstack/react-start"; 3 | import { auth } from "./auth"; 4 | 5 | export const authMiddleware = createMiddleware().server( 6 | async ({ request, next }) => { 7 | const session = await auth.api.getSession({ headers: request.headers }); 8 | if (!session) { 9 | throw redirect({ 10 | to: "/signin", 11 | }); 12 | } 13 | return next({ context: { session: session.session } }); 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /src/routes/(auth)/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import AuthWrapper from "@/components/auth/auth-wrapper"; 3 | 4 | export const Route = createFileRoute("/(auth)/forgot-password")({ 5 | head: () => ({ 6 | meta: [{ title: "Forgot Password - TanStack + Better Auth Boilerplate" }], 7 | }), 8 | component: () => ( 9 | 14 | ), 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/sidebar/nav-secondary.tsx: -------------------------------------------------------------------------------- 1 | import { HelpCircle } from "lucide-react"; 2 | 3 | import { 4 | SidebarMenu, 5 | SidebarMenuButton, 6 | SidebarMenuItem, 7 | } from "@/components/ui/sidebar"; 8 | 9 | export function NavSecondary() { 10 | return ( 11 | 12 | 13 | 14 | 15 | How it works 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "TanStack App", 3 | "name": "Create TanStack App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/auth/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiKeyClient, 3 | customSessionClient, 4 | emailOTPClient, 5 | magicLinkClient, 6 | organizationClient, 7 | passkeyClient, 8 | twoFactorClient, 9 | } from "better-auth/client/plugins"; 10 | import { createAuthClient } from "better-auth/react"; 11 | import { env } from "@/env/client"; 12 | 13 | export const authClient = createAuthClient({ 14 | baseURL: env.VITE_BASE_URL, 15 | plugins: [ 16 | magicLinkClient(), 17 | twoFactorClient(), 18 | emailOTPClient(), 19 | organizationClient(), 20 | apiKeyClient(), 21 | passkeyClient(), 22 | customSessionClient(), 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/default-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@tanstack/react-router"; 2 | import { Button } from "./ui/button"; 3 | 4 | export function DefaultNotFound() { 5 | return ( 6 |
7 |

The page you are looking for does not exist.

8 |

9 | 12 | 15 |

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = useState(undefined); 7 | 8 | useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 12 | }; 13 | mql.addEventListener("change", onChange); 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 15 | return () => mql.removeEventListener("change", onChange); 16 | }, []); 17 | 18 | return !!isMobile; 19 | } 20 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { createServerOnlyFn } from "@tanstack/react-start"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import postgres from "postgres"; 4 | import { env } from "@/env/server"; 5 | // biome-ignore lint/performance/noNamespaceImport: we need to use a namespace import to avoid tree shaking 6 | import * as schema from "./schema"; 7 | 8 | // Disable prefetch as it is not supported for "Transaction" pool mode 9 | const client = postgres(env.DATABASE_URL, { 10 | prepare: false, 11 | max: 10, 12 | idle_timeout: 30_000, 13 | }); 14 | 15 | const getDatabase = createServerOnlyFn(() => drizzle({ client, schema })); 16 | 17 | export const db = getDatabase(); 18 | -------------------------------------------------------------------------------- /src/routes/(app)/route.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; 2 | import { appConfig } from "@/constants/config"; 3 | import { authQueryOptions } from "@/lib/auth/queries"; 4 | 5 | export const Route = createFileRoute("/(app)")({ 6 | component: Outlet, 7 | beforeLoad: async ({ context }) => { 8 | const user = await context.queryClient.ensureQueryData({ 9 | ...authQueryOptions(), 10 | revalidateIfStale: true, 11 | }); 12 | if (!user) { 13 | throw redirect({ to: appConfig.authRoutes.signin }); 14 | } 15 | 16 | // re-return to update type as non-null for child routes 17 | return { user }; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/routes/(auth)/verify-otp.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import z from "zod"; 3 | import AuthWrapper from "@/components/auth/auth-wrapper"; 4 | 5 | const verifyOtpUserSchema = z.object({ 6 | email: z.string({ message: "Invalid email address" }), 7 | type: z 8 | .enum(["sign-in", "email-verification", "forget-password"]) 9 | .catch("sign-in"), 10 | }); 11 | 12 | export const Route = createFileRoute("/(auth)/verify-otp")({ 13 | component: () => ( 14 | 19 | ), 20 | validateSearch: verifyOtpUserSchema, 21 | }); 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from "@tailwindcss/vite"; 2 | import { tanstackStart } from "@tanstack/react-start/plugin/vite"; 3 | import viteReact from "@vitejs/plugin-react"; 4 | import { nitro } from "nitro/vite"; 5 | import { defineConfig } from "vite"; 6 | import viteTsConfigPaths from "vite-tsconfig-paths"; 7 | 8 | const config = defineConfig(() => ({ 9 | server: { 10 | port: 3000, 11 | }, 12 | plugins: [ 13 | // this is the plugin that enables path aliases 14 | viteTsConfigPaths({ 15 | projects: ["./tsconfig.json"], 16 | }), 17 | tailwindcss(), 18 | nitro(), 19 | tanstackStart(), 20 | viteReact(), 21 | ], 22 | })); 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /src/types/organizations.ts: -------------------------------------------------------------------------------- 1 | import type { Organization } from "better-auth/plugins"; 2 | import type { authClient } from "@/lib/auth/auth-client"; 3 | 4 | export type MemberInfer = typeof authClient.$Infer.Member; 5 | 6 | export type InvitationInfer = typeof authClient.$Infer.Invitation; 7 | 8 | export type OrganizationRelations = Organization & { 9 | members: MemberInfer[]; 10 | invitations: InvitationInfer[]; 11 | }; 12 | 13 | export type InvitationWithOrganization = InvitationInfer & { 14 | organizationName: string; 15 | organizationSlug: string; 16 | }; 17 | 18 | export type OrganizationMemberRole = "member" | "admin" | "owner"; 19 | export type ProviderType = "github" | "google" | "facebook" | "apple"; 20 | -------------------------------------------------------------------------------- /src/routes/(auth)/reset-password.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import z from "zod"; 3 | import AuthWrapper from "@/components/auth/auth-wrapper"; 4 | 5 | const resetPasswordUserSchema = z.object({ 6 | token: z.string().min(1, { message: "Token is required" }), 7 | }); 8 | 9 | export const Route = createFileRoute("/(auth)/reset-password")({ 10 | head: () => ({ 11 | meta: [{ title: "Reset Password - TanStack + Better Auth Boilerplate" }], 12 | }), 13 | component: () => ( 14 | 19 | ), 20 | validateSearch: resetPasswordUserSchema, 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/env/server.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { config } from "dotenv"; 3 | import { z } from "zod"; 4 | 5 | config(); 6 | 7 | export const env = createEnv({ 8 | server: { 9 | DATABASE_URL: z.string(), 10 | BETTER_AUTH_URL: z.string().default("http://localhost:3000"), 11 | BETTER_AUTH_SECRET: z.string().min(1), 12 | BETTER_AUTH_EMAIL_FROM: z.string(), 13 | 14 | PASSKEY_RP_ID: z.string(), 15 | RESEND_API_KEY: z.string().min(1), 16 | 17 | GITHUB_CLIENT_ID: z.string(), 18 | GITHUB_CLIENT_SECRET: z.string(), 19 | GOOGLE_CLIENT_ID: z.string(), 20 | GOOGLE_CLIENT_SECRET: z.string(), 21 | }, 22 | 23 | runtimeEnv: process.env, 24 | 25 | emptyStringAsUndefined: true, 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/(auth)/route.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; 2 | import { appConfig } from "@/constants/config"; 3 | import { authQueryOptions } from "@/lib/auth/queries"; 4 | 5 | export const Route = createFileRoute("/(auth)")({ 6 | component: RouteComponent, 7 | beforeLoad: async ({ context }) => { 8 | const user = await context.queryClient.ensureQueryData({ 9 | ...authQueryOptions(), 10 | revalidateIfStale: true, 11 | }); 12 | if (user) { 13 | throw redirect({ 14 | to: appConfig.authRoutes.onboarding, 15 | }); 16 | } 17 | }, 18 | }); 19 | 20 | function RouteComponent() { 21 | return ( 22 |
23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |