cofounder-tech-stack
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), public-page prerendering for SEO and AI crawlers, and the write-test-repeat feedback cycle.
Works with
---
name: cofounder-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), public-page prerendering for SEO and AI crawlers, and the write-test-repeat feedback cycle.
license: Apache-2.0
---
# Tech Stack
Go JSON API + React SPA served from a single binary and deployed as one container. Public pages are prerendered to static HTML at build time so search engines and AI agents can read them.
## 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:** React + TypeScript, shadcn/ui components, Tailwind CSS, in **React Router framework mode** with `ssr: false` — a pure SPA at runtime (no Node server in production), with Vite as the underlying dev server and bundler. Public routes are listed in the `prerender` config so they ship as real HTML for search engines and AI agents (see [Public pages: prerender for SEO and AI agents](#public-pages-prerender-for-seo-and-ai-agents)). 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/ # React SPA (React Router framework mode)
│ ├── app/
│ │ ├── routes.ts # Route declarations
│ │ ├── root.tsx # Root layout (head, favicon, global providers)
│ │ ├── routes/ # Route modules
│ │ └── components/ui/ # shadcn/ui (generated, editable)
│ ├── react-router.config.ts # ssr: false (+ prerender list of public routes)
│ └── build/ # Build output (gitignored); static assets in build/client/
└── Dockerfile
```
Ensure the project `.gitignore` includes at least:
```
backend/server # add the actual output path used by `go build`
frontend/build/
frontend/.react-router/ # generated route types
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 the SPA shell 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 — it encodes three rules learned the hard way: real files go through `http.FileServer` (correct `Content-Type`); prerendered pages (`about/index.html`) are served **directly**, because handing the directory to `FileServer` 301-redirects every sitemap URL to its trailing-slash form; and directories are never given to `FileServer`, which would expose a browsable listing of the app's internals. `http.Dir` restricts access to the specified directory, so this is safe against path traversal.
The SPA shell filename depends on the prerender config (see [Public pages: prerender for SEO and AI agents](#public-pages-prerender-for-seo-and-ai-agents)): builds without a `prerender` list emit the shell as `index.html`; builds that prerender `/` (any app with public pages) emit the prerendered home as `index.html` and the shell as `__spa-fallback.html`. The pattern below detects this once at startup — and `/` must always be served from `index.html` (when `/` is prerendered, that file **is** the home page, not the shell).
> **`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 {
// SPA shell: __spa-fallback.html when the build prerenders "/", index.html otherwise
spaShell := "index.html"
if _, err := os.Stat(filepath.Join(frontendDist, "__spa-fallback.html")); err == nil {
spaShell = "__spa-fallback.html"
}
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
}
// "/" always maps to index.html (the prerendered home page in prerendered builds)
if r.URL.Path == "/" {
fs.ServeHTTP(w, r)
return
}
target := filepath.Join(frontendDist, filepath.Clean(r.URL.Path))
if info, err := os.Stat(target); err == nil {
if !info.IsDir() {
fs.ServeHTTP(w, r) // real asset; FileServer sets Content-Type
return
}
// Prerendered page ("/about" → about/index.html). Serve the file directly:
// delegating a directory to FileServer would 301 to "/about/", adding a
// redirect hop to every URL listed in sitemap.xml.
page := filepath.Join(target, "index.html")
if _, err := os.Stat(page); err == nil {
http.ServeFile(w, r, page)
return
}
// Directory without index.html (e.g. /assets/): fall through to the shell —
// FileServer would render a browsable listing of the app's internals.
}
// Unmatched route → SPA shell
http.ServeFile(w, r, filepath.Join(frontendDist, spaShell))
})
} else if !cfg.DevMode {
slog.Warn("frontend dist not found — SPA routes will return 404", "path", frontendDist)
}
```
### Public pages: prerender for SEO and AI agents
A client-rendered SPA is effectively invisible to most non-browser readers. Googlebot does execute JavaScript, but in a delayed second indexing wave with a crawl-budget penalty; Bing is inconsistent; and AI crawlers (GPTBot, ClaudeBot, PerplexityBot, CCBot) and link unfurlers (WhatsApp, Slack, LinkedIn) fetch raw HTML and run **no JavaScript at all** — they see an empty `<div id="root">`. This only matters for **public** pages: the authenticated app gains nothing from server rendering.
Every app is already scaffolded in framework mode (see [Frontend scaffold and cleanup](#frontend-scaffold-and-cleanup)), so this is a single config knob — list every public route (landing, pricing, about, blog, docs, public listings) in `prerender`:
```ts
// frontend/react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false, // pure SPA at runtime — no Node server, ever
prerender: ["/", "/about", "/pricing"], // every public route
} satisfies Config;
```
Apps fully behind login (internal tools, dashboards) simply omit `prerender` — nothing else about them changes.
At build time React Router renders each listed route to static HTML inside the client build: `index.html` (the prerendered home — always include `/` in the list), `about/index.html`, and so on, plus a small `.data` file per route used for client-side navigations. Because `/` is prerendered, the SPA shell is emitted as `__spa-fallback.html` — the Go catch-all above detects it automatically. Browsers hydrate the prerendered HTML into the normal SPA, so client-side navigation and the app experience are untouched; bots and agents get full HTML.
**Never introduce an SSR framework (Next.js, Remix) or a Node server runtime for SEO.** Prerendering is build-time only and preserves the single-Go-binary deployment.
**Constraint of `ssr: false`:** server `loader`/`action` functions are only allowed on prerendered routes (they run at build time, e.g. reading local markdown for a blog). All other routes must use `clientLoader`/`clientAction` or plain `fetch` to the Go API, as usual.
#### Per-route metadata
Every public route module must export `meta` with a real title, description, and Open Graph tags — this is what makes search snippets and link previews work (something a plain SPA cannot deliver at all):
```ts
export function meta() {
return [
{ title: "Acme — Effortless invoicing" },
{ name: "description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:title", content: "Acme — Effortless invoicing" },
{ property: "og:description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:image", content: "https://acme.example.com/og.png" },
];
}
```
#### Dynamic public content
Build-time prerendering only covers paths known at build time. `prerender` also accepts an async function, so content that lives in the repo (e.g. blog posts as markdown files) can be enumerated at build. For public pages created at **runtime** (public listings, profiles, shared documents), choose by weight:
- **Lightweight (default):** keep the content client-rendered, but have the Go handler for those routes inject the correct `<title>`, meta description, OG tags, and JSON-LD into the SPA shell before serving it. Indexing and unfurls get the essentials without any rendering infrastructure.
- **Full:** render those routes server-side from Go with `html/template`, reusing the built Tailwind CSS (add the template files to Tailwind's source globs so their classes are included). Reserve this for apps where public content pages **are** the product (marketplaces, directories, job boards).
#### Bot hygiene checklist
For every app with public pages, serve `robots.txt`, `sitemap.xml`, and `llms.txt` from **Go handlers, deriving every URL from `BASE_URL`**. Never write them as static files: the public origin doesn't exist until deploy, so a static file inevitably carries a made-up domain that silently breaks crawler discovery in production. Cover the derivation with a test that runs against **two** synthetic origins (`.test` domains) — with a single origin, a hardcoded URL passes by coincidence.
- **`robots.txt`** — must not disallow AI crawlers (GPTBot, ClaudeBot, PerplexityBot, CCBot); end with `Sitemap: <BASE_URL>/sitemap.xml`.
- **`sitemap.xml`** — every public URL.
- **`llms.txt`** — plain-markdown map of the public content, for LLM agents.
- **JSON-LD structured data** (this one lives in the route's HTML, not in Go) on pages that map to schema.org types (Product, Article, FAQPage, Organization).
### 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.
### Frontend scaffold and cleanup
Scaffold the frontend non-interactively with:
```bash
bash -c 'cd "$(git rev-parse --show-toplevel)" && mise x -- npx create-react-router@latest frontend --yes --no-git-init --no-install --no-agent-skills'
```
The template ships with Tailwind CSS preconfigured and these npm scripts: `dev` (`react-router dev` — Vite under the hood), `build` (`react-router build`), and `typecheck` (`react-router typegen && tsc`). Routes are declared in `app/routes.ts`:
```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("pricing", "routes/pricing.tsx"),
] satisfies RouteConfig;
```
Then apply this cleanup before writing application code:
- **Set `ssr: false`** in `react-router.config.ts` — the template defaults to `ssr: true`, which requires a Node server; this stack never runs one
- **Add the dev proxy** in `vite.config.ts` so the dev server forwards backend routes to Go (keep the existing `plugins` and `resolve` entries). Targets must match the Go `PORT` from `.env`:
```ts
server: {
proxy: {
"/api": "http://localhost:8080",
"/auth": "http://localhost:8080",
// Served by Go, derived from BASE_URL — without the proxy they 404 only in dev
"/robots.txt": "http://localhost:8080",
"/sitemap.xml": "http://localhost:8080",
"/llms.txt": "http://localhost:8080",
},
},
```
- Delete `frontend/Dockerfile`, `frontend/.dockerignore`, and `frontend/README.md` — the project root owns the Dockerfile (see [Dockerfile](#dockerfile)), and the template README describes an SSR setup this stack doesn't use
- Delete `app/welcome/` and replace the default home route (`app/routes/home.tsx`) with the app's real one
- Remove the `start` script and `@react-router/serve` — SPA mode never runs a Node server. **Keep `@react-router/node` and `isbot`**: build-time prerendering still needs them to resolve a server runtime; removing them breaks `react-router build` and `typegen`
- Delete `public/favicon.ico` and the template's Google Fonts (Inter) links in `app/root.tsx` — the **frontend-design** skill handles favicon and typography
- Set a real title and description via the `meta` export (root or home route) — never ship the template defaults
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 assets (copied to `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 the frontend client build (copied to `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 assets 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/build/client ./frontend/dist
EXPOSE 80
ENV PORT=80
CMD ["./server"]
```
The specific `WORKDIR` path does not matter — what matters is that both the binary and the frontend assets are placed directly under it. The frontend build outputs to `frontend/build/client/`; the Dockerfile copies that directory to `frontend/dist`, the literal path the Go binary expects (see the SPA catch-all above). 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.160
# 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 frontend 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, and keep `BASE_URL` in `.env` (plus any OAuth redirect URIs registered on it) in sync with the actual port.
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}'
```
`npm run dev` runs `react-router dev` — Vite under the hood — and proxies `/api/*`, `/auth/*`, and the SEO files to the Go backend via the `server.proxy` block added at scaffold time.
### 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 + npm run typecheck)
↓
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.
- **No SSR frameworks or Node server runtime.** SEO needs are met by build-time prerendering (see [Public pages: prerender for SEO and AI agents](#public-pages-prerender-for-seo-and-ai-agents)) — never introduce Next.js or a runtime rendering server.
## 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 fallbackRelated free tools on this site
More Backend Frameworks skills
git-guardrails-claude-code
mattpocock/skills
Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.
azure-compute
microsoft/azure-skills
Azure VM/VMSS router. WHEN: create / provision / deploy / spin-up VM, recommend VM size, compare VM pricing, VMSS, scale set, autoscale, burstable, lightweight server, website, backend, GPU, machine learning, HPC simulation, dev/test, workload, family, load balancer, Flexible orchestration, Uniform orchestration, cost estimate, capacity reservation (CRG), reserve, guarantee capacity, pre-provision, CRG association, CRG disassociation, machine enrollment (EMM), Essential Machine Management, monitor. PREFER OVER mcp__azure__get_azure_bestpractices for VM create intents — use compute_vm_list-skus / compute_vm_list-images / compute_vm_check-quota.
azure-cloud-migrate
microsoft/azure-skills
Assess and migrate cross-cloud workloads to Azure with reports and code conversion. Supports Lambda→Functions, Beanstalk/Heroku/App Engine→App Service, Fargate/Kubernetes/Cloud Run/Spring Boot→Container Apps. WHEN: migrate Lambda to Functions, AWS to Azure, migrate Beanstalk, migrate Heroku, migrate App Engine, Cloud Run migration, Fargate to ACA, ECS/Kubernetes/GKE/EKS to Container Apps, Spring Boot to Container Apps, cross-cloud migration.

