Skip to content

Development Setup

Prerequisites

  • Java 21 (all modules compile and run with Java 21)
  • Maven 3.8+ (Maven wrapper mvnw is included in the repo)
  • Docker and Docker Compose

Quick Start (Full Stack)

Starting the complete stack requires three repositories and multiple terminal sessions.

Backend (schemastack-be)

bash
# Terminal 1 — Quarkus REST + infrastructure (Docker, RabbitMQ, PostgreSQL)
./scripts/dev-start.sh

# Terminal 2 — Spring Boot Processor (DDL/migrations)
./scripts/processor-start.sh

# Terminal 3 — Spring Boot Workspace API (dynamic CRUD)
./scripts/workspace-api-start.sh

# Terminal 4 — ngrok tunnel for LemonSqueezy webhooks
ngrok http 8080

Frontend (schemastack-fe)

bash
# Terminal 5 — App (Data Platform)
ng serve --port=4200 app

# Terminal 6 — Admin Console
ng serve --port=4201 admin

Docs (schemastack-docs)

bash
npm run dev        # Public docs
npm run dev:dev    # Dev docs

Backend Tests

bash
# Run full test suite in an isolated copy (non-blocking)
./scripts/run-tests-background.sh

# Run only a specific test module
./scripts/run-tests-background.sh workspace-api
./scripts/run-tests-background.sh metadata
./scripts/run-tests-background.sh organisation
./scripts/run-tests-background.sh processor

# Check progress / follow output
./scripts/run-tests-background.sh --status
./scripts/run-tests-background.sh --watch

# Auto-run tests on file changes (requires: brew install fswatch)
./scripts/run-tests-watch.sh                    # Watch all tests
./scripts/run-tests-watch.sh workspace-api      # Watch single module
./scripts/run-tests-watch.sh --idle 5           # Custom idle seconds
./scripts/run-tests-watch.sh --stop             # Stop watcher

Infrastructure Services

Docker Compose starts two services:

ServicePortCredentials
PostgreSQL 18localhost:5432postgres / your-password, database dynamicdb
RabbitMQ AMQPlocalhost:5673guest / guest
RabbitMQ Managementlocalhost:15673guest / guest

Non-standard RabbitMQ port

RabbitMQ runs on 5673 (not the default 5672) to avoid conflicts with other local services. The management UI is on 15673.

Environment Files

FilePurpose
.envBase configuration (database, JWT, mailer, RabbitMQ)
.env.localLocal development overrides
.env.testTest configuration with TestContainers and in-memory messaging
~/.docker-java.propertiesRequired for Docker Desktop 4.52+ (Engine 29.x) — see Testing > Testcontainers compatibility

OAuth Provider Setup (Google & GitHub)

OAuth social login requires client credentials from each provider. These are not needed to run the app locally unless you're testing OAuth flows.

Google

  1. Go to Google Cloud Console → Credentials
  2. Create a project (or select existing)
  3. Click Create Credentials → OAuth 2.0 Client ID
  4. Application type: Web application
  5. Add Authorized redirect URIs:
    • Development: http://localhost:4200/auth/callback/google
    • Production: https://app.schemastack.io/auth/callback/google
  6. Copy the Client ID and Client Secret

GitHub

  1. Go to GitHub → Settings → Developer settings → OAuth Apps
  2. Click New OAuth App
  3. Set Authorization callback URL:
    • Development: http://localhost:4200/auth/callback/github
    • Production: https://app.schemastack.io/auth/callback/github
  4. Copy the Client ID and Client Secret

Configuration

Add to .env.local:

bash
OAUTH_GOOGLE_CLIENT_ID=your-google-client-id
OAUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
OAUTH_GITHUB_CLIENT_ID=your-github-client-id
OAUTH_GITHUB_CLIENT_SECRET=your-github-client-secret

The backend endpoint is POST /api/auth/oauth/{provider} (where provider is google or github). The frontend sends the authorization code received from the provider callback.

Timezone (UTC Everywhere)

All services, databases, and JVMs are configured to use UTC. This prevents timezone mismatch bugs between layers.

Where it's configured

LayerFileSetting
PostgreSQL containerdocker-compose.override.ymlcommand: -c timezone=UTC + TZ: UTC env
Quarkus JVMscripts/dev-start.shTZ=UTC env + -Duser.timezone=UTC JVM arg
Quarkus Hibernate (metadata)quarkus/metadata/metadata-persistence/.../application.propertiesquarkus.hibernate-orm.jdbc.timezone=UTC
Quarkus Hibernate (organisation)quarkus/organisation/organisation-persistence/.../application.propertiesquarkus.hibernate-orm.jdbc.timezone=UTC
Processor JVMscripts/processor-start.shTZ=UTC env + -Duser.timezone=UTC JVM arg
Processor Hibernatespring-boot/processor/processor-service/.../application.propertiesspring.jpa.properties.hibernate.jdbc.time_zone=UTC
Processor Jacksonspring-boot/processor/processor-service/.../application.propertiesspring.jackson.time-zone=UTC
Workspace API JVMscripts/workspace-api-start.shTZ=UTC env + -Duser.timezone=UTC JVM arg
Workspace API Jacksonspring-boot/workspace-api/workspace-api-service/.../application.propertiesspring.jackson.time-zone=UTC
Workspace API dynamic SessionFactoriesDynamicEntitySessionFactoryBuilder.javahibernate.jdbc.time_zone=UTC

Why three layers per service

  • TZ env var — sets the OS-level timezone for the process (affects LocalDateTime.now(), log timestamps)
  • -Duser.timezone=UTC — sets the JVM timezone (affects java.util.Date, JDBC drivers)
  • hibernate.jdbc.time_zone=UTC — tells Hibernate to normalize all Timestamp/Instant values to UTC when reading/writing to the database

All three are needed because each layer can independently default to the host machine's local timezone.

Production

Production Docker containers set TZ: UTC in their Compose/ECS task definitions and pass -Duser.timezone=UTC as a JVM argument. The Hibernate and Jackson properties are baked into the application properties files.

Building

bash
# Full recompile without tests (fast)
./mvnw clean install -DskipTests=true

# Full recompile with tests (slow)
./mvnw clean install -DskipTests=false

# Build a specific module
./mvnw clean install -pl quarkus/metadata/metadata-rest -am

Service URLs

ServiceURL
Quarkus REST APIhttp://localhost:8080
Spring Boot Processorhttp://localhost:8082
Spring Boot Workspace APIhttp://localhost:8083
RabbitMQ Managementhttp://localhost:15673

Data Persistence

Docker volumes persist data between restarts:

  • postgres_data — PostgreSQL data
  • rabbitmq_data — RabbitMQ state
  • rabbitmq_logs — RabbitMQ logs

To reset all data: docker compose down -v

RabbitMQ Consumer Timeout

The Docker Compose config sets a 30-second consumer timeout (consumer_timeout 30000). This prevents zombie consumers from accumulating during Quarkus dev mode hot-reloads.

Scripts Reference

All scripts live in the scripts/ directory.

Startup & Shutdown

ScriptDescription
dev-start.shStart Docker services + Quarkus dev mode. Pass --reset to drop and recreate dynamicdb. Also purges dev RabbitMQ queues and cleans up failed migrations.
dev-stop.shKill Maven processes (graceful, then force) and docker compose down.
processor-start.shStart the Spring Boot Processor on port 8082. Checks that Quarkus is running on 8080 first.
workspace-api-start.shStart the Spring Boot Workspace API on port 8083. Checks that Quarkus is running on 8080 first.

Build Helpers

ScriptDescription
install-contracts.shBuild and install messaging-contracts to local Maven (~/.m2). Run this after changing shared message DTOs so both Quarkus and Spring Boot pick them up.

Testing

ScriptDescription
run-tests-background.shRsync to isolated directory and run tests in background. Pass a module name (metadata, organisation, processor, workspace-api) to run only that module's tests. Use --watch to follow output, --status to check progress. Sends macOS notifications.
run-tests-watch.shWatch for file changes using fswatch and auto-run tests after idle (default 10s, --idle <seconds>). Accepts optional module name to scope tests. --stop to stop. Requires brew install fswatch.

Diagnostics

ScriptDescription
rabbitmq-status.shQuery RabbitMQ Management API. Subcommands: status (queue overview), messages [queue] (peek at messages), dlq (dead letter queue), purge <queue> (empty a queue).

Database Diagnostics (scripts/diagnostics/)

Run via diagnostics/run-all-diagnostics.sh (interactive — prompts for credentials and runs all checks), or individually:

ScriptRun againstDescription
check-duplicate-columns.sqlMetadata DB (dynamicdb)Find duplicate columns in the metadata tables. Drift detection crashes with "Duplicate key" errors when these exist.
cleanup-duplicate-columns.sqlMetadata DB (dynamicdb)Delete duplicate columns, keeping the first occurrence (lowest ID). Runs in a transaction — any error rolls back.
check-workspace-catalog.sqlWorkspace DB (customer)Check PostgreSQL pg_attribute system catalog for duplicate columns. Rows found = real database corruption (not just a code bug).

Liquibase Migrations

Three independent Liquibase changelogs (the metadata master includes the other two):

ModuleMaster FileChangesets
Metadatametadata-persistence/.../changelog-metadata.xml46 (1.0 — 1.47)
Organisationorganisation-persistence/.../changelog-organisation.xml11 (1.0 — 6.0)
Messagingmessaging-infrastructure/.../changelog-messaging.xml1 (1.0)
Total3 masters58 production changesets

Key migration landmarks:

VersionWhat
1.0Initial schema (entities, views, columns)
1.3RBAC permissions
1.20Relationship column support
1.21-1.22Bulk action jobs
1.23-1.25Filter presets + sorts
1.26Sync operation tracking
1.27Outbox messages
1.32View guest access
1.40Workspace API keys
1.47Usage tracking
org-4.4Two-factor authentication
org-6.0Subscription tiers

Dev seed data files (context="dev") provide local development fixtures: test users, organisations, workspace memberships.

Deployment

Two deployment patterns:

Lambda (REST API)

dockerfile
FROM public.ecr.aws/lambda/provided:al2
COPY target/function/bootstrap /var/task/bootstrap
CMD ["bootstrap"]

Quarkus native binary built for Lambda. Stateless, cold start optimized.

Fargate (Consumer Worker)

Multi-stage Maven build → JVM-based Quarkus fast-jar:

  • Build: maven:3.9.9-eclipse-temurin-21 with Maven cache mount
  • Runtime: eclipse-temurin:21-jre-alpine
  • Build arg: QUARKUS_PROFILE=worker
  • Copies cert.pem for TLS
  • Port 8080

Claude Code — Chrome DevTools MCP Setup

When Claude Code runs inside Docker, it can control a browser on the host machine via the Chrome DevTools Protocol (CDP). This enables visual testing, screenshot capture, and UI interaction directly from Claude.

1. Launch Brave with Remote Debugging

On the host machine (macOS), start Brave with CDP enabled:

bash
/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
  --remote-debugging-port=9222 \
  --remote-allow-origins="*" \
  --remote-debugging-address=0.0.0.0 \
  --user-data-dir="$HOME/.config/quarkus-mcp"

Key flags:

  • --remote-debugging-port=9222 — CDP WebSocket port
  • --remote-allow-origins="*" — allow connections from Docker
  • --remote-debugging-address=0.0.0.0 — listen on all interfaces (not just localhost)
  • --user-data-dir — separate profile so it doesn't interfere with your main browser

2. Get the WebSocket Endpoint

After Brave launches, fetch the CDP WebSocket URL:

bash
curl -s http://localhost:9222/json/version | jq -r '.webSocketDebuggerUrl'

This returns something like:

ws://localhost:9222/devtools/browser/e0f483c8-d843-4d3d-98d4-9c8877a76136

3. Configure .mcp.json

In the workspace root (.mcp.json), add the chrome-devtools server. Replace the WebSocket URL with the one from step 2:

json
{
  "mcpServers": {
    "chrome-devtools": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "-y",
        "chrome-devtools-mcp@latest",
        "--wsEndpoint",
        "ws://host.docker.internal:9222/devtools/browser/<browser-id>",
        "--wsHeaders",
        "{\"Host\":\"localhost\"}"
      ],
      "env": {}
    }
  }
}

Key details:

  • host.docker.internal — Docker's magic hostname that resolves to the host machine
  • --wsHeaders '{"Host":"localhost"}' — required because Brave validates the Host header against its allowed origins; without this, the connection is rejected
  • The browser ID in the WebSocket URL changes each time Brave restarts — update it after restarting

4. Available Capabilities

Once connected, Claude Code can:

  • Navigate — open URLs, go back/forward, reload
  • Take snapshots — get the accessibility tree (DOM structure with UIDs)
  • Take screenshots — capture the viewport as an image
  • Click/fill/type — interact with elements by UID
  • Evaluate scripts — run JavaScript in the page context
  • Inspect network/console — view requests and console messages
  • Emulate — set viewport size, dark mode, network throttling

SchemaStack Internal Developer Documentation