From b79a0b48f8154479eda96d4a33195cfbf4cdcd8b Mon Sep 17 00:00:00 2001 From: GRACENOBLE Date: Thu, 25 Jun 2026 16:15:09 +0300 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20add=20ADRs=20and=20deployment=20run?= =?UTF-8?q?book=20=E2=80=94=20closes=20#63=20#64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/adr/ — 8 Architecture Decision Records covering Go/Gin, PostgreSQL, Next.js App Router, Firebase Auth, Android/Compose, Redis/Asynq, Testcontainers, and Cloudflare R2; each records context, decision, positive consequences, and trade-offs - RUNBOOK.md — staging/production deployment guide covering environment variables, first-time setup, backend/web/mobile deploy, database migrations, secrets management, rollback procedures, monitoring (Prometheus, Sentry, pprof), and common operational tasks --- RUNBOOK.md | 389 +++++++++++++++++++++++++++++ docs/adr/0000-index.md | 33 +++ docs/adr/0001-go-gin-backend.md | 43 ++++ docs/adr/0002-postgresql-goose.md | 36 +++ docs/adr/0003-nextjs-app-router.md | 39 +++ docs/adr/0004-firebase-auth.md | 43 ++++ docs/adr/0005-android-compose.md | 41 +++ docs/adr/0006-redis-asynq.md | 36 +++ docs/adr/0007-testcontainers.md | 41 +++ docs/adr/0008-cloudflare-r2.md | 38 +++ 10 files changed, 739 insertions(+) create mode 100644 RUNBOOK.md create mode 100644 docs/adr/0000-index.md create mode 100644 docs/adr/0001-go-gin-backend.md create mode 100644 docs/adr/0002-postgresql-goose.md create mode 100644 docs/adr/0003-nextjs-app-router.md create mode 100644 docs/adr/0004-firebase-auth.md create mode 100644 docs/adr/0005-android-compose.md create mode 100644 docs/adr/0006-redis-asynq.md create mode 100644 docs/adr/0007-testcontainers.md create mode 100644 docs/adr/0008-cloudflare-r2.md diff --git a/RUNBOOK.md b/RUNBOOK.md new file mode 100644 index 0000000..bc4ca59 --- /dev/null +++ b/RUNBOOK.md @@ -0,0 +1,389 @@ +# Deployment Runbook + +Operational guide for deploying and maintaining the fullstack template in staging and production. + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Environment variables](#environment-variables) +- [First-time production setup](#first-time-production-setup) +- [Deploying the backend](#deploying-the-backend) +- [Deploying the web app](#deploying-the-web-app) +- [Deploying the mobile app](#deploying-the-mobile-app) +- [Database migrations](#database-migrations) +- [Secrets management](#secrets-management) +- [Rollback procedures](#rollback-procedures) +- [Monitoring and alerting](#monitoring-and-alerting) +- [Common operations](#common-operations) + +--- + +## Prerequisites + +| Tool | Purpose | Min version | +|---|---|---| +| Docker | Local Postgres + Prometheus/Grafana | 24 | +| Go | Build the backend binary | 1.25 | +| Node.js | Build the web app | 22 | +| pnpm | Web package manager | any | +| `gh` CLI | GitHub operations | 2 | +| `psql` | Database access / inspection | any | + +Production accounts required: +- **Google Firebase** project with Authentication enabled +- **Cloudflare R2** bucket (if file storage is used) +- **Sentry** project for error tracking (optional) +- **Mailjet** account for transactional email (optional) +- A PostgreSQL host: [Supabase](https://supabase.com), [Neon](https://neon.tech), [AWS RDS](https://aws.amazon.com/rds/), or [Railway](https://railway.app) +- A Redis host: [Upstash](https://upstash.com), [Redis Cloud](https://redis.com/redis-enterprise-cloud/), or [Railway](https://railway.app) + +--- + +## Environment variables + +### Backend (`backend/.env` → production secrets manager) + +| Variable | Required | Description | +|---|---|---| +| `PORT` | Yes | HTTP server port (default `8080`) | +| `ENV` | Yes | `local` / `staging` / `production` | +| `BLUEPRINT_DB_HOST` | Yes | PostgreSQL host | +| `BLUEPRINT_DB_PORT` | Yes | PostgreSQL port (default `5432`) | +| `BLUEPRINT_DB_DATABASE` | Yes | Database name | +| `BLUEPRINT_DB_USERNAME` | Yes | Database user | +| `BLUEPRINT_DB_PASSWORD` | Yes | Database password | +| `BLUEPRINT_DB_SSLMODE` | Yes | `disable` (local) / `require` (production) | +| `FIREBASE_PROJECT_ID` | Yes | Firebase project ID | +| `FIREBASE_SERVICE_ACCOUNT_JSON` | Yes | Service account key (single-line JSON) | +| `REDIS_URL` | No | Redis connection URL — omit to disable cache/queues | +| `CORS_ALLOWED_ORIGINS` | Yes | Comma-separated list, e.g. `https://app.example.com` | +| `SENTRY_DSN` | No | Sentry DSN — omit to disable | +| `RATE_LIMIT_RPS` | No | Requests/sec per IP (default: disabled) | +| `MAILJET_API_KEY` | No | Mailjet API key — omit to disable email | +| `R2_ACCOUNT_ID` | No | Cloudflare R2 account ID — omit to disable storage | + +### Web (`web/.env.local` → Vercel environment variables) + +| Variable | Required | Description | +|---|---|---| +| `AUTH_SECRET` | Yes | `openssl rand -base64 32` | +| `FIREBASE_PROJECT_ID` | Yes | Same project as backend | +| `FIREBASE_SERVICE_ACCOUNT_JSON` | Yes | Same service account as backend | +| `NEXT_PUBLIC_FIREBASE_API_KEY` | Yes | Firebase client config | +| `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN` | Yes | Firebase client config | +| `NEXT_PUBLIC_FIREBASE_PROJECT_ID` | Yes | Firebase client config | +| `BACKEND_URL` | Yes | Go backend URL (server-side only) | +| `NEXT_PUBLIC_BACKEND_URL` | Yes | Go backend URL (client-side) | +| `NEXT_PUBLIC_SENTRY_DSN` | No | Sentry DSN | + +--- + +## First-time production setup + +### 1. Provision infrastructure + +```bash +# PostgreSQL — example using Supabase CLI +supabase init && supabase start # local reference +# For production: create a Supabase project at supabase.com + +# Redis — example using Upstash +# Create a database at upstash.com → copy the Redis URL +``` + +### 2. Create Firebase project + +1. Go to [Firebase Console](https://console.firebase.google.com) → Add project. +2. Enable **Authentication** → Sign-in methods → Google + Email/Password. +3. Add an Android app: package `com.company.template` → download `google-services.json`. +4. Add a web app → copy the client config into `web/.env.local`. +5. Project settings → Service accounts → Generate new private key → download JSON. +6. Minify to a single line: `jq -c . < service-account.json` → set as `FIREBASE_SERVICE_ACCOUNT_JSON`. + +### 3. Apply database migrations + +```bash +# Point to production DB +export BLUEPRINT_DB_HOST= +export BLUEPRINT_DB_USERNAME= +export BLUEPRINT_DB_PASSWORD= +export BLUEPRINT_DB_DATABASE= +export BLUEPRINT_DB_SSLMODE=require + +cd backend && make migrate-up +``` + +### 4. Deploy backend (first time) + +See [Deploying the backend](#deploying-the-backend). + +--- + +## Deploying the backend + +The backend compiles to a single binary. Choose one deployment model: + +### Option A — Docker (recommended) + +```dockerfile +FROM golang:1.25-alpine AS build +WORKDIR /app +COPY . . +RUN go build -o /server ./cmd/api + +FROM alpine:3.21 +COPY --from=build /server /server +EXPOSE 8080 +CMD ["/server"] +``` + +```bash +docker build -t fullstack-backend:latest . +docker push /fullstack-backend:latest +# Deploy image to Railway, Fly.io, Cloud Run, ECS, etc. +``` + +### Option B — Binary on a VM + +```bash +cd backend +GOOS=linux GOARCH=amd64 go build -o server-linux ./cmd/api +scp server-linux user@host:/opt/app/server +ssh user@host "systemctl restart app" +``` + +### Environment injection + +Inject all backend environment variables as process environment (not a `.env` file) in production. Most PaaS platforms have a secrets/env UI. For self-hosted, use systemd `EnvironmentFile` or a secrets manager (AWS SSM, Doppler, 1Password Secrets Automation). + +--- + +## Deploying the web app + +### Vercel (recommended) + +```bash +# Install Vercel CLI once +pnpm add -g vercel + +cd web +vercel --prod +``` + +Set all environment variables in the Vercel dashboard under **Settings → Environment Variables**. Variables prefixed `NEXT_PUBLIC_` are embedded at build time. + +### Self-hosted (Docker / Railway) + +```bash +cd web +pnpm build # outputs .next/ +# Use the official Next.js Docker image or a Dockerfile with `next start` +``` + +The `BACKEND_URL` must be the internal URL of the backend container (e.g. `http://backend:8080`) when both run in the same Docker network. + +--- + +## Deploying the mobile app + +### Debug APK (internal testing) + +```bash +cd mobile +./gradlew assembleDebug +# Output: app/build/outputs/apk/debug/app-debug.apk +``` + +### Release APK / AAB (Google Play) + +1. Generate a signing keystore (once): + ```bash + keytool -genkey -v -keystore release.jks -alias release -keyalg RSA -keysize 2048 -validity 10000 + ``` +2. Add signing config to `mobile/app/build.gradle.kts` (do not commit the keystore). +3. Build signed release bundle: + ```bash + cd mobile + ./gradlew bundleRelease + # Output: app/build/outputs/bundle/release/app-release.aab + ``` +4. Upload the `.aab` to [Google Play Console](https://play.google.com/console). + +### CI signing (GitHub Actions) + +Store the keystore as a base64 secret: +```bash +base64 -w 0 release.jks > release.jks.b64 +# Add content as GitHub secret KEYSTORE_BASE64 +``` + +In the workflow: +```yaml +- name: Decode keystore + run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > mobile/app/release.jks +``` + +--- + +## Database migrations + +**Rule: always apply migrations before deploying a new backend version.** + +```bash +# Check pending migrations +cd backend && make migrate-status + +# Apply all pending migrations +cd backend && make migrate-up + +# Apply only the next migration (safer for large schemas) +cd backend && make migrate-up-one + +# Roll back the last migration +cd backend && make migrate-down + +# View current version +cd backend && make migrate-version +``` + +**Never edit or delete an applied migration.** If a migration was applied incorrectly, add a new migration to correct it. + +### Zero-downtime migrations + +For tables with millions of rows, prefer: +1. Additive changes first (add nullable column, add index `CONCURRENTLY`). +2. Deploy code that handles both old and new schema. +3. Backfill data in batches. +4. Add `NOT NULL` constraint in a separate migration after backfill. + +--- + +## Secrets management + +### Local development + +Copy `.env.example` to `.env` (backend) and `.env.local` (web). Never commit either file. + +```bash +cp backend/.env.example backend/.env +cp web/.env.example web/.env.local +``` + +### Staging / production + +| Platform | Recommended approach | +|---|---| +| Vercel | Environment Variables UI → mark secrets as "Production" only | +| Railway | Variables tab per service | +| Fly.io | `fly secrets set KEY=value` | +| AWS | SSM Parameter Store + IAM role | +| Self-hosted | Doppler, 1Password Secrets, or `systemd EnvironmentFile` | + +**Rotation procedure:** +1. Generate new secret value. +2. Add new value to secrets manager. +3. Redeploy the service (picks up new value from environment). +4. Revoke the old value. + +--- + +## Rollback procedures + +### Backend rollback + +```bash +# Redeploy the previous image tag +docker pull /fullstack-backend: +# Update the deployment to point to the previous tag +``` + +If the new version introduced a migration that needs to be undone: +```bash +cd backend && make migrate-down # rolls back last applied migration +# Then redeploy previous binary +``` + +### Web rollback + +On Vercel: **Deployments** tab → select the previous successful deployment → **Promote to Production**. + +On self-hosted: redeploy the previous Docker image or binary. + +### Mobile rollback + +Mobile apps cannot be forced-rollback for users who have already updated. Options: +- Use a **feature flag** to disable the problematic feature remotely. +- Release a hotfix version to the Play Store (fastest path: ~1-2 hrs for expedited review). +- Halt the staged rollout in Google Play Console before it reaches 100%. + +--- + +## Monitoring and alerting + +### Prometheus + Grafana (local / self-hosted) + +```bash +cd backend && make docker-run # starts Postgres + Prometheus + Grafana +``` + +- Prometheus: `http://localhost:9090` +- Grafana: `http://localhost:3001` (default credentials: `admin` / `admin`, override with `GRAFANA_ADMIN_USER` / `GRAFANA_ADMIN_PASSWORD`) +- Metrics endpoint: `http://localhost:8080/metrics` (restricted to loopback/private IPs in production) + +### Sentry error tracking + +Set `SENTRY_DSN` (backend) and `NEXT_PUBLIC_SENTRY_DSN` (web). Errors are captured automatically; the backend uses the Gin middleware, the web uses the Next.js Sentry plugin. + +### pprof profiling + +Available at `/debug/pprof/` — always restricted to loopback and RFC 1918 addresses: + +```bash +# CPU profile (30 s) +go tool pprof http://localhost:8080/debug/pprof/profile + +# Heap snapshot +go tool pprof http://localhost:8080/debug/pprof/heap + +# Goroutine dump +curl http://localhost:8080/debug/pprof/goroutine?debug=1 +``` + +--- + +## Common operations + +### Restart the backend + +```bash +# Docker / Railway / Fly.io +docker restart +# or trigger a redeploy in the platform UI +``` + +### Access the production database + +```bash +psql "postgresql://:@:5432/?sslmode=require" +``` + +### Inspect job queues (Asynq) + +In debug mode, the Asynqmon UI is at `http://localhost:8080/admin/queues`. For production, either: +- Run the backend locally pointed at the production Redis URL, or +- Use the [Asynq CLI](https://github.com/hibiken/asynq): `asynq dash --uri=` + +### Clear Redis cache + +```bash +redis-cli -u FLUSHDB # clears the current database only +``` + +### Scale the backend horizontally + +The backend is stateless — multiple instances can run behind a load balancer. Ensure: +- `REDIS_URL` is set (rate limiter uses Redis for distributed counting). +- Database connection pool size (`BLUEPRINT_DB_*`) is tuned per instance. +- The load balancer forwards `X-Forwarded-For` and is listed in trusted proxies if needed. diff --git a/docs/adr/0000-index.md b/docs/adr/0000-index.md new file mode 100644 index 0000000..95f8d5c --- /dev/null +++ b/docs/adr/0000-index.md @@ -0,0 +1,33 @@ +# Architecture Decision Records + +This directory captures significant architectural decisions made in the fullstack template. Each record documents the context, decision, and trade-offs at the time the choice was made. + +## Format + +Each ADR follows this structure: + +- **Status** — Accepted / Deprecated / Superseded by [NNNN] +- **Date** — When the decision was recorded +- **Context** — The forces at play that drove a decision +- **Decision** — What was chosen and why +- **Consequences** — What becomes easier, what becomes harder + +## Index + +| # | Title | Status | +|---|---|---| +| [0001](0001-go-gin-backend.md) | Go 1.25 + Gin for the backend API | Accepted | +| [0002](0002-postgresql-goose.md) | PostgreSQL 16 + goose migrations | Accepted | +| [0003](0003-nextjs-app-router.md) | Next.js 16 App Router + React 19 | Accepted | +| [0004](0004-firebase-auth.md) | Firebase Authentication (cross-platform) | Accepted | +| [0005](0005-android-compose.md) | Android + Kotlin + Jetpack Compose + Material3 | Accepted | +| [0006](0006-redis-asynq.md) | Redis + Asynq for background jobs and event streaming | Accepted | +| [0007](0007-testcontainers.md) | Testcontainers for backend integration tests | Accepted | +| [0008](0008-cloudflare-r2.md) | Cloudflare R2 for object storage | Accepted | + +## Adding a new ADR + +1. Copy the next sequential number. +2. Create `NNNN-short-title.md` in this directory. +3. Add a row to the index table above. +4. Set **Status: Accepted** when the team aligns on the decision. diff --git a/docs/adr/0001-go-gin-backend.md b/docs/adr/0001-go-gin-backend.md new file mode 100644 index 0000000..c6f393c --- /dev/null +++ b/docs/adr/0001-go-gin-backend.md @@ -0,0 +1,43 @@ +# ADR 0001 — Go 1.25 + Gin for the backend API + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs a backend API that is: +- Statically typed with compile-time safety +- Fast enough to serve real production workloads without tuning +- Simple enough that a new contributor can navigate the codebase on day one +- Compatible with a Clean Architecture layering (domain → usecase → infrastructure → transport) + +Candidates evaluated: Go + Gin, Node.js + Fastify, Rust + Axum. + +## Decision + +Use **Go 1.25** with **Gin v1.12.0** as the HTTP framework. + +The Clean Architecture is enforced via Go package boundaries: + +``` +domain/ ← entities, no external deps +usecase/ ← application logic, depends on domain +infrastructure/ ← DB, cache, third-party integrations +transport/ ← HTTP handlers and middleware +server/ ← wires all layers +cmd/ ← entry point +``` + +## Consequences + +### Positive +- Go's goroutine model handles high concurrency with a tiny memory footprint compared to thread-per-request models. +- Compile-time type checking catches integration errors before they reach production. +- Gin's middleware chain (request ID, rate limiting, CORS, auth) composes cleanly with `gin.HandlerFunc`. +- Single binary deployment — no runtime dependency management. +- `go vet` and golangci-lint enforce style uniformly in CI. + +### Negative / trade-offs +- Go's verbosity (explicit error returns, no generics-based magic) means more boilerplate than TypeScript or Python. +- Gin does not support HTTP/2 push or WebSocket natively; Gorilla WebSocket is added for WS support. +- Contributors unfamiliar with Go's `interface`-based dependency injection need a learning curve before they can add new layers cleanly. diff --git a/docs/adr/0002-postgresql-goose.md b/docs/adr/0002-postgresql-goose.md new file mode 100644 index 0000000..e3fda1c --- /dev/null +++ b/docs/adr/0002-postgresql-goose.md @@ -0,0 +1,36 @@ +# ADR 0002 — PostgreSQL 16 + goose migrations + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs a primary relational store that: +- Supports ACID transactions and foreign key constraints +- Has a wide managed-hosting ecosystem (Supabase, AWS RDS, Neon, Railway) +- Can be run locally via Docker Compose with no cloud dependency +- Integrates cleanly with Go's `database/sql` interface + +Schema evolution must be versioned, repeatable, and reviewable in pull requests. + +## Decision + +Use **PostgreSQL 16** (Docker image `postgres:16`) as the primary database. + +Use **goose v3** (via `github.com/pressly/goose/v3`) for schema migrations, SQL-only (no Go migration files). Migration files live in `backend/internal/infrastructure/database/migrations/` and are applied with `make migrate-up`. + +Use **pgx v5** (`github.com/jackc/pgx/v5`) as the driver via its `stdlib` adapter so repository code only depends on `database/sql` interfaces, not pgx types. + +## Consequences + +### Positive +- SQL migrations are version-controlled, human-readable, and reviewable in PRs. +- `goose` supports up/down migrations, named versions, and status queries — all exposed via the `Makefile`. +- pgx v5 via `stdlib` means repository interfaces stay portable; switching drivers requires no business logic changes. +- PostgreSQL's JSONB columns, full-text search, and window functions are available if needed. +- Testcontainers spins a real Postgres container in CI, so integration tests run against the same engine as production. + +### Negative / trade-offs +- Docker must be running locally for `make docker-run` and integration tests. +- Migrations cannot be rolled back automatically after data has been written; rollbacks require a new forward migration. +- No ORM — raw SQL requires more boilerplate but gives full control over query plans and avoids N+1 surprises. diff --git a/docs/adr/0003-nextjs-app-router.md b/docs/adr/0003-nextjs-app-router.md new file mode 100644 index 0000000..8448d1c --- /dev/null +++ b/docs/adr/0003-nextjs-app-router.md @@ -0,0 +1,39 @@ +# ADR 0003 — Next.js 16 App Router + React 19 + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs a web layer that: +- Supports server-side rendering and static generation without a separate server +- Enables Server Components to reduce client JavaScript bundle size +- Has a mature deployment target (Vercel, Railway, Docker) +- Works with TypeScript 5 and Tailwind CSS v4 + +Candidates evaluated: Next.js App Router, Remix, SvelteKit. + +## Decision + +Use **Next.js 16** with the **App Router** (not the legacy Pages Router), **React 19**, and **TypeScript 5**. + +Key conventions enforced: +- Default to **Server Components** — add `"use client"` only when browser APIs or React hooks are needed, pushed to the smallest possible component. +- Route groups `(auth)/` and `(dashboard)/` organise unauthenticated vs. protected pages without affecting URLs. +- **Tailwind CSS v4** for all styling — no CSS modules, no inline styles. +- **tRPC + TanStack Query** for type-safe client→server RPC calls. +- Error boundaries: `app/error.tsx`, `app/not-found.tsx`, `app/global-error.tsx`. + +## Consequences + +### Positive +- Server Components eliminate round-trips for data that doesn't need to be interactive. +- App Router colocates layouts, loading states, and error boundaries with the routes they belong to. +- TypeScript end-to-end (tRPC infers types from router definitions, no manual API typing). +- Vercel deploys with zero config; Railway and Docker are also viable. + +### Negative / trade-offs +- The `"use client"` boundary requires discipline — violations silently increase the bundle. +- App Router's caching model (fetch cache, `no-store`, `revalidate`) is nuanced; misuse causes stale data or redundant fetches. +- Server Component testing requires Playwright E2E; unit testing is limited to extracted logic and Client Components. +- Next.js 16 is a major version with breaking changes from Next.js 13/14; contributors should read `node_modules/next/dist/docs/` before writing code. diff --git a/docs/adr/0004-firebase-auth.md b/docs/adr/0004-firebase-auth.md new file mode 100644 index 0000000..ab32787 --- /dev/null +++ b/docs/adr/0004-firebase-auth.md @@ -0,0 +1,43 @@ +# ADR 0004 — Firebase Authentication (cross-platform) + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs an authentication system that: +- Works across all three layers: Go backend, Next.js web, and Android +- Supports Google OAuth and email/password out of the box +- Handles token lifecycle (refresh, revocation) without server-side session storage +- Has a generous free tier suitable for early-stage projects + +Candidates evaluated: Firebase Auth, Auth0, Supabase Auth, custom JWT. + +## Decision + +Use **Firebase Authentication** for identity management across all three layers. + +**Flow:** +1. User signs in via the Firebase client SDK (web or Android). +2. Firebase issues a short-lived ID token (1 hour). +3. The client sends the ID token as `Authorization: Bearer ` to the Go backend. +4. The Go backend verifies the token using the Firebase Admin SDK (`firebase.google.com/go/v4`). +5. On the web, NextAuth wraps Firebase sign-in with a server-side session; only the decoded claims (uid, email, name) are stored in the NextAuth JWT — the raw Firebase ID token is not forwarded. + +**Error responses** from the backend FirebaseAuth middleware use the standard envelope: +```json +{"error": {"code": "UNAUTHORIZED", "message": "..."}} +``` + +## Consequences + +### Positive +- Google OAuth, email/password, Apple, and phone auth are all available with no custom code. +- Firebase ID tokens are JWTs verifiable offline once the public keys are cached — no Firebase round-trip per request. +- Cross-platform SDK consistency: the same Firebase project serves web and Android. +- The Firebase Console provides user management, analytics, and remote config at no cost at typical template scale. + +### Negative / trade-offs +- Vendor lock-in to Google Firebase. Migrating to a self-hosted solution requires replacing the Admin SDK verification and all client-side sign-in flows. +- The NextAuth/Firebase bridge means the backend cannot receive a Bearer token from the web layer — the web sends `X-User-Id` as an identifier instead of a verifiable token. A future improvement would store a Firebase custom token in the NextAuth session. +- `google-services.json` (Android) and `GoogleService-Info.plist` (iOS, if added) are project-specific secrets and must not be committed. diff --git a/docs/adr/0005-android-compose.md b/docs/adr/0005-android-compose.md new file mode 100644 index 0000000..21e9e23 --- /dev/null +++ b/docs/adr/0005-android-compose.md @@ -0,0 +1,41 @@ +# ADR 0005 — Android + Kotlin + Jetpack Compose + Material3 + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs a mobile layer. Key requirements: +- Native Android UI (not a WebView wrapper) +- Type-safe, concise UI code that co-locates layout and logic +- Consistent with Google's current design system recommendations +- Single-activity architecture to avoid Fragment back-stack complexity + +The template targets Android only; iOS would require a separate React Native or Flutter track. + +## Decision + +Use **Android** with **Kotlin 2.2**, **Jetpack Compose BOM 2026.02**, and **Material3**. + +Key conventions enforced: +- **Single Activity** — `MainActivity` is the sole Android Activity. All navigation is handled by `Navigation Compose` and `AppNavGraph`. +- **No logic in `@Composable` functions** — screens are stateless; state is hoisted to ViewModels or the calling composable. +- **Material3 only** — `MaterialTheme.colorScheme.*` and `MaterialTheme.typography.*` for all colours and text styles; no hardcoded values. +- **Version catalog** — all dependency versions declared in `mobile/gradle/libs.versions.toml`. +- **`UiState` sealed class** — generic loading/error/success state for ViewModels that fetch data. +- **Spotless + ktlint** — code formatting enforced in CI via `./gradlew spotlessCheck`. + +## Consequences + +### Positive +- Compose's declarative model eliminates XML layout files and `findViewById` boilerplate. +- Material3 theming tokens mean a single colour palette change propagates everywhere. +- The Single Activity / Navigation Compose pattern makes deep links and back-stack management straightforward. +- Kotlin coroutines + `StateFlow` integrate cleanly with Compose's `collectAsStateWithLifecycle()`. +- JVM unit tests (MockWebServer, JUnit 4) run without an emulator; instrumented tests (`createComposeRule()`) verify UI on device/emulator. + +### Negative / trade-offs +- iOS is not covered. Adding iOS requires a separate codebase (Swift/SwiftUI) or migrating to a cross-platform framework. +- Compose's recomposition model requires care around `remember`, `derivedStateOf`, and `LaunchedEffect` — misuse causes performance issues or stale state. +- Instrumented tests require a running emulator or physical device and are excluded from the standard CI gate. +- `google-services.json` must be written to `mobile/app/` during CI from a secret; it is gitignored locally. diff --git a/docs/adr/0006-redis-asynq.md b/docs/adr/0006-redis-asynq.md new file mode 100644 index 0000000..5869305 --- /dev/null +++ b/docs/adr/0006-redis-asynq.md @@ -0,0 +1,36 @@ +# ADR 0006 — Redis + Asynq for background jobs and event streaming + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs infrastructure for: +- Deferred/background tasks (welcome emails, notification dispatch, data processing) +- Event-driven messaging between services or components +- Rate-limited or scheduled work (cron jobs, retries with backoff) + +Candidates evaluated: Asynq + Redis, RabbitMQ, SQS, Temporal. + +## Decision + +Use **Redis** (`github.com/redis/go-redis/v9`) as the backing store with two layers: + +1. **Asynq** (`github.com/hibiken/asynq`) for task queues — priority queues, retries, scheduled tasks, and the Asynqmon web UI for monitoring. +2. **Redis Streams** (built-in Redis) for event sourcing / pub-sub patterns between producers and consumers. + +**Both features are opt-in.** Omitting `REDIS_URL` from the environment disables both the cache and all queue/stream features. The Redis Streams consumer is intentionally **not auto-started** — the wiring block in `cmd/api/main.go` is commented out and documented in `backend/docs/streams.md`. + +## Consequences + +### Positive +- Asynq provides exactly-once delivery guarantees via Redis SET NX, plus automatic retries and dead-letter queues. +- The Asynqmon UI (available at `/admin/queues` in debug mode) gives visibility into queues without a separate tool. +- Redis Streams' consumer group model allows multiple workers to process events in parallel with acknowledgement semantics. +- A single Redis instance serves the cache (`redis.Client`), Asynq broker, and stream producer — reducing infrastructure complexity. + +### Negative / trade-offs +- Redis adds an operational dependency. If Redis is unavailable, all queue and stream features fail. +- Asynq does not support message ordering within a single queue; use Redis Streams if strict ordering is required. +- The Redis Streams consumer is opt-in by design — contributors must uncomment and adapt the wiring in `cmd/api/main.go` for their use case. +- Redis Streams have no built-in dead-letter queue; consumer-side retry logic must be implemented manually. diff --git a/docs/adr/0007-testcontainers.md b/docs/adr/0007-testcontainers.md new file mode 100644 index 0000000..5dcce3f --- /dev/null +++ b/docs/adr/0007-testcontainers.md @@ -0,0 +1,41 @@ +# ADR 0007 — Testcontainers for backend integration tests + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +Backend integration tests need to verify that repository code, SQL queries, and migrations work correctly against a real database engine. Options considered: +- Mock the database (in-memory fake or `sqlmock`) +- Use an in-process SQLite substitute +- Spin a real PostgreSQL container per test run (Testcontainers) + +## Decision + +Use **Testcontainers for Go** (`github.com/testcontainers/testcontainers-go`) to spin real PostgreSQL and Redis containers for integration tests. + +Each integration test package follows the `TestMain` + `mustStartPostgresContainer()` pattern from `internal/infrastructure/database/postgres/health_repository_test.go`: + +```go +func TestMain(m *testing.M) { + db = mustStartPostgresContainer() + os.Exit(m.Run()) +} +``` + +Tests create their own schema via `testDB.Exec(...)` — they do **not** run goose migrations. This keeps tests independent of migration history and avoids state leakage between test runs. + +**Database mocking is explicitly prohibited.** Handler tests may mock usecase interfaces; usecase tests may mock repository interfaces. Neither may mock the database itself. + +## Consequences + +### Positive +- Tests run against the exact same PostgreSQL version (16) as production — no mock/prod divergence. +- Container lifecycle is managed automatically; the Ryuk reaper cleans up orphaned containers. +- PostgreSQL-specific features (JSONB, `ON CONFLICT`, `RETURNING`) are tested correctly. +- CI (GitHub Actions, `ubuntu-latest`) runs Docker natively, so Testcontainers works without extra setup. + +### Negative / trade-offs +- Docker must be running locally for integration tests. On Windows, rootless Docker mode is not supported by Testcontainers — contributors must use Docker Desktop. +- Integration tests are slower than unit tests (container startup ~1–2 s per package). The `make itest` target runs them separately from `make test` so the fast unit-test loop is not penalised. +- If Docker is unavailable (CI without Docker, or a locked-down corporate machine), integration tests cannot run. diff --git a/docs/adr/0008-cloudflare-r2.md b/docs/adr/0008-cloudflare-r2.md new file mode 100644 index 0000000..21894d9 --- /dev/null +++ b/docs/adr/0008-cloudflare-r2.md @@ -0,0 +1,38 @@ +# ADR 0008 — Cloudflare R2 for object storage + +**Status:** Accepted +**Date:** 2026-06-25 + +## Context + +The template needs file/object storage for user uploads (avatars, attachments, exports). Requirements: +- S3-compatible API to avoid vendor-specific client code +- No egress fees for download traffic +- Opt-in — projects that don't need file storage should not pay for it +- Presigned URL support so the backend does not proxy file bytes + +Candidates evaluated: Cloudflare R2, AWS S3, Supabase Storage. + +## Decision + +Use **Cloudflare R2** accessed via the **AWS SDK v2** (`github.com/aws/aws-sdk-go-v2/service/s3`), configured with R2's S3-compatible endpoint (`https://.r2.cloudflarestorage.com`). + +The storage feature is **opt-in**: omitting `R2_ACCOUNT_ID`, `R2_ACCESS_KEY`, and `R2_SECRET_KEY` from the environment disables the `/api/v1/storage/presign` and `/api/v1/storage/:key` routes. The `Handler` struct's `storageService` field will be `nil`, and `RegisterRoutes` conditionally skips registration. + +Upload flow: +1. Client calls `POST /api/v1/storage/presign` → backend returns a presigned S3 PUT URL (valid 15 min). +2. Client uploads directly to R2 using the presigned URL — backend never proxies bytes. +3. The public URL (`R2_PUBLIC_URL`) is returned for download access. + +## Consequences + +### Positive +- Zero egress fees from R2 regardless of download volume — significant cost saving at scale compared to S3. +- The AWS SDK v2 means switching to S3 or any S3-compatible provider (MinIO, Backblaze B2) requires only environment variable changes, no code changes. +- Presigned URLs eliminate the backend as a bottleneck for large file uploads. +- The opt-in pattern means projects without file storage have zero R2 surface area. + +### Negative / trade-offs +- Cloudflare account required. The free R2 tier covers 10 GB storage and 1M Class A operations/month. +- `R2_PUBLIC_URL` must be configured separately (custom domain or `*.r2.dev` subdomain); the bucket is private by default. +- No local emulator for R2 is provided — integration tests for the storage layer require either a live R2 bucket or MinIO running locally. From 75741378dd0491578423642f69f7c536b726eb97 Mon Sep 17 00:00:00 2001 From: GRACENOBLE Date: Thu, 25 Jun 2026 16:17:02 +0300 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20update=20TEMPLATE=5FSTATUS.md=20?= =?UTF-8?q?=E2=80=94=20#63/#64=20in=20review=20(PR=20#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TEMPLATE_STATUS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TEMPLATE_STATUS.md b/TEMPLATE_STATUS.md index fa93381..9468bed 100644 --- a/TEMPLATE_STATUS.md +++ b/TEMPLATE_STATUS.md @@ -48,8 +48,8 @@ Tracks all identified gaps from the June 2026 template analysis. Issues live in | # | Issue | Status | |---|-------|--------| -| [#63](https://github.com/GRACENOBLE/fullstack-template/issues/63) | `infra` Architecture Decision Records (ADRs) for key technology choices | ⬜ Open | -| [#64](https://github.com/GRACENOBLE/fullstack-template/issues/64) | `infra` Deployment runbook for staging and production | ⬜ Open | +| [#63](https://github.com/GRACENOBLE/fullstack-template/issues/63) | `infra` Architecture Decision Records (ADRs) for key technology choices | 🔁 In review (PR #68) | +| [#64](https://github.com/GRACENOBLE/fullstack-template/issues/64) | `infra` Deployment runbook for staging and production | 🔁 In review (PR #68) | --- @@ -64,4 +64,4 @@ Tracks all identified gaps from the June 2026 template analysis. Issues live in --- -_Last updated: 2026-06-25 — PR #66 merged, all first-week friction issues done. Next: medium priority (#59–#62)._ +_Last updated: 2026-06-25 — PR #67 merged (medium priority #59–#62); PR #68 open (nice-to-haves #63–#64)._ From 00494bfc342324831ba694d6b88592a5d3c58a6c Mon Sep 17 00:00:00 2001 From: GRACENOBLE Date: Thu, 25 Jun 2026 16:19:41 +0300 Subject: [PATCH 3/3] chore: mark medium-priority issues #59-#62 as done in TEMPLATE_STATUS --- TEMPLATE_STATUS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TEMPLATE_STATUS.md b/TEMPLATE_STATUS.md index 9468bed..d5c2c00 100644 --- a/TEMPLATE_STATUS.md +++ b/TEMPLATE_STATUS.md @@ -37,10 +37,10 @@ Tracks all identified gaps from the June 2026 template analysis. Issues live in | # | Issue | Status | |---|-------|--------| -| [#59](https://github.com/GRACENOBLE/fullstack-template/issues/59) | `mobile` Settings screen: show user profile + sign-out button | ⬜ Open | -| [#60](https://github.com/GRACENOBLE/fullstack-template/issues/60) | `web` Dashboard page: fetch and display `/api/v1/me` | ⬜ Open | -| [#61](https://github.com/GRACENOBLE/fullstack-template/issues/61) | `backend` Redis stream consumers: wire with feature flag or document as opt-in | ⬜ Open | -| [#62](https://github.com/GRACENOBLE/fullstack-template/issues/62) | `backend` pprof endpoints for runtime profiling (gated to internal network) | ⬜ Open | +| [#59](https://github.com/GRACENOBLE/fullstack-template/issues/59) | `mobile` Settings screen: show user profile + sign-out button | ✅ Done (merged) | +| [#60](https://github.com/GRACENOBLE/fullstack-template/issues/60) | `web` Dashboard page: fetch and display `/api/v1/me` | ✅ Done (merged) | +| [#61](https://github.com/GRACENOBLE/fullstack-template/issues/61) | `backend` Redis stream consumers: wire with feature flag or document as opt-in | ✅ Done (merged) | +| [#62](https://github.com/GRACENOBLE/fullstack-template/issues/62) | `backend` pprof endpoints for runtime profiling (gated to internal network) | ✅ Done (merged) | ---