# CodeAI.md - AI Coding Assistant Context
<!-- Content for your AGENTS.md / CLAUDE.md: what an agent must know to write correct code HERE -->
<!-- Codebase map, patterns, anti-patterns, MCP access, and what to confirm before implementing -->
<!-- Last updated: 2026-07-27 -->

## Where This File Lives

This is content for the agent context file at the root of your repository. Two filenames matter:

- **AGENTS.md** - the cross-tool standard for agent instructions. Plain Markdown, no YAML frontmatter, no required schema. It is read natively by the major coding agents, editor assistants, and CLIs, so one file serves whatever tool a teammate happens to use.
- **CLAUDE.md** - Claude Code's own context file. Claude Code reads AGENTS.md as well and merges both, plus any files in subdirectories, so you do not need to duplicate content between them.

**Precedence is nearest-file-wins.** The context file closest to the file being edited governs, and an explicit instruction in the prompt overrides every file on disk. In a monorepo that means you should not write one enormous root file:

```text
repo/
  AGENTS.md               # what the product is, how to build and test everything
  packages/
    api/AGENTS.md         # API-only: router pattern, migration workflow
    web/AGENTS.md         # UI-only: component conventions, design tokens
    worker/AGENTS.md      # queue semantics, idempotency requirements
```

Keep each file short and specific. Analysis across thousands of public repositories found that the most effective context files were roughly 20 to 30 lines. Three sections earn their place almost everywhere, in this order:

1. **Project overview** - what it is, language and framework, with versions pinned somewhere the agent can read rather than restated in prose.
2. **Build and test commands** - the exact command with its flags, not the name of the tool. "Run the tests with Vitest" is useless; `npm run test:unit -- --run` is actionable.
3. **Code style rules that DIFFER from language defaults** - do not restate what your formatter already enforces.

Everything else belongs somewhere that loads on demand: a skill, a runbook, a linked document the agent opens when the task calls for it. Context is not free. Every token in an always-loaded file competes with the code the agent actually needs to read.

Rules that must never be skipped do not belong in prose at all. A context file is advisory - a model can decide a paragraph does not apply. Encode non-negotiables as a hook, a lint rule, or a CI gate so they execute whether or not the agent read this far. See `coding.md` for the enforcement hierarchy.

## Codebase Overview

**Project**: Vaultline - Secure Document Management Platform
**Language**: TypeScript, strict mode
**Runtime**: Node, current LTS line - the exact version is pinned in `.nvmrc`
**Framework**: Next.js App Router with tRPC
**Database**: PostgreSQL via Drizzle ORM
**Auth**: NextAuth with Google and email/password providers

Confirm the environment before you start, rather than assuming versions from memory:

```bash
node --version                                  # expect the version pinned in .nvmrc
npm ls next drizzle-orm @trpc/server --depth=0  # expect the ranges pinned in package.json
```

### High-Level Architecture

```text
vaultline/
  src/
    app/                     Next.js App Router pages
      (auth)/                Auth route group (login, register, reset)
      (dashboard)/           Authenticated route group
        documents/           Document listing, search, filters
        folders/             Folder management and navigation
        settings/            User and org settings
        layout.tsx           Dashboard shell with sidebar
      api/trpc/              tRPC endpoint handler
      layout.tsx             Root layout (providers, fonts, metadata)
      page.tsx               Landing page (marketing)
    components/
      ui/                    Design-system primitives (button, dialog, input)
      documents/             Document-specific components
      folders/               Folder tree and navigation
      shared/                Cross-feature components (search, empty states)
    server/
      db/                    Drizzle schema, migrations, seed
        schema.ts            All table definitions
        migrations/          SQL migration files
        index.ts             DB connection singleton
      trpc/
        routers/             Feature-specific routers
        context.ts           Request context (session, db)
        index.ts             Root router + type exports
      services/              Business logic, no framework dependencies
        document.service.ts
        folder.service.ts
        permission.service.ts
        storage.service.ts
    lib/                     Shared utilities (utils, constants, validators)
    hooks/                   Custom React hooks
    types/                   Shared TypeScript types
  tests/
    unit/                    Fast, isolated tests
    integration/             tRPC router tests against a real database
    e2e/                     Playwright browser tests
  drizzle.config.ts
  next.config.ts
```

Data flows in one direction. An agent that puts logic in the wrong layer produces code that passes review by looking familiar and fails later:

```mermaid
graph LR
  A[Server Component] --> B[tRPC Router]
  C[Client Component] --> B
  B --> D[Permission Service]
  B --> E[Domain Service]
  E --> F[(Drizzle / PostgreSQL)]
  E --> G[Storage Service]
```

### Key Entry Points

- **App shell**: `src/app/(dashboard)/layout.tsx` - sidebar, header, auth guard
- **tRPC root router**: `src/server/trpc/index.ts` - every API procedure
- **Database schema**: `src/server/db/schema.ts` - every table definition
- **Auth config**: `src/lib/auth.ts` - providers, callbacks, session strategy

## MCP Servers This Repo Expects

Static prose cannot tell an agent the current schema, the open issues, or last night's error rate. Tool access can. Document which Model Context Protocol servers this repository expects, what each is for, what credential scope it needs, and - most importantly - whether it can write. Declare them in the repo's MCP config (commonly `.mcp.json`) so a fresh clone gets the same wiring.

| Server | Purpose | Credential and scope | Access |
|--------|---------|----------------------|--------|
| `postgres-dev` | Schema introspection, query plans, seed-data inspection | `MCP_PG_URL`, points at the local dev database only | Read-only |
| `postgres-replica` | Verify query shape against realistic row counts | Read-replica role, no write grants, no PII columns | Read-only |
| `github` | Issues, pull requests, CI status, cross-repo code search | Fine-grained token: contents read, issues and PRs write, this repo only | Write-capable |
| `sentry` | Recent error groups and stack traces for this service | Project-scoped read token | Read-only |
| `storage-dev` | Presigned upload URLs for exercising the upload path | Bucket-scoped role, `vaultline-dev-uploads` only | Write-capable |

**Rules for tool access**:

- Never point a write-capable server at production. If a task genuinely needs production data, use the read-only replica and redact before it enters context.
- Prefer the read-only server when both exist. Reach for the write-capable one only when the task is to change something.
- If an expected server is unavailable, say so and stop. Guessing at a schema you could have introspected is the most expensive kind of confident wrong answer.
- Record what a server is NOT for. `github` is for issues and CI status, not for pushing branches - branches go through the normal git workflow in `coding.md`.

## Conventions Specific to This Repo

The full naming table and the team-wide standards live in `coding.md` - that file owns them, and this one does not repeat them. What follows is only the set of rules that are specific to this codebase or that differ from the language defaults, which is the part an agent cannot infer.

```typescript
// Strict mode is ON. No implicit any, no unused variables.
// Type-only imports use the 'type' keyword so the bundler can drop them.
import type { Document } from '@/types';
import { formatDate } from '@/lib/utils';

// Interfaces for object shapes, type aliases for unions and intersections.
interface DocumentListProps {
  folderId: string;
  sortBy: 'name' | 'created' | 'modified';
  onSelect: (doc: Document) => void;
}

// Exported functions always declare an explicit return type.
export function calculateStorageUsage(files: FileRecord[]): number {
  return files.reduce((total, file) => total + file.sizeBytes, 0);
}

// 'as const' objects instead of enums.
export const DOCUMENT_STATUS = {
  DRAFT: 'draft',
  PUBLISHED: 'published',
  ARCHIVED: 'archived',
} as const;

// The service layer returns a Result type instead of throwing.
type Result<T> = { success: true; data: T } | { success: false; error: string };
```

Repo-specific naming deltas on top of the table in `coding.md`:

- Database columns are `snake_case` in the schema and `camelCase` in TypeScript. Drizzle does the mapping; do not hand-write either side.
- Environment variables carry the app prefix: `VAULTLINE_DATABASE_URL`, `VAULTLINE_S3_BUCKET`. An unprefixed variable is a bug, not a shortcut.
- Hook files are named for the hook they export: `use-document-search.ts` exports `useDocumentSearch`.
- Service files end in `.service.ts` and export functions, not a default class.

### Component Boundary

```typescript
// Server Components are the default. Add 'use client' only for hooks,
// event handlers, or browser APIs - never "just in case".

// Server Component: fetch at the component level.
export default async function DocumentsPage({ params }: { params: { folderId: string } }) {
  const documents = await trpc.document.listByFolder({ folderId: params.folderId });
  return <DocumentList documents={documents} />;
}

// Client Component: interaction only.
'use client';
export function DocumentSearch({ onResultClick }: DocumentSearchProps) {
  const [query, setQuery] = useState('');
  const results = trpc.document.search.useQuery({ query }, { enabled: query.length > 2 });
  // ...
}
```

Server components pass data down; client components handle interaction. Do not fetch in a client component when a server component above it already can.

## Patterns to Follow

The examples below come from one stack - App Router, tRPC, Drizzle. Replace them with your own. The point is not the stack; it is that a concrete, copyable example outperforms an abstract rule by a wide margin. An agent asked to "follow the service layer pattern" will invent one. An agent shown twelve lines of the real thing will match it.

### Router Pattern

```typescript
// File: src/server/trpc/routers/document.ts
import { z } from 'zod';
import { protectedProcedure, router } from '../trpc';
import * as documentService from '@/server/services/document.service';

export const documentRouter = router({
  getById: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input, ctx }) => {
      const doc = await documentService.getById(input.id);
      if (!doc) throw new TRPCError({ code: 'NOT_FOUND' });
      // Permission check happens before the record leaves the router.
      await ctx.permissions.assertCanView(ctx.session.userId, doc);
      return doc;
    }),

  create: protectedProcedure
    .input(createDocumentSchema)   // Zod schema imported from lib/validators.ts
    .mutation(async ({ input, ctx }) => {
      return documentService.create({ ...input, ownerId: ctx.session.userId });
    }),
});
```

### Service Layer Pattern

```typescript
// Services hold business logic and import nothing from the HTTP or tRPC layer.
// File: src/server/services/document.service.ts
import { db } from '@/server/db';
import { documents } from '@/server/db/schema';
import { eq } from 'drizzle-orm';

export async function getById(id: string): Promise<Document | null> {
  const [doc] = await db.select().from(documents).where(eq(documents.id, id)).limit(1);
  return doc ?? null;
}

export async function create(data: CreateDocumentInput): Promise<Document> {
  const [doc] = await db.insert(documents).values(data).returning();
  return doc;
}

// One service per domain entity. Cross-entity logic gets its own service,
// for example permission.service.ts.
```

### Error Handling Pattern

```typescript
// Transport layer: throw a typed error with the right code.
throw new TRPCError({ code: 'NOT_FOUND', message: 'Document not found' });

// Service layer: return a Result and let the caller decide.
export async function moveDocument(
  docId: string,
  targetFolderId: string,
): Promise<Result<Document>> {
  const folder = await folderService.getById(targetFolderId);
  if (!folder) return { success: false, error: 'Target folder not found' };

  const [updated] = await db
    .update(documents)
    .set({ folderId: targetFolderId, updatedAt: new Date() })
    .where(eq(documents.id, docId))
    .returning();

  return { success: true, data: updated };
}

// UI layer: error boundaries and toasts. Never surface a raw error string.
```

## Anti-Patterns to Avoid

Negative constraints are worth writing down. An agent will not infer that something is forbidden just because no example does it.

```typescript
// DO NOT fetch in a client component when a server component could.
// Bad:
'use client';
export function DocumentList() {
  const { data } = trpc.document.list.useQuery();
  return <ul>{data?.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
}
// Good: fetch in the parent server component and pass the data down.

// DO NOT put business logic in a router.
// Bad: multi-step transforms and validation inside the procedure body.
// Good: thin routers that delegate to a service.

// DO NOT write raw SQL strings. Use the query builder so types stay honest.

// DO NOT skip a permission check. Every read and every write verifies access.
// Bad: return documentService.getById(input.id);
// Good: const doc = await documentService.getById(input.id);
//       await ctx.permissions.assertCanView(ctx.session.userId, doc);

// DO NOT import server-only modules into a client component.
// Bad: import { db } from '@/server/db';  // inside a 'use client' file
// Good: call through tRPC.

// DO NOT add a dependency to solve a ten-line problem. Ask first.

// DO NOT edit generated files: drizzle migrations already applied,
// lockfiles, or anything under a directory marked generated.
```

## Testing Strategy

```text
tests/
  unit/           Fast and isolated
    services/     Service functions with the database layer mocked
    utils/        Utility functions
    validators/   Schema validation
  integration/    Real database, real router
    routers/      Procedure-level tests
    services/     Service tests against a test database
  e2e/            Browser tests
    auth.spec.ts
    documents.spec.ts
    sharing.spec.ts
```

```bash
npm run verify              # lint + types + unit tests: run this before saying "done"
npm test                    # unit tests only
npm run test:integration    # needs a running PostgreSQL
npm run test:e2e            # Playwright
npm run test:coverage       # coverage report
```

```typescript
// Unit test: mock the database boundary, not the function under test.
import { describe, it, expect, vi } from 'vitest';
import * as documentService from '@/server/services/document.service';

vi.mock('@/server/db', () => ({
  db: { select: vi.fn(), insert: vi.fn(), update: vi.fn(), delete: vi.fn() },
}));

describe('documentService.create', () => {
  it('creates a document with the default status', async () => {
    // Arrange, Act, Assert
  });

  it('rejects files over the size limit', async () => {
    // Validation path
  });
});

// E2E test: assert on roles and text, not CSS classes.
import { test, expect } from '@playwright/test';

test('a user can upload and view a document', async ({ page }) => {
  await page.goto('/documents');
  await page.getByRole('button', { name: 'Upload' }).click();
  // ...
});
```

New behaviour ships with a test. A bug fix ships with the test that would have caught it. A change that only touches types still has to pass `npm run verify`.

## Agent Guidelines

### When Writing New Code
- Read a similar existing file first and match it. Consistency beats your preferred style.
- Use the ORM query builder for every database operation.
- Add a Zod schema in `src/lib/validators.ts` for every procedure input.
- Server Components by default.
- Add the permission check before returning any record.
- Prefer editing an existing file over creating a new one.

### When Refactoring
- Run `npm run verify` after every change. Zero type errors is the bar, not a goal.
- Preserve existing coverage. If behaviour changes, change the test in the same commit and say so.
- Keep the service layer framework-agnostic.
- If you split a file, update every import in the same change - a half-moved module is worse than an unmoved one.

### Common Gotchas
- **ORM identity**: this project uses Drizzle. The Prisma API is different and the two do not mix; check `package.json` before writing query code.
- **Router style**: App Router only. There is no `pages/` directory and no `getServerSideProps`.
- **Client hooks**: the procedure hook is `trpc.router.procedure.useQuery()`, not a single top-level hook.
- **Vendored UI**: the primitives under `src/components/ui/` are copied into the repo, not installed from a package. Edit them directly; do not try to override them through a theme.
- **File uploads** always go through `storage.service.ts`, which issues presigned URLs. A procedure must never accept file bytes.
- **Timestamps** are stored as UTC. Converting for display is the client's job.

### Confirm Before Implementing
Ask these out loud in your plan rather than guessing. A wrong assumption here costs more than the question does:

- Is there an existing service function that already does this?
- Does this path need a permission check, and at which access level?
- Server component or client component?
- Which Zod schema validates this input, and does it already exist?
- Does this change the database schema? If so, where is the migration and is it reversible?
- Is there a closely similar pattern already in the codebase to match?
- Does this need a new dependency, and has anyone approved it?
