kofi-stack-template-generator 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -243,11 +243,11 @@ function shouldIncludeFile(templatePath, config) {
243
243
  var EMBEDDED_TEMPLATES = {
244
244
  "base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
245
245
  "base/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "organizeImports": {\n "enabled": true\n },\n "linter": {\n "enabled": true,\n "rules": {\n "recommended": true\n }\n },\n "formatter": {\n "enabled": true,\n "indentStyle": "space",\n "indentWidth": 2\n },\n "javascript": {\n "formatter": {\n "quoteStyle": "single",\n "semicolons": "asNeeded"\n }\n },\n "files": {\n "ignore": [\n "node_modules",\n ".next",\n "dist",\n ".turbo",\n "coverage",\n ".vercel",\n "_generated"\n ]\n }\n}\n',
246
- "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Arcjet\nARCJET_KEY=\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n\n# Email (Resend)\nRESEND_API_KEY=\n",
247
- "convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [GitHub, Google],\n})\n",
246
+ "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Email (Resend) - https://resend.com\nRESEND_API_KEY=\nRESEND_FROM_EMAIL=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'convex-fs')}}\n\n# Convex FS - Built-in file storage (no additional config needed)\n{{/if}}\n{{#if (eq integrations.uploads 'r2')}}\n\n# Cloudflare R2\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET=\nR2_ENDPOINT=\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (eq integrations.payments 'stripe')}}\n\n# Stripe\nSTRIPE_SECRET_KEY=\nSTRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n{{#if (eq integrations.payments 'polar')}}\n\n# Polar\nPOLAR_ACCESS_TOKEN=\nPOLAR_WEBHOOK_SECRET=\nPOLAR_ORGANIZATION_ID=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Convex Rate Limiter - No additional config needed (uses Convex backend)\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n",
247
+ "convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { Password } from '@convex-dev/auth/providers/Password'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [\n GitHub,\n Google,\n Password,\n ],\n})\n\n// Email Verification Setup (Optional)\n// ------------------------------------\n// By default, email/password auth works without email verification.\n// To require email verification, configure the Password provider with ResendOTP:\n//\n// 1. Install the Resend component: npm install @convex-dev/resend\n// 2. Update the Password provider:\n//\n// import { ResendOTP } from '@convex-dev/auth/providers/ResendOTP'\n// import { Resend } from '@convex-dev/resend'\n//\n// const resend = new Resend()\n//\n// Password({\n// verify: ResendOTP({\n// resend,\n// from: process.env.RESEND_FROM_EMAIL!,\n// }),\n// })\n//\n// See: https://labs.convex.dev/auth/config/passwords\n",
248
248
  "convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\nauth.addHttpRoutes(http)\n\nexport default http\n",
249
249
  "convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { authTables } from '@convex-dev/auth/server'\nimport { v } from 'convex/values'\n\nexport default defineSchema({\n ...authTables,\n // Add your custom tables here\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // authorId: v.id('users'),\n // createdAt: v.number(),\n // }).index('by_author', ['authorId']),\n})\n",
250
- "convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
250
+ "convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
251
251
  "convex/package.json.hbs": `{{#if (eq structure 'monorepo')}}{
252
252
  "name": "@repo/backend",
253
253
  "version": "0.1.0",
@@ -269,7 +269,13 @@ var EMBEDDED_TEMPLATES = {
269
269
  "dependencies": {
270
270
  "convex": "^1.25.0",
271
271
  "@convex-dev/auth": "^0.0.90",
272
- "@auth/core": "^0.37.0"
272
+ "@auth/core": "^0.37.0",
273
+ "@convex-dev/resend": "^0.1.0"{{#if (eq integrations.uploads 'convex-fs')}},
274
+ "@convex-dev/convex-fs": "^0.1.0"{{/if}}{{#if (eq integrations.uploads 'r2')}},
275
+ "@convex-dev/cloudflare-r2": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'stripe')}},
276
+ "@convex-dev/stripe": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'polar')}},
277
+ "@convex-dev/polar": "^0.1.0"{{/if}}{{#if (includes addons 'rate-limiting')}},
278
+ "@convex-dev/rate-limiter": "^0.1.0"{{/if}}
273
279
  },
274
280
  "devDependencies": {
275
281
  "typescript": "^5.0.0"
@@ -427,7 +433,7 @@ var EMBEDDED_TEMPLATES = {
427
433
  `,
428
434
  "marketing/nextjs/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n',
429
435
  "marketing/payload/_env.example.hbs": '# Database (Supabase PostgreSQL)\nDATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres"\n\n# Payload CMS\nPAYLOAD_SECRET="" # Generate with: openssl rand -base64 32\n\n# Scheduled Jobs\nCRON_SECRET="" # Generate with: openssl rand -base64 32\n\n# Draft Previews\nPREVIEW_SECRET="" # Generate with: openssl rand -base64 32\n\n# S3 Storage (Supabase Storage)\nS3_BUCKET="media"\nS3_ACCESS_KEY_ID=""\nS3_SECRET_ACCESS_KEY=""\nS3_REGION="auto"\nS3_ENDPOINT="https://[PROJECT].supabase.co/storage/v1/s3"\n',
430
- "marketing/payload/_env.local.hbs": '# Database (Supabase PostgreSQL)\nDATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres"\n\n# Payload CMS\nPAYLOAD_SECRET="" # Generate with: openssl rand -base64 32\n\n# Scheduled Jobs\nCRON_SECRET="" # Generate with: openssl rand -base64 32\n\n# Draft Previews\nPREVIEW_SECRET="" # Generate with: openssl rand -base64 32\n\n# S3 Storage (Supabase Storage)\nS3_BUCKET="media"\nS3_ACCESS_KEY_ID=""\nS3_SECRET_ACCESS_KEY=""\nS3_REGION="auto"\nS3_ENDPOINT="https://[PROJECT].supabase.co/storage/v1/s3"\n',
436
+ "marketing/payload/_env.local.hbs": '# Database (Supabase PostgreSQL)\nDATABASE_URL=\n\n# Payload CMS\nPAYLOAD_SECRET=\n\n# Scheduled Jobs\nCRON_SECRET=\n\n# Draft Previews\nPREVIEW_SECRET=\n\n# S3 Storage (Supabase Storage)\nS3_BUCKET="media"\nS3_ACCESS_KEY_ID=\nS3_SECRET_ACCESS_KEY=\nS3_REGION="auto"\nS3_ENDPOINT=\n',
431
437
  "marketing/payload/next.config.ts.hbs": "import { withPayload } from '@payloadcms/next/withPayload'\nimport type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n transpilePackages: ['@repo/ui'],\n experimental: {\n turbo: {},\n },\n}\n\nexport default withPayload(nextConfig)\n",
432
438
  "marketing/payload/package.json.hbs": '{\n "name": "@repo/marketing",\n "version": "0.1.0",\n "private": true,\n "scripts": {\n "dev": "next dev --turbopack -p 3001",\n "build": "next build",\n "start": "next start",\n "lint": "biome check .",\n "lint:fix": "biome check --write .",\n "typecheck": "tsc --noEmit",\n "db:push": "payload migrate",\n "db:seed": "tsx src/seed.ts"\n },\n "dependencies": {\n "next": "^15.4.10",\n "react": "^19.0.0",\n "react-dom": "^19.0.0",\n "payload": "^3.70.0",\n "@payloadcms/db-postgres": "^3.0.0",\n "@payloadcms/next": "^3.0.0",\n "@payloadcms/richtext-lexical": "^3.0.0",\n "@payloadcms/storage-s3": "^3.0.0",\n "@payloadcms/plugin-seo": "^3.0.0",\n "@repo/ui": "workspace:*"\n },\n "devDependencies": {\n "@repo/config-typescript": "workspace:*",\n "@types/node": "^20.0.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "tailwindcss": "^4.0.0",\n "@tailwindcss/postcss": "^4.0.0",\n "postcss": "^8.4.0",\n "sass": "^1.86.0",\n "typescript": "^5.0.0",\n "tsx": "^4.0.0"\n }\n}\n',
433
439
  "marketing/payload/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
@@ -645,66 +651,123 @@ export default function RootLayout({
645
651
  }
646
652
  `,
647
653
  "web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
654
+ "web/src/app/(auth)/layout.tsx.hbs": 'export default function AuthLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <div className="min-h-screen flex items-center justify-center bg-muted/50 p-4">\n <div className="w-full max-w-md">\n {children}\n </div>\n </div>\n )\n}\n',
655
+ "web/src/app/(auth)/sign-in/page.tsx.hbs": "import { SignInForm } from '@/components/auth/sign-in-form'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n",
656
+ "web/src/app/(auth)/sign-up/page.tsx.hbs": "import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n",
648
657
  "web/src/app/globals.css.hbs": '@import "tailwindcss";\n@import "tw-animate-css";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n',
649
658
  "web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
650
659
  "web/src/app/page.tsx.hbs": `'use client'
651
660
 
652
- import { useAuth } from '@/lib/auth'
661
+ import { useQuery } from 'convex/react'
662
+ import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'
663
+ import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
664
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
665
+ import { Skeleton } from '@/components/ui/skeleton'
653
666
 
654
667
  export default function HomePage() {
655
- const { isAuthenticated, isLoading, signIn, signOut, user } = useAuth()
668
+ const user = useQuery(api.users.viewer)
656
669
 
657
- if (isLoading) {
670
+ if (user === undefined) {
658
671
  return (
659
- <main className="min-h-screen flex items-center justify-center">
660
- <div className="animate-pulse">Loading...</div>
661
- </main>
672
+ <DashboardLayout title="Dashboard">
673
+ <div className="space-y-6">
674
+ <Skeleton className="h-8 w-64" />
675
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
676
+ <Skeleton className="h-32" />
677
+ <Skeleton className="h-32" />
678
+ <Skeleton className="h-32" />
679
+ </div>
680
+ </div>
681
+ </DashboardLayout>
662
682
  )
663
683
  }
664
684
 
665
685
  return (
666
- <main className="min-h-screen flex flex-col items-center justify-center p-8">
667
- <div className="max-w-2xl text-center space-y-8">
668
- <h1 className="text-4xl font-bold">{{projectName}}</h1>
669
- <p className="text-xl text-muted-foreground">
670
- Built with Next.js, Convex, Better-Auth, and shadcn/ui
671
- </p>
686
+ <DashboardLayout title="Dashboard">
687
+ <div className="space-y-6">
688
+ <div>
689
+ <h1 className="text-3xl font-bold tracking-tight">
690
+ Welcome back{user?.name ? \`, \${user.name}\` : ''}!
691
+ </h1>
692
+ <p className="text-muted-foreground">
693
+ Here's what's happening with your project today.
694
+ </p>
695
+ </div>
672
696
 
673
- {isAuthenticated ? (
674
- <div className="space-y-4">
675
- <p className="text-lg">
676
- Welcome, <span className="font-semibold">{user?.name || user?.email}</span>!
677
- </p>
678
- <button
679
- onClick={() => signOut()}
680
- className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
681
- >
682
- Sign Out
683
- </button>
684
- </div>
685
- ) : (
686
- <div className="space-y-4">
687
- <p className="text-muted-foreground">
688
- Sign in to get started
689
- </p>
690
- <div className="flex gap-4 justify-center">
691
- <button
692
- onClick={() => signIn('github')}
693
- className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition"
694
- >
695
- Sign in with GitHub
696
- </button>
697
- <button
698
- onClick={() => signIn('google')}
699
- className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
700
- >
701
- Sign in with Google
702
- </button>
703
- </div>
704
- </div>
705
- )}
697
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
698
+ <Card>
699
+ <CardHeader>
700
+ <CardTitle>Getting Started</CardTitle>
701
+ <CardDescription>Quick start guide for your app</CardDescription>
702
+ </CardHeader>
703
+ <CardContent>
704
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
705
+ <li>Customize your dashboard layout</li>
706
+ <li>Add new pages to the sidebar</li>
707
+ <li>Connect your data sources</li>
708
+ </ul>
709
+ </CardContent>
710
+ </Card>
711
+
712
+ <Card>
713
+ <CardHeader>
714
+ <CardTitle>Documentation</CardTitle>
715
+ <CardDescription>Learn how to build with Kofi Stack</CardDescription>
716
+ </CardHeader>
717
+ <CardContent>
718
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
719
+ <li>
720
+ <a
721
+ href="https://docs.convex.dev"
722
+ target="_blank"
723
+ rel="noopener noreferrer"
724
+ className="text-primary hover:underline"
725
+ >
726
+ Convex Documentation
727
+ </a>
728
+ </li>
729
+ <li>
730
+ <a
731
+ href="https://ui.shadcn.com"
732
+ target="_blank"
733
+ rel="noopener noreferrer"
734
+ className="text-primary hover:underline"
735
+ >
736
+ shadcn/ui Components
737
+ </a>
738
+ </li>
739
+ <li>
740
+ <a
741
+ href="https://nextjs.org/docs"
742
+ target="_blank"
743
+ rel="noopener noreferrer"
744
+ className="text-primary hover:underline"
745
+ >
746
+ Next.js Documentation
747
+ </a>
748
+ </li>
749
+ </ul>
750
+ </CardContent>
751
+ </Card>
706
752
 
707
- <div className="pt-8 border-t border-border">
753
+ <Card>
754
+ <CardHeader>
755
+ <CardTitle>Your Stack</CardTitle>
756
+ <CardDescription>Technologies powering your app</CardDescription>
757
+ </CardHeader>
758
+ <CardContent>
759
+ <ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
760
+ <li>Next.js 15 with App Router</li>
761
+ <li>Convex for backend & database</li>
762
+ <li>Convex Auth for authentication</li>
763
+ <li>shadcn/ui components</li>
764
+ <li>Tailwind CSS for styling</li>
765
+ </ul>
766
+ </CardContent>
767
+ </Card>
768
+ </div>
769
+
770
+ <div className="pt-4 border-t">
708
771
  <p className="text-sm text-muted-foreground">
709
772
  Created with{' '}
710
773
  <a
@@ -718,13 +781,481 @@ export default function HomePage() {
718
781
  </p>
719
782
  </div>
720
783
  </div>
721
- </main>
784
+ </DashboardLayout>
785
+ )
786
+ }
787
+ `,
788
+ "web/src/components/auth/sign-in-form.tsx.hbs": `'use client'
789
+
790
+ import { useState } from 'react'
791
+ import { useRouter } from 'next/navigation'
792
+ import Link from 'next/link'
793
+ import { useAuthActions } from '@convex-dev/auth/react'
794
+ import { Button } from '@/components/ui/button'
795
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
796
+ import { Input } from '@/components/ui/input'
797
+ import { Label } from '@/components/ui/label'
798
+ import { Separator } from '@/components/ui/separator'
799
+
800
+ export function SignInForm() {
801
+ const router = useRouter()
802
+ const { signIn } = useAuthActions()
803
+ const [email, setEmail] = useState('')
804
+ const [password, setPassword] = useState('')
805
+ const [isLoading, setIsLoading] = useState(false)
806
+ const [error, setError] = useState<string | null>(null)
807
+
808
+ const handleSubmit = async (e: React.FormEvent) => {
809
+ e.preventDefault()
810
+ setIsLoading(true)
811
+ setError(null)
812
+
813
+ try {
814
+ const formData = new FormData()
815
+ formData.append('email', email)
816
+ formData.append('password', password)
817
+ formData.append('flow', 'signIn')
818
+
819
+ await signIn('password', formData)
820
+ router.push('/')
821
+ } catch (err) {
822
+ setError('Invalid email or password')
823
+ } finally {
824
+ setIsLoading(false)
825
+ }
826
+ }
827
+
828
+ const handleSocialSignIn = (provider: 'github' | 'google') => {
829
+ void signIn(provider)
830
+ }
831
+
832
+ return (
833
+ <Card>
834
+ <CardHeader className="text-center">
835
+ <CardTitle className="text-2xl">Welcome back</CardTitle>
836
+ <CardDescription>Sign in to your account to continue</CardDescription>
837
+ </CardHeader>
838
+ <CardContent>
839
+ <form onSubmit={handleSubmit} className="space-y-4">
840
+ <div className="space-y-2">
841
+ <Label htmlFor="email">Email</Label>
842
+ <Input
843
+ id="email"
844
+ type="email"
845
+ placeholder="you@example.com"
846
+ value={email}
847
+ onChange={(e) => setEmail(e.target.value)}
848
+ required
849
+ />
850
+ </div>
851
+ <div className="space-y-2">
852
+ <Label htmlFor="password">Password</Label>
853
+ <Input
854
+ id="password"
855
+ type="password"
856
+ placeholder="Enter your password"
857
+ value={password}
858
+ onChange={(e) => setPassword(e.target.value)}
859
+ required
860
+ />
861
+ </div>
862
+
863
+ {error && (
864
+ <p className="text-sm text-destructive">{error}</p>
865
+ )}
866
+
867
+ <Button type="submit" className="w-full" disabled={isLoading}>
868
+ {isLoading ? 'Signing in...' : 'Sign In'}
869
+ </Button>
870
+ </form>
871
+
872
+ <div className="relative my-6">
873
+ <div className="absolute inset-0 flex items-center">
874
+ <Separator className="w-full" />
875
+ </div>
876
+ <div className="relative flex justify-center text-xs uppercase">
877
+ <span className="bg-card px-2 text-muted-foreground">Or continue with</span>
878
+ </div>
879
+ </div>
880
+
881
+ <div className="grid grid-cols-2 gap-4">
882
+ <Button variant="outline" onClick={() => handleSocialSignIn('github')}>
883
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
884
+ <path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
885
+ </svg>
886
+ GitHub
887
+ </Button>
888
+ <Button variant="outline" onClick={() => handleSocialSignIn('google')}>
889
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
890
+ <path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
891
+ <path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
892
+ <path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
893
+ <path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
894
+ </svg>
895
+ Google
896
+ </Button>
897
+ </div>
898
+
899
+ <p className="mt-6 text-center text-sm text-muted-foreground">
900
+ Don&apos;t have an account?{' '}
901
+ <Link href="/sign-up" className="text-primary hover:underline font-medium">
902
+ Sign up
903
+ </Link>
904
+ </p>
905
+ </CardContent>
906
+ </Card>
907
+ )
908
+ }
909
+ `,
910
+ "web/src/components/auth/sign-up-form.tsx.hbs": `'use client'
911
+
912
+ import { useState } from 'react'
913
+ import { useRouter } from 'next/navigation'
914
+ import Link from 'next/link'
915
+ import { useAuthActions } from '@convex-dev/auth/react'
916
+ import { Button } from '@/components/ui/button'
917
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
918
+ import { Input } from '@/components/ui/input'
919
+ import { Label } from '@/components/ui/label'
920
+ import { Separator } from '@/components/ui/separator'
921
+
922
+ export function SignUpForm() {
923
+ const router = useRouter()
924
+ const { signIn } = useAuthActions()
925
+ const [name, setName] = useState('')
926
+ const [email, setEmail] = useState('')
927
+ const [password, setPassword] = useState('')
928
+ const [isLoading, setIsLoading] = useState(false)
929
+ const [error, setError] = useState<string | null>(null)
930
+
931
+ const handleSubmit = async (e: React.FormEvent) => {
932
+ e.preventDefault()
933
+ setIsLoading(true)
934
+ setError(null)
935
+
936
+ if (password.length < 8) {
937
+ setError('Password must be at least 8 characters')
938
+ setIsLoading(false)
939
+ return
940
+ }
941
+
942
+ try {
943
+ const formData = new FormData()
944
+ formData.append('name', name)
945
+ formData.append('email', email)
946
+ formData.append('password', password)
947
+ formData.append('flow', 'signUp')
948
+
949
+ await signIn('password', formData)
950
+ router.push('/')
951
+ } catch (err) {
952
+ setError('Failed to create account. Email may already be in use.')
953
+ } finally {
954
+ setIsLoading(false)
955
+ }
956
+ }
957
+
958
+ const handleSocialSignIn = (provider: 'github' | 'google') => {
959
+ void signIn(provider)
960
+ }
961
+
962
+ return (
963
+ <Card>
964
+ <CardHeader className="text-center">
965
+ <CardTitle className="text-2xl">Create an account</CardTitle>
966
+ <CardDescription>Enter your details to get started</CardDescription>
967
+ </CardHeader>
968
+ <CardContent>
969
+ <form onSubmit={handleSubmit} className="space-y-4">
970
+ <div className="space-y-2">
971
+ <Label htmlFor="name">Name</Label>
972
+ <Input
973
+ id="name"
974
+ type="text"
975
+ placeholder="Your name"
976
+ value={name}
977
+ onChange={(e) => setName(e.target.value)}
978
+ required
979
+ />
980
+ </div>
981
+ <div className="space-y-2">
982
+ <Label htmlFor="email">Email</Label>
983
+ <Input
984
+ id="email"
985
+ type="email"
986
+ placeholder="you@example.com"
987
+ value={email}
988
+ onChange={(e) => setEmail(e.target.value)}
989
+ required
990
+ />
991
+ </div>
992
+ <div className="space-y-2">
993
+ <Label htmlFor="password">Password</Label>
994
+ <Input
995
+ id="password"
996
+ type="password"
997
+ placeholder="At least 8 characters"
998
+ value={password}
999
+ onChange={(e) => setPassword(e.target.value)}
1000
+ required
1001
+ minLength={8}
1002
+ />
1003
+ </div>
1004
+
1005
+ {error && (
1006
+ <p className="text-sm text-destructive">{error}</p>
1007
+ )}
1008
+
1009
+ <Button type="submit" className="w-full" disabled={isLoading}>
1010
+ {isLoading ? 'Creating account...' : 'Create Account'}
1011
+ </Button>
1012
+ </form>
1013
+
1014
+ <div className="relative my-6">
1015
+ <div className="absolute inset-0 flex items-center">
1016
+ <Separator className="w-full" />
1017
+ </div>
1018
+ <div className="relative flex justify-center text-xs uppercase">
1019
+ <span className="bg-card px-2 text-muted-foreground">Or continue with</span>
1020
+ </div>
1021
+ </div>
1022
+
1023
+ <div className="grid grid-cols-2 gap-4">
1024
+ <Button variant="outline" onClick={() => handleSocialSignIn('github')}>
1025
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
1026
+ <path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
1027
+ </svg>
1028
+ GitHub
1029
+ </Button>
1030
+ <Button variant="outline" onClick={() => handleSocialSignIn('google')}>
1031
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
1032
+ <path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
1033
+ <path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
1034
+ <path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
1035
+ <path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
1036
+ </svg>
1037
+ Google
1038
+ </Button>
1039
+ </div>
1040
+
1041
+ <p className="mt-6 text-center text-sm text-muted-foreground">
1042
+ Already have an account?{' '}
1043
+ <Link href="/sign-in" className="text-primary hover:underline font-medium">
1044
+ Sign in
1045
+ </Link>
1046
+ </p>
1047
+ </CardContent>
1048
+ </Card>
1049
+ )
1050
+ }
1051
+ `,
1052
+ "web/src/components/dashboard/app-sidebar.tsx.hbs": `'use client'
1053
+
1054
+ import {
1055
+ AudioWaveform,
1056
+ Command,
1057
+ GalleryVerticalEnd,
1058
+ Home,
1059
+ Settings,
1060
+ ChevronsUpDown,
1061
+ LogOut,
1062
+ User,
1063
+ } from 'lucide-react'
1064
+ import { useRouter } from 'next/navigation'
1065
+ import { useAuthActions } from '@convex-dev/auth/react'
1066
+ import { useQuery } from 'convex/react'
1067
+ import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
1068
+ import {
1069
+ Sidebar,
1070
+ SidebarContent,
1071
+ SidebarFooter,
1072
+ SidebarGroup,
1073
+ SidebarGroupContent,
1074
+ SidebarGroupLabel,
1075
+ SidebarHeader,
1076
+ SidebarMenu,
1077
+ SidebarMenuButton,
1078
+ SidebarMenuItem,
1079
+ } from '@/components/ui/sidebar'
1080
+ import {
1081
+ DropdownMenu,
1082
+ DropdownMenuContent,
1083
+ DropdownMenuItem,
1084
+ DropdownMenuSeparator,
1085
+ DropdownMenuTrigger,
1086
+ } from '@/components/ui/dropdown-menu'
1087
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
1088
+
1089
+ const navigation = [
1090
+ {
1091
+ title: 'Home',
1092
+ url: '/',
1093
+ icon: Home,
1094
+ },
1095
+ {
1096
+ title: 'Settings',
1097
+ url: '/settings',
1098
+ icon: Settings,
1099
+ },
1100
+ ]
1101
+
1102
+ export function AppSidebar() {
1103
+ const router = useRouter()
1104
+ const { signOut } = useAuthActions()
1105
+ const user = useQuery(api.users.viewer)
1106
+
1107
+ const handleSignOut = async () => {
1108
+ await signOut()
1109
+ router.push('/sign-in')
1110
+ }
1111
+
1112
+ const getInitials = (name?: string | null) => {
1113
+ if (!name) return 'U'
1114
+ return name
1115
+ .split(' ')
1116
+ .map((n) => n[0])
1117
+ .join('')
1118
+ .toUpperCase()
1119
+ .slice(0, 2)
1120
+ }
1121
+
1122
+ return (
1123
+ <Sidebar>
1124
+ <SidebarHeader>
1125
+ <SidebarMenu>
1126
+ <SidebarMenuItem>
1127
+ <SidebarMenuButton size="lg" asChild>
1128
+ <a href="/">
1129
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
1130
+ <GalleryVerticalEnd className="size-4" />
1131
+ </div>
1132
+ <div className="flex flex-col gap-0.5 leading-none">
1133
+ <span className="font-semibold">{{projectName}}</span>
1134
+ <span className="text-xs text-muted-foreground">Dashboard</span>
1135
+ </div>
1136
+ </a>
1137
+ </SidebarMenuButton>
1138
+ </SidebarMenuItem>
1139
+ </SidebarMenu>
1140
+ </SidebarHeader>
1141
+ <SidebarContent>
1142
+ <SidebarGroup>
1143
+ <SidebarGroupLabel>Navigation</SidebarGroupLabel>
1144
+ <SidebarGroupContent>
1145
+ <SidebarMenu>
1146
+ {navigation.map((item) => (
1147
+ <SidebarMenuItem key={item.title}>
1148
+ <SidebarMenuButton asChild>
1149
+ <a href={item.url}>
1150
+ <item.icon className="size-4" />
1151
+ <span>{item.title}</span>
1152
+ </a>
1153
+ </SidebarMenuButton>
1154
+ </SidebarMenuItem>
1155
+ ))}
1156
+ </SidebarMenu>
1157
+ </SidebarGroupContent>
1158
+ </SidebarGroup>
1159
+ </SidebarContent>
1160
+ <SidebarFooter>
1161
+ <SidebarMenu>
1162
+ <SidebarMenuItem>
1163
+ <DropdownMenu>
1164
+ <DropdownMenuTrigger asChild>
1165
+ <SidebarMenuButton
1166
+ size="lg"
1167
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
1168
+ >
1169
+ <Avatar className="h-8 w-8 rounded-lg">
1170
+ <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />
1171
+ <AvatarFallback className="rounded-lg">
1172
+ {getInitials(user?.name)}
1173
+ </AvatarFallback>
1174
+ </Avatar>
1175
+ <div className="grid flex-1 text-left text-sm leading-tight">
1176
+ <span className="truncate font-semibold">{user?.name ?? 'User'}</span>
1177
+ <span className="truncate text-xs text-muted-foreground">
1178
+ {user?.email ?? ''}
1179
+ </span>
1180
+ </div>
1181
+ <ChevronsUpDown className="ml-auto size-4" />
1182
+ </SidebarMenuButton>
1183
+ </DropdownMenuTrigger>
1184
+ <DropdownMenuContent
1185
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
1186
+ side="bottom"
1187
+ align="end"
1188
+ sideOffset={4}
1189
+ >
1190
+ <DropdownMenuItem asChild>
1191
+ <a href="/settings">
1192
+ <User className="mr-2 size-4" />
1193
+ Profile
1194
+ </a>
1195
+ </DropdownMenuItem>
1196
+ <DropdownMenuItem asChild>
1197
+ <a href="/settings">
1198
+ <Settings className="mr-2 size-4" />
1199
+ Settings
1200
+ </a>
1201
+ </DropdownMenuItem>
1202
+ <DropdownMenuSeparator />
1203
+ <DropdownMenuItem onClick={handleSignOut}>
1204
+ <LogOut className="mr-2 size-4" />
1205
+ Sign out
1206
+ </DropdownMenuItem>
1207
+ </DropdownMenuContent>
1208
+ </DropdownMenu>
1209
+ </SidebarMenuItem>
1210
+ </SidebarMenu>
1211
+ </SidebarFooter>
1212
+ </Sidebar>
1213
+ )
1214
+ }
1215
+ `,
1216
+ "web/src/components/dashboard/dashboard-layout.tsx.hbs": `'use client'
1217
+
1218
+ import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
1219
+ import { Separator } from '@/components/ui/separator'
1220
+ import {
1221
+ Breadcrumb,
1222
+ BreadcrumbItem,
1223
+ BreadcrumbList,
1224
+ BreadcrumbPage,
1225
+ } from '@/components/ui/breadcrumb'
1226
+ import { AppSidebar } from './app-sidebar'
1227
+
1228
+ interface DashboardLayoutProps {
1229
+ children: React.ReactNode
1230
+ title?: string
1231
+ }
1232
+
1233
+ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {
1234
+ return (
1235
+ <SidebarProvider>
1236
+ <AppSidebar />
1237
+ <SidebarInset>
1238
+ <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
1239
+ <SidebarTrigger className="-ml-1" />
1240
+ <Separator orientation="vertical" className="mr-2 h-4" />
1241
+ <Breadcrumb>
1242
+ <BreadcrumbList>
1243
+ <BreadcrumbItem>
1244
+ <BreadcrumbPage>{title}</BreadcrumbPage>
1245
+ </BreadcrumbItem>
1246
+ </BreadcrumbList>
1247
+ </Breadcrumb>
1248
+ </header>
1249
+ <main className="flex-1 p-4 md:p-6">{children}</main>
1250
+ </SidebarInset>
1251
+ </SidebarProvider>
722
1252
  )
723
1253
  }
724
1254
  `,
725
1255
  "web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs'\nimport { ConvexReactClient } from 'convex/react'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsProvider client={convex}>\n {children}\n </ConvexAuthNextjsProvider>\n )\n}\n",
726
- "web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth, useMutation, useQuery } from 'convex/react'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../../convex/_generated/api{{/if}}'\n\nexport function useAuth() {\n const { isAuthenticated, isLoading } = useConvexAuth()\n const { signIn, signOut } = useAuthActions()\n const user = useQuery(api.users.current)\n\n return {\n isAuthenticated,\n isLoading,\n user,\n signIn: (provider: 'github' | 'google') => {\n void signIn(provider)\n },\n signOut: () => {\n void signOut()\n },\n }\n}\n",
1256
+ "web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth, useQuery } from 'convex/react'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'\n\nexport function useAuth() {\n const { isAuthenticated, isLoading } = useConvexAuth()\n const { signIn, signOut } = useAuthActions()\n const user = useQuery(api.users.viewer)\n\n return {\n isAuthenticated,\n isLoading,\n user,\n signIn: (provider: 'github' | 'google') => {\n void signIn(provider)\n },\n signInWithPassword: async (email: string, password: string, flow: 'signIn' | 'signUp' = 'signIn', name?: string) => {\n const formData = new FormData()\n formData.append('email', email)\n formData.append('password', password)\n formData.append('flow', flow)\n if (name) {\n formData.append('name', name)\n }\n await signIn('password', formData)\n },\n signOut: () => {\n void signOut()\n },\n }\n}\n",
727
1257
  "web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
1258
+ "web/src/proxy.ts.hbs": "import {\n convexAuthNextjsProxy,\n createRouteMatcher,\n nextjsProxyRedirect,\n} from '@convex-dev/auth/nextjs/server'\n\nconst isPublicRoute = createRouteMatcher(['/sign-in', '/sign-up'])\n\nexport default convexAuthNextjsProxy(async (request, { convexAuth }) => {\n const isAuthenticated = await convexAuth.isAuthenticated()\n\n // Redirect unauthenticated users to /sign-up\n if (!isPublicRoute(request) && !isAuthenticated) {\n return nextjsProxyRedirect(request, '/sign-up')\n }\n\n // Redirect authenticated users from auth pages to / (dashboard)\n if (isPublicRoute(request) && isAuthenticated) {\n return nextjsProxyRedirect(request, '/')\n }\n})\n",
728
1259
  "web/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n'
729
1260
  };
730
1261
 
@@ -843,19 +1374,21 @@ async function postProcess(vfs, config) {
843
1374
  function generateDevScript(vfs, scriptsPath, config) {
844
1375
  const isMonorepo = config.structure === "monorepo";
845
1376
  const webAppDir = isMonorepo ? "apps/web" : ".";
1377
+ const backendDir = isMonorepo ? "packages/backend" : ".";
846
1378
  const devScript = `#!/usr/bin/env node
847
1379
  /**
848
1380
  * Dev Script - Starts Next.js and Convex dev servers
849
1381
  */
850
1382
 
851
1383
  import { spawn, execSync } from 'child_process'
852
- import { existsSync, readFileSync } from 'fs'
1384
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
853
1385
  import { resolve, dirname } from 'path'
854
1386
  import { fileURLToPath } from 'url'
855
1387
 
856
1388
  const __dirname = dirname(fileURLToPath(import.meta.url))
857
1389
  const rootDir = resolve(__dirname, '..')
858
1390
  const webAppDir = resolve(rootDir, '${webAppDir}')
1391
+ const backendDir = resolve(rootDir, '${backendDir}')
859
1392
 
860
1393
  function loadEnvFile(dir) {
861
1394
  const envPath = resolve(dir, '.env.local')
@@ -880,6 +1413,33 @@ function loadEnvFile(dir) {
880
1413
  return env
881
1414
  }
882
1415
 
1416
+ function syncEnvToWebApp() {
1417
+ // In monorepo, Convex creates .env.local in backend package
1418
+ // Web app needs NEXT_PUBLIC_CONVEX_URL to connect to Convex
1419
+ const backendEnv = loadEnvFile(backendDir)
1420
+ const webEnvPath = resolve(webAppDir, '.env.local')
1421
+
1422
+ if (backendEnv.NEXT_PUBLIC_CONVEX_URL) {
1423
+ const webEnv = loadEnvFile(webAppDir)
1424
+
1425
+ // Only sync if web app doesn't have the URL or it's different
1426
+ if (webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL) {
1427
+ let content = ''
1428
+ if (existsSync(webEnvPath)) {
1429
+ content = readFileSync(webEnvPath, 'utf-8')
1430
+ // Remove existing NEXT_PUBLIC_CONVEX_URL line if present
1431
+ content = content.split('\\n').filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=')).join('\\n')
1432
+ if (content && !content.endsWith('\\n')) content += '\\n'
1433
+ }
1434
+ content += \`NEXT_PUBLIC_CONVEX_URL=\${backendEnv.NEXT_PUBLIC_CONVEX_URL}\\n\`
1435
+ writeFileSync(webEnvPath, content)
1436
+ console.log('\u2713 Synced NEXT_PUBLIC_CONVEX_URL to web app\\n')
1437
+ }
1438
+ }
1439
+
1440
+ return backendEnv
1441
+ }
1442
+
883
1443
  async function checkAndInstall() {
884
1444
  if (!existsSync(resolve(rootDir, 'node_modules'))) {
885
1445
  console.log('\u{1F4E6} Installing dependencies...\\n')
@@ -888,9 +1448,9 @@ async function checkAndInstall() {
888
1448
  }
889
1449
 
890
1450
  function startDevServers() {
891
- const localEnv = loadEnvFile(webAppDir)
1451
+ ${isMonorepo ? "const backendEnv = syncEnvToWebApp()" : "const backendEnv = loadEnvFile(webAppDir)"}
892
1452
 
893
- if (!localEnv.CONVEX_DEPLOYMENT) {
1453
+ if (!backendEnv.CONVEX_DEPLOYMENT) {
894
1454
  console.log('\u26A0\uFE0F Convex not configured. Run: pnpm dev:setup\\n')
895
1455
  console.log('Starting Next.js only...\\n')
896
1456
  spawn('pnpm', ['${isMonorepo ? "dev:web" : "dev:next"}'], {