Skip to content
Back to skills

Type Safety Chain

ASecurity

Implement the full six-layer type safety chain in a new project — DB → Drizzle schema → Zod schemas → services → API routes → React hooks. Installs dependencies, writes all infrastructure files, generates all layers from the live database, and validates each layer bottom-up. Use for new projects that need this architecture from scratch, or to verify and repair an existing chain. Supabase MCP required for DB verification.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 26, 2026
databasestypescriptgobashsqlreactnodeapidatabase

Works with

  • cli
  • api
  • mcp

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 26, 2026

npx -y skills add JoshuaShepherd/my-skills --skill type-safety-chain --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Type Safety Chain?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Type Safety Chain
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/joshuashepherd-type-safety-chain/badge)](https://www.skillsdirectory.com/skills/joshuashepherd-type-safety-chain)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: type-safety-chain
description: "Implement the full six-layer type safety chain in a new project — DB → Drizzle schema → Zod schemas → services → API routes → React hooks. Installs dependencies, writes all infrastructure files, generates all layers from the live database, and validates each layer bottom-up. Use for new projects that need this architecture from scratch, or to verify and repair an existing chain. Supabase MCP required for DB verification."
user-invocable: true
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Agent, mcp__supabase__list_tables, mcp__supabase__execute_sql, mcp__supabase__get_project, mcp__supabase__list_projects, mcp__supabase__generate_typescript_types
---

Implement the full type safety chain for: $ARGUMENTS

$ARGUMENTS should include:
- The Supabase project ID to connect to (e.g. `vhaiiiykcukrlyvwlgip`), OR "list" to browse projects
- Optionally: "verify-only" to audit an existing chain without writing files
- Optionally: "schema-only" to only generate the Drizzle schema (Layer 1) and stop
- Optionally: "generate-all" to regenerate all layers after schema is already in place
- Empty — ask the user for the Supabase project and target directory

---

## Purpose

This skill bootstraps or verifies the complete **six-layer type safety chain** used in this platform:

```
Layer 1: Drizzle Schema      src/lib/database/schema.ts          (source of truth)
Layer 2: Zod Schemas         src/lib/schemas/index.ts            (runtime contracts)
Layer 3: Services            src/lib/services/simplified/        (business logic)
Layer 4: API Routes          src/app/api/simplified/             (HTTP endpoints)
Layer 5: React Hooks         src/hooks/simplified/               (data fetching)
Layer 6: UI Components       src/components/simplified/          (presentation — wired externally)
```

Types flow **downstream only**. Never import a higher layer into a lower one. This skill implements Layers 1–5 and validates 1–6 when Layer 6 exists.

---

## Phase 0 — Identify the Target

Before doing anything:

1. If `$ARGUMENTS` is empty or "list", call `mcp__supabase__list_projects` and show available projects. Ask the user which project to target and which local directory is the project root.

2. If a project ID is given, call `mcp__supabase__get_project` to confirm the project is accessible and note the project URL and region.

3. Ask the user: **"Is this a new project (full bootstrap) or an existing project (verify/repair)?"**
   - **New project** → proceed through all phases
   - **Existing project** → skip Phase 2, go directly to Phase 4 (verify the existing chain)
   - **verify-only mode** → read existing files, call MCP, report gaps, no writes

4. Ask the user: **"Which reference project should I compare the chain structure against?"** The canonical reference is `alan-hirsch`. If the user names a project, note it. The reference is used only to verify the chain shape — not to copy code verbatim.

5. Confirm the target working directory. All paths in this skill are relative to that root.

---

## Phase 1 — Verify Database via Supabase MCP

Use `mcp__supabase__execute_sql` on the target project to count and list tables:

```sql
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
  AND table_type = 'BASE TABLE'
ORDER BY table_name;
```

Record the full table list. This is the **ground truth** the Drizzle schema must match.

Also run:

```sql
SELECT
  table_name,
  column_name,
  data_type,
  udt_name,
  is_nullable,
  column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position
LIMIT 500;
```

This gives column-level insight for verifying schema accuracy.

Report to the user:
- Total table count found in DB
- First 20 table names (preview)
- Any tables that exist in the DB but not in the local schema (if schema.ts already exists)
- Any tables in the local schema that don't exist in the DB

---

## Phase 2 — Install Dependencies (New Projects Only)

For a new project, check `package.json` to see if these packages are already installed. Install any that are missing:

### Required packages

```bash
# Core DB + ORM
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit drizzle-zod

# Validation
pnpm add zod

# React Query (data fetching for hooks)
pnpm add @tanstack/react-query

# Dev tools
pnpm add -D tsx dotenv
```

Check TypeScript config — the project needs `"moduleResolution": "bundler"` or `"node16"` for path aliases to work. If `tsconfig.json` doesn't have `"@/*"` path aliases configured, add them:

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

---

## Phase 3 — Write Infrastructure Files (New Projects Only)

Write these files only if they don't already exist. Never overwrite hand-written files — ask before overwriting.

### 3a. Drizzle config

**`drizzle.config.ts`** (project root):

```typescript
import { config as dotenvConfig } from "dotenv";
import { defineConfig } from "drizzle-kit";

dotenvConfig({ path: ".env" });
dotenvConfig({ path: ".env.local" });

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is not set");
}

export default defineConfig({
  schema: "./src/lib/database/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
});
```

### 3b. Database client

**`src/lib/database/client.ts`**:

```typescript
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is not set");
}

const queryClient = postgres(process.env.DATABASE_URL);
export const db = drizzle(queryClient, { schema });
```

### 3c. Tenant util

**`src/lib/tenant.ts`**:

```typescript
/**
 * Tenant context: one organization per deployment via TENANT_ORG_ID.
 * Services use this to scope queries by organization_id.
 */
export function getTenantOrgId(): string | null {
  const id = process.env.TENANT_ORG_ID;
  return id ?? null;
}
```

### 3d. Query utils (for hooks)

**`src/hooks/simplified/query-utils.ts`**:

```typescript
/**
 * Shared query utilities for generated hooks.
 */
export function buildQueryString(filters?: Record<string, unknown>): string {
  if (!filters) return "";
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(filters)) {
    if (value !== undefined && value !== null) {
      params.set(key, String(value));
    }
  }
  const qs = params.toString();
  return qs ? `?${qs}` : "";
}
```

### 3e. Base service

**`src/lib/services/simplified/base.service.ts`** — write the full SimplifiedService base class:

```typescript
import { eq, and, asc, desc, type SQL } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core";
import type { z } from "zod";
import { db } from "@/lib/database/client";
import { getTenantOrgId } from "@/lib/tenant";

// ---- Result<T> Pattern ----

export type Ok<T> = { success: true; data: T };
export type Err = { success: false; error: { code: string; message: string } };
export type Result<T> = Ok<T> | Err;

// ---- Helpers ----

function toSnakeCase(s: string): string {
  return s.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
}

const META_KEYS = new Set(["limit", "offset", "search"]);

// ---- SimplifiedService Base Class ----

export abstract class SimplifiedService<
  TTable extends PgTable,
  TSelect,
  TInsert,
  TUpdate,
  TFilters extends { limit?: number; offset?: number },
> {
  protected table: TTable;
  protected selectSchema: z.ZodType<TSelect>;
  protected insertSchema: z.ZodType<TInsert>;
  protected updateSchema: z.ZodType<TUpdate>;
  protected filtersSchema: z.ZodType<TFilters>;

  constructor(
    table: TTable,
    selectSchema: z.ZodType<TSelect>,
    insertSchema: z.ZodType<TInsert>,
    updateSchema: z.ZodType<TUpdate>,
    filtersSchema: z.ZodType<TFilters>,
  ) {
    this.table = table;
    this.selectSchema = selectSchema;
    this.insertSchema = insertSchema;
    this.updateSchema = updateSchema;
    this.filtersSchema = filtersSchema;
  }

  protected ok<T>(data: T): Ok<T> {
    return { success: true, data };
  }

  protected fail(code: string, message: string): Err {
    return { success: false, error: { code, message } };
  }

  protected buildFilterConditions(filters?: TFilters): SQL[] {
    const conditions: SQL[] = [];
    if (!filters) return conditions;
    const tableAny = this.table as any;
    for (const [key, value] of Object.entries(filters)) {
      if (META_KEYS.has(key) || value === undefined || value === null) continue;
      const col = tableAny[key] ?? tableAny[toSnakeCase(key)];
      if (col) conditions.push(eq(col, value));
    }
    return conditions;
  }

  async list(filters?: TFilters): Promise<Result<TSelect[]>> {
    try {
      const tableAny = this.table as any;
      const conditions = this.buildFilterConditions(filters);
      if ("organization_id" in tableAny && tableAny.organization_id != null) {
        const tenantOrgId = getTenantOrgId();
        if (!tenantOrgId) {
          return this.fail("TENANT_NOT_CONFIGURED", "TENANT_ORG_ID is required for this resource.");
        }
        conditions.unshift(eq(tableAny.organization_id, tenantOrgId));
      }
      const limit = filters?.limit ?? 50;
      const offset = filters?.offset ?? 0;
      const where = conditions.length > 0 ? and(...conditions) : undefined;
      const rows = await (db as any).select().from(this.table).where(where).limit(limit).offset(offset);
      return this.ok(rows as TSelect[]);
    } catch (e: any) {
      return this.fail("LIST_ERROR", e.message);
    }
  }

  async getById(id: string): Promise<Result<TSelect | null>> {
    try {
      const tableAny = this.table as any;
      if (!tableAny.id) return this.fail("NO_ID_COLUMN", "Table does not have an id column");
      if ("organization_id" in tableAny && tableAny.organization_id != null) {
        const tenantOrgId = getTenantOrgId();
        if (!tenantOrgId) return this.fail("TENANT_NOT_CONFIGURED", "TENANT_ORG_ID is required.");
        const rows = await (db as any).select().from(this.table)
          .where(and(eq(tableAny.id, id), eq(tableAny.organization_id, tenantOrgId))).limit(1);
        return this.ok((rows[0] as TSelect) ?? null);
      }
      const rows = await (db as any).select().from(this.table).where(eq(tableAny.id, id)).limit(1);
      return this.ok((rows[0] as TSelect) ?? null);
    } catch (e: any) {
      return this.fail("GET_ERROR", e.message);
    }
  }

  async create(data: TInsert): Promise<Result<TSelect>> {
    try {
      const parsed = this.insertSchema.parse(data);
      const rows = await (db as any).insert(this.table).values(parsed).returning();
      return this.ok(rows[0] as TSelect);
    } catch (e: any) {
      if (e.name === "ZodError") return this.fail("VALIDATION_ERROR", e.message);
      return this.fail("CREATE_ERROR", e.message);
    }
  }

  async update(id: string, data: TUpdate): Promise<Result<TSelect>> {
    try {
      const existing = await this.getById(id);
      if (!existing.success) return existing;
      if (existing.data === null) return this.fail("NOT_FOUND", `Record with id ${id} not found`);
      const parsed = this.updateSchema.parse(data);
      const tableAny = this.table as any;
      if (!tableAny.id) return this.fail("NO_ID_COLUMN", "Table does not have an id column");
      const rows = await (db as any).update(this.table).set(parsed).where(eq(tableAny.id, id)).returning();
      if (rows.length === 0) return this.fail("NOT_FOUND", `Record with id ${id} not found`);
      return this.ok(rows[0] as TSelect);
    } catch (e: any) {
      if (e.name === "ZodError") return this.fail("VALIDATION_ERROR", e.message);
      return this.fail("UPDATE_ERROR", e.message);
    }
  }

  async delete(id: string): Promise<Result<{ deleted: boolean }>> {
    try {
      const existing = await this.getById(id);
      if (!existing.success) return existing;
      if (existing.data === null) return this.fail("NOT_FOUND", `Record with id ${id} not found`);
      const tableAny = this.table as any;
      if (!tableAny.id) return this.fail("NO_ID_COLUMN", "Table does not have an id column");
      const rows = await (db as any).delete(this.table).where(eq(tableAny.id, id)).returning();
      if (rows.length === 0) return this.fail("NOT_FOUND", `Record with id ${id} not found`);
      return this.ok({ deleted: true });
    } catch (e: any) {
      return this.fail("DELETE_ERROR", e.message);
    }
  }

  async getBySlug(slug: string): Promise<Result<TSelect | null>> {
    try {
      const tableAny = this.table as any;
      if (!tableAny.slug) return this.fail("NO_SLUG_COLUMN", "Table does not have a slug column");
      if ("organization_id" in tableAny && tableAny.organization_id != null) {
        const tenantOrgId = getTenantOrgId();
        if (!tenantOrgId) return this.fail("TENANT_NOT_CONFIGURED", "TENANT_ORG_ID is required.");
        const rows = await (db as any).select().from(this.table)
          .where(and(eq(tableAny.slug, slug), eq(tableAny.organization_id, tenantOrgId))).limit(1);
        return this.ok((rows[0] as TSelect) ?? null);
      }
      const rows = await (db as any).select().from(this.table).where(eq(tableAny.slug, slug)).limit(1);
      return this.ok((rows[0] as TSelect) ?? null);
    } catch (e: any) {
      return this.fail("GET_BY_SLUG_ERROR", e.message);
    }
  }

  async listByColumn(
    column: string,
    value: unknown,
    orderByColumn?: string,
    direction: "asc" | "desc" = "asc",
  ): Promise<Result<TSelect[]>> {
    try {
      const tableAny = this.table as any;
      if (!tableAny[column]) return this.fail("NO_COLUMN", `Table does not have column: ${column}`);
      let query = (db as any).select().from(this.table).where(eq(tableAny[column], value));
      if (orderByColumn && tableAny[orderByColumn]) {
        query = query.orderBy((direction === "desc" ? desc : asc)(tableAny[orderByColumn]));
      }
      const rows = await query;
      return this.ok(rows as TSelect[]);
    } catch (e: any) {
      return this.fail("LIST_BY_COLUMN_ERROR", e.message);
    }
  }
}
```

### 3f. Generation scripts

Write each script to `scripts/`. These read `schema.ts` and generate the upper layers. Copy each script exactly as shown below — do not alter the logic.

**`scripts/generate-schema.ts`** — introspects the live DB and writes `src/lib/database/schema.ts`. Full file contents: read from the reference project at `scripts/generate-schema.ts`. This script handles:
- Connecting to `DATABASE_URL` (from `.env.local`)
- Fetching tables, columns, foreign keys, unique constraints
- Topological sorting by FK dependencies
- Mapping PostgreSQL types to Drizzle column builders
- Using `id()`, `createdAt()`, `updatedAt()` helpers for standard patterns
- Writing `src/lib/database/schema.ts`

**`scripts/generate-zod-schemas.ts`** — reads `schema.ts`, writes `src/lib/schemas/index.ts`. For each table generates:
- `EntitySelectSchema`, `EntityInsertSchema`, `EntityUpdateSchema`, `EntityFiltersSchema`
- Types: `Entity`, `EntityCreate`, `EntityUpdate`, `EntityFilters`
- `BaseFiltersSchema` with `limit`, `offset`, `search`
- Entity filter extensions for `id`, `status`, `user_id`, `content_type` when those columns exist

**`scripts/generate-services.ts`** — reads `schema.ts`, writes `src/lib/services/simplified/<kebab>.service.ts` for each table + `index.ts` barrel. Each service class:
- Named `EntityService` (PascalCase)
- Extends `SimplifiedService<typeof tableVar, Entity, EntityCreate, EntityUpdate, EntityFilters>`
- Exports a singleton: `export const entityVarService = new EntityService()`

**`scripts/generate-routes.ts`** — reads `schema.ts`, writes `src/app/api/simplified/<kebab>/route.ts` for each table. Each route exports `GET`, `POST`, `PATCH`, `DELETE` handlers following the standard pattern (filters in query params for GET/DELETE, body for POST/PATCH, `id` extracted from body for PATCH).

**`scripts/generate-hooks.ts`** — reads `schema.ts`, writes `src/hooks/simplified/<kebab>.hooks.ts` + `index.ts` barrel. Each file exports:
- `entityVarKeys` query key factory
- `useEntityList(filters?)` — useQuery calling GET
- `useEntity(id)` — useQuery with `enabled: !!id`
- `useEntityCreate()` — useMutation calling POST, invalidates lists
- `useEntityUpdate()` — useMutation calling PATCH, invalidates lists + detail
- `useEntityDelete()` — useMutation calling DELETE, invalidates lists
- Imports `buildQueryString` from `./query-utils`

> **Important:** When writing the generation scripts, read the actual scripts from the reference project (`scripts/generate-*.ts`) via the Read tool and write them verbatim. Do not recreate them from scratch — use the exact, battle-tested implementations.

### 3g. Validation scripts

Write each validation script to `scripts/`. Read from the reference project and write verbatim:

| Script | What it validates |
|--------|------------------|
| `scripts/validate-db-alignment.ts` | Layer 1: schema.ts table count === DB table count |
| `scripts/validate-semantic-alignment.ts` | Layer 2: all Zod exports exist for every table |
| `scripts/validate-services-alignment.ts` | Layer 3: service file + class + extends SimplifiedService |
| `scripts/validate-routes-alignment.ts` | Layer 4: route file + GET/POST/PATCH/DELETE exports |
| `scripts/validate-hooks-alignment.ts` | Layer 5: hooks file + keys + all 4 hook exports + QueryClientProvider |
| `scripts/validate-ui-alignment.ts` | Layer 6: List component exists importing from hooks |

### 3h. Package.json scripts

Add to `package.json` scripts (merge, don't replace existing):

```json
{
  "scripts": {
    "drizzle:gen": "drizzle-kit generate",
    "drizzle:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
    "db:check": "tsx scripts/validate-db-alignment.ts",
    "generate:schemas": "tsx scripts/generate-zod-schemas.ts",
    "contracts:check": "tsx scripts/validate-semantic-alignment.ts",
    "generate:services": "tsx scripts/generate-services.ts",
    "services:check": "tsx scripts/validate-services-alignment.ts",
    "generate:routes": "tsx scripts/generate-routes.ts",
    "routes:check": "tsx scripts/validate-routes-alignment.ts",
    "generate:hooks": "tsx scripts/generate-hooks.ts",
    "hooks:check": "tsx scripts/validate-hooks-alignment.ts",
    "generate:ui": "tsx scripts/generate-ui-components.ts",
    "ui:check": "tsx scripts/validate-ui-alignment.ts",
    "validate:all": "pnpm db:check && pnpm contracts:check && pnpm services:check && pnpm routes:check && pnpm hooks:check && pnpm ui:check"
  }
}
```

---

## Phase 4 — Generate Layer 1: Drizzle Schema

Run:

```bash
npx tsx scripts/generate-schema.ts
```

This reads the live database and writes `src/lib/database/schema.ts`. After completion:

1. Read the generated file and count the `export const X = pgTable(` lines.
2. Compare to the DB table count from Phase 1.
3. Run `pnpm db:check` (or `npx tsx scripts/validate-db-alignment.ts`) and show the JSON output.
4. **Required status: LOCKED** (both counts must match).

If the count doesn't match:
- Use `mcp__supabase__execute_sql` to list the specific tables that are missing or extra
- Manually inspect the generated schema for type-annotated exports (`export const X: PgTableWithColumns<any> = pgTable(`) — these are counted by the validator but not by the L2–L6 scripts
- Report the discrepancy to the user before proceeding

---

## Phase 5 — Generate Layer 2: Zod Schemas

Run:

```bash
pnpm generate:schemas
```

Then validate:

```bash
pnpm contracts:check
```

Show the JSON output. **Required status: LOCKED.**

If any schemas are missing:
- Run `pnpm generate:schemas` again to regenerate
- If still missing, the table likely has a type annotation in schema.ts — check if it needs to be added to the generator's regex

---

## Phase 6 — Generate Layer 3: Services

Run:

```bash
pnpm generate:services
```

Then validate:

```bash
pnpm services:check
```

Show the JSON output. **Required status: LOCKED.**

Confirm `src/lib/services/simplified/base.service.ts` exists. If missing, write it from Phase 3e above.

---

## Phase 7 — Generate Layer 4: API Routes

Run:

```bash
pnpm generate:routes
```

Then validate:

```bash
pnpm routes:check
```

Show the JSON output. **Required status: VALIDATED** (GET/POST/PATCH/DELETE present for each entity).

---

## Phase 8 — Generate Layer 5: React Hooks

Run:

```bash
pnpm generate:hooks
```

Then validate:

```bash
pnpm hooks:check
```

Show the JSON output. **Required status: LOCKED.**

Note: The hooks validator also checks that `src/app/providers.tsx` (or equivalent) exports a `QueryClientProvider`. If this file doesn't exist in the target project, create it or ask the user where React Query is initialized. The `QueryClientProvider` must wrap the app tree for hooks to work.

If providers.tsx doesn't exist, create a minimal one:

```typescript
// src/app/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
```

---

## Phase 9 — Cross-Reference with Supabase MCP

After generation, perform a final DB cross-reference:

1. Use `mcp__supabase__execute_sql` to list all tables in the target project.
2. Read `src/lib/database/schema.ts` and count `pgTable` exports.
3. Compare:
   - Tables in DB but not in schema → missing from Layer 1
   - Tables in schema but not in DB → schema has phantom tables
4. For any mismatch: report to the user with exact table names.

Also spot-check a sample table by comparing its columns:

```sql
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = '<sample_table>'
ORDER BY ordinal_position;
```

Read the corresponding Drizzle definition in `schema.ts` and verify the columns match.

---

## Phase 10 — Run Full Validation

```bash
pnpm validate:all
```

Show the complete output. All layers must pass:

| Layer | Command | Required Status |
|-------|---------|-----------------|
| 1 | `pnpm db:check` | LOCKED |
| 2 | `pnpm contracts:check` | LOCKED |
| 3 | `pnpm services:check` | LOCKED |
| 4 | `pnpm routes:check` | VALIDATED |
| 5 | `pnpm hooks:check` | LOCKED |
| 6 | `pnpm ui:check` | VALIDATED (skip if no L6 yet) |

If any layer fails, fix bottom-up:
- **Layer 1 fails**: regenerate schema, align with DB
- **Layer 2 fails**: run `pnpm generate:schemas`, or fix the generator regex for type-annotated tables
- **Layer 3 fails**: run `pnpm generate:services`, check base.service.ts exists
- **Layer 4 fails**: run `pnpm generate:routes`
- **Layer 5 fails**: run `pnpm generate:hooks`, check providers.tsx has QueryClientProvider
- **Layer 6 fails**: run `pnpm generate:ui` (or note that L6 is not yet implemented)

---

## Phase 11 — Final Report

Output a structured report:

```markdown
## Type Safety Chain: Implementation Complete

### Database
- **Project:** [supabase project ID]
- **Tables in DB:** [N]
- **Tables in schema.ts:** [N]
- **Alignment:** LOCKED ✓ / UNLOCKED ✗

### Layer Status
| Layer | Name | Status | Count |
|-------|------|--------|-------|
| 1 | Drizzle Schema | LOCKED | N tables |
| 2 | Zod Schemas | LOCKED | N entities |
| 3 | Services | LOCKED | N services |
| 4 | API Routes | VALIDATED | N routes |
| 5 | React Hooks | LOCKED | N hook files |
| 6 | UI Components | VALIDATED / NOT YET | N components |

### Files Written
- `drizzle.config.ts`
- `src/lib/database/client.ts`
- `src/lib/database/schema.ts` (generated from DB)
- `src/lib/tenant.ts`
- `src/lib/schemas/index.ts` (generated)
- `src/lib/services/simplified/base.service.ts`
- `src/lib/services/simplified/index.ts` + N entity services
- `src/app/api/simplified/<N entity routes>`
- `src/hooks/simplified/query-utils.ts`
- `src/hooks/simplified/index.ts` + N hook files
- `scripts/generate-schema.ts`
- `scripts/generate-zod-schemas.ts`
- `scripts/generate-services.ts`
- `scripts/generate-routes.ts`
- `scripts/generate-hooks.ts`
- `scripts/validate-*.ts` (6 validation scripts)

### Issues / Gaps
- [Any tables that don't match, missing columns, schema discrepancies]
- [Any layers that failed validation and were fixed or need attention]

### Next Steps
- Wire Layer 6 (UI): run `pnpm generate:ui` then use the `/stitch-react` or `/new-page` skill to build pages
- Add custom services to `src/lib/services/custom/` for non-CRUD logic
- Add custom hooks to `src/hooks/custom/` for non-entity queries
- Set `TENANT_ORG_ID` in `.env.local` if tables have `organization_id` columns
- Run `pnpm validate:all` any time schema changes are made
```

---

## Naming Conventions

All names in upper layers derive from the Drizzle `export const <varName> = pgTable(...)` variable name:

| Transform | Rule | Example (input: `bookChapters`) |
|-----------|------|----------------------------------|
| PascalCase | `s[0].toUpperCase() + s.slice(1)` | `BookChapters` |
| kebab-case | Replace `([a-z])([A-Z])` → `$1-$2`, lowercase | `book-chapters` |
| Schema exports | `BookChaptersSelectSchema`, `BookChaptersInsertSchema`, etc. | — |
| Service class | `BookChaptersService extends SimplifiedService` | — |
| Service file | `book-chapters.service.ts` | — |
| Route path | `src/app/api/simplified/book-chapters/route.ts` | — |
| Hook keys | `bookChaptersKeys` | — |
| Hook functions | `useBookChaptersList`, `useBookChaptersCreate`, etc. | — |
| Hook file | `book-chapters.hooks.ts` | — |

---

## Critical Rules

1. **Types flow downstream only.** Never import from a higher layer into a lower one.
2. **Fix bottom-up.** If Layer 3 fails, don't patch the service file — check if Layer 1/2 is correct first.
3. **Generated files are not hand-edited.** If a generated file needs custom logic, add it in the `custom/` directories (`src/lib/services/custom/`, `src/hooks/custom/`, `src/app/api/custom/`).
4. **DB is the source of truth.** Never add tables to schema.ts that don't exist in the database.
5. **Never run MCP destructive operations.** Only use `execute_sql` for `SELECT` statements (read-only introspection). Never `INSERT`, `UPDATE`, `DELETE`, or `DROP` via MCP.
6. **Lock-before-proceed.** Validate each layer before generating the next. Never skip layers.
7. **`base.service.ts` is never overwritten by generators.** The generate-services script explicitly skips it.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.