- Docker and Docker Compose
- Just command runner
- Go (see
services/*/go.modfor required version) - Node.js (see
frontend/package.jsonfor required version) - protoc (for protobuf compilation)
- golang-migrate (for database migrations)
- sqlc (for type-safe SQL code generation)
# Clone and configure
cp .env.example .env
# Edit .env with your values (defaults work for local development)Key environment variables are documented in .env.example. See the full variable reference below.
just up # Build and start everything
just seed-admin # Create the initial admin user
# App: http://localhost:3000
# Grafana: http://localhost:3001When iterating on a single backend service, avoid rebuilding all containers:
just dev-infra # Start databases + monitoring
just dev-service auth # Run the auth service locally (hot reload with air)
just dev-frontend # Start the Turborepo frontend with HMRdocker-compose.dev.yml adds volume mounts (source code hot reload), debug ports (Delve debugger on 4000x), relaxed security (no HTTPS for cookies), and reduced bcrypt cost:
just up-devTo work on the UI without running any backend services or Docker containers:
just dev-mockThis starts the shell dev server with the VITE_MOCK_API=true flag, which activates MSW (Mock Service Worker) in the browser. MSW intercepts all fetch calls to /api/* before they reach the express proxy, so the backend never needs to be running.
The mock layer provides:
- An authenticated admin user (auto-logged in)
- A current-month budget period ($3,000, 50/30/20 split)
- Seven sample expenses across different categories and tags
- All eleven default tags plus one custom tag
- Dashboard aggregation data (summary, pacing, tag spending, cumulative chart, historical comparison)
- An upcoming pro-rata installment
- A second user in the admin panel for identity assumption testing
Mock data is defined in frontend/apps/shell/mocks/data.ts. Request handlers are in frontend/apps/shell/mocks/handlers.ts. To change the authenticated user (e.g., test as a non-admin), edit the currentMockUser export in data.ts.
How it works:
- The shell's
entry.client.tsxchecksimport.meta.env.VITE_MOCK_API - When
"true", it starts the MSW browser service worker before React hydration - The service worker intercepts
fetchcalls matching handler routes and returns mock responses - Unmatched requests (static assets, HMR) pass through normally
Each Go service with a proto/ directory has gRPC definitions. After modifying .proto files:
just proto auth # Generate for a specific service
just proto-all # Generate for all servicesAuth and Finance services use sqlc for type-safe database queries. After modifying SQL queries in db/queries/:
just sqlc auth
just sqlc financeMigrations use golang-migrate with SQL files in each service's db/migrations/ directory:
just migrate auth up # Run auth service migrations
just migrate finance down # Roll back finance service migrationsIn Docker Compose, migrations run automatically: PostgreSQL's init scripts execute migration files on first boot.
The services/ directory uses a Go workspace (go.work) to link all service modules. This allows cross-module imports during development while maintaining independent go.mod files per service.
cd services
go work sync # Sync the workspace after dependency changesThe frontend/ directory is a Turborepo monorepo with three apps and three shared packages:
Apps:
shell: MF host (SSR, routing, auth, layout, API proxy)finance: MF remote (dashboard, expense log, expense form, settings)admin: MF remote (admin panel, user list, identity assumption)
Packages:
@gofin/ui: shared shadcn/ui components with Tailwind@gofin/types: shared TypeScript types (API contracts, domain models)@gofin/config: shared configs (tailwind, tsconfig, eslint)
cd frontend
npx turbo dev # Start all apps in dev mode
npx turbo build # Build all apps
npx turbo lint # Lint all apps and packages
npx turbo test # Run all frontend testsAll environment variables are documented in .env.example with sensible defaults for local development:
cp .env.example .envKey variable groups:
- Shared:
JWT_SECRET,LOG_LEVEL,ENVIRONMENT - Auth Service: PostgreSQL connection, bcrypt cost, admin seed credentials
- Finance Service: PostgreSQL connection, expense service gRPC address
- Expense Service: immudb connection credentials
- Datarights Service: PostgreSQL connection, Resend API key, email sender config
- API Gateway: service addresses (gRPC and REST) for auth, expense, finance, and datarights
- MFE (Shell): API gateway URL, cookie security flag
- Grafana Auth Proxy: JWT secret, Grafana URL
See .env.example for the complete list with descriptions and example values.
The first admin user must be created before the admin panel, identity assumption, or Grafana access can function:
just seed-adminThis runs the auth service's seed-admin CLI subcommand, which reads ADMIN_USERNAME, ADMIN_EMAIL, and ADMIN_PASSWORD from environment variables. The command is idempotent: if an admin already exists, it exits successfully.
The admin then logs in through the normal UI and completes onboarding like any other user.
The datarights service runs on port 8084 (REST) and 9084 (gRPC, reserved). It handles GDPR data export: collecting user data from upstream services, assembling CSVs into a ZIP, and emailing the result.
just dev-service datarightsBy default, EMAIL_ENABLED=false in .env.example. In this mode, the service logs email content to stdout instead of sending via Resend:
{"level":"INFO","msg":"email delivery disabled: logging email content","to":"user@example.com","zip_size_bytes":24576}
To test real email delivery locally, set EMAIL_ENABLED=true and provide a valid RESEND_API_KEY in your .env.
The service reads tokens/brand.json for email template styling (colors, logo URL). In Docker, this is volume-mounted from the repo root tokens/ directory. When running locally via just dev-service datarights, set BRAND_TOKENS_PATH=./tokens/brand.json (relative to repo root).
| Variable | Default | Purpose |
|---|---|---|
DATARIGHTS_DB_URL |
(required) | PostgreSQL connection with search_path=datarights |
EMAIL_ENABLED |
false (via .env.example) |
When false, emails log to stdout. If unset, defaults to true |
RESEND_API_KEY |
(empty) | Required when EMAIL_ENABLED=true |
EMAIL_FROM |
gofin <noreply@usegofin.com> |
Sender address |
BRAND_TOKENS_PATH |
/app/tokens/brand.json |
Path to brand token JSON file |
EXPORT_MAX_CONCURRENT |
5 |
Max parallel export goroutines |
EXPORT_TIMEOUT_SECONDS |
300 |
Per-job timeout (5 minutes) |