The portal consists of a Go API backend and a React frontend that query OpenSearch indexes for catalog data. A PostgreSQL database (managed by the portal's own docker-compose) stores application state such as the variable cart.
The dev data layer (PostgreSQL + OpenSearch + mini ETL) lives in a separate repository: unic-etl4dev. Start it first to have OpenSearch running with seeded indexes.
Production uses unic-dag (Spark ETL) for the OpenSearch pipeline. In production, set
OPENSEARCH_HOSTandOPENSEARCH_PORTto point to the production cluster.
unic-etl4dev/ ← Dev data layer (separate repo)
├── postgres/ ← PG dump + restore script
├── etl/ ← Mini ETL: PG → OpenSearch
└── docker-compose.yml ← postgres, opensearch, etl-seed
unic-portal/ ← This repo
├── backend/ ← Go API (queries OpenSearch + PostgreSQL)
├── frontend/ ← React + Vite
└── docker-compose.yml ← PostgreSQL + API + frontend
- Docker (v20+ recommended)
- Docker Compose (v2+ — included with Docker Desktop)
- unic-etl4dev cloned and ready
Verify your installation:
docker --version
docker compose version-
Start the dev data layer (in a separate terminal):
cd path/to/unic-etl4dev docker compose up -dWait for OpenSearch to be healthy and the ETL seed to finish. Verify:
curl localhost:9200/_cat/indices?v -
Start the portal (API + frontend):
cd unic-portal docker compose up --build -
Open the application:
- Frontend: http://localhost:3000
- API: http://localhost:8081/api/resources
- Swagger UI: http://localhost:8081/swagger/index.html
| Command | Description |
|---|---|
docker compose up --build |
Build and start PostgreSQL + API + frontend |
docker compose up --build -d |
Same but in background |
docker compose down |
Stop all services |
docker compose logs -f api |
Follow API logs |
docker compose restart api |
Restart the API service |
| Service | Container Port | Host Port | URL |
|---|---|---|---|
| Frontend (nginx) | 80 | 3000 | http://localhost:3000 |
| API (Go) | 8080 | 8081 | http://localhost:8081 |
| PostgreSQL | 5432 | 5437 | psql -h localhost -p 5437 -U unic unic_portal |
| OpenSearch (from unic-etl4dev) | 9200 | 9200 | http://localhost:9200 |
| Swagger UI | — | 8081 | http://localhost:8081/swagger/index.html |
| Variable | Default | Description |
|---|---|---|
OPENSEARCH_HOST |
host.docker.internal (Docker) / localhost (local) |
OpenSearch hostname |
OPENSEARCH_PORT |
9200 |
OpenSearch port |
POSTGRES_HOST |
postgres (Docker) / localhost (local) |
PostgreSQL hostname |
POSTGRES_PORT |
5432 |
PostgreSQL port |
POSTGRES_USER |
unic |
PostgreSQL user |
POSTGRES_PASSWORD |
unic |
PostgreSQL password |
POSTGRES_DB |
unic_portal |
PostgreSQL database name |
In production, set OpenSearch and PostgreSQL variables to point to the production clusters.
API can't connect to OpenSearch
Make sure the dev data layer is running:
curl localhost:9200/_cat/indices?vIf using Docker, the API connects via host.docker.internal to reach OpenSearch on the host. This works on Docker Desktop (macOS/Windows) and Linux with the extra_hosts directive in docker-compose.
Rebuilding after code changes
# Rebuild and restart everything
docker compose up --build
# Or rebuild just one service
docker compose build api
docker compose restart apiFor active development, run the data layer in Docker (via unic-etl4dev) and the app services locally for faster iteration with hot-reload.
- Go (binary at
/usr/local/go/bin/go) - Node.js 22 (via nvm)
- unic-etl4dev running (
cd path/to/unic-etl4dev && docker compose up -d)
The API requires both OpenSearch (from unic-etl4dev) and PostgreSQL to be running.
1. Start PostgreSQL (e.g. via Docker):
docker run -d --name unic-portal-pg -p 5432:5432 \
-e POSTGRES_DB=unic_portal -e POSTGRES_USER=unic -e POSTGRES_PASSWORD=unic \
postgres:16-alpine2. Generate Swagger docs (required on first clone):
cd backend
/usr/local/go/bin/go install github.com/swaggo/swag/cmd/swag@latest
PATH="/usr/local/go/bin:$PATH" ~/go/bin/swag init -g cmd/api/main.go -o docs/ --parseDependency --parseInternal3. Start the API:
/usr/local/go/bin/go run ./cmd/api/The API starts on http://localhost:8080. All endpoints are under /api, e.g. http://localhost:8080/api/resources.
Environment variables:
OPENSEARCH_HOST=localhost # default
OPENSEARCH_PORT=9200 # default
POSTGRES_HOST=localhost # default
POSTGRES_PORT=5432 # default
POSTGRES_USER=unic # default
POSTGRES_PASSWORD=unic # default
POSTGRES_DB=unic_portal # default
cd frontend
source ~/.nvm/nvm.sh && nvm use 22
npm install
npm run devThe frontend starts on http://localhost:5173 with hot-reload enabled.
Type-checking: Vite's dev server does not run TypeScript type checks. To catch type errors locally before committing:
npm run typecheckcd frontend
source ~/.nvm/nvm.sh && nvm use 22
npm run storybookStorybook starts on http://localhost:6006.
The backend uses swaggo/swag to generate OpenAPI documentation from handler annotations.
Viewing the docs: When the API is running, visit http://localhost:8080/swagger/index.html.
Regenerating after handler changes:
cd backend
/usr/local/go/bin/go install github.com/swaggo/swag/cmd/swag@latest
PATH="/usr/local/go/bin:$PATH" ~/go/bin/swag init -g cmd/api/main.go -o docs/ --parseDependency --parseInternalThen restart the API server — the swagger spec is embedded at compile time, so the running process must be restarted to serve the updated docs.
This generates backend/docs/ (docs.go, swagger.json, swagger.yaml). The docs/ directory is gitignored — regenerate it locally after cloning.