# Filaments.gg Foundation and Data Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Establish the non-visual Filaments.gg MVP foundation: Nuxt runtime, database schema, secure administration primitives, S3 server integration, seed/backfill tooling, and typed public server APIs.

**Architecture:** A single Nuxt 3 application contains a server-only data layer built on Drizzle and Neon Postgres. API routes consume a small catalog service layer rather than accessing database tables from route handlers; public pages and all UI are deliberately deferred. Better Auth is limited to administrator access, and server code creates S3 signed upload URLs after validating each request.

**Tech Stack:** Nuxt 3, Vue 3, TypeScript, Tailwind CSS, shadcn-vue (installed but no UI implemented), Neon Postgres, Drizzle ORM, Better Auth, S3-compatible storage, Vitest, Zod, pnpm.

## Global Constraints

- Do not create public, admin, or component UI screens in this plan; the user will approve the visual direction first.
- Use TypeScript with strict checking and server-only environment variables for `DATABASE_URL`, `BETTER_AUTH_SECRET`, and S3 credentials.
- Use UUID primary keys and unique immutable slugs for catalog entities.
- Store temperatures in Celsius, dimensions in millimetres, weights in grams, density in g/cm³, and money as integer minor units plus ISO currency.
- Keep unavailable manufacturer specifications nullable and expose them as `null`; never synthesize data.
- Persist S3 object keys and asset metadata in Postgres, never public bucket credentials.
- Compute price-per-kilogram from offer price and spool weight; do not persist it as an editable database value.
- Import scripts must be idempotent, validate rows before writing, and report inserted, updated, skipped, and failed records.
- Every route that mutates data must validate its body and require an administrator session.
- Use one focused commit per completed task. Do not implement frontend/UI tasks during this execution stage.

---

## File structure

| Path | Responsibility |
| --- | --- |
| `package.json`, `pnpm-workspace.yaml` | Package scripts and dependency manifest. |
| `nuxt.config.ts`, `tailwind.config.ts`, `app.config.ts` | Nuxt runtime, Tailwind, and application configuration. |
| `.env.example`, `server/utils/env.ts` | Required environment variable contract and validation. |
| `drizzle.config.ts`, `server/db/*` | Database client, schema, migrations, and repository helpers. |
| `server/auth/*` | Better Auth initialization and admin authorization helper. |
| `server/storage/*` | S3 client and signed-upload policy. |
| `server/catalog/*` | Catalog input schemas, read/write services, compatibility and pricing functions. |
| `server/api/*` | Validated public read APIs and protected administrative mutation APIs. |
| `scripts/import-catalog.ts`, `data/import/*` | Validated idempotent backfill command and source fixtures. |
| `tests/*` | Unit and API-level tests for every server contract. |
| `docs/contracts/*` | Stable data/API contracts shared by future UI tasks. |

## Task 1: Bootstrap the server-capable Nuxt workspace

**Assignee:** GPT-5.6 Luna — foundation

**Files:**
- Create: `package.json`
- Create: `nuxt.config.ts`
- Create: `tailwind.config.ts`
- Create: `app.vue`
- Create: `assets/css/main.css`
- Create: `.env.example`
- Create: `server/utils/env.ts`
- Create: `vitest.config.ts`
- Create: `tests/env.test.ts`

**Interfaces:**
- Produces `runtimeEnv` from `server/utils/env.ts` with validated `databaseUrl`, `betterAuthSecret`, `s3Bucket`, `s3Region`, `s3Endpoint`, `s3AccessKeyId`, and `s3SecretAccessKey` fields.
- Produces `pnpm test`, `pnpm lint`, `pnpm typecheck`, and `pnpm db:generate` scripts for all following tasks.

- [x] **Step 1: Write the failing environment test**

```ts
import { describe, expect, it } from 'vitest'
import { parseServerEnv } from '../server/utils/env'

describe('parseServerEnv', () => {
  it('rejects an invalid database URL', () => {
    expect(() => parseServerEnv({ DATABASE_URL: 'invalid' })).toThrow(
      'DATABASE_URL must be a valid URL',
    )
  })
})
```

- [x] **Step 2: Run the test to verify it fails**

Run: `pnpm test tests/env.test.ts`

Expected: FAIL because the workspace and `parseServerEnv` do not exist.

- [x] **Step 3: Scaffold and configure the workspace**

Run `pnpm dlx nuxi@latest init . --packageManager pnpm`, preserve the plan/docs files, then add the server/data/test dependencies. Configure Nuxt for TypeScript strictness and Tailwind, register `assets/css/main.css`, and add only a minimal root `<NuxtPage />` shell. Add scripts:

```json
{
  "test": "vitest run",
  "test:watch": "vitest",
  "lint": "eslint .",
  "typecheck": "nuxt typecheck",
  "db:generate": "drizzle-kit generate",
  "db:migrate": "drizzle-kit migrate",
  "import:catalog": "tsx scripts/import-catalog.ts"
}
```

Implement `parseServerEnv` with Zod so that missing storage variables are reported together and URLs are accepted only by `z.string().url()`.

- [x] **Step 4: Run baseline checks**

Run: `pnpm test tests/env.test.ts && pnpm typecheck && pnpm lint`

Expected: PASS.

- [x] **Step 5: Commit**

```bash
git add package.json pnpm-lock.yaml nuxt.config.ts tailwind.config.ts app.vue assets/css/main.css .env.example server/utils/env.ts vitest.config.ts tests/env.test.ts
git commit -m "chore: bootstrap Nuxt server workspace"
```

## Task 2: Define the Drizzle catalog, commerce, and compatibility schema

**Assignee:** GPT-5.6 Luna — data model

**Files:**
- Create: `drizzle.config.ts`
- Create: `server/db/client.ts`
- Create: `server/db/schema/catalog.ts`
- Create: `server/db/schema/commerce.ts`
- Create: `server/db/schema/compatibility.ts`
- Create: `server/db/schema/auth.ts`
- Create: `server/db/schema/index.ts`
- Create: `server/db/migrations/.gitkeep`
- Create: `tests/schema.test.ts`

**Consumes:** `runtimeEnv.databaseUrl` from Task 1.

**Produces:** Drizzle tables `manufacturers`, `materials`, `products`, `productPrintSettings`, `productSpools`, `colors`, `productColorVariants`, `assets`, `productImages`, `applicationTags`, `productApplicationTags`, `retailers`, `offers`, `printers`, `printerProductCompatibility`, and `productRelations`.

- [x] **Step 1: Write failing schema invariants**

```ts
import { describe, expect, it } from 'vitest'
import { offers, products } from '../server/db/schema'

describe('catalog schema', () => {
  it('uses UUID product ids and a unique product slug', () => {
    expect(products.id.dataType).toBe('string')
    expect(products.slug.isUnique).toBe(true)
  })

  it('stores offer money in integer minor units', () => {
    expect(offers.priceMinor.dataType).toBe('number')
  })
})
```

- [x] **Step 2: Run the test to verify it fails**

Run: `pnpm test tests/schema.test.ts`

Expected: FAIL because the schema exports do not exist.

- [x] **Step 3: Implement tables and generate the initial migration**

Use Postgres UUID defaults and explicit enum values:

```ts
export const compatibilityStatus = pgEnum('compatibility_status', [
  'compatible',
  'requirements',
  'not_recommended',
])

export const relationType = pgEnum('product_relation_type', [
  'similar', 'alternative', 'cheapest', 'premium',
  'closest_specification', 'closest_colour', 'high_speed',
])
```

Make technical properties nullable, include `rawSpecs: jsonb('raw_specs').$type<Record<string, unknown>>()`, use integer `priceMinor`, and enforce FK relationships. Add the Better Auth Drizzle tables and an application-level `role` with `admin` and `user` values. Configure `drizzle.config.ts` to write generated SQL under `server/db/migrations`, then run `pnpm db:generate`.

- [x] **Step 4: Run schema checks**

Run: `pnpm test tests/schema.test.ts && pnpm typecheck`

Expected: PASS and a generated initial SQL migration is present.

- [x] **Step 5: Commit**

```bash
git add drizzle.config.ts server/db tests/schema.test.ts
git commit -m "feat: add catalog database schema"
```

## Task 3: Add Better Auth and server-side administrator authorization

**Assignee:** GPT-5.6 Luna — auth

**Files:**
- Create: `server/auth/config.ts`
- Create: `server/auth/index.ts`
- Create: `server/utils/require-admin.ts`
- Create: `server/api/auth/[...all].ts`
- Create: `server/api/admin/health.get.ts`
- Create: `tests/require-admin.test.ts`

**Consumes:** Better Auth tables from Task 2 and `runtimeEnv.betterAuthSecret` from Task 1.

**Produces:** `requireAdmin(event): Promise<{ id: string; email: string; role: 'admin' }>` and a Better Auth handler mounted at `/api/auth/**`.

- [x] **Step 1: Write failing authorization tests**

```ts
import { describe, expect, it } from 'vitest'
import { requireAdmin } from '../server/utils/require-admin'

describe('requireAdmin', () => {
  it('rejects an anonymous request', async () => {
    await expect(requireAdmin({} as never)).rejects.toMatchObject({ statusCode: 401 })
  })

  it('rejects an authenticated non-admin', async () => {
    await expect(requireAdmin({} as never)).rejects.toMatchObject({ statusCode: 403 })
  })
})
```

- [x] **Step 2: Run the tests to verify they fail**

Run: `pnpm test tests/require-admin.test.ts`

Expected: FAIL because `requireAdmin` does not exist.

- [x] **Step 3: Implement auth configuration and guard**

Configure Better Auth with the Drizzle adapter, its generated tables, and email/password only; do not add public registration UI. Implement `requireAdmin` by reading the Better Auth session from the H3 event, returning 401 with `Unauthenticated` if absent and 403 with `Administrator access required` unless `user.role === 'admin'`. Mount the handler and protect `GET /api/admin/health` with the guard.

- [x] **Step 4: Run authorization checks**

Run: `pnpm test tests/require-admin.test.ts && pnpm typecheck`

Expected: PASS.

- [x] **Step 5: Commit**

```bash
git add server/auth server/utils/require-admin.ts server/api/auth server/api/admin/health.get.ts tests/require-admin.test.ts
git commit -m "feat: add administrator authentication guard"
```

## Task 4: Build S3 asset validation and signed-upload service

**Assignee:** GPT-5.6 Luna — storage

**Files:**
- Create: `server/storage/s3.ts`
- Create: `server/storage/upload-policy.ts`
- Create: `server/api/admin/assets/upload-url.post.ts`
- Create: `tests/upload-policy.test.ts`

**Consumes:** S3 environment variables from Task 1 and `requireAdmin` from Task 3.

**Produces:** `createProductImageUpload(input): Promise<{ assetKey: string; uploadUrl: string; expiresAt: string }>`; accepted files are JPEG, PNG, WebP, or AVIF up to 10 MiB.

- [x] **Step 1: Write failing upload-policy tests**

```ts
import { describe, expect, it } from 'vitest'
import { validateProductImageUpload } from '../server/storage/upload-policy'

describe('validateProductImageUpload', () => {
  it('rejects executable content', () => {
    expect(() => validateProductImageUpload({ mimeType: 'application/x-msdownload', sizeBytes: 10 })).toThrow('Unsupported image type')
  })

  it('rejects files larger than 10 MiB', () => {
    expect(() => validateProductImageUpload({ mimeType: 'image/webp', sizeBytes: 10 * 1024 * 1024 + 1 })).toThrow('Image exceeds 10 MiB')
  })
})
```

- [x] **Step 2: Run the tests to verify they fail**

Run: `pnpm test tests/upload-policy.test.ts`

Expected: FAIL because the upload policy does not exist.

- [x] **Step 3: Implement S3 service and protected API**

Validate MIME type and file size before calling the AWS SDK presigner. Generate object keys in the form `products/<product-id>/<uuid>.<extension>` without trusting a client filename. The route validates `{ productId, mimeType, sizeBytes }` with Zod, calls `requireAdmin`, and returns the signed URL data. Do not persist an asset record until a later admin mutation confirms the completed upload.

- [x] **Step 4: Run storage checks**

Run: `pnpm test tests/upload-policy.test.ts && pnpm typecheck`

Expected: PASS.

- [x] **Step 5: Commit**

```bash
git add server/storage server/api/admin/assets/upload-url.post.ts tests/upload-policy.test.ts
git commit -m "feat: add secure product image uploads"
```

## Task 5: Implement catalog services, price calculation, and compatibility evaluation

**Assignee:** GPT-5.6 Luna — domain services

**Files:**
- Create: `server/catalog/types.ts`
- Create: `server/catalog/price.ts`
- Create: `server/catalog/compatibility.ts`
- Create: `server/catalog/repository.ts`
- Create: `tests/price.test.ts`
- Create: `tests/compatibility.test.ts`

**Consumes:** schema tables from Task 2.

**Produces:** `pricePerKgMinor(priceMinor: number, weightG: number): number | null` and `evaluateCompatibility(input): CompatibilityResult` where `CompatibilityResult.status` is `compatible | requirements | not_recommended` and `warnings` is a string array.

- [x] **Step 1: Write failing domain tests**

```ts
import { describe, expect, it } from 'vitest'
import { pricePerKgMinor } from '../server/catalog/price'
import { evaluateCompatibility } from '../server/catalog/compatibility'

describe('pricePerKgMinor', () => {
  it('normalizes a 1 kg offer', () => expect(pricePerKgMinor(2499, 1000)).toBe(2499))
  it('returns null without a usable spool weight', () => expect(pricePerKgMinor(2499, 0)).toBeNull())
})

describe('evaluateCompatibility', () => {
  it('rejects a printer below the required nozzle temperature', () => {
    expect(evaluateCompatibility({ printer: { maxNozzleTempC: 250 }, product: { nozzleTempMinC: 260 } }).status).toBe('not_recommended')
  })
})
```

- [x] **Step 2: Run the tests to verify they fail**

Run: `pnpm test tests/price.test.ts tests/compatibility.test.ts`

Expected: FAIL because the domain functions do not exist.

- [x] **Step 3: Implement pure domain functions and repositories**

Implement price calculation with `Math.round((priceMinor * 1000) / weightG)`. Compatibility must report `not_recommended` when a hard capability cannot meet a required nozzle/bed temperature or enclosure requirement; use `requirements` for recommended hardened nozzle, drying, or a known override; otherwise use `compatible`. Create narrowly scoped repository methods for product reads, product writes, offers, and compatibility overrides. No route handler may issue ad hoc SQL.

- [x] **Step 4: Run domain checks**

Run: `pnpm test tests/price.test.ts tests/compatibility.test.ts && pnpm typecheck`

Expected: PASS.

- [x] **Step 5: Commit**

```bash
git add server/catalog tests/price.test.ts tests/compatibility.test.ts
git commit -m "feat: add pricing and compatibility services"
```

## Task 6: Create validated idempotent catalog backfill tooling

**Assignee:** GPT-5.6 Luna — importer

**Files:**
- Create: `server/catalog/import-schema.ts`
- Create: `server/catalog/importer.ts`
- Create: `scripts/import-catalog.ts`
- Create: `data/import/manufacturers.json`
- Create: `data/import/materials.json`
- Create: `data/import/products.json`
- Create: `tests/importer.test.ts`
- Create: `docs/contracts/import-format.md`

**Consumes:** repositories from Task 5.

**Produces:** `importCatalog(input): Promise<ImportReport>` where `ImportReport` contains numerical `inserted`, `updated`, `skipped`, `failed`, and an array of `{ source: string; row: number; message: string }` errors.

- [x] **Step 1: Write failing importer tests**

```ts
import { describe, expect, it } from 'vitest'
import { importCatalog } from '../server/catalog/importer'

describe('importCatalog', () => {
  it('upserts a repeat product without duplication', async () => {
    const input = { manufacturers: [{ slug: 'acme', name: 'Acme' }], products: [{ manufacturerSlug: 'acme', slug: 'pla-basic', name: 'PLA Basic', materialSlug: 'pla' }] }
    const first = await importCatalog(input)
    const second = await importCatalog(input)
    expect(first.inserted).toBeGreaterThan(0)
    expect(second.updated + second.skipped).toBeGreaterThan(0)
  })
})
```

- [x] **Step 2: Run the test to verify it fails**

Run: `pnpm test tests/importer.test.ts`

Expected: FAIL because `importCatalog` does not exist.

- [x] **Step 3: Implement schemas, importer, fixtures, and CLI report**

Validate identity fields, canonical units, material links, and non-negative money before writes. Import in dependency order—manufacturers, materials, then products/variants/offers—inside transactions where appropriate. Resolve existing records by stable slug and parent relationship. The CLI accepts `--input <directory>`, logs one summary line, writes validation errors to stderr, exits non-zero only when `failed > 0`, and never silently drops invalid records. Add minimal representative fixture data, not a fictitious 200-product catalog.

- [x] **Step 4: Run importer verification**

Run: `pnpm test tests/importer.test.ts && pnpm import:catalog -- --input data/import && pnpm typecheck`

Expected: PASS; repeat the import command once to confirm idempotence.

- [x] **Step 5: Commit**

```bash
git add server/catalog scripts/import-catalog.ts data/import tests/importer.test.ts docs/contracts/import-format.md
git commit -m "feat: add idempotent catalog importer"
```

## Task 7: Expose typed public read APIs and admin mutation contracts

**Assignee:** GPT-5.6 Luna — API contracts

**Files:**
- Create: `server/api/products/index.get.ts`
- Create: `server/api/products/[manufacturer]/[slug].get.ts`
- Create: `server/api/materials/index.get.ts`
- Create: `server/api/printers/index.get.ts`
- Create: `server/api/offers/[productId].get.ts`
- Create: `server/api/admin/products/index.post.ts`
- Create: `server/api/admin/products/[id].patch.ts`
- Create: `server/api/admin/offers/index.post.ts`
- Create: `server/api/admin/printers/index.post.ts`
- Create: `server/catalog/api-schema.ts`
- Create: `docs/contracts/api.md`
- Create: `tests/api-schema.test.ts`

**Consumes:** `requireAdmin` from Task 3 and catalog repository/functions from Task 5.

**Produces:** validated public read endpoints and protected create/update contracts; every mutation uses a Zod schema exported from `server/catalog/api-schema.ts`.

- [x] **Step 1: Write failing API-schema tests**

```ts
import { describe, expect, it } from 'vitest'
import { productCreateSchema } from '../server/catalog/api-schema'

describe('productCreateSchema', () => {
  it('rejects a product without a manufacturer id', () => {
    expect(productCreateSchema.safeParse({ name: 'PLA Basic', slug: 'pla-basic' }).success).toBe(false)
  })

  it('accepts absent optional specifications', () => {
    expect(productCreateSchema.safeParse({ manufacturerId: '00000000-0000-4000-8000-000000000000', materialId: '00000000-0000-4000-8000-000000000001', name: 'PLA Basic', slug: 'pla-basic' }).success).toBe(true)
  })
})
```

- [x] **Step 2: Run the test to verify it fails**

Run: `pnpm test tests/api-schema.test.ts`

Expected: FAIL because the input schemas do not exist.

- [x] **Step 3: Implement contracts and route handlers**

Return typed DTOs from repository calls rather than raw Drizzle rows. Product list parameters support `material`, `manufacturer`, `minHeatDeflectionTempC`, `maxPricePerKgMinor`, `abrasive`, `enclosureRequirement`, `dryingRequirement`, and `limit`; reject malformed parameters with HTTP 400. Public offers omit any private source metadata. Admin create/patch routes authenticate first, parse Zod schemas, and respond with 201/200 only after successful repository operations. Document each endpoint’s path, query/body schema, response fields, and error status in `docs/contracts/api.md`.

- [x] **Step 4: Run contract checks**

Run: `pnpm test tests/api-schema.test.ts && pnpm typecheck && pnpm lint`

Expected: PASS.

- [x] **Step 5: Commit**

```bash
git add server/api server/catalog/api-schema.ts docs/contracts/api.md tests/api-schema.test.ts
git commit -m "feat: add catalog API contracts"
```

## Task 8: Verify the backend foundation and prepare the UI handoff

**Assignee:** GPT-5.6 Luna — verification

**Files:**
- Create: `scripts/catalog-quality-report.ts`
- Create: `tests/catalog-quality-report.test.ts`
- Create: `docs/contracts/ui-handoff.md`

**Consumes:** all completed Tasks 1–7.

**Produces:** `pnpm catalog:quality` report for published products missing material, primary image, spool weight, or current offer; UI-ready DTO and route contract documentation.

- [x] **Step 1: Write the failing quality-report test**

```ts
import { describe, expect, it } from 'vitest'
import { summarizeCatalogQuality } from '../scripts/catalog-quality-report'

describe('summarizeCatalogQuality', () => {
  it('reports published products without a primary image', () => {
    expect(summarizeCatalogQuality([{ published: true, materialId: 'm1', primaryImageId: null, spoolWeightG: 1000, currentOfferId: 'o1' }]).missingPrimaryImage).toBe(1)
  })
})
```

- [x] **Step 2: Run the test to verify it fails**

Run: `pnpm test tests/catalog-quality-report.test.ts`

Expected: FAIL because the report function does not exist.

- [x] **Step 3: Implement reporting and UI handoff**

Add `catalog:quality` to `package.json`. The script prints counts for each critical missing data category and exits zero so it can be used during iterative backfills. `ui-handoff.md` must list each public endpoint, response DTO, nullability rules, and the three compatibility statuses; it must not contain wireframes or UI implementation instructions.

- [x] **Step 4: Run final foundation verification**

Run: `pnpm test && pnpm typecheck && pnpm lint && pnpm catalog:quality && git status --short`

Expected: all checks PASS; `git status --short` is empty after committing this task.

- [x] **Step 5: Commit**

```bash
git add scripts/catalog-quality-report.ts tests/catalog-quality-report.test.ts docs/contracts/ui-handoff.md package.json
git commit -m "chore: verify catalog foundation"
```

## Deferred after visual approval

The following planned work is intentionally not executable until the user approves a homepage/UI direction: shadcn-vue component installation and tokens, public/app layouts, homepage, product catalog pages, filtering controls, product/material/printer pages, global search interface, Finder wizard, comparison tables, alternatives UI, and admin portal screens. The API and contract documentation produced above are the stable handoff boundary for those agents.

## Plan self-review

- Spec coverage: this plan implements every non-visual V1 dependency—runtime, database, auth, storage, imports, core domain calculations, APIs, and data-quality tooling. All public and admin screens are intentionally deferred per user direction.
- Placeholder scan: no `TBD`, `TODO`, or unspecified implementation steps are present.
- Interface consistency: API contracts consume the repository/auth/services introduced in earlier tasks; UI-facing DTO details are documented before UI work begins.
