This file is read by the `/mvp build` orchestrator at the start of each build session when the stack is JavaScript (Vite + TypeScript + React 19 + TailwindCSS 4). The contents are injected into every agent prompt under a `## TypeScript/React Conventions — MANDATORY` section.
From experimentalnpx claudepluginhub dionridley/claude-plugins --plugin experimentalmvp/conventions/This file is read by the /mvp build orchestrator at the start of each build session when the stack is JavaScript (Vite + TypeScript + React 19 + TailwindCSS 4). The contents are injected into every agent prompt under a ## TypeScript/React Conventions — MANDATORY section.
Include this block verbatim in all TypeScript/React agent prompts:
## TypeScript/React Conventions — MANDATORY
**TypeScript:**
- NEVER use `any` type — use `unknown` and narrow, or define a proper type/interface
- ALWAYS read an existing file with the Read tool before modifying it
- Only use Write directly for files that do not yet exist
**Paths and working directory:**
- NEVER use absolute paths — no `/Users/...`, no `~/...`
- ALWAYS use relative paths: `src/pages/Home.tsx`, `server/db/schema.ts`
- You are already in the project root — do NOT `cd` to absolute paths
- If a command must run in a subdirectory, use `cd subdir && command` with a relative path only
**`verbatimModuleSyntax` and `import type`:**
- `tsconfig.json` uses `"verbatimModuleSyntax": true` — type-only exports are stripped at bundle time
- ALWAYS import types and interfaces with `import type { ... }` — NEVER mix types into value imports
- WRONG: `import { api, FormDetail, FormPage } from '../lib/api'`
- CORRECT:
```ts
import { api } from '../lib/api'
import type { FormDetail, FormPage } from '../lib/api'
export type Foo, use import type { Foo }Components:
Navigation:
<Link to="..."> and useNavigate() — never window.locationForms:
useActionState for form handling — not manual useState + handlersStyling:
@import "tailwindcss" is the entry point in src/index.css — no config file needed in v4Database (Drizzle ORM + better-sqlite3):
src/db/schema.tsdrizzle-kit push to apply schema changes during development (no migration files needed)src/lib/db/ — never query directly from components.filter(), .map(), or .reduce() callbacks — all DB calls must be at the top level of async functionsSeed data:
const existing = db.select().from(schema.forms).where(eq(schema.forms.slug, 'my-form')).get()
if (!existing) { await db.insert(schema.forms).values(...) }
answers when you seed responses) so related screens feel real during demosAPI URL contracts:
src/lib/routes.ts constants file imported by both Express routes and the fetch clientTidewave (app introspection):
mcp__tidewave__* tools to verify DB contents, check API responses, and confirm server behaviorShared types (server/lib/types.ts):
server/lib/types.ts — never duplicated across filesimport type { ... } from '../lib/types'import type { ... } from '../../server/lib/types'API response shape — mutations must return the full resource:
PATCH, PUT, and POST endpoints MUST return the same shape as the corresponding GET for that resource — the full nested object, not just the raw DB row or { id }setState(await api.things.update(...)) without any reshapingExpress route ordering:
:id, :slug) MUST be declared AFTER any literal path segments at the same levelrouter.get('/:id', ...) declared before router.get('/stats', ...)router.get('/stats', ...) then router.get('/:id', ...)React event handler anti-patterns:
onChange + setTimeout(() => onSubmit()) to trigger a submission after a state update — this captures stale state and causes race conditionsonChange={e => { setValue(e.target.value); setTimeout(() => handleSubmit(), 0) }}onChange={e => { const v = e.target.value; setValue(v); handleSubmit(v) }}
---
## Stack Reference
### Project Structure
src/
components/ # Shared UI components
pages/ # Route-level page components
lib/
api.ts # Typed fetch client — all types must use export type
routes.ts # API route constants (shared by server and client)
server/
db/
schema.ts # Drizzle schema definitions
client.ts # Drizzle db client
seed.ts # Idempotent seed script
migrations/ # Generated by drizzle-kit
lib/
types.ts # Shared types (server responses ↔ client state)
routes.ts # Express route path constants
index.css # TailwindCSS 4 entry point (@import "tailwindcss")
main.tsx # React entry point
App.tsx # Router setup
server/
index.ts # Express server
db/
schema.ts # Drizzle schema
seed.ts # Seed script (must be idempotent)
### Tidewave Setup
Tidewave provides MCP-based introspection for the running application. Add during scaffold:
```bash
npm install @tidewave/tidewave
Add to server/index.ts:
import { createTidewaveMiddleware } from '@tidewave/tidewave'
// Dev only
if (process.env.NODE_ENV !== 'production') {
app.use('/tidewave', createTidewaveMiddleware({ app }))
}
api.ts Pattern — export type RequiredEvery type exported from api.ts must use export type so consumers know to use import type:
// src/lib/api.ts
export type FormDetail = {
id: string
slug: string
title: string
}
export type FormPage = {
id: string
questions: Question[]
}
// Value export — imported normally
export const api = {
forms: {
list: () => fetch('/api/forms').then(r => r.json()),
get: (slug: string) => fetch(`/api/forms/${slug}`).then(r => r.json()),
}
}
Consumers:
import { api } from '../lib/api'
import type { FormDetail, FormPage } from '../lib/api'
MVP uses non-default ports to avoid conflicts with other running applications:
3500 (default is 3001 — too common)3600 (default is 5173 — too common)Always read ports from state.json — do NOT hardcode port numbers in application code. The server template uses process.env.PORT || 3500 so the port can be overridden via environment variable.
At build session start, the main agent checks that these ports are free and reads stored PIDs to clean up any stale processes from previous sessions. Never kill processes by port (lsof -ti:[port] | xargs kill) — this kills by port regardless of ownership and can affect other applications.
After every agent completes, run:
npx tsc --noEmit
Fix all type errors before the next phase begins. This catches import type violations, missing types, and dead code before they become runtime blank-page bugs.
// src/lib/db/things.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createThing, listThings } from './things'
describe('things', () => {
it('creates a thing with valid data', () => {
const result = createThing({ name: 'Test' })
expect(result.name).toBe('Test')
})
})
When Playwright is available, always run these steps first in Phase 7:
browser_console_messages — a blank page always has console errors that point to the root cause