Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
psql_bp:
image: postgres:latest
image: postgres:16
Comment thread
coderabbitai[bot] marked this conversation as resolved.
restart: unless-stopped
environment:
POSTGRES_DB: ${BLUEPRINT_DB_DATABASE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func mustStartPostgresContainer() (func(context.Context, ...testcontainers.Termi

container, err := tcpostgres.Run(
context.Background(),
"postgres:latest",
"postgres:16",
tcpostgres.WithDatabase(dbName),
tcpostgres.WithUsername(dbUser),
tcpostgres.WithPassword(dbPwd),
Expand Down
1 change: 1 addition & 0 deletions web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Read the relevant doc before implementing. These are kept in sync with the code
|---|---|
| App Router, route files, layouts, navigation | [`docs/routing.md`](docs/routing.md) |
| Server Components, data fetching, Server Actions | [`docs/data-fetching.md`](docs/data-fetching.md) |
| tRPC routers, React Query, client/server usage | [`docs/trpc.md`](docs/trpc.md) |
| Tailwind CSS v4, theme tokens, dark mode | [`docs/styling.md`](docs/styling.md) |
| Component conventions, TypeScript rules | [`docs/components.md`](docs/components.md) |
| Testing setup, patterns, what to test | [`docs/testing.md`](docs/testing.md) |
Expand Down
14 changes: 14 additions & 0 deletions web/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { type NextRequest } from 'next/server'
import { createTRPCContext } from '@/server/trpc'
import { appRouter } from '@/server/routers/_app'

const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ req }),
})

export { handler as GET, handler as POST }
5 changes: 4 additions & 1 deletion web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -27,7 +28,9 @@ export default function RootLayout({
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full flex flex-col">
<Providers>{children}</Providers>
</body>
</html>
);
}
10 changes: 8 additions & 2 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import About from "@/components/home/about";
import Hero from "@/components/home/hero";

const page = () => {
return (
<div></div>
<div>
<Hero />
<About />
</div>
);
};

export default page;
export default page;
7 changes: 7 additions & 0 deletions web/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import { TRPCProvider } from '@/lib/trpc/client'

export function Providers({ children }: { children: React.ReactNode }) {
return <TRPCProvider>{children}</TRPCProvider>
}
5 changes: 5 additions & 0 deletions web/components/home/about.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const About = () => {
return <section></section>;
};

export default About;
5 changes: 5 additions & 0 deletions web/components/home/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Hero = () => {
return <section></section>;
};

export default Hero;
3 changes: 2 additions & 1 deletion web/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ The `docs` agent reads this index first to locate the right file.
| Topic | File | Source files covered |
|---|---|---|
| App Router structure & route conventions | [routing.md](routing.md) | `app/layout.tsx`, `app/page.tsx`, `next.config.ts` |
| Data fetching patterns | [data-fetching.md](data-fetching.md) | `app/page.tsx`, `app/layout.tsx` |
| Data fetching patterns | [data-fetching.md](data-fetching.md) | `app/page.tsx`, `app/layout.tsx`, `lib/trpc/server.ts`, `lib/trpc/client.tsx` |
| tRPC v11 + React Query v5 — routers, context, client/server usage | [trpc.md](trpc.md) | `server/trpc.ts`, `server/routers/_app.ts`, `lib/trpc/client.tsx`, `lib/trpc/server.ts`, `app/providers.tsx` |
| Styling with Tailwind CSS v4 | [styling.md](styling.md) | `app/globals.css`, `postcss.config.mjs` |
| Component conventions | [components.md](components.md) | `app/` (all component files) |
| Testing patterns | [testing.md](testing.md) | `vitest.config.ts`, `vitest.setup.ts`, `__tests__/page.test.tsx` |
Expand Down
120 changes: 91 additions & 29 deletions web/docs/components.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
---
topic: components
last_verified: 2026-06-14
last_verified: 2026-06-23
sources:
- app/layout.tsx
- app/page.tsx
- components/ui/button.tsx
- components/common/container.tsx
- components/common/h1.tsx
- components/common/h2.tsx
- components/common/h3.tsx
- components/common/loader.tsx
- components/home/hero.tsx
- components/home/about.tsx
- lib/utils.ts
---

# Component Conventions
Expand All @@ -21,44 +28,99 @@ Only when you need:

Do not add `"use client"` to layouts, pages, or wrapper components just because a child needs it — push `"use client"` down to the smallest possible component.

## File placement
## Directory structure
```
app/ # route files only (page.tsx, layout.tsx, loading.tsx, error.tsx)
components/ # shared reusable components
ui/ # generic primitives (Button, Input, Modal, etc.)
[feature]/ # feature-specific components (e.g., components/auth/)
lib/ # utility functions, helpers, non-React code
types/ # shared TypeScript type definitions
components/
ui/ — shadcn primitives (avatar, badge, button, card, dialog, dropdown-menu,
input, label, separator, skeleton, sonner)
common/ — cross-feature reusable UI (container, h1, h2, h3, loader)
home/ — feature-scoped components (hero, about)
layout/ — layout wrappers (navbar, footer, etc.)
lib/
utils.ts — cn() utility and other non-React helpers
types/ — shared TypeScript type definitions
```

## Component file structure
Route files (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`) live in `app/` only.

## `cn()` utility
All conditional class merging uses `cn()` from `lib/utils.ts`:
```ts
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
Import it as `import { cn } from "@/lib/utils"` in every component that merges classes.

## `components/ui/` — shadcn primitives
Generated by shadcn/ui. Use `class-variance-authority` (`cva`) for variant logic and `Slot` from `radix-ui` for the `asChild` pattern. Do not hand-edit these files directly; re-run the shadcn CLI to update them.

Pattern from `button.tsx`:
```tsx
// components/ui/Button.tsx
import type { ButtonHTMLAttributes } from 'react';
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary';
};
const buttonVariants = cva("<base-classes>", {
variants: {
variant: { default: "...", outline: "...", secondary: "...", ghost: "...", destructive: "...", link: "..." },
size: { default: "...", xs: "...", sm: "...", lg: "...", icon: "...", "icon-xs": "...", "icon-sm": "...", "icon-lg": "..." },
},
defaultVariants: { variant: "default", size: "default" },
})

export function Button({ variant = 'primary', children, ...props }: ButtonProps) {
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<button
className={variant === 'primary' ? 'bg-foreground text-background' : 'bg-background text-foreground border'}
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
>
{children}
</button>
);
/>
)
}

export { Button, buttonVariants }
```

## `components/common/` — hand-crafted cross-feature components
These are hand-written and use `cn()` with Tailwind classes directly. They do not use `cva`.

`Container` — constrains max width and centers content:
```tsx
const Container = ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div className={cn("mx-auto max-w-7xl", className)}>{children}</div>
)
```

`H1`, `H2`, `H3` — semantic heading wrappers that forward a `className` prop through `cn()`. `H2` makes `className` optional; `H1` and `H3` require it.

`Loader` — accepts a required `className` prop and renders a `div` with those classes applied via `cn()`.

## `components/home/` — feature-scoped components
Components specific to the home feature. Scoped to avoid polluting `common/`.

## TypeScript rules
- No `any`. Use proper interfaces or `unknown`.
- Extend native HTML element types (`ButtonHTMLAttributes`, `InputHTMLAttributes`) for wrapper components.
- Keep component prop types in the same file as the component unless shared across multiple files.
- Shared types → `types/` directory.
- For shadcn primitives, extend `React.ComponentProps<"element">` (not `ButtonHTMLAttributes`) and combine with `VariantProps<typeof variantsFn>`.
- For common/feature components, inline prop types in the same file unless the type is shared across multiple files — then move it to `types/`.
- Import types with `import type` to keep runtime bundles clean.
- React 19: JSX transform is automatic — no `import React from "react"` needed unless you reference `React.*` directly.

## Imports
- Use TypeScript path aliases from `tsconfig.json` — check it before writing relative import chains.
- Import React only when needed (React 19 — JSX transform is automatic, no `import React` needed).
- Import types with `import type` to keep runtime bundles clean.
Use TypeScript path aliases (`@/`) from `tsconfig.json`. Do not write deep relative import chains.
42 changes: 41 additions & 1 deletion web/docs/data-fetching.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
---
topic: data-fetching
last_verified: 2026-06-14
last_verified: 2026-06-24
sources:
- app/page.tsx
- app/layout.tsx
- server/trpc.ts
- server/routers/_app.ts
- server/routers/health.ts
- lib/trpc/client.tsx
- lib/trpc/server.ts
- app/providers.tsx
---

# Data Fetching
Expand Down Expand Up @@ -71,3 +77,37 @@ Store in an env var for production: `process.env.NEXT_PUBLIC_API_URL` (client-ac

## Caching (Next.js 16)
Next.js 16 changes default caching behavior from v14. Check `web/node_modules/next/dist/docs/` for the current defaults before assuming cached or uncached behavior.

## tRPC (preferred for typed API calls)

For calls to the Go backend that benefit from end-to-end type safety, use tRPC instead of bare `fetch`.

**When to use tRPC vs. plain `fetch`:**
- Use tRPC when calling procedures already defined in `server/routers/` — you get compile-time type inference and no manual `res.json()` casting.
- Use plain `fetch` for one-off external APIs, webhooks, or cases where a tRPC router would be disproportionate overhead.

**Server Components** — use `createServerCaller` from `lib/trpc/server.ts`:
```tsx
import { createServerCaller } from '@/lib/trpc/server'

export default async function HealthPage() {
const caller = await createServerCaller()
const health = await caller.health.query()
return <p>Status: {health.status}</p>
}
```

**Client Components** — use `trpc.<router>.<procedure>.useQuery()` or `useMutation()` from `lib/trpc/client.tsx`:
```tsx
'use client'
import { trpc } from '@/lib/trpc/client'

export function HealthStatus() {
const { data, isLoading, isError } = trpc.health.query.useQuery()
if (isLoading) return <p>Loading…</p>
if (isError) return <p>Error</p>
return <p>Status: {data?.status}</p>
}
```

See [`docs/trpc.md`](trpc.md) for full setup details, context, protected procedures, and how to add new routers.
Loading
Loading