The broken-model UUID fields used z.string().uuid().optional(), which only accepts undefined — not the '' defaults. When the broken serial matched an existing part, those fields unmounted before their FormMessage could render, so handleSubmit aborted on hidden errors and the mutation never fired. Accept the empty-string sentinel alongside UUIDs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vector
Hardware parts inventory system. Tracks serialized parts across sites → rooms → bins and hosts (with externally-driven state/stack lifecycle), with a full audit trail, repair/RMA workflow, per-tech custody for broken-part holds and pre-staged spares, tag-based organization, category-per-model taxonomy, manufacturer EOL tracking, and signed webhook delivery for external integrations.
Vector 2.0 is a ground-up TypeScript rewrite of the original JavaScript codebase, delivered as a pnpm + Turbo monorepo with shadcn/ui on the frontend and a service-layered Express API on the backend.
Architecture
vector/
├── apps/
│ ├── api/ Express 5 + Prisma + zod. Controllers → services → tx.
│ ├── web/ React 19 + Vite + TanStack Query/Table + shadcn/ui.
│ └── e2e/ Playwright smoke tests (login, parts, repairs, admin).
├── packages/
│ ├── db/ Prisma schema, migrations, seed, singleton client.
│ ├── shared/ zod schemas + DTOs — single source of truth for the API contract.
│ ├── ui/ shadcn primitives + Vector design tokens.
│ └── config/ Shared tsconfig presets + Tailwind preset.
├── .gitea/workflows/ CI (lint · typecheck · test · build) and gated E2E job.
├── docker-compose.yml Postgres + Redis for local development.
└── turbo.json Pipeline: `build`, `test`, `typecheck`, `lint`.
The API is split along a strict controller → service → transaction boundary. Every service function takes (tx, input, actor) so controllers can compose multiple services inside a single prisma.$transaction, guaranteeing that part mutations and their PartEvent audit rows are atomic.
Tech stack
| Layer | Choice |
|---|---|
| Language | TypeScript strict mode across all workspaces |
| API | Express 5, Prisma 5, zod, JWT + refresh-token rotation, CSRF, helmet |
| Web | React 19, Vite, TanStack Query, TanStack Table, react-hook-form, nuqs |
| UI | shadcn/ui on Radix primitives, Tailwind v4, Sonner toasts, Recharts |
| Database | SQLite for dev (Postgres cutover prepared — schema is portable) |
| Observability | pino (JSON logs in prod, pretty in dev) + request-scoped requestId |
| Testing | Vitest (unit, coverage gate on services/lib), Playwright (E2E) |
| Monorepo tools | pnpm workspaces + Turborepo |
| CI | Gitea Actions + Renovate (self-hosted) |
Getting started
Prerequisites
- Node.js ≥ 20
- pnpm ≥ 10 (
corepack enableornpm i -g pnpm) - Docker (optional; required for Postgres + Redis)
Install
pnpm install
pnpm -C packages/db exec prisma generate
Environment
Copy the API env template and generate a JWT secret:
cp apps/api/.env.example apps/api/.env
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" # paste into JWT_SECRET
The default DATABASE_URL points at a local SQLite file. To use the containerized Postgres instead, start Docker and update the URL:
docker compose up -d postgres redis
# then edit apps/api/.env:
# DATABASE_URL=postgresql://vector:vector@localhost:5432/vector
Database
pnpm -C packages/db exec prisma migrate dev # apply migrations
pnpm -C packages/db run db:seed # create default admin:admin user
Run
pnpm dev
- API: http://localhost:3001
- Web: http://localhost:5173 (proxies
/api→ API) - Default login:
admin/admin— change immediately.
Common tasks
All tasks run at the workspace root; Turbo fans them out to the right packages with caching.
| Command | What it does |
|---|---|
pnpm dev |
Run apps/api and apps/web concurrently |
pnpm build |
Build every package and app |
pnpm typecheck |
tsc --noEmit across every workspace |
pnpm lint |
ESLint across every workspace |
pnpm test |
Run all Vitest unit test suites |
pnpm -C apps/api test:coverage |
Unit tests + v8 coverage report on services/lib (report only) |
pnpm -C apps/e2e test |
Run Playwright smoke tests (requires stack running + creds) |
pnpm -C packages/db run db:studio |
Open Prisma Studio against the current database |
pnpm -C packages/db run db:reset |
Drop schema, re-migrate, re-seed |
Testing
Unit tests live next to the code they cover (*.test.ts). Coverage is reported on apps/api/src/{services,lib}/** and uploaded by CI as an artifact — no threshold gate today; add one once service-level coverage catches up.
packages/shared— zod schema contracts (parts, webhooks, auth).apps/api— pure helpers (signBody,csvCell,http-error), plus the analytics aggregator tested with an in-memoryTxdouble.
End-to-end tests in apps/e2e/ run against a live stack. Each spec skips itself unless TEST_USERNAME and TEST_PASSWORD are present in the environment:
pnpm dev # in one terminal
TEST_USERNAME=admin TEST_PASSWORD=admin \
pnpm -C apps/e2e exec playwright install --with-deps chromium
TEST_USERNAME=admin TEST_PASSWORD=admin \
pnpm -C apps/e2e test
Reports land in apps/e2e/playwright-report/.
Continuous integration
CI runs on Gitea Actions — .gitea/workflows/ci.yaml. Every push and pull request executes:
pnpm install --frozen-lockfileprisma generatepnpm lintpnpm typecheckpnpm -C packages/shared test+pnpm -C apps/api test:coveragepnpm build- API coverage uploaded as an artifact.
A second Playwright job is gated behind the repository variable ENABLE_E2E=true and requires these secrets:
| Secret | Purpose |
|---|---|
E2E_BASE_URL |
URL of a running Vector stack |
E2E_USERNAME |
Test admin username |
E2E_PASSWORD |
Test admin password |
Dependency updates come from a self-hosted Renovate instance configured via renovate.json — grouped minor/patch PRs weekly, auto-merge for dev-dep patches, prisma + @prisma/client grouped together, Radix/shadcn held for manual review.
Conventions
- API shape: all responses follow
{ code, message, requestId, details? }for errors. Paginated lists use{ data, page, pageSize, total }. - Validation: every request body and query string is parsed through a zod schema from
@vector/sharedbefore it reaches a controller. No ad-hoc validation inside controllers. - Query keys: the web app uses a hierarchical factory at apps/web/src/lib/queryKeys.ts. Invalidate by domain (
queryKeys.parts.all) or by filter (queryKeys.parts.list(filters)). - Commits: Conventional Commits — Renovate already expects them.
- Webhooks: every delivery is signed with HMAC-SHA256 over
${timestamp}.${body}and sent with headersx-vector-signature,x-vector-timestamp,x-vector-event, and the recursion-guardx-vector-webhook: v1.
Roadmap
All nine phases of the 2.0 rewrite are in-tree:
| Phase | Scope |
|---|---|
| 0 | Monorepo foundation (pnpm + Turbo, apps scaffolded) |
| 1 | Strict TypeScript, Prisma schema, env validation, singleton client |
| 2 | Service-layered API, transactional mutations, auth hardening, pagination |
| 3 | Schema extensions (hosts, repairs, tags, categories, webhooks, FTS-ready) |
| 4 | shadcn/ui design system + DataTable<T>, Form pattern, query-keys factory |
| 5 | Page-by-page rewrite (Parts, Locations split, Manufacturers, Users) |
| 6 | Feature slice — Repair/RMA, Tags, Bulk ops, Saved views |
| 7 | Analytics dashboard (Recharts) + EOL + Webhooks + streaming CSV export |
| 8 | Vitest + Playwright + Gitea Actions CI + Renovate |
Deferred follow-ups
- Postgres cutover. Schema is already portable; the data-migration script lives at
packages/db/POSTGRES_FTS.md. - BullMQ worker. Replace the in-process webhook emitter in apps/api/src/lib/webhook-emitter.ts with a Redis-backed worker. Signature is stable — one-line swap.
- PDF audit export via
@react-pdf/rendererin the worker. - CSV import wizard UI to pair with the existing
CsvImportJobstaging table.