| 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:
chmod 0600 .env
Example contents:
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):
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:
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
User-provided secrets (OAuth credentials, SMTP passwords, API keys, etc.):
-
Write .env with REPLACE_WITH_ placeholders for user secrets. For example:
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"
-
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:
{ 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. |
-
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.
-
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.
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
}
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
- Pub/Sub:
LISTEN/NOTIFY — no extension needed; combine with a persistence layer for durability. See 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
- 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
- Vectors:
pgvector — embeddings storage and similarity search with HNSW and IVFFlat indexes. See references/pgvector.md
- JSON validation:
pg_jsonschema — validate json/jsonb columns against JSON Schema via CHECK constraints. See 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 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.
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)
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:
# 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:
[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:
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).
-
Ensure mise.toml has the [settings] section above; add it if missing.
-
Update mise itself:
brew upgrade mise
mise self-update -y
-
Upgrade the project tools:
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.
CONTAINER_NAME="$(basename "$(pwd)")-db"
podman run -d \
--name "$CONTAINER_NAME" \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
supabase/postgres:17.6.1.142
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).
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:
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 -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 -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:
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
Vite proxies /api/* and /auth/* to the Go backend.
Stopping all project containers
REPO_NAME="$(basename "$(pwd)")"
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 -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):
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
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
- Key routes (home, dashboard, main features).
- Authenticated routes (via dev login).
- Pages affected by this session's work.
- 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:
- Write or update the SQL queries in
backend/internal/database/queries/*.sql
- Run sqlc generate:
bash -c 'cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate'
- 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.
- 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
- 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
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 -- SMTP gateway setup: Gmail (prototyping) and Locaweb (production) for sending e-mails (reminders, auth links, notifications, etc.)
- references/google-auth.md -- Google Auth OAuth setup: Google Cloud Console configuration, consent screen, credentials
Postgres Extension References
- references/pgroonga.md -- PGroonga full-text search: operators, ranking, highlighting, CJK support
- references/pgmq.md -- pgmq message queue: SQL examples for send, read, archive, delete
- references/pg-cron.md -- pg_cron + pg_net: scheduled jobs, HTTP triggers, common patterns
- references/pgvector.md -- pgvector similarity search: distance operators, HNSW/IVFFlat indexes, tuning
- references/pg-jsonschema.md -- pg_jsonschema validation: CHECK constraint pattern, core functions
- references/notify-patterns.md -- LISTEN/NOTIFY + persistence: pgmq for job queues, regular tables for data updates, polling fallback