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/.turbo/turbo-build.log +6 -6
- package/dist/index.js +616 -56
- package/package.json +8 -8
- package/src/generator.ts +32 -3
- package/src/templates.generated.ts +17 -9
- package/templates/convex/_env.local.hbs +31 -5
- package/templates/convex/convex/auth.ts.hbs +28 -1
- package/templates/convex/convex/users.ts.hbs +12 -0
- package/templates/convex/package.json.hbs +7 -1
- package/templates/marketing/payload/_env.local.hbs +7 -7
- package/templates/web/src/app/(auth)/layout.tsx.hbs +13 -0
- package/templates/web/src/app/(auth)/sign-in/page.tsx.hbs +5 -0
- package/templates/web/src/app/(auth)/sign-up/page.tsx.hbs +5 -0
- package/templates/web/src/app/page.tsx.hbs +101 -47
- package/templates/web/src/components/auth/sign-in-form.tsx.hbs +121 -0
- package/templates/web/src/components/auth/sign-up-form.tsx.hbs +141 -0
- package/templates/web/src/components/dashboard/app-sidebar.tsx.hbs +163 -0
- package/templates/web/src/components/dashboard/dashboard-layout.tsx.hbs +38 -0
- package/templates/web/src/lib/auth.ts.hbs +13 -3
- package/templates/web/src/proxy.ts.hbs +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kofi-stack-template-generator",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -10,12 +10,6 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "pnpm run prebuild && tsup src/index.ts --format esm --dts",
|
|
15
|
-
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
16
|
-
"prebuild": "node scripts/generate-templates.js",
|
|
17
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
-
},
|
|
19
13
|
"dependencies": {
|
|
20
14
|
"kofi-stack-types": "^2.0.3",
|
|
21
15
|
"handlebars": "^4.7.8",
|
|
@@ -25,5 +19,11 @@
|
|
|
25
19
|
"@types/node": "^20.0.0",
|
|
26
20
|
"tsup": "^8.0.0",
|
|
27
21
|
"typescript": "^5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "pnpm run prebuild && tsup src/index.ts --format esm --dts",
|
|
25
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
26
|
+
"prebuild": "node scripts/generate-templates.js",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|
package/src/generator.ts
CHANGED
|
@@ -208,6 +208,7 @@ function generateDevScript(
|
|
|
208
208
|
): void {
|
|
209
209
|
const isMonorepo = config.structure === 'monorepo'
|
|
210
210
|
const webAppDir = isMonorepo ? 'apps/web' : '.'
|
|
211
|
+
const backendDir = isMonorepo ? 'packages/backend' : '.'
|
|
211
212
|
|
|
212
213
|
const devScript = `#!/usr/bin/env node
|
|
213
214
|
/**
|
|
@@ -215,13 +216,14 @@ function generateDevScript(
|
|
|
215
216
|
*/
|
|
216
217
|
|
|
217
218
|
import { spawn, execSync } from 'child_process'
|
|
218
|
-
import { existsSync, readFileSync } from 'fs'
|
|
219
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
219
220
|
import { resolve, dirname } from 'path'
|
|
220
221
|
import { fileURLToPath } from 'url'
|
|
221
222
|
|
|
222
223
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
223
224
|
const rootDir = resolve(__dirname, '..')
|
|
224
225
|
const webAppDir = resolve(rootDir, '${webAppDir}')
|
|
226
|
+
const backendDir = resolve(rootDir, '${backendDir}')
|
|
225
227
|
|
|
226
228
|
function loadEnvFile(dir) {
|
|
227
229
|
const envPath = resolve(dir, '.env.local')
|
|
@@ -246,6 +248,33 @@ function loadEnvFile(dir) {
|
|
|
246
248
|
return env
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
function syncEnvToWebApp() {
|
|
252
|
+
// In monorepo, Convex creates .env.local in backend package
|
|
253
|
+
// Web app needs NEXT_PUBLIC_CONVEX_URL to connect to Convex
|
|
254
|
+
const backendEnv = loadEnvFile(backendDir)
|
|
255
|
+
const webEnvPath = resolve(webAppDir, '.env.local')
|
|
256
|
+
|
|
257
|
+
if (backendEnv.NEXT_PUBLIC_CONVEX_URL) {
|
|
258
|
+
const webEnv = loadEnvFile(webAppDir)
|
|
259
|
+
|
|
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
|
+
let content = ''
|
|
263
|
+
if (existsSync(webEnvPath)) {
|
|
264
|
+
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')
|
|
267
|
+
if (content && !content.endsWith('\\n')) content += '\\n'
|
|
268
|
+
}
|
|
269
|
+
content += \`NEXT_PUBLIC_CONVEX_URL=\${backendEnv.NEXT_PUBLIC_CONVEX_URL}\\n\`
|
|
270
|
+
writeFileSync(webEnvPath, content)
|
|
271
|
+
console.log('✓ Synced NEXT_PUBLIC_CONVEX_URL to web app\\n')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return backendEnv
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
async function checkAndInstall() {
|
|
250
279
|
if (!existsSync(resolve(rootDir, 'node_modules'))) {
|
|
251
280
|
console.log('📦 Installing dependencies...\\n')
|
|
@@ -254,9 +283,9 @@ async function checkAndInstall() {
|
|
|
254
283
|
}
|
|
255
284
|
|
|
256
285
|
function startDevServers() {
|
|
257
|
-
const
|
|
286
|
+
${isMonorepo ? 'const backendEnv = syncEnvToWebApp()' : 'const backendEnv = loadEnvFile(webAppDir)'}
|
|
258
287
|
|
|
259
|
-
if (!
|
|
288
|
+
if (!backendEnv.CONVEX_DEPLOYMENT) {
|
|
260
289
|
console.log('⚠️ Convex not configured. Run: pnpm dev:setup\\n')
|
|
261
290
|
console.log('Starting Next.js only...\\n')
|
|
262
291
|
spawn('pnpm', ['${isMonorepo ? 'dev:web' : 'dev:next'}'], {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Auto-generated file. Do not edit manually.
|
|
2
2
|
// Run 'pnpm prebuild' to regenerate.
|
|
3
|
-
// Generated: 2026-01-
|
|
4
|
-
// Template count:
|
|
3
|
+
// Generated: 2026-01-14T01:39:16.805Z
|
|
4
|
+
// Template count: 86
|
|
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=\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#
|
|
10
|
-
"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
|
|
9
|
+
"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",
|
|
10
|
+
"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",
|
|
11
11
|
"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",
|
|
12
12
|
"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",
|
|
13
|
-
"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",
|
|
14
|
-
"convex/package.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"name\": \"@repo/backend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"convex dev\",\n \"dev:setup\": \"convex dev --configure --until-success\",\n \"deploy\": \"convex deploy\"\n },\n \"dependencies\": {\n \"convex\": \"^1.25.0\",\n \"@convex-dev/auth\": \"^0.0.90\",\n \"@auth/core\": \"^0.37.0\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.0.0\"\n }\n}{{/if}}\n",
|
|
13
|
+
"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",
|
|
14
|
+
"convex/package.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"name\": \"@repo/backend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"convex dev\",\n \"dev:setup\": \"convex dev --configure --until-success\",\n \"deploy\": \"convex deploy\"\n },\n \"dependencies\": {\n \"convex\": \"^1.25.0\",\n \"@convex-dev/auth\": \"^0.0.90\",\n \"@auth/core\": \"^0.37.0\",\n \"@convex-dev/resend\": \"^0.1.0\"{{#if (eq integrations.uploads 'convex-fs')}},\n \"@convex-dev/convex-fs\": \"^0.1.0\"{{/if}}{{#if (eq integrations.uploads 'r2')}},\n \"@convex-dev/cloudflare-r2\": \"^0.1.0\"{{/if}}{{#if (eq integrations.payments 'stripe')}},\n \"@convex-dev/stripe\": \"^0.1.0\"{{/if}}{{#if (eq integrations.payments 'polar')}},\n \"@convex-dev/polar\": \"^0.1.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@convex-dev/rate-limiter\": \"^0.1.0\"{{/if}}\n },\n \"devDependencies\": {\n \"typescript\": \"^5.0.0\"\n }\n}{{/if}}\n",
|
|
15
15
|
"convex/tsconfig.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true,\n \"outDir\": \"dist\"\n },\n \"include\": [\"convex/**/*.ts\"],\n \"exclude\": [\"node_modules\"]\n}{{/if}}\n",
|
|
16
16
|
"integrations/posthog/src/components/providers/posthog-provider.tsx.hbs": "'use client'\n\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PHProvider } from 'posthog-js/react'\nimport { useEffect } from 'react'\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',\n person_profiles: 'identified_only',\n capture_pageview: false, // We capture pageviews manually\n })\n }, [])\n\n return <PHProvider client={posthog}>{children}</PHProvider>\n}\n",
|
|
17
17
|
"marketing/nextjs/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n transpilePackages: ['@repo/ui'],\n}\n\nexport default nextConfig\n",
|
|
@@ -22,7 +22,7 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
22
22
|
"marketing/nextjs/src/app/page.tsx.hbs": "export default function HomePage() {\n return (\n <main className=\"min-h-screen\">\n {/* Hero Section */}\n <section className=\"relative overflow-hidden bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800\">\n <div className=\"container mx-auto px-4 py-24 sm:py-32\">\n <div className=\"text-center\">\n <h1 className=\"text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl\">\n Welcome to {{projectName}}\n </h1>\n <p className=\"mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto\">\n A modern full-stack application built with Next.js, Convex, and Better-Auth.\n </p>\n <div className=\"mt-10 flex items-center justify-center gap-x-6\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-gray-900 dark:bg-white px-6 py-3 text-sm font-semibold text-white dark:text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Get Started\n </a>\n <a\n href=\"#features\"\n className=\"text-sm font-semibold leading-6 text-gray-900 dark:text-white\"\n >\n Learn more <span aria-hidden=\"true\">→</span>\n </a>\n </div>\n </div>\n </div>\n </section>\n\n {/* Features Section */}\n <section id=\"features\" className=\"py-24 sm:py-32\">\n <div className=\"container mx-auto px-4\">\n <div className=\"text-center mb-16\">\n <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl\">\n Everything you need\n </h2>\n <p className=\"mt-4 text-lg text-gray-600 dark:text-gray-300\">\n Built with the best tools for modern web development\n </p>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Lightning Fast\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Built on Next.js with Turbopack for instant hot reload and optimized production builds.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Real-time Database\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Powered by Convex for automatic real-time sync and type-safe queries.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Secure Auth\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Better-Auth provides secure, flexible authentication with social login support.\n </p>\n </div>\n </div>\n </div>\n </section>\n\n {/* CTA Section */}\n <section className=\"py-24 sm:py-32 bg-gray-900 dark:bg-gray-800\">\n <div className=\"container mx-auto px-4 text-center\">\n <h2 className=\"text-3xl font-bold tracking-tight text-white sm:text-4xl\">\n Ready to get started?\n </h2>\n <p className=\"mt-4 text-lg text-gray-300 max-w-2xl mx-auto\">\n Start building your next project with our full-stack template.\n </p>\n <div className=\"mt-10\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-white px-6 py-3 text-sm font-semibold text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Launch App\n </a>\n </div>\n </div>\n </section>\n\n {/* Footer */}\n <footer className=\"border-t border-gray-200 dark:border-gray-700\">\n <div className=\"container mx-auto px-4 py-12\">\n <div className=\"text-center text-gray-600 dark:text-gray-300\">\n <p>\n Built with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-gray-900 dark:text-white hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </footer>\n </main>\n )\n}\n",
|
|
23
23
|
"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",
|
|
24
24
|
"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",
|
|
25
|
-
"marketing/payload/_env.local.hbs": "# Database (Supabase PostgreSQL)\nDATABASE_URL=\
|
|
25
|
+
"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",
|
|
26
26
|
"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",
|
|
27
27
|
"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",
|
|
28
28
|
"marketing/payload/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
@@ -75,11 +75,19 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
75
75
|
"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",
|
|
76
76
|
"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{{/if}} \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"convex\": \"^1.25.0\",\n \"@convex-dev/auth\": \"^0.0.90\",\n \"@auth/core\": \"^0.37.0\",\n \"@hugeicons/react\": \"^0.3.0\",\n \"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",
|
|
77
77
|
"web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
78
|
+
"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",
|
|
79
|
+
"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",
|
|
80
|
+
"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",
|
|
78
81
|
"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",
|
|
79
82
|
"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",
|
|
80
|
-
"web/src/app/page.tsx.hbs": "'use client'\n\nimport {
|
|
83
|
+
"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",
|
|
84
|
+
"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 { useAuthActions } from '@convex-dev/auth/react'\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 { signIn } = useAuthActions()\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 formData = new FormData()\n formData.append('email', email)\n formData.append('password', password)\n formData.append('flow', 'signIn')\n\n await signIn('password', formData)\n router.push('/')\n } catch (err) {\n setError('Invalid email or password')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = (provider: 'github' | 'google') => {\n void signIn(provider)\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'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",
|
|
85
|
+
"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 { useAuthActions } from '@convex-dev/auth/react'\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 { signIn } = useAuthActions()\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 formData = new FormData()\n formData.append('name', name)\n formData.append('email', email)\n formData.append('password', password)\n formData.append('flow', 'signUp')\n\n await signIn('password', formData)\n router.push('/')\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 = (provider: 'github' | 'google') => {\n void signIn(provider)\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",
|
|
86
|
+
"web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n AudioWaveform,\n Command,\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'\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 { signOut } = useAuthActions()\n const user = useQuery(api.users.viewer)\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",
|
|
87
|
+
"web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2 border-b px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbPage>{title}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </header>\n <main className=\"flex-1 p-4 md:p-6\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n",
|
|
81
88
|
"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",
|
|
82
|
-
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth,
|
|
89
|
+
"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",
|
|
83
90
|
"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",
|
|
91
|
+
"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",
|
|
84
92
|
"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"
|
|
85
93
|
}
|
|
@@ -12,12 +12,28 @@ AUTH_GOOGLE_SECRET=
|
|
|
12
12
|
|
|
13
13
|
# Better Auth Secret (generate with: openssl rand -base64 32)
|
|
14
14
|
BETTER_AUTH_SECRET=
|
|
15
|
+
|
|
16
|
+
# Email (Resend) - https://resend.com
|
|
17
|
+
RESEND_API_KEY=
|
|
18
|
+
RESEND_FROM_EMAIL=
|
|
15
19
|
{{#if (eq integrations.analytics 'posthog')}}
|
|
16
20
|
|
|
17
21
|
# PostHog
|
|
18
22
|
NEXT_PUBLIC_POSTHOG_KEY=
|
|
19
23
|
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
|
|
20
24
|
{{/if}}
|
|
25
|
+
{{#if (eq integrations.uploads 'convex-fs')}}
|
|
26
|
+
|
|
27
|
+
# Convex FS - Built-in file storage (no additional config needed)
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#if (eq integrations.uploads 'r2')}}
|
|
30
|
+
|
|
31
|
+
# Cloudflare R2
|
|
32
|
+
R2_ACCESS_KEY_ID=
|
|
33
|
+
R2_SECRET_ACCESS_KEY=
|
|
34
|
+
R2_BUCKET=
|
|
35
|
+
R2_ENDPOINT=
|
|
36
|
+
{{/if}}
|
|
21
37
|
{{#if (eq integrations.uploads 'uploadthing')}}
|
|
22
38
|
|
|
23
39
|
# UploadThing
|
|
@@ -36,10 +52,23 @@ AWS_S3_BUCKET=
|
|
|
36
52
|
# Vercel Blob
|
|
37
53
|
BLOB_READ_WRITE_TOKEN=
|
|
38
54
|
{{/if}}
|
|
55
|
+
{{#if (eq integrations.payments 'stripe')}}
|
|
56
|
+
|
|
57
|
+
# Stripe
|
|
58
|
+
STRIPE_SECRET_KEY=
|
|
59
|
+
STRIPE_WEBHOOK_SECRET=
|
|
60
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
61
|
+
{{/if}}
|
|
62
|
+
{{#if (eq integrations.payments 'polar')}}
|
|
63
|
+
|
|
64
|
+
# Polar
|
|
65
|
+
POLAR_ACCESS_TOKEN=
|
|
66
|
+
POLAR_WEBHOOK_SECRET=
|
|
67
|
+
POLAR_ORGANIZATION_ID=
|
|
68
|
+
{{/if}}
|
|
39
69
|
{{#if (includes addons 'rate-limiting')}}
|
|
40
70
|
|
|
41
|
-
#
|
|
42
|
-
ARCJET_KEY=
|
|
71
|
+
# Convex Rate Limiter - No additional config needed (uses Convex backend)
|
|
43
72
|
{{/if}}
|
|
44
73
|
{{#if (includes addons 'monitoring')}}
|
|
45
74
|
|
|
@@ -47,6 +76,3 @@ ARCJET_KEY=
|
|
|
47
76
|
SENTRY_DSN=
|
|
48
77
|
SENTRY_AUTH_TOKEN=
|
|
49
78
|
{{/if}}
|
|
50
|
-
|
|
51
|
-
# Email (Resend)
|
|
52
|
-
RESEND_API_KEY=
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import GitHub from '@auth/core/providers/github'
|
|
2
2
|
import Google from '@auth/core/providers/google'
|
|
3
|
+
import { Password } from '@convex-dev/auth/providers/Password'
|
|
3
4
|
import { convexAuth } from '@convex-dev/auth/server'
|
|
4
5
|
|
|
5
6
|
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
|
6
|
-
providers: [
|
|
7
|
+
providers: [
|
|
8
|
+
GitHub,
|
|
9
|
+
Google,
|
|
10
|
+
Password,
|
|
11
|
+
],
|
|
7
12
|
})
|
|
13
|
+
|
|
14
|
+
// Email Verification Setup (Optional)
|
|
15
|
+
// ------------------------------------
|
|
16
|
+
// By default, email/password auth works without email verification.
|
|
17
|
+
// To require email verification, configure the Password provider with ResendOTP:
|
|
18
|
+
//
|
|
19
|
+
// 1. Install the Resend component: npm install @convex-dev/resend
|
|
20
|
+
// 2. Update the Password provider:
|
|
21
|
+
//
|
|
22
|
+
// import { ResendOTP } from '@convex-dev/auth/providers/ResendOTP'
|
|
23
|
+
// import { Resend } from '@convex-dev/resend'
|
|
24
|
+
//
|
|
25
|
+
// const resend = new Resend()
|
|
26
|
+
//
|
|
27
|
+
// Password({
|
|
28
|
+
// verify: ResendOTP({
|
|
29
|
+
// resend,
|
|
30
|
+
// from: process.env.RESEND_FROM_EMAIL!,
|
|
31
|
+
// }),
|
|
32
|
+
// })
|
|
33
|
+
//
|
|
34
|
+
// See: https://labs.convex.dev/auth/config/passwords
|
|
@@ -11,3 +11,15 @@ export const current = query({
|
|
|
11
11
|
return user
|
|
12
12
|
},
|
|
13
13
|
})
|
|
14
|
+
|
|
15
|
+
// Alias for current user - used by dashboard components
|
|
16
|
+
export const viewer = query({
|
|
17
|
+
args: {},
|
|
18
|
+
handler: async (ctx) => {
|
|
19
|
+
const userId = await auth.getUserId(ctx)
|
|
20
|
+
if (!userId) return null
|
|
21
|
+
|
|
22
|
+
const user = await ctx.db.get(userId)
|
|
23
|
+
return user
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"convex": "^1.25.0",
|
|
21
21
|
"@convex-dev/auth": "^0.0.90",
|
|
22
|
-
"@auth/core": "^0.37.0"
|
|
22
|
+
"@auth/core": "^0.37.0",
|
|
23
|
+
"@convex-dev/resend": "^0.1.0"{{#if (eq integrations.uploads 'convex-fs')}},
|
|
24
|
+
"@convex-dev/convex-fs": "^0.1.0"{{/if}}{{#if (eq integrations.uploads 'r2')}},
|
|
25
|
+
"@convex-dev/cloudflare-r2": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'stripe')}},
|
|
26
|
+
"@convex-dev/stripe": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'polar')}},
|
|
27
|
+
"@convex-dev/polar": "^0.1.0"{{/if}}{{#if (includes addons 'rate-limiting')}},
|
|
28
|
+
"@convex-dev/rate-limiter": "^0.1.0"{{/if}}
|
|
23
29
|
},
|
|
24
30
|
"devDependencies": {
|
|
25
31
|
"typescript": "^5.0.0"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Database (Supabase PostgreSQL)
|
|
2
|
-
DATABASE_URL=
|
|
2
|
+
DATABASE_URL=
|
|
3
3
|
|
|
4
4
|
# Payload CMS
|
|
5
|
-
PAYLOAD_SECRET=
|
|
5
|
+
PAYLOAD_SECRET=
|
|
6
6
|
|
|
7
7
|
# Scheduled Jobs
|
|
8
|
-
CRON_SECRET=
|
|
8
|
+
CRON_SECRET=
|
|
9
9
|
|
|
10
10
|
# Draft Previews
|
|
11
|
-
PREVIEW_SECRET=
|
|
11
|
+
PREVIEW_SECRET=
|
|
12
12
|
|
|
13
13
|
# S3 Storage (Supabase Storage)
|
|
14
14
|
S3_BUCKET="media"
|
|
15
|
-
S3_ACCESS_KEY_ID=
|
|
16
|
-
S3_SECRET_ACCESS_KEY=
|
|
15
|
+
S3_ACCESS_KEY_ID=
|
|
16
|
+
S3_SECRET_ACCESS_KEY=
|
|
17
17
|
S3_REGION="auto"
|
|
18
|
-
S3_ENDPOINT=
|
|
18
|
+
S3_ENDPOINT=
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function AuthLayout({
|
|
2
|
+
children,
|
|
3
|
+
}: {
|
|
4
|
+
children: React.ReactNode
|
|
5
|
+
}) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen flex items-center justify-center bg-muted/50 p-4">
|
|
8
|
+
<div className="w-full max-w-md">
|
|
9
|
+
{children}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|