---
name: tech-stack
description: Go + React full-stack architecture with iterative local development. Use this skill when scaffolding a new app, adding features, fixing bugs, running the local dev loop, or when the user asks to "run the app locally", "run the app on my computer", "start the app", "rodar o programa", "rodar o app", "executar localmente", or any equivalent request to launch the application in their local environment. Covers project layout, database migrations, sqlc code generation, local Supabase/Postgres via Podman, Postgres extensions (pgmq, pg_cron, pgroonga, pgvector, pg_jsonschema, LISTEN/NOTIFY), and the write-test-repeat feedback cycle.
---

# Tech Stack

Go JSON API + React SPA served from a single binary and deployed as one container.

## Architecture

**Backend:** Go stdlib `net/http` router, `pgx/v5` for Postgres, `sqlc` for query generation, `slog` for logging, embedded SQL migrations via `go:embed`.

**Frontend:** Vite + React + TypeScript, shadcn/ui components, Tailwind CSS, React Router. ALWAYS use the **frontend-design** skill when writing or modifying frontend code — it provides the design direction for layouts, styling, component aesthetics, and UI polish.

## Project Layout

```
.
├── backend/
│   ├── cmd/server/main.go       # Entrypoint
│   ├── internal/
│   │   ├── config/              # Env var parsing
│   │   ├── database/
│   │   │   ├── migrations/*.sql # Embedded, forward-only
│   │   │   ├── queries/*.sql    # sqlc source
│   │   │   └── sqlc/            # Generated — do not edit
│   │   └── handler/             # HTTP handlers (JSON API)
│   ├── go.mod
│   ├── go.sum
│   └── sqlc.yaml
├── e2e/                        # Visual checks (Playwright screenshots)
│   └── package.json
├── frontend/                    # Vite + React SPA
│   ├── src/
│   │   ├── components/ui/       # shadcn/ui (generated, editable)
│   │   └── pages/               # Route-level components
│   └── dist/                    # Build output (gitignored)
└── Dockerfile
```

Ensure the project `.gitignore` includes at least:

```
backend/server  # add the actual output path used by `go build`
frontend/dist/
frontend/node_modules/
e2e/node_modules/
.venv/
.env
```

`backend/cmd/server/server` is the locally compiled Go binary produced by `go build`. It must not be committed.

`.env` holds **all** service connection strings and application secrets needed to run locally. It must **never** be committed. After creating or updating it, restrict permissions:

```bash
chmod 0600 .env
```

Example contents:

```env
DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
BASE_URL="http://localhost:5173"
REDIS_URL="redis://localhost:6379"
N8N_WEBHOOK_URL="http://localhost:5678/webhook"
```

Always quote values with double quotes — some values (e.g., SMTP app passwords) contain spaces.

The Go backend reads these values via `os.Getenv()`. During local development, load the file before starting the server using `. .env` (POSIX dot syntax — not `source`, which is a bash builtin and may not work in all shells):

```bash
set -a && . .env && set +a
```

This bridges the gap between local and deployed: locally `.env` provides the values; deployed, Kamal config provides them. The Go code stays the same (`os.Getenv("REDIS_URL")`).

### Entering secrets in the local `.env` file

**Never ask for secret values through the chat.**

**Agent-generated secrets** (JWT keys, session secrets, HMAC keys, etc.) — generate and write directly:

```bash
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
```

**User-provided secrets** (OAuth credentials, SMTP passwords, API keys, etc.):

1. Write `.env` with `REPLACE_WITH_` placeholders for user secrets. For example:

   ```env
   DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
   JWT_SECRET="<already generated by agent>"
   GOOGLE_CLIENT_ID="REPLACE_WITH_GOOGLE_CLIENT_ID"
   GOOGLE_CLIENT_SECRET="REPLACE_WITH_GOOGLE_CLIENT_SECRET"
   SMTP_PASSWORD="REPLACE_WITH_SMTP_APP_PASSWORD"
   ```
2. Open `.env` in the user's editor using the full absolute path. Do not offer pasting values in the chat as an alternative. Pick the
   right command for the user's platform — detect WSL on Linux via:

   ```bash
   { grep -qi microsoft /proc/version 2>/dev/null \
     || grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; } \
     && echo WSL
   ```

   | Platform | Command (the agent runs it directly) |
   |----------|---------------------------------------|
   | macOS                      | `open -a TextEdit /Users/name/project/.env` |
   | Windows (native, git bash) | `start notepad "C:\Users\name\project\.env"` |
   | WSL on Windows             | `notepad.exe /home/name/project/.env` (WSL maps the path automatically) |
   | Linux (native)             | Ask the user to run `nano <ABSOLUTE_PATH>/.env` in a separate terminal. |

3. Walk them through obtaining each credential (use reference docs where applicable: `references/google-auth.md`, `references/smtp-gateway.md`), and expressly tell them not to paste values into the chat. Ask them to save and confirm.
4. Validate: read `.env`, check `REPLACE_WITH_` prefixes are gone. **Never output actual secret values.**

`mise.toml` should **not** be gitignored — it is committed to the repo so all developers use the same tool versions.

## Project Documentation

Maintain these artifacts in the project's `docs/` directory:

### `docs/PRD.md` — Product Requirements Document

Describes **WHAT** the web app delivers, not how it works. Must be independent from technical implementation.

**Sections:** Overview, Target Users, Core Features, User Flows, Non-Functional Requirements, Out of Scope.

When gathering requirements:
- Help the user fill in blanks when requirements are vague, incomplete, ambiguous, or contradictory.
- Suggest ideas the user may not have thought of that make sense given the web app's context.
- Keep the PRD always up-to-date as requirements evolve.

### `docs/TASKS.md` — Development Task Tracker

Generated from the PRD, reflecting development phases. Tasks must account for the technical context of the available skills.

**Format per task:** Task name | Status (Pending / In Progress / Done / Blocked) | Reason/Notes

Keep up-to-date as work progresses.

### `docs/adr/NNN-topic.md` — Architecture Decision Records

Numbered markdown files for each technical decision.

**Template:**
```
# NNN - Title

**Status:** Accepted | Rejected | Superseded by [ADR-NNN]

## Context
[What situation or requirement prompted this decision]

## Decision
[What was decided]

## Rationale
[Why this choice was made]

## Trade-offs
**Pros:**
- ...

**Cons:**
- ...

## Alternatives Considered
- [Alternative 1]: [Why discarded]
- [Alternative 2]: [Why discarded]
```

Focus on **why** a choice was made, what was considered, and what was discarded. No need for deep implementation details — the code itself is the documentation.

### `docs/INFRASTRUCTURE.md` — Service Inventory

Lists every service the app depends on beyond the Go binary. Created when the first accessory is introduced; may be empty for apps with no external services.

**Format:**

| Name | Image | Local Port | Env Var | Type |
|------|-------|-----------|---------|------|
| db | supabase/postgres:17.6.1.111 | 5432 | DATABASE_URL | backend |
| redis | redis:7-alpine | 6379 | REDIS_URL | backend |
| n8n | n8nio/n8n:latest | 5678 | — | standalone |

**Type:** `backend` = consumed by web/workers via env var. `standalone` = accessed directly by user in browser (no Go integration).

## Key Decisions

### Single-binary serving

The Go server handles everything: API routes under `/api/`, frontend static files, and SPA routing. In development, Vite's dev server proxies API calls to the Go backend.

In local development, the Vite dev server is the user-facing entry point — all browser traffic goes through it, and the proxy forwards `/api/*` and `/auth/*` to Go transparently. This means external-facing URLs — including OAuth redirect URIs configured in third-party consoles (e.g., Google Cloud) — must use the Vite port, not the Go backend port. Set `BASE_URL` in `.env` to the Vite dev server origin (e.g., `http://localhost:5173`). In deployed environments, `BASE_URL` is set to the app's public URL (e.g., `https://myapp.example.com`) via the deploy config — see the **app-deploy** skill. The Go backend reads `BASE_URL` from the environment in all cases — no Host header inference needed.

The SPA catch-all **must not** serve `index.html` for every non-API path. Doing so returns HTML with `text/html` content type for `.js`, `.css`, and other hashed assets under `/assets/`, causing browsers to reject them with MIME type errors — the app will appear completely broken in production even though it works in development (where Vite's dev server handles assets directly). Always use the pattern below: check whether the requested path matches a real file in `dist/` and serve it via `http.FileServer` (which sets the correct `Content-Type` automatically), falling back to `index.html` only for SPA routes that don't correspond to files on disk. `http.Dir` restricts access to the specified directory, so this is safe against path traversal.

> **`frontendDist` must be the literal string `"frontend/dist"`.** Never use `"../frontend/dist"`, never use an absolute path like `"/frontend/dist"`, and never add fallback logic that tries multiple paths. The binary's working directory in production is the Dockerfile's `WORKDIR` — not `backend/`. The path `../frontend/dist` seems correct when looking at the local repo layout (Go runs from `backend/`), but in the Docker container it escapes to the wrong parent and causes a 404. This mistake is invisible during local development because Vite serves the frontend and the static-file handler is not registered when `DEV_MODE` is set — the error only surfaces after deploy. The Dockerfile section below shows the matching layout.

```go
frontendDist := "frontend/dist"
if _, err := os.Stat(frontendDist); err == nil && !cfg.DevMode {
    fs := http.FileServer(http.Dir(frontendDist))
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/auth/") {
            http.NotFound(w, r)
            return
        }
        // Serve real files from dist; fall back to index.html for SPA routes
        if r.URL.Path != "/" {
            if _, err := os.Stat(filepath.Join(frontendDist, filepath.Clean(r.URL.Path))); err == nil {
                fs.ServeHTTP(w, r)
                return
            }
        }
        http.ServeFile(w, r, filepath.Join(frontendDist, "index.html"))
    })
} else if !cfg.DevMode {
    slog.Warn("frontend dist not found — SPA routes will return 404", "path", frontendDist)
}
```

### Postgres first

PostgreSQL is the primary external service. Use Postgres-backed alternatives whenever possible:
- **Queues:** `pgmq` — lightweight message queue with visibility timeout, archive, and batch operations. See [references/pgmq.md](references/pgmq.md)
- **Pub/Sub:** `LISTEN`/`NOTIFY` — no extension needed; combine with a persistence layer for durability. See [references/notify-patterns.md](references/notify-patterns.md)
- **Caching:** unlogged tables
- **Scheduling:** `pg_cron` + `pg_net` — in-database cron plus async HTTP requests for triggering app endpoints on a schedule. **Preferred over container-level cron.** See [references/pg-cron.md](references/pg-cron.md)
- **Search:** `pgroonga` — full-text search for all languages including CJK, with boolean queries, ranking, highlighting, and JSONB search. No configuration needed. When the app involves searchable content (products, articles, listings, messages, logs), **proactively propose adding search**. Falls back to native `tsvector`/`tsquery` only when a simpler built-in solution suffices for a single well-supported language. See [references/pgroonga.md](references/pgroonga.md)
- **Vectors:** `pgvector` — embeddings storage and similarity search with HNSW and IVFFlat indexes. See [references/pgvector.md](references/pgvector.md)
- **JSON validation:** `pg_jsonschema` — validate `json`/`jsonb` columns against JSON Schema via CHECK constraints. See [references/pg-jsonschema.md](references/pg-jsonschema.md)
- **Geospatial:** `postgis` — geometry types, spatial indexes, and geographic functions. See <https://postgis.net/>
- **HTTP from SQL:** `pg_net` — asynchronous HTTP/HTTPS requests from SQL; used with `pg_cron` for scheduled calls or from triggers for webhooks
- Other notable extensions: `pgjwt`, `pg_stat_statements`, `pgaudit`, `pg_hashids`

All 60+ bundled extensions from `supabase/postgres` are available.

When the application needs a capability that Postgres and its extensions cannot provide — a pre-built tool like n8n, a specialized system like Kafka, or a use case where a dedicated service is clearly superior — add it as an accessory. See [Local Services](#local-services) for running it locally and `docs/INFRASTRUCTURE.md` for recording it. At deployment time, each accessory gets its own VM (see the **app-deploy** skill).

### sqlc for all queries

All SQL lives in `backend/internal/database/queries/*.sql`. Run `cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate` after changes. **Never write raw SQL strings in Go handler code.**

Always include `emit_json_tags: true` in `sqlc.yaml` so that generated Go structs include lowercase JSON tags (e.g., `json:"id"` instead of exporting `ID` as-is). Without this, the API returns PascalCase field names that don't match frontend expectations.

### Migrations at startup

Embedded SQL files applied in order before the server accepts traffic. Forward-only, numbered sequentially (`001_create_users.sql`, `002_add_tasks.sql`, …). Each migration should be idempotent where possible (`CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`).

The `go:embed` directive only accepts files in the same directory or subdirectories of the file that declares it — paths with `..` are rejected by the compiler. Place the embed directive in a Go file next to the `migrations/` directory (e.g., `backend/internal/database/migrate.go`), not in `cmd/server/main.go`.

### Database connection retry

The Go backend should retry the database connection at startup (up to 6 attempts with exponential backoff: 1 s, 2 s, 4 s, 8 s, 16 s, 32 s — ~63 s total). This handles parallel startup — Preview starts all servers simultaneously, so the backend may come up before the database is ready — and is also good practice for production deployments.

```go
var pool *pgxpool.Pool
for i := range 6 {
    pool, err = pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err == nil {
        if err = pool.Ping(ctx); err == nil {
            break
        }
        pool.Close()
    }
    delay := time.Second * (1 << i) // 1s, 2s, 4s, 8s, 16s, 32s
    slog.Warn("database not ready, retrying", "attempt", i+1, "delay", delay, "err", err)
    time.Sleep(delay)
}
if err != nil {
    slog.Error("failed to connect to database", "err", err)
    os.Exit(1)
}
```

### Real-time updates via SSE

The Go backend listens for Postgres `NOTIFY` events and holds open a standard HTTP response with `Content-Type: text/event-stream` for each connected client. The React frontend uses the browser's built-in `EventSource` API. SSE is preferred over WebSockets to avoid adverse proxy configurations.

### Vite scaffold cleanup

After running `npm create vite`, remove all Vite/React template defaults before writing application code:

- Delete `frontend/.git` if it exists (Vite scaffolding initializes its own git repo — it must be removed to avoid a nested repository)
- Delete `public/vite.svg` (the Vite logo — must not ship as the app's favicon)
- Delete `src/assets/react.svg`
- Set `<title>` in `index.html` to the app's actual name (not `"frontend"` or `"Vite + React"`)
- Replace the boilerplate `App.tsx`/`App.css` with the application's root component

Do **not** create a placeholder `public/favicon.svg` — the **frontend-design** skill handles favicon generation separately.

## Deployment Constraints

These match the **app-deploy** skill requirements:

- Always source port from `PORT` env var (set to `80` for deployed app, `8080` for local dev)
- Health check: **`GET /up` → HTTP 200**
- Database via `DATABASE_URL` (preferred) or individual `POSTGRES_*` env vars — **fail hard if missing**
- File storage at `BLOB_STORAGE_PATH` (e.g. `/data/blobs`)
- PostgreSQL as the primary data store (with 60+ bundled extensions via `supabase/postgres`). If the application needs services beyond what Postgres provides, additional accessories can be added via the deploy skill's Kamal layer
- No ORMs, no JavaScript frameworks beyond React, no CSS preprocessors

## Dockerfile

Multi-stage: (1) build frontend with Node, (2) build Go binary, (3) minimal Alpine runtime with binary + `frontend/dist/` + CA certs. The Go binary embeds migrations; frontend assets are copied alongside the binary.

The `FROM` tags in the Dockerfile use the same major versions as `mise.toml` with the `-alpine` suffix — e.g., `FROM node:24-alpine` and `FROM golang:1-alpine`. Docker Hub resolves these floating tags to the latest minor/patch at build time, so the Dockerfile stays in sync with `mise.toml` without manual version lookups. When a new Go or Node minor/patch is released, the next build picks it up automatically. The GHA BuildKit cache detects the manifest change and rebuilds from that layer down.

**The runtime stage must place the binary and `frontend/dist/` as siblings under `WORKDIR`:**

```dockerfile
# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

# Stage 2: Build backend
FROM golang:1-alpine AS backend-build
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

# Stage 3: Runtime — binary and frontend/dist as siblings under WORKDIR
FROM alpine:3
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=backend-build /server ./server
COPY --from=frontend-build /app/frontend/dist ./frontend/dist
EXPOSE 80
ENV PORT=80
CMD ["./server"]
```

The specific `WORKDIR` path does not matter — what matters is that both the binary and `frontend/dist/` are placed directly under it. The Go code uses `frontendDist := "frontend/dist"` (see the SPA catch-all above), so the Dockerfile must place the assets at that relative path from the binary's working directory. Since `CMD` runs from `WORKDIR`, copying both into `WORKDIR` satisfies this. In local dev, Vite serves the frontend, so the Go handler for static files is only registered when `DEV_MODE` is not set.

**Do not create a `.dockerignore` file.** The multi-stage build already keeps the final image small, and a `.dockerignore` that accidentally excludes files needed by `go:embed` (e.g., `migrations/`) will break the build with no clear error at authoring time.

## Local Development

All tools are invoked via **mise** (set up by **computer-setup**) using the `mise x` command, which reads `mise.toml` and runs the tool at the pinned version without requiring shell activation. The database runs as a `supabase/postgres` container via **podman** (also set up by **computer-setup**), matching the production image.

> **Container naming convention:** Each project's database container is named `<repo_name>-db` (e.g., `myapp-db`), where `<repo_name>` is the basename of the project's root directory. This prevents collisions when multiple cofounder projects coexist on the same machine. Derive the name once at the start of the session and use it consistently for all `podman` commands.

> **Critical: `go.mod` lives in `backend/`, not in the project root.** All Go and sqlc commands (`mise x -- go run`, `mise x -- go build`, `mise x -- go test`, `mise x -- go mod tidy`, `mise x -- sqlc generate`) **must** execute from the `backend/` directory. Always include `cd "$(git rev-parse --show-toplevel)/backend" &&` inside the `bash -c` string. When a command chain involves multiple layers of shell invocation (bash → go), prefer writing a small helper script instead of nesting everything in a single `bash -c` string — this avoids the most common source of repeated build failures.

### Project tool versions

On first setup (when `mise.toml` does not yet exist in the project root), create it manually:

```toml
[tools]
go = "1"
sqlc = "1"
python = "3.14"
node = "24"
jq = "1"

[settings]
python.compile = false
```

`python.compile = false` is required — it pins Python to the newest *precompiled* patch instead of compiling from source (which fails without build deps).

Then trust and install the tools:

```bash
mise trust
mise install
```

This `mise.toml` is committed to the repo, ensuring all developers use the same versions. `mise.toml` specifies major versions only (e.g., `go = "1"`); mise resolves these to the latest stable minor/patch at install time. To change a major version, edit `mise.toml` and re-run `mise install`.

All tool invocations in this skill use the `mise x` command (e.g., `mise x -- go run ./cmd/server`). This runs the tool at the version specified in `mise.toml` without requiring shell activation hooks — it works reliably in Claude Code's non-interactive shell, in Preview's `launch.json`, and in any script context.

### Upgrade tools

**Run on every session** (this is not part of service startup — it is a standalone step that must execute every time this skill is loaded).

1. Ensure `mise.toml` has the `[settings]` section above; add it if missing.
2. Update mise itself:

   ```bash
   # macOS:
   brew upgrade mise
   # Linux, WSL, Windows:
   mise self-update -y
   ```

3. Upgrade the project tools:

   ```bash
   mise upgrade
   ```

Use `mise upgrade`, not `mise install` — `install` skips already-installed versions; `upgrade` checks for newer patches.

### 1. Start the database and local services

The command below uses the default Postgres port `5432`. If that port is already in use on the host (by another project's container, or another Postgres instance) and the container fails to start, pick any free port, update `DATABASE_URL` in `.env` to use it, and change the `-p` flag to match it. The `env` file is the source of truth.

```bash
# Derive the container name from the repo directory
CONTAINER_NAME="$(basename "$(pwd)")-db"

# Start supabase/postgres container (matching production image)
# Important: provide only the POSTGRES_PASSWORD environment variable. The database is started with both user and database name preset to `postgres`.
# Match the port in DATABASE_URL in .env.
podman run -d \
  --name "$CONTAINER_NAME" \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  supabase/postgres:17.6.1.136

# Verify it's ready (uses container exec instead of pg_isready)
podman exec "$CONTAINER_NAME" pg_isready -U postgres
```

#### Local Services

When the application needs services beyond Postgres, run them as podman containers locally. Each service maps to an accessory that will be provisioned as a dedicated VM in deployment.

#### Naming convention

Every project container is named `<repo_name>-<accessory_name>`:

```
myapp-db
myapp-redis
myapp-n8n
```

The `-db` convention already exists for Postgres. Extend it to all accessories. This enables the cleanup pattern (see [Stopping all project containers](#stopping-all-project-containers)).

#### Start-or-create pattern

Containers persist across sessions. On a fresh session the containers from the previous session may already exist (stopped). Always use the start-or-create pattern instead of a bare `podman run`:

```bash
podman start myapp-redis 2>/dev/null || \
  podman run -d --name myapp-redis -p 6379:6379 redis:7-alpine
```

`podman start` succeeds silently if the container exists (running or stopped). If it doesn't exist, the fallback `podman run` creates it. This prevents "name already in use" errors on session resume.

#### Two accessory types

**Backend-connected** (Redis, Kafka, etc.): start-or-create → readiness check → add env var to `.env` → add Go client → record in `docs/INFRASTRUCTURE.md`.

**Standalone** (n8n, WordPress, etc.): start-or-create → readiness check → give user `localhost:<port>` link → record in `docs/INFRASTRUCTURE.md`. No Go code unless the backend also calls its API.

**Hybrid** (e.g., n8n with webhook): treat as standalone (browser link) AND add backend env var (e.g., `N8N_WEBHOOK_URL`). Type stays `standalone`; the env var column signals the backend dependency.

#### Port conflict detection

Before starting a container, check `podman ps -a --format '{{.Names}} {{.Ports}}' | grep '<port>'`. If the default port is already in use, pick any free port, update the URL env var in `.env` (e.g., `REDIS_URL`) to use it, and expose that port in the container. The `.env` file is the source of truth.

#### Common recipes

| Accessory | Image | Port | Readiness check |
|-----------|-------|------|-----------------|
| Redis | `redis:7-alpine` | 6379 | `podman exec <name> redis-cli ping` |
| n8n | `n8nio/n8n:latest` | 5678 | `curl -s http://localhost:5678/healthz` |
| Meilisearch | `getmeili/meilisearch:latest` | 7700 | `curl -s http://localhost:7700/health` |
| WordPress | `wordpress:latest` | 8080 | `curl -s http://localhost:8080` |
| WAHA | `devlikeapro/waha:latest` | 3000 | `curl -s http://localhost:3000/api/health` |

These are starting points. The agent should check the image's documentation for the correct ports and readiness endpoints.

#### Local vs. deployed hostnames

| Service | Local (`.env`) | Deployed hostname |
|---------|---------------|-------------------|
| Postgres | `localhost:5432` | `db:5432` |
| Redis | `localhost:6379` | `redis:6379` |
| n8n | `localhost:5678` | `n8n:5678` |

Locally, all services are on `localhost`. Deployed, each accessory gets its own VM — hostname matches the accessory name via CloudStack internal DNS. Never use public IPs for inter-service communication. `.env` (local) and Kamal config (deployed) provide the values; Go reads them identically via `os.Getenv()`.

### 2. Start the Go API (terminal 1)

```bash
bash -c 'ROOT="$(git rev-parse --show-toplevel)" && set -a && . "$ROOT/.env" && set +a && cd "$ROOT/backend" && DEV_MODE=1 mise x -- go run ./cmd/server'
```

### 3. Start the Vite dev server (terminal 2)

```bash
bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npm install && mise x -- npm run dev'
```

Don't assume the default Vite port (5173) — Vite automatically picks the next available port when the default is already in use by another project. After starting the dev server in the background, read the task output and look for the `Local:` line in Vite's startup banner (e.g., `Local: http://localhost:5174/`). Use the URL from that line — not a hardcoded port — for all subsequent access.

If the task output is no longer available, detect the port from the OS:

```bash
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
```

Vite proxies `/api/*` and `/auth/*` to the Go backend.

### Stopping all project containers

```bash
REPO_NAME="$(basename "$(pwd)")"

# Stop and remove all containers for this project
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman stop
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman rm
```

This relies on the naming convention (`<repo_name>-<accessory_name>`) and removes all project containers at once.

## Visual Check (Playwright screenshots)

> **Do NOT use Claude Desktop Preview** servers. Start Go backend and Vite dev server manually (steps 2–3 above).

After tests pass, take headless screenshots and review visually. Works on all platforms.

### Setup

`e2e/` at project root has a minimal `package.json` with `playwright`. On first setup:

```bash
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npm install && mise x -- npx playwright install chromium'
```

The `e2e/` directory is completely separate from `frontend/` — the Dockerfile never touches it, so it has no impact on the production image.

### Taking screenshots

Confirm the actual Vite port first (auto-increments if 5173 is taken):

```bash
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
```

```bash
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npx playwright screenshot --viewport-size="1280,720" --full-page http://localhost:5173 /tmp/homepage.png'
```

For mobile (if PRD requires): `--viewport-size="375,812"`. For authenticated routes: write a script in `e2e/` that calls `POST /api/dev/login`, gets a cookie, navigates, and screenshots. Read screenshots with the Read tool.

### Scope of visual check

1. Key routes (home, dashboard, main features).
2. Authenticated routes (via dev login).
3. Pages affected by this session's work.
4. Aesthetics — alignment, padding, spacing, readability, contrast.

Fix anything that looks off and re-screenshot before committing.

## Local Development Feedback Loop

**Write code + tests → run tests → visual check → fix & repeat → update docs → commit & push → wrap up.**

```
Write/Edit Code + Tests
    ↓
Start services (podman, go run, npm run dev)
    ↓
Run tests (Layer 1: Go, Layer 2: Vitest + tsc -b)
    ↓
All pass? ──No──► Fix & repeat
    ↓ Yes
Visual check (screenshots)
    ↓
Looks right? ──No──► Fix & repeat
    ↓ Yes
Update docs (TASKS, PRD, ADRs, INFRASTRUCTURE)
    ↓
Commit & push → Wrap up session
```

After committing, present the session wrap-up as defined in the cofounder agent.

### sqlc workflow

**Always follow this order — never skip step 3:**

1. Write or update the SQL queries in `backend/internal/database/queries/*.sql`
2. Run sqlc generate:
   ```bash
   bash -c 'cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate'
   ```
3. **Read the generated `.go` files** in `backend/internal/database/sqlc/` to confirm exact struct/field names before writing handlers. sqlc names are unpredictable:
- Positional parameters with type casts (e.g., `$2::boolean`) become `Column2`, `Column3`, etc. — not the column names.
- Queries with partial `RETURNING` clauses generate a separate row type (e.g., `UpsertUserRow`) distinct from the full model (`User`).
- Assuming field names without reading the output leads to type mismatch errors that require back-and-forth corrections.
4. Write the Go handlers using the exact names from the generated files. Never hand-write SQL in Go files.

## Conventions

- **Thin handlers:** parse request → call database → return JSON. No business logic in handlers.
- **Logging:** `slog` exclusively. Never `fmt.Println` or `log.Println`.
- **Validation:** Server-side validation for all inputs. Never trust client-side validation alone.
- **Authorization:** Checks in every handler, not just middleware.
- **Frontend components:** `bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npx shadcn@latest add <component>'`
- **No ORMs.** SQL through sqlc only.
- **No CSS preprocessors.** Tailwind CSS only.
- **No additional JavaScript frameworks.** React + React Router only.

## Authorization Best Practices

If the application has a user login area, **self sign-in with username and password** is acceptable for quickly prototyping the app, but **never for production** — the security of this method is weak.

**Recommended approach:** start with self sign-in (username + password) for prototyping, then move as soon as possible to one or both of:

- **Email with magic link** — practical, doesn't require memorization. Requires configuration of an SMTP gateway. See [references/smtp-gateway.md](references/smtp-gateway.md)
- **Google Auth** — practical, doesn't require memorization. Relies on Google's security mechanisms. Requires configuration of Google Auth (cost free). See [references/google-auth.md](references/google-auth.md)

## Dev Login for Testing

During local development, the agent needs to test pages behind authentication (via the visual check's Playwright screenshots). Magic link and Google Auth flows cannot be completed in automated tests, so the backend must expose a **dev-only login endpoint** that bypasses the real auth flow.

### How it works

The backend registers a `POST /api/dev/login` route **only when `DEV_MODE=1`** is set. The guard must be at route registration time (not middleware) so the endpoint physically does not exist without the flag.

This endpoint accepts a user identifier (e.g., email), looks up the user, and creates a session using the exact same mechanism the real auth flow uses — same cookie name, same token format, same session store. The only difference is that it skips the external provider (magic link email or Google OAuth).

### Test user

The dev login handler should create the user on the fly if it doesn't already exist (`INSERT ... ON CONFLICT DO NOTHING`). This keeps the test user out of migrations, which run in all environments including production.

### Security

- The endpoint must **never** be registered when `DEV_MODE` is unset — enforced by the guard at route registration time.
- Production deployments must **never** set `DEV_MODE`. The deploy skill does not include it.

## Other References

- **[references/smtp-gateway.md](references/smtp-gateway.md)** -- SMTP gateway setup: Gmail (prototyping) and Locaweb (production) for sending e-mails (reminders, auth links, notifications, etc.)
- **[references/google-auth.md](references/google-auth.md)** -- Google Auth OAuth setup: Google Cloud Console configuration, consent screen, credentials

## Postgres Extension References

- **[references/pgroonga.md](references/pgroonga.md)** -- PGroonga full-text search: operators, ranking, highlighting, CJK support
- **[references/pgmq.md](references/pgmq.md)** -- pgmq message queue: SQL examples for send, read, archive, delete
- **[references/pg-cron.md](references/pg-cron.md)** -- pg_cron + pg_net: scheduled jobs, HTTP triggers, common patterns
- **[references/pgvector.md](references/pgvector.md)** -- pgvector similarity search: distance operators, HNSW/IVFFlat indexes, tuning
- **[references/pg-jsonschema.md](references/pg-jsonschema.md)** -- pg_jsonschema validation: CHECK constraint pattern, core functions
- **[references/notify-patterns.md](references/notify-patterns.md)** -- LISTEN/NOTIFY + persistence: pgmq for job queues, regular tables for data updates, polling fallback
