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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ jobs:
- name: Install dependencies
run: go mod download

- name: Check Swagger docs are up to date
run: |
make swagger
git diff --exit-code docs/swagger/

- name: Vet
run: go vet ./...

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/mobile-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ jobs:
- name: Lint
run: ./gradlew lint

- name: Check formatting (ktlint via Spotless)
run: ./gradlew spotlessCheck

- name: Run unit tests
run: ./gradlew test
56 changes: 45 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,19 @@ Feature suggestions are welcome. Open an issue with:

```bash
# Backend
cd backend && go vet ./...
cd backend && make lint # golangci-lint
cd backend && make test

# Web
cd web && pnpm lint && pnpm build
cd web && pnpm lint
cd web && pnpm test
cd web && pnpm build

# Mobile
cd mobile && ./gradlew lint && ./gradlew test
cd mobile && ./gradlew lint
cd mobile && ./gradlew spotlessCheck # ktlint formatting
cd mobile && ./gradlew test
```

6. **Open your pull request** against `main`. Fill in the PR template including:
Expand All @@ -82,21 +88,47 @@ Feature suggestions are welcome. Open an issue with:

## Development Setup

See [README.md — Getting Started](README.md#getting-started) for the full setup guide.
### Prerequisites

Quick summary:
| Tool | Minimum version | Check |
|---|---|---|
| Go | 1.25 | `go version` |
| Node.js | 22 | `node --version` |
| pnpm | any | `pnpm --version` |
| Docker Desktop | running | `docker info` |
| JDK | 17 | `java -version` |
| Android SDK | API 35 | `$ANDROID_HOME` set |

### First-time setup

Run the setup script once after cloning. It checks prerequisites, installs dependencies, copies `.env` files, and runs database migrations:

```bash
# macOS / Linux
./setup.sh

# Windows
./setup.ps1
```

### Starting all services

```bash
# Start the database
cd backend && make docker-run
# macOS / Linux — starts Postgres, backend, and web in one terminal
./dev.sh

# Backend (hot reload)
cd backend && make watch
# Windows — opens three separate PowerShell windows
./dev.ps1
```

# Web (hot reload)
cd web && pnpm dev
Or start services individually:

```bash
cd backend && make docker-run # Postgres
cd backend && make watch # backend hot reload → :8080
cd web && pnpm dev # web → :3000

# Mobile — open mobile/ in Android Studio, or build via Gradle
# Mobile — open mobile/ in Android Studio, or:
cd mobile && ./gradlew assembleDebug
```

Expand All @@ -119,7 +151,9 @@ This project uses topic-based documentation in `backend/docs/` and `web/docs/` t
| Component patterns or TypeScript conventions | `web/docs/components.md` |
| Composable conventions, theme, or Material3 usage | `mobile/docs/compose-conventions.md` |
| Activity setup, lifecycle, or ViewModel pattern | `mobile/docs/architecture.md` |
| UiState<T>, UiStateContent, or loading/error patterns | `mobile/docs/ui-states.md` |
| Test setup or testing patterns (mobile) | `mobile/docs/testing.md` |
| Data table component or TanStack Table usage | `web/docs/data-table.md` |

### How to update a doc

Expand Down
67 changes: 67 additions & 0 deletions TEMPLATE_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Template Readiness Status

Tracks all identified gaps from the June 2026 template analysis. Issues live in the [fullstack-template GitHub project](https://github.com/users/GRACENOBLE/projects/8).

---

## Priority: Critical — Day 1 blockers

| # | Issue | Status |
|---|-------|--------|
| [#44](https://github.com/GRACENOBLE/fullstack-template/issues/44) | `backend` CORS origin hardcoded to localhost:3000 → make config-driven | ✅ Done (merged) |
| [#45](https://github.com/GRACENOBLE/fullstack-template/issues/45) | `web` `.env.example` missing BACKEND_URL, SENTRY_ORG, SENTRY_PROJECT | ✅ Done (merged) |
| [#46](https://github.com/GRACENOBLE/fullstack-template/issues/46) | `backend` Standardized API response envelope (`JSON`, `JSONError` helpers) | ✅ Done (merged) |
| [#47](https://github.com/GRACENOBLE/fullstack-template/issues/47) | `backend` Request ID middleware (`X-Request-ID`, propagated to logs) | ✅ Done (merged) |
| [#48](https://github.com/GRACENOBLE/fullstack-template/issues/48) | `web` Error pages: `not-found.tsx`, `error.tsx`, `global-error.tsx` | ✅ Done (merged) |
| [#49](https://github.com/GRACENOBLE/fullstack-template/issues/49) | `mobile` HTTP client for backend API calls (OkHttp + Firebase token interceptor) | ✅ Done (merged) |

---

## Priority: High — First-week friction

| # | Issue | Status |
|---|-------|--------|
| [#50](https://github.com/GRACENOBLE/fullstack-template/issues/50) | `infra` Root dev script to start all services with one command | 🔁 In review |
| [#51](https://github.com/GRACENOBLE/fullstack-template/issues/51) | `backend` Add `.golangci.yml` linter config | 🔁 In review |
| [#52](https://github.com/GRACENOBLE/fullstack-template/issues/52) | `mobile` Add ktlint and integrate into CI | 🔁 In review |
| [#53](https://github.com/GRACENOBLE/fullstack-template/issues/53) | `infra` Renovate / Dependabot for automated dependency updates | 🔁 In review |
| [#54](https://github.com/GRACENOBLE/fullstack-template/issues/54) | `infra` First-run setup script for new contributors | 🔁 In review |
| [#55](https://github.com/GRACENOBLE/fullstack-template/issues/55) | `backend` Swagger generation check in CI (fail if stale) | 🔁 In review |
| [#56](https://github.com/GRACENOBLE/fullstack-template/issues/56) | `mobile` Loading state and skeleton screen pattern | 🔁 In review |
| [#57](https://github.com/GRACENOBLE/fullstack-template/issues/57) | `mobile` Error state and retry UI pattern (`UiState<T>` sealed class) | 🔁 In review |
| [#58](https://github.com/GRACENOBLE/fullstack-template/issues/58) | `web` Data table with sorting, filtering, and pagination (TanStack Table) | 🔁 In review |

---

## Priority: Medium — Polish

| # | 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 |

---

## Priority: Low — Nice-to-have

| # | 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 |

---

## Legend

| Symbol | Meaning |
|--------|---------|
| ✅ Done | Merged to main |
| 🔁 In review | PR open, pending merge |
| ⬜ Open | Not started |
| 🚧 In progress | Branch exists, work ongoing |

---

_Last updated: 2026-06-25 — #50–#58 implemented (first-week friction), PR pending._
30 changes: 30 additions & 0 deletions backend/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: "2"

linters:
default: none
enable:
- errcheck # check errors are handled
- gosimple # simplify code
- govet # go vet checks
- ineffassign # detect ineffectual assignments
- staticcheck # staticcheck suite
- unused # unused code
- gofmt # formatting
- goimports # import ordering
- misspell # spelling mistakes
- revive # drop-in golint replacement
- bodyclose # HTTP response body close check
- noctx # HTTP requests without context
- exhaustive # exhaustive switch on enums (skip if too noisy — comment it out)

linters-settings:
revive:
rules:
- name: exported
disabled: true # don't require godoc on all exports

issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck # test code can ignore errors freely
7 changes: 6 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ swagger:
@echo "Generating Swagger docs..."
@go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g cmd/api/main.go -o docs/swagger

# Lint the application
lint:
@echo "Linting..."
@golangci-lint run ./...

# Live Reload
watch:
@powershell -ExecutionPolicy Bypass -Command "if (Get-Command air -ErrorAction SilentlyContinue) { \
Expand Down Expand Up @@ -77,6 +82,6 @@ migrate-version:
migrate-create:
@go run ./cmd/migrate create $(name)

.PHONY: all build run test clean watch docker-run docker-down itest swagger \
.PHONY: all build run test clean watch lint docker-run docker-down itest swagger \
migrate-up migrate-up-one migrate-down migrate-down-to migrate-reset \
migrate-status migrate-version migrate-create
13 changes: 13 additions & 0 deletions backend/docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ sources:
- internal/bootstrap/bootstrap.go
- internal/infrastructure/database/postgres/db.go
- pkg/firebase/admin.go
- .golangci.yml
- Makefile
---

# Environment Variables
Expand Down Expand Up @@ -58,6 +60,17 @@ The `.gitignore` in `backend/` excludes `.env` (verify before committing).

Docker Compose reads the same `.env` file to configure the Postgres container, so the values must be consistent between the app and Docker.

## Quality commands

| Command | What it runs |
|---|---|
| `make test` | `go test ./... -v` |
| `make itest` | `go test ./internal/infrastructure/... -v` (requires Docker) |
| `make lint` | `golangci-lint run ./...` |
| `make swagger` | regenerates `docs/swagger/` from swaggo annotations |

`make lint` uses the config in `backend/.golangci.yml`. Enabled linters: `errcheck`, `gosimple`, `govet`, `ineffassign`, `staticcheck`, `unused`, `gofmt`, `goimports`, `misspell`, `revive`, `bodyclose`, `noctx`, `exhaustive`. The `revive` `exported` rule is disabled. Run `make lint` locally before pushing; CI also runs it.

## Adding a new environment variable
1. Add to `backend/.env` with a descriptive name.
2. Read it in `internal/bootstrap/bootstrap.go` inside `loadConfig()` and store it on `Config`.
Expand Down
25 changes: 25 additions & 0 deletions dev.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# dev.ps1 — start all services in separate PowerShell windows (Windows equivalent of dev.sh)
# Usage: .\dev.ps1
# Close each terminal window manually when done.

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition

Write-Host "[postgres] Starting Docker Compose (Postgres)..." -ForegroundColor Blue
Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$ScriptDir\backend'; make docker-run" `
-WindowStyle Normal

Write-Host "[backend] Waiting 5s for Postgres, then starting Go backend on :8080..." -ForegroundColor Green
Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$ScriptDir\backend'; Start-Sleep 5; make watch" `
-WindowStyle Normal

Write-Host "[web] Starting Next.js dev server on :3000..." -ForegroundColor Yellow
Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$ScriptDir\web'; pnpm dev" `
-WindowStyle Normal

Write-Host ""
Write-Host "All three services are starting in separate windows:" -ForegroundColor Cyan
Write-Host " - Postgres (blue window)" -ForegroundColor Blue
Write-Host " - Go backend (green window) → http://localhost:8080" -ForegroundColor Green
Write-Host " - Next.js web (yellow window) → http://localhost:3000" -ForegroundColor Yellow
Write-Host ""
Write-Host "Close each window individually when you are done." -ForegroundColor Cyan
35 changes: 35 additions & 0 deletions dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# dev.sh — start all services in parallel in a single terminal
# Usage: ./dev.sh
# Press Ctrl+C to stop everything.

set -euo pipefail

BLUE='\033[1;34m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
RESET='\033[0m'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Kill all background jobs when the script exits (Ctrl+C or error)
trap 'echo ""; echo "Stopping all services..."; kill $(jobs -p) 2>/dev/null; wait' EXIT

# ── Postgres (Docker Compose) ──────────────────────────────────────────────────
echo -e "${BLUE}[postgres] Starting Docker Compose (Postgres)...${RESET}"
(cd "$SCRIPT_DIR/backend" && make docker-run) &

# ── Backend (Air hot-reload) — wait for Postgres to be ready ──────────────────
(
echo -e "${GREEN}[backend] Waiting 5s for Postgres to be healthy...${RESET}"
sleep 5
echo -e "${GREEN}[backend] Starting Go backend with Air (hot-reload) on :8080...${RESET}"
cd "$SCRIPT_DIR/backend" && make watch
) &

# ── Web (Next.js) ─────────────────────────────────────────────────────────────
echo -e "${YELLOW}[web] Starting Next.js dev server on :3000...${RESET}"
(cd "$SCRIPT_DIR/web" && pnpm dev) &

# Block until Ctrl+C triggers the trap above
wait
4 changes: 4 additions & 0 deletions mobile/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[*.kt]
ktlint_standard_function-naming = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_no-wildcard-imports = disabled
1 change: 1 addition & 0 deletions mobile/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.google.services)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.spotless)
}

val localProps = Properties().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class GreetingTest {

@get:Rule
val composeTestRule = createComposeRule()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RegisterScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

Expand Down Expand Up @@ -81,12 +80,13 @@ class RegisterScreenTest {
fun registerScreen_clickCreateAccount_invokesCallback() {
var registerCalled = false
setContent(
registerForm = RegisterFormState(
name = "Alice",
email = "alice@example.com",
password = "pass",
confirmPassword = "pass",
),
registerForm =
RegisterFormState(
name = "Alice",
email = "alice@example.com",
password = "pass",
confirmPassword = "pass",
),
onRegister = { registerCalled = true },
)
// Use the test tag to target the button specifically, not the heading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class HomeScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class OnboardingScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

Expand Down
Loading
Loading