Appearance
Development Setup
Prerequisites
- Java 21 (all modules compile and run with Java 21)
- Maven 3.8+ (Maven wrapper
mvnwis 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 8080Frontend (schemastack-fe)
bash
# Terminal 5 — App (Data Platform)
ng serve --port=4200 app
# Terminal 6 — Admin Console
ng serve --port=4201 adminDocs (schemastack-docs)
bash
npm run dev # Public docs
npm run dev:dev # Dev docsBackend 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 watcherInfrastructure Services
Docker Compose starts two services:
| Service | Port | Credentials |
|---|---|---|
| PostgreSQL 18 | localhost:5432 | postgres / your-password, database dynamicdb |
| RabbitMQ AMQP | localhost:5673 | guest / guest |
| RabbitMQ Management | localhost:15673 | guest / 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
| File | Purpose |
|---|---|
.env | Base configuration (database, JWT, mailer, RabbitMQ) |
.env.local | Local development overrides |
.env.test | Test configuration with TestContainers and in-memory messaging |
~/.docker-java.properties | Required 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
- Go to Google Cloud Console → Credentials
- Create a project (or select existing)
- Click Create Credentials → OAuth 2.0 Client ID
- Application type: Web application
- Add Authorized redirect URIs:
- Development:
http://localhost:4200/auth/callback/google - Production:
https://app.schemastack.io/auth/callback/google
- Development:
- Copy the Client ID and Client Secret
GitHub
- Go to GitHub → Settings → Developer settings → OAuth Apps
- Click New OAuth App
- Set Authorization callback URL:
- Development:
http://localhost:4200/auth/callback/github - Production:
https://app.schemastack.io/auth/callback/github
- Development:
- 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-secretThe 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
| Layer | File | Setting |
|---|---|---|
| PostgreSQL container | docker-compose.override.yml | command: -c timezone=UTC + TZ: UTC env |
| Quarkus JVM | scripts/dev-start.sh | TZ=UTC env + -Duser.timezone=UTC JVM arg |
| Quarkus Hibernate (metadata) | quarkus/metadata/metadata-persistence/.../application.properties | quarkus.hibernate-orm.jdbc.timezone=UTC |
| Quarkus Hibernate (organisation) | quarkus/organisation/organisation-persistence/.../application.properties | quarkus.hibernate-orm.jdbc.timezone=UTC |
| Processor JVM | scripts/processor-start.sh | TZ=UTC env + -Duser.timezone=UTC JVM arg |
| Processor Hibernate | spring-boot/processor/processor-service/.../application.properties | spring.jpa.properties.hibernate.jdbc.time_zone=UTC |
| Processor Jackson | spring-boot/processor/processor-service/.../application.properties | spring.jackson.time-zone=UTC |
| Workspace API JVM | scripts/workspace-api-start.sh | TZ=UTC env + -Duser.timezone=UTC JVM arg |
| Workspace API Jackson | spring-boot/workspace-api/workspace-api-service/.../application.properties | spring.jackson.time-zone=UTC |
| Workspace API dynamic SessionFactories | DynamicEntitySessionFactoryBuilder.java | hibernate.jdbc.time_zone=UTC |
Why three layers per service
TZenv var — sets the OS-level timezone for the process (affectsLocalDateTime.now(), log timestamps)-Duser.timezone=UTC— sets the JVM timezone (affectsjava.util.Date, JDBC drivers)hibernate.jdbc.time_zone=UTC— tells Hibernate to normalize allTimestamp/Instantvalues 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 -amService URLs
| Service | URL |
|---|---|
| Quarkus REST API | http://localhost:8080 |
| Spring Boot Processor | http://localhost:8082 |
| Spring Boot Workspace API | http://localhost:8083 |
| RabbitMQ Management | http://localhost:15673 |
Data Persistence
Docker volumes persist data between restarts:
postgres_data— PostgreSQL datarabbitmq_data— RabbitMQ staterabbitmq_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
| Script | Description |
|---|---|
dev-start.sh | Start Docker services + Quarkus dev mode. Pass --reset to drop and recreate dynamicdb. Also purges dev RabbitMQ queues and cleans up failed migrations. |
dev-stop.sh | Kill Maven processes (graceful, then force) and docker compose down. |
processor-start.sh | Start the Spring Boot Processor on port 8082. Checks that Quarkus is running on 8080 first. |
workspace-api-start.sh | Start the Spring Boot Workspace API on port 8083. Checks that Quarkus is running on 8080 first. |
Build Helpers
| Script | Description |
|---|---|
install-contracts.sh | Build 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
| Script | Description |
|---|---|
run-tests-background.sh | Rsync 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.sh | Watch 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
| Script | Description |
|---|---|
rabbitmq-status.sh | Query 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:
| Script | Run against | Description |
|---|---|---|
check-duplicate-columns.sql | Metadata DB (dynamicdb) | Find duplicate columns in the metadata tables. Drift detection crashes with "Duplicate key" errors when these exist. |
cleanup-duplicate-columns.sql | Metadata DB (dynamicdb) | Delete duplicate columns, keeping the first occurrence (lowest ID). Runs in a transaction — any error rolls back. |
check-workspace-catalog.sql | Workspace 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):
| Module | Master File | Changesets |
|---|---|---|
| Metadata | metadata-persistence/.../changelog-metadata.xml | 46 (1.0 — 1.47) |
| Organisation | organisation-persistence/.../changelog-organisation.xml | 11 (1.0 — 6.0) |
| Messaging | messaging-infrastructure/.../changelog-messaging.xml | 1 (1.0) |
| Total | 3 masters | 58 production changesets |
Key migration landmarks:
| Version | What |
|---|---|
| 1.0 | Initial schema (entities, views, columns) |
| 1.3 | RBAC permissions |
| 1.20 | Relationship column support |
| 1.21-1.22 | Bulk action jobs |
| 1.23-1.25 | Filter presets + sorts |
| 1.26 | Sync operation tracking |
| 1.27 | Outbox messages |
| 1.32 | View guest access |
| 1.40 | Workspace API keys |
| 1.47 | Usage tracking |
| org-4.4 | Two-factor authentication |
| org-6.0 | Subscription 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-21with Maven cache mount - Runtime:
eclipse-temurin:21-jre-alpine - Build arg:
QUARKUS_PROFILE=worker - Copies
cert.pemfor 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-9c8877a761363. 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