@techstream/quark-create-app 1.8.0 → 1.10.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 (80) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -3
  3. package/src/index.js +415 -150
  4. package/src/utils.js +36 -0
  5. package/src/utils.test.js +63 -0
  6. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  7. package/templates/base-project/.github/copilot-instructions.md +55 -0
  8. package/templates/base-project/.github/workflows/release.yml +37 -8
  9. package/templates/base-project/CLAUDE.md +273 -0
  10. package/templates/base-project/README.md +72 -30
  11. package/templates/base-project/apps/web/next.config.js +5 -1
  12. package/templates/base-project/apps/web/package.json +7 -5
  13. package/templates/base-project/apps/web/public/quark.svg +46 -0
  14. package/templates/base-project/apps/web/railway.json +2 -2
  15. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  16. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  17. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  18. package/templates/base-project/apps/web/src/app/api/health/route.js +56 -17
  19. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  20. package/templates/base-project/apps/web/src/app/global-error.js +53 -0
  21. package/templates/base-project/apps/web/src/app/globals.css +121 -15
  22. package/templates/base-project/apps/web/src/app/icon.svg +46 -0
  23. package/templates/base-project/apps/web/src/app/layout.js +1 -0
  24. package/templates/base-project/apps/web/src/app/not-found.js +35 -0
  25. package/templates/base-project/apps/web/src/app/page.js +38 -5
  26. package/templates/base-project/apps/web/src/lib/theme.js +23 -0
  27. package/templates/base-project/apps/web/src/proxy.js +10 -2
  28. package/templates/base-project/package.json +16 -1
  29. package/templates/base-project/packages/db/package.json +4 -4
  30. package/templates/base-project/packages/db/src/client.js +6 -1
  31. package/templates/base-project/packages/db/src/index.js +1 -0
  32. package/templates/base-project/packages/db/src/ping.js +66 -0
  33. package/templates/base-project/scripts/doctor.js +261 -0
  34. package/templates/base-project/turbo.json +2 -1
  35. package/templates/config/package.json +1 -0
  36. package/templates/config/src/index.js +1 -3
  37. package/templates/config/src/validate-env.js +79 -3
  38. package/templates/jobs/package.json +2 -1
  39. package/templates/ui/README.md +67 -0
  40. package/templates/ui/package.json +1 -0
  41. package/templates/ui/src/badge.js +32 -0
  42. package/templates/ui/src/badge.test.js +42 -0
  43. package/templates/ui/src/button.js +64 -15
  44. package/templates/ui/src/button.test.js +34 -5
  45. package/templates/ui/src/card.js +58 -0
  46. package/templates/ui/src/card.test.js +59 -0
  47. package/templates/ui/src/checkbox.js +35 -0
  48. package/templates/ui/src/checkbox.test.js +35 -0
  49. package/templates/ui/src/dialog.js +139 -0
  50. package/templates/ui/src/dialog.test.js +15 -0
  51. package/templates/ui/src/index.js +16 -0
  52. package/templates/ui/src/input.js +15 -0
  53. package/templates/ui/src/input.test.js +27 -0
  54. package/templates/ui/src/label.js +14 -0
  55. package/templates/ui/src/label.test.js +22 -0
  56. package/templates/ui/src/select.js +42 -0
  57. package/templates/ui/src/select.test.js +27 -0
  58. package/templates/ui/src/skeleton.js +14 -0
  59. package/templates/ui/src/skeleton.test.js +22 -0
  60. package/templates/ui/src/table.js +75 -0
  61. package/templates/ui/src/table.test.js +69 -0
  62. package/templates/ui/src/textarea.js +15 -0
  63. package/templates/ui/src/textarea.test.js +27 -0
  64. package/templates/ui/src/theme-constants.js +24 -0
  65. package/templates/ui/src/theme.js +132 -0
  66. package/templates/ui/src/toast.js +229 -0
  67. package/templates/ui/src/toast.test.js +23 -0
  68. package/templates/{base-project/apps/worker → worker}/package.json +2 -2
  69. package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
  70. package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
  71. package/templates/base-project/apps/web/public/file.svg +0 -1
  72. package/templates/base-project/apps/web/public/globe.svg +0 -1
  73. package/templates/base-project/apps/web/public/next.svg +0 -1
  74. package/templates/base-project/apps/web/public/vercel.svg +0 -1
  75. package/templates/base-project/apps/web/public/window.svg +0 -1
  76. /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
  77. /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
  78. /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
  79. /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
  80. /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
package/src/utils.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared utilities for @techstream/quark-create-app
3
+ */
4
+
5
+ /**
6
+ * Format a project slug into a human-friendly application name.
7
+ *
8
+ * - Hyphens, underscores, and dots are treated as word separators
9
+ * - Each word is title-cased
10
+ * - Degenerate inputs (all separators) fall back to "Quark App"
11
+ *
12
+ * @param {string} projectName - Raw project slug (e.g. "my-cool-app")
13
+ * @returns {string} Title-cased display name (e.g. "My Cool App")
14
+ *
15
+ * @example
16
+ * formatProjectDisplayName("my-cool-app") // "My Cool App"
17
+ * formatProjectDisplayName("my.app") // "My App"
18
+ * formatProjectDisplayName("my_app_v2") // "My App V2"
19
+ * formatProjectDisplayName("myapp") // "Myapp"
20
+ * formatProjectDisplayName("---") // "Quark App"
21
+ */
22
+ export function formatProjectDisplayName(projectName) {
23
+ const normalized = projectName
24
+ .replace(/[-_.]+/g, " ")
25
+ .replace(/\s+/g, " ")
26
+ .trim();
27
+
28
+ if (!normalized) {
29
+ return "Quark App";
30
+ }
31
+
32
+ return normalized
33
+ .split(" ")
34
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
35
+ .join(" ");
36
+ }
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { formatProjectDisplayName } from "./utils.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Happy path — common slug patterns
7
+ // ---------------------------------------------------------------------------
8
+
9
+ test("formatProjectDisplayName - hyphen-separated slug", () => {
10
+ assert.strictEqual(formatProjectDisplayName("my-cool-app"), "My Cool App");
11
+ });
12
+
13
+ test("formatProjectDisplayName - underscore-separated slug", () => {
14
+ assert.strictEqual(formatProjectDisplayName("my_app_v2"), "My App V2");
15
+ });
16
+
17
+ test("formatProjectDisplayName - dot-separated slug", () => {
18
+ assert.strictEqual(formatProjectDisplayName("my.app"), "My App");
19
+ });
20
+
21
+ test("formatProjectDisplayName - single word (no separators)", () => {
22
+ assert.strictEqual(formatProjectDisplayName("myapp"), "Myapp");
23
+ });
24
+
25
+ test("formatProjectDisplayName - already title-cased input", () => {
26
+ // slice(1) preserves remaining chars as-is — only the first char is forced uppercase
27
+ assert.strictEqual(formatProjectDisplayName("MyApp"), "MyApp");
28
+ });
29
+
30
+ test("formatProjectDisplayName - mixed separators", () => {
31
+ assert.strictEqual(formatProjectDisplayName("my-app_v2.0"), "My App V2 0");
32
+ });
33
+
34
+ test("formatProjectDisplayName - consecutive separators collapse to single space", () => {
35
+ assert.strictEqual(formatProjectDisplayName("my--app"), "My App");
36
+ assert.strictEqual(formatProjectDisplayName("my___app"), "My App");
37
+ assert.strictEqual(formatProjectDisplayName("my-._app"), "My App");
38
+ });
39
+
40
+ test("formatProjectDisplayName - numbers preserved in words", () => {
41
+ assert.strictEqual(formatProjectDisplayName("app-v2"), "App V2");
42
+ assert.strictEqual(formatProjectDisplayName("project-123"), "Project 123");
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Edge cases — degenerate / boundary inputs
47
+ // ---------------------------------------------------------------------------
48
+
49
+ test("formatProjectDisplayName - all separators returns fallback", () => {
50
+ assert.strictEqual(formatProjectDisplayName("---"), "Quark App");
51
+ assert.strictEqual(formatProjectDisplayName("___"), "Quark App");
52
+ assert.strictEqual(formatProjectDisplayName("..."), "Quark App");
53
+ assert.strictEqual(formatProjectDisplayName("-._-"), "Quark App");
54
+ });
55
+
56
+ test("formatProjectDisplayName - preserves casing of rest of word", () => {
57
+ // The function only forces the first char uppercase; the remainder is untouched
58
+ assert.strictEqual(formatProjectDisplayName("myApp"), "MyApp");
59
+ });
60
+
61
+ test("formatProjectDisplayName - short single-char name", () => {
62
+ assert.strictEqual(formatProjectDisplayName("a"), "A");
63
+ });
@@ -0,0 +1,172 @@
1
+ ---
2
+ description: Quark project conventions for __QUARK_PROJECT_NAME__. Covers all layers: UI, backend, database, auth, jobs, testing, and deployment.
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Quark Project: __QUARK_PROJECT_NAME__
7
+
8
+ Package scope: `@__QUARK_SCOPE__` — use this for ALL workspace packages.
9
+ Scaffolded: __QUARK_SCAFFOLD_DATE__
10
+
11
+ ---
12
+
13
+ ## Non-Negotiable Rules
14
+
15
+ 1. **ESM only.** Always `import`/`export`. Never `require()` or `module.exports`.
16
+ 2. **No TypeScript.** Plain `.js` and `.jsx` files only — no `.ts`, `.tsx`, or type annotations.
17
+ 3. **Validate everything at boundaries.** Server Actions and API routes must use Zod. No raw `req.body` access without parsing.
18
+ 4. **Use AppError — not Error.** Throw `AppError` / `ValidationError` / `NotFoundError` from `@techstream/quark-core/errors`. Never `throw new Error()` in application code.
19
+ 5. **Log — never console.** Use `createLogger(name)` from `@techstream/quark-core`. Zero `console.log` / `console.error` in non-test code.
20
+ 6. **DB models always have timestamps.** Every Prisma model: `createdAt DateTime @default(now())` + `updatedAt DateTime @updatedAt`.
21
+ 7. **Workspace import scope.** Use `@__QUARK_SCOPE__/*` for local packages. Never `@techstream/quark-db`, `@techstream/quark-config`, etc.
22
+ 8. **Co-located tests.** `feature.test.js` lives next to `feature.js`. Tests use `node --test` — no Jest, no Vitest.
23
+
24
+ ---
25
+
26
+ ## Import Reference
27
+
28
+ ```javascript
29
+ // ✅ Workspace packages — always @__QUARK_SCOPE__/*:
30
+ import { prisma, user, post } from "@__QUARK_SCOPE__/db";
31
+ import { loadConfig } from "@__QUARK_SCOPE__/config";
32
+ import { Button, Card, Input } from "@__QUARK_SCOPE__/ui";
33
+ import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
34
+
35
+ // ✅ Published runtime — @techstream/quark-core:
36
+ import { AppError, ValidationError, NotFoundError } from "@techstream/quark-core/errors";
37
+ import { createLogger, getCurrentSession } from "@techstream/quark-core";
38
+ import { createQueue, addJob } from "@techstream/quark-core";
39
+ import { metrics, cache, storage } from "@techstream/quark-core";
40
+
41
+ // ❌ Never:
42
+ import { Button } from "@/components/ui/button"; // wrong path convention
43
+ import { prisma } from "@techstream/quark-db"; // wrong scope for workspace pkg
44
+ import something from "@techstream/quark-config"; // wrong scope for workspace pkg
45
+ ```
46
+
47
+ ---
48
+
49
+ ## UI & Design System
50
+
51
+ - Styling: **Tailwind CSS utility classes only**. No inline styles, no CSS modules.
52
+ - Components from `@__QUARK_SCOPE__/ui`. All accept `className` for overrides.
53
+ - Available: `Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox`, `Badge`, `Card`/`CardHeader`/`CardTitle`/`CardContent`/`CardFooter`, `Table`/`TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell`, `Skeleton`, `Dialog` *(client)*, `Toast`/`useToast` *(client)*.
54
+ - Loading states → `<Skeleton>` + `<Suspense>` boundaries.
55
+ - User feedback → `useToast()` (mark component `"use client"`).
56
+ - Modals → `<Dialog>` (mark parent `"use client"`).
57
+ - Default to **Server Components**. Only add `"use client"` when necessary.
58
+
59
+ ---
60
+
61
+ ## Server Action Pattern
62
+
63
+ ```javascript
64
+ "use server";
65
+ import { z } from "zod";
66
+ import { ValidationError, AppError } from "@techstream/quark-core/errors";
67
+ import { getCurrentSession, createLogger } from "@techstream/quark-core";
68
+ import { prisma } from "@__QUARK_SCOPE__/db";
69
+
70
+ const log = createLogger("action:example");
71
+ const schema = z.object({ title: z.string().min(1).max(255) });
72
+
73
+ export async function createExample(formData) {
74
+ const session = await getCurrentSession();
75
+ if (!session) throw new AppError("Unauthorized", 401);
76
+
77
+ const parsed = schema.safeParse(Object.fromEntries(formData));
78
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
79
+
80
+ const result = await prisma.example.create({ data: parsed.data });
81
+ log.info("created", { id: result.id });
82
+ return result;
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## API Route Pattern
89
+
90
+ ```javascript
91
+ import { NextResponse } from "next/server";
92
+ import { AppError } from "@techstream/quark-core/errors";
93
+ import { createLogger } from "@techstream/quark-core";
94
+
95
+ const log = createLogger("api:example");
96
+
97
+ export async function GET(request) {
98
+ try {
99
+ return NextResponse.json({ data });
100
+ } catch (error) {
101
+ log.error("request failed", { error });
102
+ if (error instanceof AppError)
103
+ return NextResponse.json({ error: error.message }, { status: error.statusCode });
104
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
105
+ }
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Database
112
+
113
+ - Schema: `packages/db/prisma/schema.prisma`
114
+ - Query helpers: `packages/db/src/queries.js` — add functions here, don't call `prisma.*` directly in components or actions.
115
+ - Always run `pnpm db:generate` after schema changes, then `pnpm db:migrate`.
116
+
117
+ ---
118
+
119
+ ## Auth
120
+
121
+ ```javascript
122
+ import { getCurrentSession } from "@techstream/quark-core";
123
+
124
+ const session = await getCurrentSession(); // null if unauthenticated
125
+ if (!session) redirect("/login"); // Server Component
126
+ if (!session) throw new AppError("Unauthorized", 401); // Server Action / API
127
+ if (session.user.role !== "ADMIN") throw new AppError("Forbidden", 403);
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Background Jobs
133
+
134
+ ```javascript
135
+ // Dispatch (from Server Action or API route):
136
+ import { createQueue, addJob } from "@techstream/quark-core";
137
+ import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
138
+ const queue = createQueue("default");
139
+ await addJob(queue, JOB_TYPES.MY_JOB, { userId });
140
+
141
+ // Handle (apps/worker/src/handlers/):
142
+ import { createLogger } from "@techstream/quark-core";
143
+ const log = createLogger("job:my-job");
144
+ export async function handleMyJob({ data }) { log.info("processing", data); }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Testing
150
+
151
+ ```javascript
152
+ import { test } from "node:test";
153
+ import assert from "node:assert";
154
+
155
+ test("feature", async (t) => {
156
+ await t.test("does X", async () => {
157
+ assert.strictEqual(actual, expected);
158
+ });
159
+ });
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Environment
165
+
166
+ New env vars must be registered in `packages/config/src/validate-env.js` before use. Load config via `loadConfig()` from `@__QUARK_SCOPE__/config`.
167
+
168
+ ---
169
+
170
+ ## Deployment
171
+
172
+ Railway: two services (`web` + `worker`). Migrations auto-run on deploy via `releaseCommand` in `apps/web/railway.json`. Env vars managed in Railway dashboard — never committed.
@@ -5,3 +5,58 @@
5
5
  <file>.github/skills/project-context/SKILL.md</file>
6
6
  </skill>
7
7
  </skills>
8
+
9
+ # __QUARK_PROJECT_NAME__ — Project Conventions
10
+
11
+ > Scaffolded with Quark on __QUARK_SCAFFOLD_DATE__. Package scope: `@__QUARK_SCOPE__`
12
+ > Also see `CLAUDE.md` at the project root for the full context reference.
13
+
14
+ ## Non-Negotiable Rules
15
+
16
+ - **ESM only** — `import`/`export` everywhere. Never `require()`.
17
+ - **No TypeScript** — `.js` and `.jsx` only.
18
+ - **Validate at every boundary** — Zod schemas on all Server Actions and API routes.
19
+ - **Errors** — `AppError` / `ValidationError` from `@techstream/quark-core/errors`. Never `throw new Error()`.
20
+ - **Logging** — `createLogger(name)` from `@techstream/quark-core`. No `console.log` in production code.
21
+ - **DB models** — Always add `createdAt` and `updatedAt` to every Prisma model.
22
+ - **Workspace imports** — Use `@__QUARK_SCOPE__/*` for local packages (`db`, `config`, `ui`, `jobs`). Never `@techstream/quark-db` etc.
23
+ - **Tests** — Co-located `*.test.js`, run with `node --test`.
24
+
25
+ ## Import Patterns
26
+
27
+ ```javascript
28
+ // Workspace packages:
29
+ import { prisma, user } from "@__QUARK_SCOPE__/db";
30
+ import { loadConfig } from "@__QUARK_SCOPE__/config";
31
+ import { Button, Card, Input } from "@__QUARK_SCOPE__/ui";
32
+ import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
33
+
34
+ // Published runtime:
35
+ import { AppError, ValidationError } from "@techstream/quark-core/errors";
36
+ import { createLogger, getCurrentSession, createQueue, addJob } from "@techstream/quark-core";
37
+ ```
38
+
39
+ ## UI & Design System
40
+
41
+ Components from `@__QUARK_SCOPE__/ui` — Tailwind-only, Server Component safe:
42
+ `Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox`, `Badge`,
43
+ `Card`/`CardHeader`/`CardTitle`/`CardContent`/`CardFooter`,
44
+ `Table`/`TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell`,
45
+ `Skeleton`, `Dialog` *(client)*, `Toast`/`useToast` *(client)*.
46
+
47
+ All accept `className`. Never import from `@/components/ui/*`.
48
+
49
+ ## Standard Patterns
50
+
51
+ ```javascript
52
+ // Server Action:
53
+ "use server";
54
+ const parsed = schema.safeParse(Object.fromEntries(formData));
55
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
56
+ const session = await getCurrentSession();
57
+ if (!session) throw new AppError("Unauthorized", 401);
58
+
59
+ // New env var: register in packages/config/src/validate-env.js first.
60
+ // New DB model: schema.prisma → pnpm db:generate → pnpm db:migrate.
61
+ // New job: define type in packages/jobs/src/index.js, handler in apps/worker/src/handlers/.
62
+ ```
@@ -3,9 +3,13 @@ name: Release
3
3
  on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
- notes:
7
- description: "Release notes (optional auto-generated if blank)"
6
+ prerelease:
7
+ description: "Tag as pre-release (staging-only, skips production deploy)"
8
8
  required: false
9
+ default: false
10
+ type: boolean
11
+
12
+ concurrency: ${{ github.workflow }}
9
13
 
10
14
  permissions:
11
15
  contents: write
@@ -17,22 +21,47 @@ jobs:
17
21
  steps:
18
22
  - uses: actions/checkout@v4
19
23
  with:
24
+ ref: main
20
25
  fetch-depth: 0
21
26
 
22
- - name: Generate date-based tag
27
+ - name: Configure git
28
+ run: |
29
+ git config user.name "github-actions[bot]"
30
+ git config user.email "github-actions[bot]@users.noreply.github.com"
31
+
32
+ - name: Push to production branch
33
+ if: ${{ !inputs.prerelease }}
34
+ run: |
35
+ git fetch origin
36
+ if git ls-remote --exit-code --heads origin production > /dev/null; then
37
+ git checkout production && git merge --ff-only origin/main
38
+ else
39
+ git checkout -b production origin/main
40
+ fi
41
+ git push origin production
42
+ git checkout main
43
+
44
+ - name: Generate tag
23
45
  id: tag
24
46
  run: |
25
47
  BASE="v$(date +%Y.%m.%d)"
26
- EXISTING=$(git tag -l "${BASE}*" | wc -l | tr -d ' ')
48
+ SUFFIX="${{ inputs.prerelease && '-rc' || '' }}"
49
+ # Count only exact-format matches to avoid rc tags polluting production counts
50
+ EXISTING=$(git tag -l | grep -cE "^${BASE}${SUFFIX}(\.[0-9]+)?$" || true)
27
51
  if [ "$EXISTING" -eq "0" ]; then
28
- echo "tag=${BASE}" >> "$GITHUB_OUTPUT"
52
+ echo "tag=${BASE}${SUFFIX}" >> "$GITHUB_OUTPUT"
29
53
  else
30
- echo "tag=${BASE}.${EXISTING}" >> "$GITHUB_OUTPUT"
54
+ echo "tag=${BASE}${SUFFIX}.${EXISTING}" >> "$GITHUB_OUTPUT"
31
55
  fi
32
56
 
57
+ - name: Tag release
58
+ run: |
59
+ git tag -a "${{ steps.tag.outputs.tag }}" -m "Release ${{ steps.tag.outputs.tag }}"
60
+ git push origin "${{ steps.tag.outputs.tag }}"
61
+
33
62
  - name: Create GitHub Release
34
63
  uses: softprops/action-gh-release@v2
35
64
  with:
36
65
  tag_name: ${{ steps.tag.outputs.tag }}
37
- generate_release_notes: ${{ github.event.inputs.notes == '' }}
38
- body: ${{ github.event.inputs.notes }}
66
+ generate_release_notes: true
67
+ prerelease: ${{ inputs.prerelease }}
@@ -0,0 +1,273 @@
1
+ # __QUARK_PROJECT_NAME__ — AI Context
2
+
3
+ > Scaffolded with [Quark](https://github.com/Bobnoddle/quark) on __QUARK_SCAFFOLD_DATE__.
4
+ > **Keep this file updated** as your project grows — it's what Claude Code, Cursor, and other AI tools read first.
5
+
6
+ ## Quick Start
7
+
8
+ ```bash
9
+ docker compose up -d # Start PostgreSQL, Redis, Mailpit
10
+ pnpm install # Install dependencies
11
+ pnpm db:generate # Generate Prisma client
12
+ pnpm db:migrate # Apply database migrations
13
+ pnpm db:seed # Seed development data
14
+ pnpm dev # Start web + worker
15
+ ```
16
+
17
+ ## Scaffolding Another Project (non-interactive)
18
+
19
+ ```bash
20
+ # With default features (ui + jobs):
21
+ npx @techstream/quark-create-app my-app --no-prompts
22
+
23
+ # With specific features:
24
+ npx @techstream/quark-create-app my-app --no-prompts --features ui,jobs
25
+ npx @techstream/quark-create-app my-app --no-prompts --features ui,jobs,admin
26
+ npx @techstream/quark-create-app my-app --no-prompts --features "" # minimal (db + config only)
27
+ ```
28
+
29
+ ## Project Structure
30
+
31
+ ```
32
+ __QUARK_PROJECT_NAME__/
33
+ ├── apps/
34
+ │ ├── web/ # Next.js 16 (App Router, Server Actions)
35
+ │ └── worker/ # BullMQ background worker
36
+ ├── packages/
37
+ │ ├── db/ # Prisma schema, client, query functions
38
+ │ ├── config/ # Environment validation & shared config
39
+ __QUARK_OPTIONAL_PACKAGES__├── docker-compose.yml
40
+ ├── .env # Local environment secrets (never commit)
41
+ └── .env.example # Template — copy to .env to get started
42
+ ```
43
+
44
+ ## Tech Stack
45
+
46
+ | Layer | Technology |
47
+ |---|---|
48
+ | Framework | Next.js 16 (App Router) |
49
+ | Language | JavaScript — ESM only, no TypeScript |
50
+ | Database | PostgreSQL 16 + Prisma 7 |
51
+ | Queue | BullMQ + Redis 7 |
52
+ | Auth | NextAuth v5 |
53
+ | Validation | Zod 4 |
54
+ | UI | Tailwind CSS + `@__QUARK_SCOPE__/ui` components |
55
+ | Email | Nodemailer (Mailpit local / Resend or Zeptomail prod) |
56
+ | Storage | Local filesystem or S3/R2 (pluggable via `STORAGE_PROVIDER`) |
57
+ | Logging | Structured logger via `createLogger()` from `@techstream/quark-core` |
58
+ | Metrics | Prometheus via `metrics` singleton from `@techstream/quark-core` |
59
+ | Testing | Node.js built-in `node --test` |
60
+ | Linting | Biome |
61
+ | Monorepo | Turborepo + pnpm workspaces |
62
+ | Deployment | Railway (web service + worker service) |
63
+
64
+ ## Package Imports
65
+
66
+ ```javascript
67
+ // Published (installed from npm) — use @techstream/ prefix:
68
+ import { AppError, ValidationError, NotFoundError, UnauthorizedError } from "@techstream/quark-core/errors";
69
+ import { createLogger } from "@techstream/quark-core";
70
+ import { getCurrentSession } from "@techstream/quark-core";
71
+ import { createQueue, addJob } from "@techstream/quark-core";
72
+ import { metrics } from "@techstream/quark-core";
73
+ import { cache } from "@techstream/quark-core";
74
+ import { rateLimit } from "@techstream/quark-core";
75
+ import { storage } from "@techstream/quark-core";
76
+
77
+ // Workspace packages — ALWAYS use @__QUARK_SCOPE__/* (never @techstream/quark-*):
78
+ import { prisma, user } from "@__QUARK_SCOPE__/db"; // add more as you create query helpers
79
+ import { loadConfig } from "@__QUARK_SCOPE__/config";
80
+ import { Button, Card, Input } from "@__QUARK_SCOPE__/ui"; // if ui package selected
81
+ import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs"; // if jobs package selected
82
+ ```
83
+
84
+ ## UI & Design System
85
+
86
+ All UI components come from `@__QUARK_SCOPE__/ui`. They are Tailwind-only, dependency-free, and Server Component-safe.
87
+
88
+ **Available components:**
89
+ - `Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox` — form primitives
90
+ - `Badge` — status labels
91
+ - `Card` / `CardHeader` / `CardTitle` / `CardContent` / `CardFooter` — content containers
92
+ - `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` — data tables
93
+ - `Skeleton` — loading placeholders
94
+ - `Dialog` *(client)* — modal dialogs
95
+ - `Toast` / `useToast` *(client)* — notifications
96
+
97
+ **Rules:**
98
+ - All styling via Tailwind CSS utility classes. No inline styles, no CSS modules.
99
+ - Import from `@__QUARK_SCOPE__/ui` — never from `@/components/ui/*` or direct paths.
100
+ - Every component accepts `className` for Tailwind overrides.
101
+ - Loading states → `<Skeleton>` / `<Suspense>`.
102
+ - User feedback → `useToast()` hook (client component).
103
+ - Modals → `<Dialog>` (mark parent as `"use client"`).
104
+ - Server Components are the default — only add `"use client"` when the component uses hooks, browser APIs, or the marked client components above.
105
+
106
+ ## Server Actions (preferred for all mutations)
107
+
108
+ ```javascript
109
+ "use server";
110
+ import { z } from "zod";
111
+ import { ValidationError, AppError } from "@techstream/quark-core/errors";
112
+ import { getCurrentSession } from "@techstream/quark-core";
113
+ import { createLogger } from "@techstream/quark-core";
114
+ import { prisma } from "@__QUARK_SCOPE__/db";
115
+
116
+ const log = createLogger("action:example");
117
+
118
+ const schema = z.object({
119
+ title: z.string().min(1).max(255),
120
+ body: z.string().optional(),
121
+ });
122
+
123
+ export async function createExample(formData) {
124
+ const session = await getCurrentSession();
125
+ if (!session) throw new AppError("Unauthorized", 401);
126
+
127
+ const parsed = schema.safeParse(Object.fromEntries(formData));
128
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
129
+
130
+ const result = await prisma.example.create({ data: parsed.data });
131
+ log.info("created", { id: result.id });
132
+ return result;
133
+ }
134
+ ```
135
+
136
+ ## API Routes
137
+
138
+ ```javascript
139
+ // apps/web/src/app/api/example/route.js
140
+ import { NextResponse } from "next/server";
141
+ import { AppError } from "@techstream/quark-core/errors";
142
+ import { createLogger } from "@techstream/quark-core";
143
+
144
+ const log = createLogger("api:example");
145
+
146
+ export async function GET(request) {
147
+ try {
148
+ // ... logic
149
+ return NextResponse.json({ data });
150
+ } catch (error) {
151
+ log.error("request failed", { error });
152
+ if (error instanceof AppError) {
153
+ return NextResponse.json({ error: error.message }, { status: error.statusCode });
154
+ }
155
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## Database
161
+
162
+ - Schema: `packages/db/prisma/schema.prisma`
163
+ - Query helpers: `packages/db/src/queries.js`
164
+ - Every model **must** include `createdAt DateTime @default(now())` and `updatedAt DateTime @updatedAt`.
165
+ - After any schema change: `pnpm db:generate` then `pnpm db:migrate`.
166
+
167
+ ```javascript
168
+ // Add query functions in packages/db/src/queries.js:
169
+ export const example = {
170
+ findMany: (args) => prisma.example.findMany(args),
171
+ findById: (id) => prisma.example.findUnique({ where: { id } }),
172
+ create: (data) => prisma.example.create({ data }),
173
+ update: (id, data) => prisma.example.update({ where: { id }, data }),
174
+ delete: (id) => prisma.example.delete({ where: { id } }),
175
+ };
176
+ ```
177
+
178
+ Do not call `prisma.*` directly inside pages or Server Actions — use the query helper functions.
179
+
180
+ ## Auth & Authorization
181
+
182
+ ```javascript
183
+ import { getCurrentSession } from "@techstream/quark-core";
184
+
185
+ // Server Component — check session:
186
+ const session = await getCurrentSession();
187
+ if (!session) redirect("/login");
188
+
189
+ // Server Action — guard:
190
+ const session = await getCurrentSession();
191
+ if (!session) throw new AppError("Unauthorized", 401);
192
+
193
+ // Role check (ADMIN | VIEWER defined in Prisma schema):
194
+ if (session.user.role !== "ADMIN") throw new AppError("Forbidden", 403);
195
+ ```
196
+
197
+ ## Background Jobs (if jobs package selected)
198
+
199
+ ```javascript
200
+ // 1. Dispatch from web (Server Action or API route):
201
+ import { createQueue, addJob } from "@techstream/quark-core";
202
+ import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
203
+ const queue = createQueue("default");
204
+ await addJob(queue, JOB_TYPES.SEND_EMAIL, { userId: session.user.id, template: "welcome" });
205
+
206
+ // 2. Handle in apps/worker/src/handlers/<job-name>.js:
207
+ import { createLogger } from "@techstream/quark-core";
208
+ const log = createLogger("job:send-email");
209
+ export async function handleSendEmail({ data }) {
210
+ log.info("processing", data);
211
+ // ... job logic
212
+ }
213
+ ```
214
+
215
+ ## Environment Variables
216
+
217
+ Validated at startup in `packages/config/src/validate-env.js`. **Always add new env vars there before using them.**
218
+
219
+ Config loaded via `loadConfig()` from `@__QUARK_SCOPE__/config`.
220
+
221
+ Key variables:
222
+ | Variable | Purpose |
223
+ |---|---|
224
+ | `DATABASE_URL` / `POSTGRES_*` | PostgreSQL connection |
225
+ | `REDIS_URL` / `REDIS_HOST` + `REDIS_PORT` | Redis connection |
226
+ | `NEXTAUTH_SECRET` | Auth JWT encryption (`openssl rand -base64 32`) |
227
+ | `APP_URL` | Production domain (auto-derived from `PORT` in dev) |
228
+ | `STORAGE_PROVIDER` | `local` (default) or `s3` |
229
+ | `APP_NAME` / `APP_DESCRIPTION` | Used in metadata, emails, page titles |
230
+
231
+ ## Testing
232
+
233
+ ```javascript
234
+ import { test } from "node:test";
235
+ import assert from "node:assert";
236
+
237
+ test("feature name", async (t) => {
238
+ await t.test("does the thing", async () => {
239
+ // arrange → act → assert
240
+ assert.strictEqual(actual, expected);
241
+ });
242
+ });
243
+ ```
244
+
245
+ - Tests are co-located: `feature.test.js` lives next to `feature.js`.
246
+ - Run all: `pnpm test` | Run one package: `pnpm --filter @__QUARK_SCOPE__/web test`.
247
+ - Test utilities and model factories: `@techstream/quark-core/testing`.
248
+
249
+ ## Deployment
250
+
251
+ Railway with two services: **web** (`apps/web`) and **worker** (`apps/worker`).
252
+
253
+ - Migrations run automatically on every deploy via `releaseCommand` in `apps/web/railway.json`.
254
+ - Environment variables live in Railway dashboard — never in committed files.
255
+ - Staging: auto-deploys on push to `main`. Production: deploy from a version tag.
256
+ - Generate a production seed: `SEED_PROFILE=minimal pnpm db:seed`.
257
+
258
+ ## Key Files
259
+
260
+ | File | Purpose |
261
+ |---|---|
262
+ | `packages/db/prisma/schema.prisma` | Database schema — edit this to add models |
263
+ | `packages/db/src/queries.js` | Database query functions — add helpers here |
264
+ | `packages/config/src/validate-env.js` | Environment variable validation — register new vars here |
265
+ | `apps/web/src/app/` | Next.js pages and API routes |
266
+ | `apps/web/src/lib/auth.js` | NextAuth configuration |
267
+ | `apps/worker/src/handlers/` | Background job handler functions |
268
+ | `packages/ui/src/` | UI component source |
269
+ | `.github/skills/project-context/SKILL.md` | VS Code Copilot skill context |
270
+
271
+ ---
272
+
273
+ *Update this file whenever you make structural changes: new packages, deployment targets, major new conventions, or significant architectural decisions. The AI tools you use daily depend on it being accurate.*