Backend API and admin dashboard for THS Armada. Provides REST endpoints consumed by the public website (armada.nu) and a React-Admin interface for content management.
- Tech Stack
- Prerequisites
- Getting Started
- Database migrations
- VS Code workspace and launches
- Project Structure
- Testing
- API
- Swagger docs
- Cache Revalidation
- CI / CD
- Operations notes
- Infrastructure as code
- Adding a New Resource
- Language: Go 1.26
- Router: Gorilla Mux
- ORM: GORM (Postgres)
- Auth: JWT (Bearer tokens)
- Hot reload: Air (in Docker dev mode)
- Framework: React-Admin v5
- Build tool: Vite
- Language: TypeScript
- UI: MUI (Material UI)
- Deployment: Google Cloud Run (containerized Go API + bundled React-Admin frontend)
- Ingress: External HTTPS load balancing in production; Cloud Run domain mapping in staging
- Database: Supabase PostgreSQL (production project with a persistent staging branch)
- File storage: Supabase Storage (local dev: MinIO)
- Docker and Docker Compose (required for local development)
- Go 1.26+ (optional, for running Go tooling directly)
- Node.js 24+ and pnpm (optional, for running frontend tooling directly)
-
Clone the repo
git clone https://github.com/armada-ths/ArmadaCMS.git cd ArmadaCMS -
Set up environment variables
cp .env.example .env
The defaults are pre-configured for the local Docker stack. See
.env.examplefor the full list of variables. -
Start the local development stack
docker compose -f docker-compose.dev.yml up --build
This starts the Go API (Air hot reload), the React-Admin frontend (Vite HMR), Postgres, and MinIO in one Docker Compose workflow.
Only the first run requires
--build. After that, use:docker compose -f docker-compose.dev.yml up
Local connection (e.g. for a DB GUI):
postgres:postgres@localhost:5432/armadacmsMinIO console: http://localhost:9001 (login:
minioadmin/minioadmin)To stop the stack without deleting data:
docker compose -f docker-compose.dev.yml stop
To remove the containers while keeping named volumes available for reuse:
docker compose -f docker-compose.dev.yml down
-
Optionally clone a remote database
scripts/import-remote-db.ps1clones a remote PostgreSQL database into the local Postgres container, replacing the localarmadacmsdatabase. Fill in theSOURCE_DB_*vars in.env(see.env.example), then run:./scripts/import-remote-db.ps1
The remote database must be reachable from your machine — for Supabase, allowlist your IP under Project Settings → Networking → Network restrictions. Prefer cloning staging over production to avoid handling real data locally.
After cloning a Supabase database, AutoMigrate needs to be disabled in order to avoid schema conflicts. Set
DB_ENABLE_AUTOMIGRATE=falsein.envbefore starting the server. To apply a local SQL migration file manually, runcat supabase/migrations/<migration-file>.sql | docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d armadacms. -
Verify the app is running
Once the development stack is running, the following URLs are available:
- API: http://localhost:8080/api/v1/
- Admin UI: http://localhost:5173
- Health check: http://localhost:8080/health
Local development uses GORM AutoMigrate, which runs automatically on every server startup (controlled by DB_ENABLE_AUTOMIGRATE, default true). No extra steps are needed.
Remote environments (staging, production) use checked-in SQL migrations. The Supabase project is connected to this GitHub repository, so migrations are applied automatically on every push/merge to the tracked branches — no manual CLI commands required.
supabase/config.tomlconfigures the Supabase project link.supabase/seed.sqlbootstraps deterministic roles and feature flags for remote environment resets.supabase/migrations/holds all checked-in SQL migrations.
To create a new migration, generate a diff against the current remote schema:
pnpx supabase db diff -f <migration-name>To validate that all migrations apply cleanly from scratch:
pnpx supabase db resetRoles and feature flags are seeded from supabase/seed.sql alongside the initial admin user (username and password: admin). This seed only runs in non-production contexts (new branches, db reset) so hardcoded credentials are acceptable.
This repo includes shared VS Code configuration in .vscode/:
tasks.json— shared Docker tasks fordocker dev up,docker dev up --build,docker dev stop, anddocker dev downlaunch.json— aDockerlaunch that starts the dev stack via the shared task and opens the admin UI
If you work across both repos, use the shared workspace file committed in armada.nu:
../armada.nu/Armada.code-workspace
That workspace opens both repositories with portable relative paths and includes multi-repo compound launches.
ArmadaCMS/
├── main.go # Entry point — routing, auto-migration, server startup
├── auth/
│ └── middleware.go # JWT Bearer token auth middleware
├── Controllers/ # HTTP handlers (one per resource)
├── models/ # GORM model structs
├── db/
│ └── connect.go # Postgres connection setup
├── infra/
│ └── terraform/ # Terraform layout, shared conventions, and provider-specific roots
├── utils/ # Helpers (S3 upload, JWT, password hashing)
├── frontend/ # React-Admin SPA (Vite)
│ └── src/
│ ├── App.tsx # Resource registrations
│ ├── dataProvider.ts # Custom ra-data-simple-rest provider
│ ├── components/ # List, Create, Edit per resource
│ └── context/ # Auth provider, API endpoint config
├── Dockerfile.dev # Lightweight dev image (Go + Air only)
├── Dockerfile.prod # Production multi-stage build for Cloud Run
├── docker-compose.yml # Docker Compose (production-style)
└── docker-compose.dev.yml # Docker Compose (hot-reload dev)
ArmadaCMS includes Go unit tests (currently focused on auth/ and utils/) and frontend unit tests using vitest (currently focused on utils/).
-
Run all Go tests locally:
go test -race -count=1 ./... -
Run tests for specific Go packages:
go test ./auth/... ./utils/... -
Run frontend unit tests:
cd frontend && pnpm run test
All endpoints are under /api/v1. Routes are split into:
- Public (no auth):
GETendpoints for resources like exhibitors, events, profiles, teams, dates. - Protected (Bearer JWT):
POST,PUT,DELETEand admin-onlyGETendpoints.
Browser cross-origin access is restricted to armada.nu, common local development
origins, and any exact origins listed in the optional comma-separated
CORS_ALLOWED_ORIGINS environment variable.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/login |
No | Get JWT tokens |
GET |
/api/v1/exhibitors |
No | List all exhibitors |
POST |
/api/v1/exhibitors |
Required | Create exhibitor |
PUT |
/api/v1/exhibitors/{id} |
Required | Update exhibitor |
DELETE |
/api/v1/exhibitors/{id} |
Required | Delete exhibitor |
GET |
/api/v1/dates |
No | Get fair dates |
GET |
/health |
No | Health check |
The API is documented with Swagger / OpenAPI 2.0 using swaggo/swag.
Swagger UI is served at /swagger/index.html:
| Environment | URL |
|---|---|
| Local dev | http://localhost:8080/swagger/index.html |
| Staging | https://staging.cms.armada.nu/swagger/index.html |
| Production | https://cms.armada.nu/swagger/index.html |
Click Authorize in the UI and enter Bearer <token> (token obtained from POST /api/v1/login) to test protected endpoints.
Run this command from the repo root whenever you add or change routes or annotations:
swag init --generalInfo main.go --output docs --parseInternalThis overwrites docs/docs.go, docs/swagger.json, and docs/swagger.yaml. Commit these generated files alongside your code changes.
Prerequisites: install the
swagCLI once withgo install github.com/swaggo/swag/cmd/swag@latest.
Write operations automatically purge the public site's ISR cache via utils.RevalidateTag(tag), which POSTs to armada.nu's /api/revalidate endpoint. Tags are passed as the trailing revalidateTags ...string argument to the audit helpers. Requires REVALIDATION_URL and REVALIDATION_SECRET env vars (silently skipped if unset). See armada.nu/.github/copilot-instructions.md for the full tag inventory.
CI is handled by GitHub Actions and CD by Google Cloud Build.
Repository checks live in .github/workflows/ and are path-filtered so unchanged areas are skipped cleanly:
go-checks.yml— for Go files,go.mod,go.sum, and workflow changes; runsgo vet ./...,golangci-lint run, andgo test -race -count=1 ./....frontend-checks.yml— forfrontend/**and workflow changes; infrontend/, runspnpm install --frozen-lockfile,pnpm run lint:check,pnpm run type-check,pnpm run format:check, andpnpm run test.supabase-checks.yml— forsupabase/**and workflow changes; starts the local Supabase stack, runssupabase db reset --local, and verifies migrations apply cleanly.
All three workflows run on pushes to main and staging for matching paths, and on pull requests. Each workflow ends with an aggregate status job so checks pass when work is intentionally skipped because no relevant files changed. Superseded runs for the same workflow and branch or pull request are cancelled automatically, and every job has a timeout.
Deployments are handled by Google Cloud Build using cloudbuild.yaml.
- Cloud Build builds the production container from
Dockerfile.prodand pushes images to Artifact Registry. - Branch pushes to
mainandstagingdeploy the resulting image to the corresponding Cloud Run service. - PR builds use the secret-free
cloudbuild-pr.yamlconfiguration with a dedicated unprivileged service account. They validate the container build without publishing or deploying an image; external contributors require an owner or collaborator to comment/gcbrunfirst. - Trusted branch builds always build the commit SHA, publish that image, and deploy it.
- The pipeline creates and updates GitHub deployment statuses via the configured GitHub App credentials.
The GitHub → Cloud Build trigger wiring is managed in this repository's Terraform configuration, primarily in infra/terraform/gcp/prod/cloud_build.tf and infra/terraform/gcp/staging/cloud_build.tf. Those roots provision the branch and PR triggers; cloudbuild.yaml defines the trusted branch build/deploy flow and cloudbuild-pr.yaml defines unprivileged PR validation.
- Production runs on Cloud Run with Supabase (PostgreSQL) and Supabase Storage for file uploads.
- The server reads
PORTfrom the environment and falls back to8080. - Production database connections use
DB_SSLMODE=require. - Tune
DB_MAX_OPEN_CONNS,DB_MAX_IDLE_CONNS, and Cloud Run max instances together to stay within Postgres connection limits.
Terraform documentation is split by scope:
infra/terraform/README.md— shared layout, conventions, workspace naming, and cross-workspace wiringinfra/terraform/gcp/prod/README.md— GCP production root details and workspace setupinfra/terraform/gcp/staging/README.md— GCP staging root details and workspace setupinfra/terraform/supabase/prod/README.md— Supabase production root details and workspace setup
Use those documents as the canonical source for infrastructure specifics rather than duplicating them here.
- Create a model in
models/with GORM struct tags and camelCase JSON tags. - Register the model in
db.DB.AutoMigrate(...)inmain.go. - Write a SQL migration in
supabase/migrations/for the schema change. - Create a controller in
Controllers/following existing CRUD patterns. - Add routes in
main.go(public for reads, protected for writes). - Create
List,Create,Editcomponents infrontend/src/components/{Resource}/. - Register the
<Resource>infrontend/src/App.tsx. - If the resource has file uploads, add it to the multipart list in
frontend/src/dataProvider.ts. - If the resource is displayed on the public site, pass the matching cache tag to the audit helper's
revalidateTagsargument (e.g."blog-posts") and ensure the same tag is used in the Next.js data hook onarmada.nu.