kofi-stack-template-generator 2.1.14 → 2.1.16

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,7 +243,7 @@ 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=\nNEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_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",
246
+ "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_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
247
  "convex/convex/auth.config.ts.hbs": "import { getAuthConfigProvider } from '@convex-dev/better-auth/auth-config'\nimport type { AuthConfig } from 'convex/server'\n\nexport default {\n providers: [getAuthConfigProvider()],\n} satisfies AuthConfig\n",
248
248
  "convex/convex/auth.ts.hbs": "import { createClient, type GenericCtx } from '@convex-dev/better-auth'\nimport { convex } from '@convex-dev/better-auth/plugins'\nimport { betterAuth } from 'better-auth/minimal'\nimport { components } from './_generated/api'\nimport type { DataModel } from './_generated/dataModel'\nimport { query } from './_generated/server'\nimport authConfig from './auth.config'\n\nconst siteUrl = process.env.SITE_URL || 'http://localhost:3000'\n\nexport const authComponent = createClient<DataModel>(components.betterAuth)\n\n// Build social providers only if credentials are configured\nconst socialProviders: Record<string, { clientId: string; clientSecret: string }> = {}\n\nif (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {\n socialProviders.github = {\n clientId: process.env.GITHUB_CLIENT_ID,\n clientSecret: process.env.GITHUB_CLIENT_SECRET,\n }\n}\n\nif (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {\n socialProviders.google = {\n clientId: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n }\n}\n\nexport const createAuth = (ctx: GenericCtx<DataModel>) => {\n return betterAuth({\n baseURL: siteUrl,\n database: authComponent.adapter(ctx),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: false,\n },\n socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,\n plugins: [convex({ authConfig })],\n })\n}\n\nexport const getCurrentUser = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n",
249
249
  "convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
@@ -586,7 +586,7 @@ export default function RootLayout({
586
586
  "packages/ui/src/index.ts.hbs": "export { cn } from './lib/utils'\n// Export components as they are added\n// export * from './components/ui/button'\n",
587
587
  "packages/ui/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",
588
588
  "packages/ui/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2020",\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 "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["src/**/*"],\n "exclude": ["node_modules"]\n}\n',
589
- "web/_env.local.hbs": "# Convex - These values are synced from packages/backend/.env.local after running convex dev\n# Copy NEXT_PUBLIC_CONVEX_URL from packages/backend/.env.local after Convex setup\nNEXT_PUBLIC_CONVEX_URL=\n\n# Site URL for auth\nNEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\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.payments 'stripe')}}\n\n# Stripe\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n",
589
+ "web/_env.local.hbs": "# Convex - These values are synced from packages/backend/.env.local after running convex dev\n# NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL are auto-synced by dev script\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL for auth (your frontend URL)\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\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.payments 'stripe')}}\n\n# Stripe\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n",
590
590
  "web/components.json.hbs": '{\n "$schema": "https://ui.shadcn.com/schema.json",\n "style": "new-york",\n "rsc": true,\n "tsx": true,\n "tailwind": {\n "config": "",\n "css": "src/app/globals.css",\n "baseColor": "{{shadcn.baseColor}}",\n "cssVariables": true,\n "prefix": ""\n },\n "aliases": {\n "components": "@/components",\n "utils": "@/lib/utils",\n "ui": "@/components/ui",\n "lib": "@/lib",\n "hooks": "@/hooks"\n },\n "iconLibrary": "{{shadcn.iconLibrary}}"\n}\n',
591
591
  "web/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n{{#if (eq structure 'monorepo')}}\n transpilePackages: ['@repo/ui', '@repo/backend'],\n{{/if}}\n}\n\nexport default nextConfig\n",
592
592
  "web/package.json.hbs": `{
@@ -667,7 +667,7 @@ export default function RootLayout({
667
667
  "web/src/app/page.tsx.hbs": `'use client'
668
668
 
669
669
  import { useQuery } from 'convex/react'
670
- import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'
670
+ import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../convex/_generated/api{{/if}}'
671
671
  import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
672
672
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
673
673
  import { Skeleton } from '@/components/ui/skeleton'
@@ -1430,25 +1430,33 @@ function loadEnvFile(dir) {
1430
1430
 
1431
1431
  function syncEnvToWebApp() {
1432
1432
  // In monorepo, Convex creates .env.local in backend package
1433
- // Web app needs NEXT_PUBLIC_CONVEX_URL to connect to Convex
1433
+ // Web app needs NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL
1434
1434
  const backendEnv = loadEnvFile(backendDir)
1435
1435
  const webEnvPath = resolve(webAppDir, '.env.local')
1436
1436
 
1437
1437
  if (backendEnv.NEXT_PUBLIC_CONVEX_URL) {
1438
1438
  const webEnv = loadEnvFile(webAppDir)
1439
+ // Derive site URL from cloud URL (.convex.cloud -> .convex.site)
1440
+ const convexSiteUrl = backendEnv.NEXT_PUBLIC_CONVEX_URL.replace('.convex.cloud', '.convex.site')
1439
1441
 
1440
- // Only sync if web app doesn't have the URL or it's different
1441
- if (webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL) {
1442
+ // Check if sync is needed
1443
+ const needsSync = webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL ||
1444
+ webEnv.NEXT_PUBLIC_CONVEX_SITE_URL !== convexSiteUrl
1445
+
1446
+ if (needsSync) {
1442
1447
  let content = ''
1443
1448
  if (existsSync(webEnvPath)) {
1444
1449
  content = readFileSync(webEnvPath, 'utf-8')
1445
- // Remove existing NEXT_PUBLIC_CONVEX_URL line if present
1446
- content = content.split('\\n').filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=')).join('\\n')
1450
+ // Remove existing lines
1451
+ content = content.split('\\n')
1452
+ .filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=') && !line.startsWith('NEXT_PUBLIC_CONVEX_SITE_URL='))
1453
+ .join('\\n')
1447
1454
  if (content && !content.endsWith('\\n')) content += '\\n'
1448
1455
  }
1449
1456
  content += \`NEXT_PUBLIC_CONVEX_URL=\${backendEnv.NEXT_PUBLIC_CONVEX_URL}\\n\`
1457
+ content += \`NEXT_PUBLIC_CONVEX_SITE_URL=\${convexSiteUrl}\\n\`
1450
1458
  writeFileSync(webEnvPath, content)
1451
- console.log('\u2713 Synced NEXT_PUBLIC_CONVEX_URL to web app\\n')
1459
+ console.log('\u2713 Synced Convex URLs to web app\\n')
1452
1460
  }
1453
1461
  }
1454
1462
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kofi-stack-template-generator",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/generator.ts CHANGED
@@ -250,25 +250,33 @@ function loadEnvFile(dir) {
250
250
 
251
251
  function syncEnvToWebApp() {
252
252
  // In monorepo, Convex creates .env.local in backend package
253
- // Web app needs NEXT_PUBLIC_CONVEX_URL to connect to Convex
253
+ // Web app needs NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL
254
254
  const backendEnv = loadEnvFile(backendDir)
255
255
  const webEnvPath = resolve(webAppDir, '.env.local')
256
256
 
257
257
  if (backendEnv.NEXT_PUBLIC_CONVEX_URL) {
258
258
  const webEnv = loadEnvFile(webAppDir)
259
+ // Derive site URL from cloud URL (.convex.cloud -> .convex.site)
260
+ const convexSiteUrl = backendEnv.NEXT_PUBLIC_CONVEX_URL.replace('.convex.cloud', '.convex.site')
259
261
 
260
- // Only sync if web app doesn't have the URL or it's different
261
- if (webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL) {
262
+ // Check if sync is needed
263
+ const needsSync = webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL ||
264
+ webEnv.NEXT_PUBLIC_CONVEX_SITE_URL !== convexSiteUrl
265
+
266
+ if (needsSync) {
262
267
  let content = ''
263
268
  if (existsSync(webEnvPath)) {
264
269
  content = readFileSync(webEnvPath, 'utf-8')
265
- // Remove existing NEXT_PUBLIC_CONVEX_URL line if present
266
- content = content.split('\\n').filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=')).join('\\n')
270
+ // Remove existing lines
271
+ content = content.split('\\n')
272
+ .filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=') && !line.startsWith('NEXT_PUBLIC_CONVEX_SITE_URL='))
273
+ .join('\\n')
267
274
  if (content && !content.endsWith('\\n')) content += '\\n'
268
275
  }
269
276
  content += \`NEXT_PUBLIC_CONVEX_URL=\${backendEnv.NEXT_PUBLIC_CONVEX_URL}\\n\`
277
+ content += \`NEXT_PUBLIC_CONVEX_SITE_URL=\${convexSiteUrl}\\n\`
270
278
  writeFileSync(webEnvPath, content)
271
- console.log('✓ Synced NEXT_PUBLIC_CONVEX_URL to web app\\n')
279
+ console.log('✓ Synced Convex URLs to web app\\n')
272
280
  }
273
281
  }
274
282
 
@@ -1,12 +1,12 @@
1
1
  // Auto-generated file. Do not edit manually.
2
2
  // Run 'pnpm prebuild' to regenerate.
3
- // Generated: 2026-01-15T01:53:42.963Z
3
+ // Generated: 2026-01-15T02:03:58.886Z
4
4
  // Template count: 90
5
5
 
6
6
  export const EMBEDDED_TEMPLATES: Record<string, string> = {
7
7
  "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",
8
8
  "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",
9
- "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_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",
9
+ "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL (used for auth redirects)\nSITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Auth - GitHub OAuth\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Auth - Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_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",
10
10
  "convex/convex/auth.config.ts.hbs": "import { getAuthConfigProvider } from '@convex-dev/better-auth/auth-config'\nimport type { AuthConfig } from 'convex/server'\n\nexport default {\n providers: [getAuthConfigProvider()],\n} satisfies AuthConfig\n",
11
11
  "convex/convex/auth.ts.hbs": "import { createClient, type GenericCtx } from '@convex-dev/better-auth'\nimport { convex } from '@convex-dev/better-auth/plugins'\nimport { betterAuth } from 'better-auth/minimal'\nimport { components } from './_generated/api'\nimport type { DataModel } from './_generated/dataModel'\nimport { query } from './_generated/server'\nimport authConfig from './auth.config'\n\nconst siteUrl = process.env.SITE_URL || 'http://localhost:3000'\n\nexport const authComponent = createClient<DataModel>(components.betterAuth)\n\n// Build social providers only if credentials are configured\nconst socialProviders: Record<string, { clientId: string; clientSecret: string }> = {}\n\nif (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {\n socialProviders.github = {\n clientId: process.env.GITHUB_CLIENT_ID,\n clientSecret: process.env.GITHUB_CLIENT_SECRET,\n }\n}\n\nif (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {\n socialProviders.google = {\n clientId: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n }\n}\n\nexport const createAuth = (ctx: GenericCtx<DataModel>) => {\n return betterAuth({\n baseURL: siteUrl,\n database: authComponent.adapter(ctx),\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: false,\n },\n socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,\n plugins: [convex({ authConfig })],\n })\n}\n\nexport const getCurrentUser = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n",
12
12
  "convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
@@ -72,7 +72,7 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
72
72
  "packages/ui/src/index.ts.hbs": "export { cn } from './lib/utils'\n// Export components as they are added\n// export * from './components/ui/button'\n",
73
73
  "packages/ui/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",
74
74
  "packages/ui/tsconfig.json.hbs": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\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 \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\"]\n}\n",
75
- "web/_env.local.hbs": "# Convex - These values are synced from packages/backend/.env.local after running convex dev\n# Copy NEXT_PUBLIC_CONVEX_URL from packages/backend/.env.local after Convex setup\nNEXT_PUBLIC_CONVEX_URL=\n\n# Site URL for auth\nNEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\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.payments 'stripe')}}\n\n# Stripe\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n",
75
+ "web/_env.local.hbs": "# Convex - These values are synced from packages/backend/.env.local after running convex dev\n# NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL are auto-synced by dev script\nNEXT_PUBLIC_CONVEX_URL=\nNEXT_PUBLIC_CONVEX_SITE_URL=\n\n# Site URL for auth (your frontend URL)\nNEXT_PUBLIC_SITE_URL=http://localhost:3000\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.payments 'stripe')}}\n\n# Stripe\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n",
76
76
  "web/components.json.hbs": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n \"config\": \"\",\n \"css\": \"src/app/globals.css\",\n \"baseColor\": \"{{shadcn.baseColor}}\",\n \"cssVariables\": true,\n \"prefix\": \"\"\n },\n \"aliases\": {\n \"components\": \"@/components\",\n \"utils\": \"@/lib/utils\",\n \"ui\": \"@/components/ui\",\n \"lib\": \"@/lib\",\n \"hooks\": \"@/hooks\"\n },\n \"iconLibrary\": \"{{shadcn.iconLibrary}}\"\n}\n",
77
77
  "web/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n{{#if (eq structure 'monorepo')}}\n transpilePackages: ['@repo/ui', '@repo/backend'],\n{{/if}}\n}\n\nexport default nextConfig\n",
78
78
  "web/package.json.hbs": "{\n \"name\": \"{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}\",\n \"dev:next\": \"next dev --turbopack\",\n{{#unless (eq structure 'monorepo')}} \"dev:setup\": \"npx convex dev --configure --until-success\",\n{{/unless}} \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:e2e\": \"playwright test\"\n },\n \"dependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/backend\": \"workspace:*\",\n \"@repo/ui\": \"workspace:*\",\n{{/if}} \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"convex\": \"^1.25.0\",\n \"@convex-dev/better-auth\": \"^0.10.0\",\n \"better-auth\": \"1.4.9\",\n{{#unless (eq structure 'monorepo')}}{{#if (eq shadcn.iconLibrary \"hugeicons\")}} \"@hugeicons/react\": \"^0.3.0\",\n{{/if}}{{#if (eq shadcn.iconLibrary \"lucide\")}} \"lucide-react\": \"^0.469.0\",\n{{/if}}{{#if (eq shadcn.iconLibrary \"tabler\")}} \"@tabler/icons-react\": \"^3.31.0\",\n{{/if}}{{#if (eq shadcn.iconLibrary \"phosphor\")}} \"@phosphor-icons/react\": \"^2.1.7\",\n{{/if}}{{/unless}} \"class-variance-authority\": \"^0.7.0\",\n \"clsx\": \"^2.1.0\",\n \"tailwind-merge\": \"^2.5.0\",\n \"tw-animate-css\": \"^1.3.0\",\n \"resend\": \"^4.0.0\",\n \"react-email\": \"^3.0.0\",\n \"@react-email/components\": \"^0.0.36\"{{#if (eq integrations.analytics 'posthog')}},\n \"posthog-js\": \"^1.200.0\",\n \"posthog-node\": \"^5.0.0\"{{/if}}{{#if (eq integrations.analytics 'vercel')}},\n \"@vercel/analytics\": \"^1.4.0\",\n \"@vercel/speed-insights\": \"^1.1.0\"{{/if}}{{#if (eq integrations.uploads 'uploadthing')}},\n \"uploadthing\": \"^7.0.0\",\n \"@uploadthing/react\": \"^7.0.0\"{{/if}}{{#if (eq integrations.uploads 's3')}},\n \"@aws-sdk/client-s3\": \"^3.700.0\",\n \"@aws-sdk/s3-request-presigner\": \"^3.700.0\"{{/if}}{{#if (eq integrations.uploads 'vercel-blob')}},\n \"@vercel/blob\": \"^2.0.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@arcjet/next\": \"^1.0.0-beta.16\"{{/if}}{{#if (includes addons 'monitoring')}},\n \"@sentry/nextjs\": \"^8.0.0\"{{/if}}\n },\n \"devDependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/config-typescript\": \"workspace:*\",\n{{/if}} \"@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 \"typescript\": \"^5.0.0\",\n \"vitest\": \"^3.0.0\",\n \"@vitejs/plugin-react\": \"^4.3.0\",\n \"@testing-library/react\": \"^16.0.0\",\n \"jsdom\": \"^26.0.0\",\n \"playwright\": \"^1.50.0\",\n \"@playwright/test\": \"^1.50.0\"\n }\n}\n",
@@ -83,7 +83,7 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
83
83
  "web/src/app/api/auth/[...all]/route.ts.hbs": "import { handler } from '@/lib/auth-server'\n\nexport const { GET, POST } = handler\n",
84
84
  "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",
85
85
  "web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\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 <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 )\n}\n",
86
- "web/src/app/page.tsx.hbs": "'use client'\n\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'\nimport { DashboardLayout } from '@/components/dashboard/dashboard-layout'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Skeleton } from '@/components/ui/skeleton'\n\nexport default function HomePage() {\n const user = useQuery(api.users.viewer)\n\n if (user === undefined) {\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <Skeleton className=\"h-8 w-64\" />\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n </DashboardLayout>\n )\n }\n\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">\n Welcome back{user?.name ? `, ${user.name}` : ''}!\n </h1>\n <p className=\"text-muted-foreground\">\n Here's what's happening with your project today.\n </p>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Card>\n <CardHeader>\n <CardTitle>Getting Started</CardTitle>\n <CardDescription>Quick start guide for your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Customize your dashboard layout</li>\n <li>Add new pages to the sidebar</li>\n <li>Connect your data sources</li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Documentation</CardTitle>\n <CardDescription>Learn how to build with Kofi Stack</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>\n <a\n href=\"https://docs.convex.dev\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Convex Documentation\n </a>\n </li>\n <li>\n <a\n href=\"https://ui.shadcn.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n shadcn/ui Components\n </a>\n </li>\n <li>\n <a\n href=\"https://nextjs.org/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Next.js Documentation\n </a>\n </li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Your Stack</CardTitle>\n <CardDescription>Technologies powering your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Next.js 15 with App Router</li>\n <li>Convex for backend & database</li>\n <li>Convex Auth for authentication</li>\n <li>shadcn/ui components</li>\n <li>Tailwind CSS for styling</li>\n </ul>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </DashboardLayout>\n )\n}\n",
86
+ "web/src/app/page.tsx.hbs": "'use client'\n\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../convex/_generated/api{{/if}}'\nimport { DashboardLayout } from '@/components/dashboard/dashboard-layout'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Skeleton } from '@/components/ui/skeleton'\n\nexport default function HomePage() {\n const user = useQuery(api.users.viewer)\n\n if (user === undefined) {\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <Skeleton className=\"h-8 w-64\" />\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n </DashboardLayout>\n )\n }\n\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">\n Welcome back{user?.name ? `, ${user.name}` : ''}!\n </h1>\n <p className=\"text-muted-foreground\">\n Here's what's happening with your project today.\n </p>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Card>\n <CardHeader>\n <CardTitle>Getting Started</CardTitle>\n <CardDescription>Quick start guide for your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Customize your dashboard layout</li>\n <li>Add new pages to the sidebar</li>\n <li>Connect your data sources</li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Documentation</CardTitle>\n <CardDescription>Learn how to build with Kofi Stack</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>\n <a\n href=\"https://docs.convex.dev\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Convex Documentation\n </a>\n </li>\n <li>\n <a\n href=\"https://ui.shadcn.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n shadcn/ui Components\n </a>\n </li>\n <li>\n <a\n href=\"https://nextjs.org/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Next.js Documentation\n </a>\n </li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Your Stack</CardTitle>\n <CardDescription>Technologies powering your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Next.js 15 with App Router</li>\n <li>Convex for backend & database</li>\n <li>Convex Auth for authentication</li>\n <li>shadcn/ui components</li>\n <li>Tailwind CSS for styling</li>\n </ul>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </DashboardLayout>\n )\n}\n",
87
87
  "web/src/components/auth/sign-in-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignInForm() {\n const router = useRouter()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const result = await signIn.email({\n email,\n password,\n })\n\n if (result.error) {\n setError(result.error.message || 'Invalid email or password')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Invalid email or password')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Welcome back</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"Enter your password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Signing in...' : 'Sign In'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <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\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <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\"/>\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Don&apos;t have an account?{' '}\n <Link href=\"/sign-up\" className=\"text-primary hover:underline font-medium\">\n Sign up\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
88
88
  "web/src/components/auth/sign-up-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signUp, signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignUpForm() {\n const router = useRouter()\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n if (password.length < 8) {\n setError('Password must be at least 8 characters')\n setIsLoading(false)\n return\n }\n\n try {\n const result = await signUp.email({\n email,\n password,\n name,\n })\n\n if (result.error) {\n setError(result.error.message || 'Failed to create account')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Failed to create account. Email may already be in use.')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Create an account</CardTitle>\n <CardDescription>Enter your details to get started</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">Name</Label>\n <Input\n id=\"name\"\n type=\"text\"\n placeholder=\"Your name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"At least 8 characters\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n minLength={8}\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Creating account...' : 'Create Account'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <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\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <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\"/>\n <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\"/>\n <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\"/>\n <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\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Already have an account?{' '}\n <Link href=\"/sign-in\" className=\"text-primary hover:underline font-medium\">\n Sign in\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
89
89
  "web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar>\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild>\n <a href=\"/\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"flex flex-col gap-0.5 leading-none\">\n <span className=\"font-semibold\">{{projectName}}</span>\n <span className=\"text-xs text-muted-foreground\">Dashboard</span>\n </div>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {navigation.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild>\n <a href={item.url}>\n <item.icon className=\"size-4\" />\n <span>{item.title}</span>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarFooter>\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />\n <AvatarFallback className=\"rounded-lg\">\n {getInitials(user?.name)}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{user?.name ?? 'User'}</span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {user?.email ?? ''}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg\"\n side=\"bottom\"\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <User className=\"mr-2 size-4\" />\n Profile\n </a>\n </DropdownMenuItem>\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <Settings className=\"mr-2 size-4\" />\n Settings\n </a>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleSignOut}>\n <LogOut className=\"mr-2 size-4\" />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n </Sidebar>\n )\n}\n",
@@ -1,7 +1,7 @@
1
1
  # Convex
2
2
  CONVEX_DEPLOYMENT=
3
3
  NEXT_PUBLIC_CONVEX_URL=
4
- NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000
4
+ NEXT_PUBLIC_CONVEX_SITE_URL=
5
5
 
6
6
  # Site URL (used for auth redirects)
7
7
  SITE_URL=http://localhost:3000
@@ -1,9 +1,9 @@
1
1
  # Convex - These values are synced from packages/backend/.env.local after running convex dev
2
- # Copy NEXT_PUBLIC_CONVEX_URL from packages/backend/.env.local after Convex setup
2
+ # NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL are auto-synced by dev script
3
3
  NEXT_PUBLIC_CONVEX_URL=
4
+ NEXT_PUBLIC_CONVEX_SITE_URL=
4
5
 
5
- # Site URL for auth
6
- NEXT_PUBLIC_CONVEX_SITE_URL=http://localhost:3000
6
+ # Site URL for auth (your frontend URL)
7
7
  NEXT_PUBLIC_SITE_URL=http://localhost:3000
8
8
  {{#if (eq integrations.analytics 'posthog')}}
9
9
 
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useQuery } from 'convex/react'
4
- import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'
4
+ import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../convex/_generated/api{{/if}}'
5
5
  import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
7
  import { Skeleton } from '@/components/ui/skeleton'