Monorepo: frontend/ is Next.js 16 + React 19 + Tailwind 4, backend/ is a Go +
Fiber API with Postgres, docs/ holds design specs.
Every command below runs from the repo root. make help lists them all;
backend/README.md goes deeper on the API's architecture,
configuration, and testing.
| Tool | Version | Needed for |
|---|---|---|
| Bun | 1.3.5 | The only package manager for frontend/ |
| Node.js | 22 LTS | Next.js tooling targets Node |
| Go | 1.25.5+ | backend/ |
| Docker Desktop | any | The backend dev stack (API + Postgres) |
| Atlas CLI | any | Writing migrations |
| golangci-lint | any | make be-lint |
Postgres itself is not on that list — it runs in Docker alongside the API, so there is nothing to install or start by hand.
macOS
# Bun installs to ~/.bun — add ~/.bun/bin to PATH if the installer doesn't
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5"
brew install node@22 go golangci-lint ariga/tap/atlas
brew install --cask dockerHomebrew's node@22 is keg-only, so put it on your PATH:
echo 'export PATH="/opt/homebrew/opt/node@22/bin:$PATH"' >> ~/.zshrc
exec zshLinux / Windows
Bun: curl -fsSL https://bun.sh/install | bash. On Windows use WSL2 — Bun's
Windows build is not what CI runs. Get the rest from your package manager or from
nodejs.org, go.dev/dl,
docs.docker.com/engine/install, and
atlasgo.io.
The make shortcuts need make itself: on macOS run xcode-select --install,
on Linux install build-essential or your distro's equivalent.
git clone https://github.com/GenerateNU/boutline.git
cd boutline
make setup # bun install + go mod download
make dev # API + database in Docker, then the frontend dev server- Frontend: http://localhost:3000
- API: http://localhost:8000, Swagger UI at
/docs, spec at
/openapi.json
make dev starts Docker Desktop itself if it is not already running, waits for
the daemon, brings the API and its Postgres up detached, applies migrations, then
runs the frontend in the foreground. Stop the frontend with Ctrl-C and the stack
with make be-down.
Never run npm, yarn, or pnpm in frontend/ — they write a competing
lockfile. No environment variables are required: every backend value has a
default (listed in backend/README.md) and the frontend reads
none. .env* files are gitignored.
| Command | What it does |
|---|---|
make dev |
The whole stack, ready to work in |
make test |
Both test suites |
make check |
Everything CI would run, before a PR |
Per stack: fe-dev, fe-build, fe-lint, fe-typecheck, fe-test, fe-check
for the frontend, and be-up, be-down, be-logs, be-build, be-test,
be-lint, be-check for the backend.
Frontend CI and
Backend CI run the matching half of check
on every PR touching that stack. The backend workflow goes further than
be-check does: it also builds the Docker image and fails if the gorm models
and backend/migrations have drifted apart.
One thing that looks like broken setup but isn't: fe-typecheck must run
next typegen before tsc. Bare tsc reports errors CI never sees, since
src/app/layout.tsx uses a LayoutProps global that typegen writes into
.next/types.
The database is a container in the backend's compose stack, so make dev is all
it takes to get one. Two commands cover the rest:
| Command | What it does |
|---|---|
make db-shell |
psql shell on the dev database |
make db-reset |
Wipe it and bring it back with migrations applied (asks first) |
Its port is deliberately not published, so a Postgres already running on your
machine at :5432 does not clash with it.
Atlas owns the schema, generating migrations by diffing the gorm models:
make migrate-new NAME=create_examples # write the next migration
make migrate-status # what the dev database has appliedmake be-up applies pending migrations before the API starts, so there is no
separate apply step.
migrate-new needs the Atlas CLI and a running Docker daemon — Atlas spins up a
throwaway Postgres to normalise the schema before diffing. A new feature's model
must be registered in backend/cmd/atlasloader/main.go or it will never get a
migration. Read the generated SQL before committing it;
backend/README.md has the full workflow.
Generated from backend/migrations by make db-diagram, so don't edit it by
hand. Run that after adding a migration; Backend CI fails while it is stale.
Relationships are labelled with their foreign key's name, so a column the code
treats as a reference without a foreign key behind it does not appear as one.
erDiagram
bouts {
uuid id PK
uuid tournament_id FK
text location
timestamptz start_time
timestamptz started_at
timestamptz completed_at
uuid referee_id FK
bigint time_limit_seconds
bigint points_to_win
bigint group_number
text status
uuid competitor_1_id FK
uuid competitor_2_id FK
timestamptz created_at
timestamptz updated_at
uuid pool_id FK
}
bouts }o--o| competitors : fk_bouts_competitor1
bouts }o--o| competitors : fk_bouts_competitor2
bouts }o--o| pools : fk_bouts_pool
bouts }o--o| users : fk_bouts_referee
bouts }o--o| tournaments : fk_bouts_tournament
competitors {
uuid id PK
text first_name
text last_name
text rating
text team
timestamptz created_at
timestamptz updated_at
uuid tournament_id FK
uuid user_id FK
}
competitors }o--o| tournaments : fk_competitors_tournament
competitors }o--o| users : fk_competitors_user
direct_eliminations {
uuid id PK
text status
uuid event_id
timestamptz started_at
timestamptz completed_at
timestamptz created_at
timestamptz updated_at
}
events {
uuid id PK
uuid tournament_id FK
text format
text name
text status
timestamptz start_time
timestamptz started_at
timestamptz completed_at
timestamptz created_at
timestamptz updated_at
}
events }o--o| tournaments : fk_events_tournament
pool_stages {
uuid id PK
uuid event_id FK
bigint max_competitor_per_pool
text status
timestamptz start_time
timestamptz started_at
timestamptz completed_at
timestamptz created_at
timestamptz updated_at
}
pool_stages }o--o| events : fk_pool_stages_event
pools {
uuid id PK
uuid pool_stage_id FK
uuid referee_id FK
timestamptz started_at
timestamptz completed_at
timestamptz created_at
timestamptz updated_at
}
pools }o--o| pool_stages : fk_pools_pool_stage
pools }o--o| users : fk_pools_referee
scorings {
bigserial id PK
uuid created_by FK
bigint points
uuid competitor_id
uuid bout_id FK
timestamptz revoked_at
uuid revoked_by FK
timestamptz created_at
timestamptz updated_at
}
scorings }o--o| bouts : fk_scorings_bout
scorings }o--o| users : fk_scorings_creator
scorings }o--o| users : fk_scorings_revoker
tournament_users {
uuid user_id PK,FK
uuid tournament_id PK,FK
text role
timestamptz created_at
timestamptz updated_at
}
tournament_users }o--o| tournaments : fk_tournaments_memberships
tournament_users }o--o| users : fk_users_memberships
tournaments {
uuid id PK
text name
text visibility
text code
text status
uuid created_by FK
timestamptz start_time
timestamptz started_at
timestamptz completed_at
timestamptz created_at
timestamptz updated_at
}
tournaments }o--o| users : fk_users_created_tournaments
users {
uuid id PK
text email
text password
text first_name
text last_name
timestamptz created_at
timestamptz updated_at
}
The backend and its Postgres deploy to Generate's shared DigitalOcean droplet, and the frontend to Vercel. Publishing a GitHub Release ships production. Every pull request gets its own preview backend, preview database, and Vercel frontend preview. See DEPLOYMENT.md for day-to-day use and deploy/ for the droplet.