Skip to main content

Local Development & Workflow

Roja's local development environment is designed to be fast, reproducible, and container-free. Local data services (PostgreSQL 18 and Redis 8) and backend hot reloading run natively on your host machine via Nix flakes and process-compose, avoiding Docker virtualization overhead.


Prerequisites​

Before starting, install the required toolchains:

ToolVersionPurpose
Go1.27+Backend API and worker compilation
Bun1.3.14+Monorepo package manager and runtime
Node.jsv24 (see .nvmrc)Node ecosystem runtime compatibility
NixFlakes enabledNative Postgres/Redis process runner & container image builder
Atlas CLILatestDeclarative database migration engine
sqlcLatestCompiles SQL queries into type-safe Go code
AirLatestLive reload daemon for Go backend (go tool air)

Docker is optional: You do not need Docker to run Roja locally. Docker is only needed if you want to execute isolated database integration test suites (task backend:test-integration) or test Nix-built container images locally.


Step-by-Step Initial Setup​

1. Clone and Install Dependencies

Clone the monorepo and install Bun workspace packages from the repo root:

git clone https://github.com/getroja/roja.git
cd roja
bun install
2. Configure Backend Environment

Copy the example environment template in backend/:

cp backend/.env.example backend/.env

The default connection strings for native local development are:

DATABASE_URL=postgres://roja_user:roja_password@127.0.0.1:5432/roja?sslmode=disable
REDIS_URL=redis://:roja_redis_password@127.0.0.1:6379/0
PORT=8080
JWT_SECRET=local-dev-jwt-secret-do-not-use-in-production
3. Start Local Data Services

Start native PostgreSQL 18 and Redis 8 via Nix from the repo root:

task services # Equivalent to: nix run .#services

This launches both daemons natively via process-compose. Data persists across restarts in ./data/. Press Ctrl+C to cleanly shut down.

4. Apply Migrations & Generate Code

With the services running, initialize the schema and generate Go & TypeScript clients:

task apply-db # Runs Atlas migration apply against local database
task generate # Compiles sqlc queries and OpenAPI specs into Go code
5. Seed Reference & Test Data

Load default lookup tables, insurance providers, and verified test accounts:

make -C backend seed-prod
make -C backend seed-dev

Seeded accounts and test fixtures are defined in backend/db/seed.dev.sql. Test account credentials and role assignments for local simulation can be inspected in that seed fixture or reset using local dev tooling:

  • Borrower account (Verified borrower with bank account linked)
  • Lender account (Verified lender with active wallet)
  • Staff account (Staff account with operational review access)

Running the Application​

Roja supports running individual components or the full multi-tier platform simultaneously.

Common Run Modes​

# Option 1: Full-stack dev (starts services, API hot reload, workers, Web :3000, Staff :3001, Expo Metro :8081)
task full-stack # = nix run .#full-stack

# Option 2: Services + Backend API hot-reload
task dev-stack # = nix run .#dev

# Option 3: Modular execution (Run services in one terminal, app in another)
task services # Terminal 1: Postgres + Redis
task api # Terminal 2: Go API with Air (:8080)
bun --filter roja-web dev # Terminal 3: Customer Web (:3000)
bun --filter roja-staff dev -- -p 3001 # Terminal 4: Staff Dashboard (:3001)
task app # Terminal 5: Mobile Expo Metro (:8081)

Health & Diagnostic Endpoints​

Verify backend connectivity:

  • GET http://localhost:8080/healthz β€” Basic liveness probe.
  • GET http://localhost:8080/readyz β€” Readiness probe reporting PostgreSQL and Redis connectivity.
  • GET http://localhost:8080/v1/meta/version β€” Returns API version and commit hash.

The backend has no root / route; root returns 404 Not Found. All functional endpoints live under /v1/....


Database Management & Migrations​

Roja uses a declarative schema with versioned migrations powered by Atlas and sqlc.

backend/db/schema/*.sql ──(atlas diff)──► backend/db/migrations/*.sql
β”‚ β”‚
β”‚ (atlas apply) β”‚ (atlas apply)
β–Ό β–Ό
Postgres Database β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–²
β”‚ (sqlc generate)
backend/db/query/*.sql ─────────────────► backend/db/gen/*.go

Workflow for Database Changes​

1. Edit Declarative Schema

Always modify the declarative schema files in backend/db/schema/ (e.g. 02_users.sql, 07_transactions.sql, etc.). Never write manual SQL migration files directly without diffing.

2. Diff Migration with Atlas

Generate a new versioned migration from your schema changes:

task backend:migrate-diff NAME=add_borrower_fields

Atlas compares backend/db/schema/ against the migration baseline and writes a new file in backend/db/migrations/ along with an updated atlas.sum hash.

3. Apply Migration Locally

Apply the new migration to your local database:

task apply-db # = task backend:apply-db
4. Update SQL Queries & Generate sqlc

If your change requires new or updated database queries, add them to backend/db/query/*.sql, then regenerate Go models:

task generate # = task backend:generate

Zero-Downtime & Concurrent Index Rules​

In production, adding an index to a busy financial table using standard CREATE INDEX locks writes. You must follow the Concurrent Index Rule:

  1. Hand-edit the generated migration file in backend/db/migrations/.
  2. Add -- atlas:txmode none at the top of the file so Atlas executes statements outside a transaction block.
  3. Change CREATE INDEX to CREATE INDEX CONCURRENTLY.
  4. Re-calculate the migration directory integrity hash:
task backend:migrate-hash

task backend:apply-db and CI migration linters will strictly refuse to apply migrations if atlas.sum does not match the file hashes in backend/db/migrations/.

CI Migration Linter​

CI executes task backend:migrate-lint-oss on pull requests to detect destructive schema modifications (dropping columns, dropping tables, or locking alterations). If an intentional destructive change is needed, annotate the SQL line with:

-- lint:ignore <reason for intentional data loss or column removal>
ALTER TABLE users DROP COLUMN legacy_status;

Code Quality & Tooling​

Native TypeScript 7 Typecheck​

Every workspace extends tools/tsconfig/base.json and runs stable native TypeScript 7:

# Typecheck specific workspace
bun --filter roja-web typecheck
bun --filter roja-staff typecheck
bun --filter @roja/common typecheck
bun --filter @roja/docs typecheck

# Check all workspaces
bun run check

Oxc Linter and Formatter​

Roja uses the high-performance Rust-based Oxc toolchain (oxlint and oxfmt) instead of legacy ESLint:

bun run lint # Runs oxlint across all workspaces
bun run format # Formats JS/TS/JSX/JSON/CSS using oxfmt

Prettier is reserved solely for formatting Markdown, YAML, and SCSS stylesheets.


Git Worktree Conventions​

When working on isolated features, bugfixes, or PR reviews using Git worktrees, follow these repository rules:

  1. Dedicated Directory Root: Create worktrees under ~/code/worktrees/<repo>/<slug>, never inside the repository (e.g. .worktrees/ inside roja/ is forbidden).
  2. Shared node_modules: Worktrees share the primary checkout's node_modules to save disk space and eliminate repetitive bun install runs.
  3. Workspace Linking: If TypeScript compiler reports TS2305: has no exported member for an export in @roja/common or @roja/client, the Bun workspace symlinks need refreshing in the worktree. Run:
bash .claude/hooks/link-workspace-packages.sh

Next Steps​