@techstream/quark-create-app 1.5.3 → 1.7.0

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.
Files changed (53) hide show
  1. package/package.json +4 -2
  2. package/src/index.js +62 -14
  3. package/templates/base-project/.github/dependabot.yml +12 -0
  4. package/templates/base-project/.github/workflows/ci.yml +97 -0
  5. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  6. package/templates/base-project/.github/workflows/release.yml +38 -0
  7. package/templates/base-project/apps/web/biome.json +7 -0
  8. package/templates/base-project/apps/web/jsconfig.json +5 -5
  9. package/templates/base-project/apps/web/next.config.js +90 -1
  10. package/templates/base-project/apps/web/package.json +7 -7
  11. package/templates/base-project/apps/web/railway.json +15 -0
  12. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
  13. package/templates/base-project/apps/web/src/app/layout.js +3 -4
  14. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  15. package/templates/base-project/apps/web/src/app/robots.js +21 -0
  16. package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
  17. package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
  18. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
  19. package/templates/base-project/apps/web/src/proxy.js +1 -2
  20. package/templates/base-project/apps/worker/package.json +5 -5
  21. package/templates/base-project/apps/worker/railway.json +13 -0
  22. package/templates/base-project/apps/worker/src/index.js +30 -12
  23. package/templates/base-project/apps/worker/src/index.test.js +296 -15
  24. package/templates/base-project/biome.json +44 -0
  25. package/templates/base-project/docker-compose.yml +7 -4
  26. package/templates/base-project/package.json +1 -1
  27. package/templates/base-project/packages/db/package.json +1 -1
  28. package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
  29. package/templates/base-project/packages/db/prisma.config.ts +8 -10
  30. package/templates/base-project/packages/db/scripts/seed.js +117 -30
  31. package/templates/base-project/packages/db/src/client.js +1 -18
  32. package/templates/base-project/packages/db/src/connection.js +44 -0
  33. package/templates/base-project/packages/db/src/connection.test.js +119 -0
  34. package/templates/base-project/packages/db/src/queries.js +52 -118
  35. package/templates/base-project/packages/db/src/queries.test.js +0 -29
  36. package/templates/base-project/packages/db/src/schemas.js +0 -12
  37. package/templates/base-project/pnpm-workspace.yaml +4 -0
  38. package/templates/base-project/turbo.json +5 -3
  39. package/templates/config/package.json +2 -0
  40. package/templates/config/src/environment.js +270 -0
  41. package/templates/config/src/index.js +13 -18
  42. package/templates/config/src/load-config.js +135 -0
  43. package/templates/config/src/validate-env.js +123 -16
  44. package/templates/jobs/package.json +2 -2
  45. package/templates/jobs/src/definitions.test.js +34 -0
  46. package/templates/jobs/src/index.js +1 -1
  47. package/templates/ui/package.json +4 -4
  48. package/templates/ui/src/button.test.js +23 -0
  49. package/templates/ui/src/index.js +1 -3
  50. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
  51. package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
  52. package/templates/ui/src/card.js +0 -14
  53. package/templates/ui/src/input.js +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techstream/quark-create-app",
3
- "version": "1.5.3",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "quark-create-app": "src/index.js",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "dependencies": {
16
16
  "chalk": "^5.6.2",
17
- "commander": "^12.1.0",
17
+ "commander": "^14.0.3",
18
18
  "execa": "^9.6.1",
19
19
  "fs-extra": "^11.3.3",
20
20
  "prompts": "^2.4.2"
@@ -28,6 +28,8 @@
28
28
  },
29
29
  "license": "ISC",
30
30
  "scripts": {
31
+ "sync-templates": "node scripts/sync-templates.js",
32
+ "sync-templates:check": "node scripts/sync-templates.js --check",
31
33
  "test": "node test-cli.js",
32
34
  "test:build": "node test-build.js",
33
35
  "test:e2e": "node test-e2e.js",
package/src/index.js CHANGED
@@ -474,17 +474,21 @@ program
474
474
 
475
475
  // Step 8: Create .env.example file
476
476
  console.log(chalk.cyan("\n 📋 Creating environment configuration..."));
477
- const envExampleTemplate = `# --- Database Configuration ---
477
+ const envExampleTemplate = `# --- Environment ---
478
+ # Supported: development, test, staging, production (default: development)
479
+ # NODE_ENV=development
480
+
481
+ # --- Database Configuration ---
478
482
  # These map to the service names in docker-compose.yml
479
483
  # ⚠️ SECURITY WARNING: Change these default passwords in production!
480
484
  # Generate strong passwords with: openssl rand -base64 32
481
485
  POSTGRES_HOST=localhost
482
486
  POSTGRES_PORT=5432
483
- POSTGRES_USER=quark_user
487
+ POSTGRES_USER=${scope}_user
484
488
  POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
485
489
  POSTGRES_DB=${scope}_dev
486
490
  # Optional: Set DATABASE_URL to override the dynamic construction above
487
- # DATABASE_URL="postgresql://quark_user:CHANGE_ME_TO_STRONG_PASSWORD@localhost:5432/${scope}_dev?schema=public"
491
+ # DATABASE_URL="postgresql://${scope}_user:CHANGE_ME_TO_STRONG_PASSWORD@localhost:5432/${scope}_dev?schema=public"
488
492
 
489
493
  # --- Redis Configuration ---
490
494
  REDIS_HOST=localhost
@@ -492,18 +496,37 @@ REDIS_PORT=6379
492
496
  # Optional: Set REDIS_URL to override the dynamic construction above
493
497
  # REDIS_URL="redis://localhost:6379"
494
498
 
495
- # --- Mail Configuration (Mailpit in development) ---
499
+ # --- Mail Configuration ---
500
+ # Development: Mailpit local SMTP (defaults below work with docker-compose)
496
501
  MAIL_HOST=localhost
497
502
  MAIL_SMTP_PORT=1025
498
503
  MAIL_UI_PORT=8025
499
- # Optional: Set MAIL_SMTP_URL to override the dynamic construction above
500
- # MAIL_SMTP_URL="smtp://localhost:1025"
504
+
505
+ # Production SMTP: Set these instead of MAIL_* when using a real SMTP relay
506
+ # SMTP_HOST=smtp.example.com
507
+ # SMTP_PORT=587
508
+ # SMTP_SECURE=true
509
+ # SMTP_USER=your_smtp_user
510
+ # SMTP_PASSWORD=your_smtp_password
511
+
512
+ # --- Email Provider ---
513
+ # Provider: "smtp" (default) or "resend"
514
+ # EMAIL_PROVIDER=smtp
515
+ # EMAIL_FROM=App Name <noreply@yourdomain.com>
516
+
517
+ # Resend (only when EMAIL_PROVIDER=resend)
518
+ # Get your API key at: https://resend.com/api-keys
519
+ # RESEND_API_KEY=re_xxxxxxxxxxxxx
501
520
 
502
521
  # --- Application URL ---
503
522
  # In development, APP_URL is derived automatically from PORT — no need to set it.
504
523
  # In production, set this to your real domain:
505
524
  # APP_URL=https://yourdomain.com
506
525
 
526
+ # --- Application Identity ---
527
+ # APP_NAME is used in metadata, emails, and page titles.
528
+ APP_NAME=${projectName}
529
+
507
530
  # --- NextAuth Configuration ---
508
531
  # ⚠️ CRITICAL: Generate a secure secret with: openssl rand -base64 32
509
532
  # This secret is used to encrypt JWT tokens and session data
@@ -524,23 +547,36 @@ PORT=3000
524
547
  # --- Worker Configuration ---
525
548
  WORKER_CONCURRENCY=5
526
549
 
527
- # --- File Storage Configuration ---
528
- # Storage provider: "local" (default) or "s3" (S3-compatible, e.g. Cloudflare R2)
550
+ # --- File Storage ---
551
+ # Provider: "local" (default) or "s3" (S3-compatible: AWS S3, Cloudflare R2, MinIO)
529
552
  STORAGE_PROVIDER=local
530
- # Local storage directory (only used when STORAGE_PROVIDER=local)
553
+ # Local storage directory (only when STORAGE_PROVIDER=local)
531
554
  # STORAGE_LOCAL_DIR=./uploads
532
555
 
533
- # S3 / Cloudflare R2 Configuration (only used when STORAGE_PROVIDER=s3)
556
+ # S3 / Cloudflare R2 (only when STORAGE_PROVIDER=s3)
534
557
  # S3_BUCKET=your-bucket-name
535
- # S3_REGION=auto
558
+ # S3_REGION=auto # Use "auto" for Cloudflare R2
536
559
  # S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
537
560
  # S3_ACCESS_KEY_ID=your-access-key
538
561
  # S3_SECRET_ACCESS_KEY=your-secret-key
539
562
  # S3_PUBLIC_URL=https://your-public-bucket-domain.com
540
563
 
541
564
  # --- Upload Limits ---
542
- # UPLOAD_MAX_SIZE=10485760
565
+ # UPLOAD_MAX_SIZE=10485760 # Max file size in bytes (default: 10MB)
543
566
  # UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,application/pdf
567
+
568
+ # --- Rate Limiting & Security ---
569
+ # RATE_LIMIT_MAX=100 # Max requests per window (default: 1000 dev, 100 prod)
570
+ # RATE_LIMIT_WINDOW_MS=900000 # Window in ms (default: 15 minutes)
571
+ # API_BODY_SIZE_LIMIT=2097152 # 2MB (default)
572
+ # UPLOAD_SIZE_LIMIT=10485760 # 10MB proxy-level limit (default)
573
+
574
+ # --- Logging & Cache ---
575
+ # LOG_LEVEL=debug # debug, info, warn, error (default: debug dev, info prod)
576
+ # CACHE_TTL=60 # Default cache TTL in seconds (default: 60 dev, 600 prod)
577
+
578
+ # --- Database Seeding ---
579
+ # SEED_PROFILE=dev # Options: dev (default, includes audit logs + sample job), minimal (users only)
544
580
  `;
545
581
  await fs.writeFile(
546
582
  path.join(targetDir, ".env.example"),
@@ -583,7 +619,7 @@ STORAGE_PROVIDER=local
583
619
  const envContent = `# --- Database Configuration ---
584
620
  POSTGRES_HOST=localhost
585
621
  POSTGRES_PORT=${postgresPort}
586
- POSTGRES_USER=quark_user
622
+ POSTGRES_USER=${scope}_user
587
623
  POSTGRES_PASSWORD=${dbPassword}
588
624
  POSTGRES_DB=${scope}_dev
589
625
 
@@ -596,6 +632,9 @@ MAIL_HOST=localhost
596
632
  MAIL_SMTP_PORT=${mailSmtpPort}
597
633
  MAIL_UI_PORT=${mailUiPort}
598
634
 
635
+ # --- Application Identity ---
636
+ APP_NAME=${projectName}
637
+
599
638
  # --- NextAuth Configuration ---
600
639
  NEXTAUTH_SECRET=${nextAuthSecret}
601
640
 
@@ -609,6 +648,9 @@ WORKER_CONCURRENCY=5
609
648
 
610
649
  # --- File Storage ---
611
650
  STORAGE_PROVIDER=local
651
+
652
+ # --- Database Seeding ---
653
+ # SEED_PROFILE=dev # Options: dev (default), minimal (users only — use for production initial seed)
612
654
  `;
613
655
  await fs.writeFile(path.join(targetDir, ".env"), envContent);
614
656
  console.log(
@@ -725,7 +767,13 @@ STORAGE_PROVIDER=local
725
767
  console.log(chalk.white(` 1. cd ${projectName}`));
726
768
  console.log(chalk.white(` 2. docker compose up -d`));
727
769
  console.log(chalk.white(` 3. pnpm db:migrate`));
728
- console.log(chalk.white(` 4. pnpm dev\n`));
770
+ console.log(chalk.white(` 4. pnpm db:seed`));
771
+ console.log(chalk.white(` 5. pnpm dev\n`));
772
+ console.log(
773
+ chalk.dim(
774
+ ` Tip: set SEED_PROFILE=minimal in .env for a lean seed (admin user only)\n`,
775
+ ),
776
+ );
729
777
 
730
778
  console.log(chalk.cyan("Important:"));
731
779
  console.log(
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: npm
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ production:
9
+ dependency-type: production
10
+ development:
11
+ dependency-type: development
12
+ open-pull-requests-limit: 10
@@ -0,0 +1,97 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: pnpm/action-setup@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 24
21
+ cache: pnpm
22
+
23
+ - run: pnpm install --frozen-lockfile
24
+
25
+ - run: pnpm lint
26
+
27
+ test:
28
+ runs-on: ubuntu-latest
29
+ name: Test
30
+ services:
31
+ postgres:
32
+ image: postgres:16-alpine
33
+ env:
34
+ POSTGRES_USER: postgres
35
+ POSTGRES_PASSWORD: postgres
36
+ POSTGRES_DB: app_test
37
+ ports:
38
+ - 5432:5432
39
+ options: >-
40
+ --health-cmd="pg_isready -U postgres"
41
+ --health-interval=5s
42
+ --health-timeout=5s
43
+ --health-retries=5
44
+ redis:
45
+ image: redis:7-alpine
46
+ ports:
47
+ - 6379:6379
48
+ options: >-
49
+ --health-cmd="redis-cli ping"
50
+ --health-interval=5s
51
+ --health-timeout=5s
52
+ --health-retries=5
53
+ env:
54
+ POSTGRES_USER: postgres
55
+ POSTGRES_PASSWORD: postgres
56
+ POSTGRES_HOST: localhost
57
+ POSTGRES_PORT: "5432"
58
+ POSTGRES_DB: app_test
59
+ REDIS_HOST: localhost
60
+ REDIS_PORT: "6379"
61
+ NEXTAUTH_SECRET: ci-test-secret-must-be-at-least-32-characters-long
62
+ NODE_ENV: test
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+
66
+ - uses: pnpm/action-setup@v4
67
+
68
+ - uses: actions/setup-node@v4
69
+ with:
70
+ node-version: 24
71
+ cache: pnpm
72
+
73
+ - run: pnpm install --frozen-lockfile
74
+
75
+ - run: pnpm db:generate
76
+
77
+ - run: pnpm test
78
+
79
+ build:
80
+ name: Build
81
+ needs: [lint, test]
82
+ runs-on: ubuntu-latest
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+
86
+ - uses: pnpm/action-setup@v4
87
+
88
+ - uses: actions/setup-node@v4
89
+ with:
90
+ node-version: 24
91
+ cache: pnpm
92
+
93
+ - run: pnpm install --frozen-lockfile
94
+
95
+ - run: pnpm db:generate
96
+
97
+ - run: pnpm build
@@ -0,0 +1,22 @@
1
+ name: Auto-merge Dependabot
2
+
3
+ on: pull_request
4
+
5
+ permissions:
6
+ contents: write
7
+ pull-requests: write
8
+
9
+ jobs:
10
+ auto-merge:
11
+ if: github.actor == 'dependabot[bot]'
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: dependabot/fetch-metadata@v2
15
+ id: meta
16
+
17
+ - name: Auto-merge patch updates
18
+ if: steps.meta.outputs.update-type == 'version-update:semver-patch'
19
+ run: gh pr merge "$PR_URL" --auto --squash
20
+ env:
21
+ PR_URL: ${{ github.event.pull_request.html_url }}
22
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ notes:
7
+ description: "Release notes (optional — auto-generated if blank)"
8
+ required: false
9
+
10
+ permissions:
11
+ contents: write
12
+
13
+ jobs:
14
+ release:
15
+ name: Tag & Release
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Generate date-based tag
23
+ id: tag
24
+ run: |
25
+ BASE="v$(date +%Y.%m.%d)"
26
+ EXISTING=$(git tag -l "${BASE}*" | wc -l | tr -d ' ')
27
+ if [ "$EXISTING" -eq "0" ]; then
28
+ echo "tag=${BASE}" >> "$GITHUB_OUTPUT"
29
+ else
30
+ echo "tag=${BASE}.${EXISTING}" >> "$GITHUB_OUTPUT"
31
+ fi
32
+
33
+ - name: Create GitHub Release
34
+ uses: softprops/action-gh-release@v2
35
+ with:
36
+ tag_name: ${{ steps.tag.outputs.tag }}
37
+ generate_release_notes: ${{ github.event.inputs.notes == '' }}
38
+ body: ${{ github.event.inputs.notes }}
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
3
+ "extends": ["../../biome.json"],
4
+ "files": {
5
+ "includes": ["*.{js,ts,mjs,cjs,json}", "src/**", "scripts/**"]
6
+ }
7
+ }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "baseUrl": ".",
3
+ "baseUrl": "./src",
4
4
  "paths": {
5
- "@/*": ["./src/*"]
6
- }
5
+ "@/*": ["./*"]
6
+ },
7
+ "jsx": "preserve"
7
8
  },
8
- "include": ["next.env.d.ts", "**/*.js", "**/*.jsx"],
9
- "exclude": ["node_modules"]
9
+ "include": ["next.config.js"]
10
10
  }
@@ -1,6 +1,95 @@
1
1
  /** @type {import('next').NextConfig} */
2
2
  const nextConfig = {
3
- // No TypeScript configuration needed for JS-only projects
3
+ // Required for Railway deployment produces a self-contained build
4
+ // at .next/standalone that can run without node_modules.
5
+ output: "standalone",
6
+
7
+ // Support workspace package resolution (including @techstream/quark-db which uses
8
+ // the Prisma driver-adapter pattern — pure JS, no native engine binary)
9
+ transpilePackages: [
10
+ "@techstream/quark-core",
11
+ "@techstream/quark-db",
12
+ "@techstream/quark-ui",
13
+ "@techstream/quark-jobs",
14
+ ],
15
+
16
+ // Security headers
17
+ // NOTE: These are also applied by proxy.js for proxy-matched routes.
18
+ // Keeping them here as a fallback for routes the proxy doesn't match.
19
+ async headers() {
20
+ return [
21
+ {
22
+ source: "/:path*",
23
+ headers: [
24
+ {
25
+ key: "X-DNS-Prefetch-Control",
26
+ value: "on",
27
+ },
28
+ {
29
+ key: "X-Frame-Options",
30
+ value: "SAMEORIGIN",
31
+ },
32
+ {
33
+ key: "X-Content-Type-Options",
34
+ value: "nosniff",
35
+ },
36
+ {
37
+ key: "Referrer-Policy",
38
+ value: "strict-origin-when-cross-origin",
39
+ },
40
+ {
41
+ key: "Permissions-Policy",
42
+ value: "camera=(), microphone=(), geolocation=()",
43
+ },
44
+ {
45
+ key: "Content-Security-Policy",
46
+ value:
47
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';",
48
+ },
49
+ ],
50
+ },
51
+ ];
52
+ },
53
+
54
+ // Environment variables validation
55
+ env: {
56
+ APP_URL: process.env.APP_URL,
57
+ NEXTAUTH_URL: process.env.NEXTAUTH_URL || process.env.APP_URL,
58
+ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
59
+ },
60
+
61
+ // Request body size limits (security)
62
+ experimental: {
63
+ // Limit request body size to prevent DoS attacks
64
+ // Default is 4MB, we're being explicit here
65
+ // Adjust based on your needs (e.g., larger for file uploads)
66
+ serverActions: {
67
+ bodySizeLimit: "2mb", // For Server Actions
68
+ },
69
+ },
70
+
71
+ // API route configuration
72
+ async rewrites() {
73
+ return [];
74
+ },
75
+
76
+ // Compiler options for production optimization
77
+ compiler: {
78
+ removeConsole:
79
+ process.env.NODE_ENV === "production"
80
+ ? { exclude: ["error", "warn"] }
81
+ : false,
82
+ },
83
+
84
+ // Image optimization configuration
85
+ images: {
86
+ domains: [],
87
+ formats: ["image/avif", "image/webp"],
88
+ },
89
+
90
+ // Production-only settings
91
+ poweredByHeader: false,
92
+ compress: true,
4
93
  };
5
94
 
6
95
  export default nextConfig;
@@ -16,18 +16,18 @@
16
16
  "@techstream/quark-db": "workspace:*",
17
17
  "@techstream/quark-jobs": "workspace:*",
18
18
  "@techstream/quark-ui": "workspace:*",
19
- "@prisma/client": "^7.3.0",
19
+ "@prisma/client": "^7.4.0",
20
20
  "next": "16.1.6",
21
21
  "next-auth": "5.0.0-beta.30",
22
22
  "pg": "^8.18.0",
23
- "react": "19.2.0",
24
- "react-dom": "19.2.0",
23
+ "react": "19.2.4",
24
+ "react-dom": "19.2.4",
25
25
  "zod": "^4.3.6"
26
26
  },
27
27
  "devDependencies": {
28
- "@techstream/quark-config": "workspace:*",
29
- "@tailwindcss/postcss": "^4",
30
- "@types/node": "^20",
31
- "tailwindcss": "^4"
28
+ "@tailwindcss/postcss": "^4.2.0",
29
+ "@types/node": "^25.2.3",
30
+ "tailwindcss": "^4.2.0",
31
+ "@techstream/quark-config": "workspace:*"
32
32
  }
33
33
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://railway.com/railway.schema.json",
3
+ "build": {
4
+ "builder": "RAILPACK",
5
+ "buildCommand": "pnpm install --frozen-lockfile && pnpm db:generate && pnpm build",
6
+ "watchPatterns": ["apps/web/**", "packages/**"]
7
+ },
8
+ "deploy": {
9
+ "startCommand": "node apps/web/.next/standalone/server.js",
10
+ "healthcheckPath": "/api/health",
11
+ "healthcheckTimeout": 30,
12
+ "restartPolicyType": "ON_FAILURE",
13
+ "restartPolicyMaxRetries": 5
14
+ }
15
+ }
@@ -35,20 +35,19 @@ export const POST = withCsrfProtection(async (request) => {
35
35
  password: hashedPassword,
36
36
  });
37
37
 
38
- // Enqueue welcome email (fire-and-forget)
38
+ // Don't return the password
39
+ const { password: _, ...safeUser } = newUser;
40
+
41
+ // Enqueue welcome email (fire-and-forget — don't block the response)
39
42
  try {
40
43
  const emailQueue = createQueue(JOB_QUEUES.EMAIL);
41
44
  await emailQueue.add(JOB_NAMES.SEND_WELCOME_EMAIL, {
42
45
  userId: newUser.id,
43
46
  });
44
- } catch (emailError) {
45
- // Don't fail registration if email enqueue fails
46
- console.error("Failed to enqueue welcome email:", emailError);
47
+ } catch {
48
+ // Non-critical user is created even if email fails to enqueue
47
49
  }
48
50
 
49
- // Don't return the password
50
- const { password: _, ...safeUser } = newUser;
51
-
52
51
  return NextResponse.json(safeUser, { status: 201 });
53
52
  } catch (error) {
54
53
  return handleError(error);
@@ -1,7 +1,6 @@
1
- export const metadata = {
2
- title: "Quark",
3
- description: "A modern monorepo with Next.js, React, and Prisma",
4
- };
1
+ import { getSiteMetadata } from "../lib/seo/site-metadata.js";
2
+
3
+ export const metadata = getSiteMetadata();
5
4
 
6
5
  export default function RootLayout({ children }) {
7
6
  return (
@@ -0,0 +1,12 @@
1
+ import { config, getAppUrl } from "@techstream/quark-config";
2
+
3
+ export default function manifest() {
4
+ return {
5
+ name: config.appName,
6
+ short_name: config.appName,
7
+ description: config.appDescription,
8
+ start_url: "/",
9
+ display: "standalone",
10
+ id: getAppUrl(),
11
+ };
12
+ }
@@ -0,0 +1,21 @@
1
+ import { getAppUrl } from "@techstream/quark-config";
2
+ import { isWebsiteIndexable } from "../lib/seo/indexing.js";
3
+
4
+ export default function robots() {
5
+ const appUrl = getAppUrl();
6
+ const indexable = isWebsiteIndexable();
7
+
8
+ return {
9
+ rules: indexable
10
+ ? {
11
+ userAgent: "*",
12
+ allow: "/",
13
+ disallow: ["/api/"],
14
+ }
15
+ : {
16
+ userAgent: "*",
17
+ disallow: "/",
18
+ },
19
+ sitemap: indexable ? `${appUrl}/sitemap.xml` : undefined,
20
+ };
21
+ }
@@ -0,0 +1,20 @@
1
+ import { getAppUrl } from "@techstream/quark-config";
2
+ import { isWebsiteIndexable } from "../lib/seo/indexing.js";
3
+
4
+ const STATIC_ROUTES = [{ path: "/", changeFrequency: "daily", priority: 1 }];
5
+
6
+ export default function sitemap() {
7
+ if (!isWebsiteIndexable()) {
8
+ return [];
9
+ }
10
+
11
+ const appUrl = getAppUrl();
12
+ const lastModified = new Date();
13
+
14
+ return STATIC_ROUTES.map((route) => ({
15
+ url: `${appUrl}${route.path}`,
16
+ lastModified,
17
+ changeFrequency: route.changeFrequency,
18
+ priority: route.priority,
19
+ }));
20
+ }
@@ -0,0 +1,23 @@
1
+ export function isWebsiteIndexable(env = process.env) {
2
+ return (env.NODE_ENV || "").toLowerCase() === "production";
3
+ }
4
+
5
+ export function getMetadataRobots(env = process.env) {
6
+ if (isWebsiteIndexable(env)) {
7
+ return {
8
+ index: true,
9
+ follow: true,
10
+ };
11
+ }
12
+
13
+ return {
14
+ index: false,
15
+ follow: false,
16
+ nocache: true,
17
+ googleBot: {
18
+ index: false,
19
+ follow: false,
20
+ noimageindex: true,
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,33 @@
1
+ import { config, getAppUrl } from "@techstream/quark-config";
2
+ import { getMetadataRobots } from "./indexing.js";
3
+
4
+ const appUrl = getAppUrl();
5
+ const { appName, appDescription } = config;
6
+
7
+ export function getSiteMetadata() {
8
+ return {
9
+ metadataBase: new URL(appUrl),
10
+ title: {
11
+ default: appName,
12
+ template: `%s | ${appName}`,
13
+ },
14
+ description: appDescription,
15
+ applicationName: appName,
16
+ alternates: {
17
+ canonical: "/",
18
+ },
19
+ openGraph: {
20
+ type: "website",
21
+ url: appUrl,
22
+ title: appName,
23
+ description: appDescription,
24
+ siteName: appName,
25
+ },
26
+ twitter: {
27
+ card: "summary",
28
+ title: appName,
29
+ description: appDescription,
30
+ },
31
+ robots: getMetadataRobots(),
32
+ };
33
+ }
@@ -151,8 +151,7 @@ export function proxy(request) {
151
151
 
152
152
  // Apply rate limiting to API routes only
153
153
  if (pathname.startsWith("/api/")) {
154
- const ip =
155
- request.ip || request.headers.get("x-forwarded-for") || "unknown";
154
+ const ip = request.ip || "unknown";
156
155
  const rateLimitResult = checkRateLimit(ip, pathname);
157
156
 
158
157
  if (rateLimitResult.limited) {