Skip to content

System Architecture

SchemaStack is a hybrid Quarkus + Spring Boot platform. The split exists because Hibernate Reactive (used in Quarkus for non-blocking I/O) cannot do programmatic DDL generation — Spring Boot with classic Hibernate handles that part.

Deployment Model

The platform is designed for two deployment targets:

  • AWS Lambda — stateless REST APIs (metadata-rest, organisation-rest). Cold start optimized via Quarkus native builds.
  • AWS Fargate — long-running services (consumers, producers, SSE endpoints, Spring Boot processor). These maintain connections and state.

INFO

Infrastructure-as-code (CDK/SAM) is not in this repository. The codebase is Lambda-capable (quarkus-amazon-lambda-http dependency) but deployment automation lives separately.

Service Architecture

┌─────────────────────────────────────────────────────────┐
│                      Clients                            │
│            (Browser / API consumers)                    │
└──────────┬─────────────────────────────┬────────────────┘
           │ REST                        │ SSE
           ▼                             ▼
┌──────────────────┐          ┌──────────────────┐
│  Quarkus REST    │          │  Quarkus SSE     │
│  (Lambda)        │          │  (Fargate)       │
│  - metadata-rest │          │  - metadata-sse  │
│  - org-rest      │          │  - org-sse       │
└────────┬─────────┘          └────────▲─────────┘
         │                             │
         │ RabbitMQ                     │ RabbitMQ broadcast
         ▼                             │
┌──────────────────┐          ┌──────────────────┐
│  Quarkus         │          │  Quarkus         │
│  Consumers       │──────────│  Producers       │
│  (Fargate)       │          │  (Fargate)       │
└────────┬─────────┘          └──────────────────┘
         │ RabbitMQ

┌──────────────────────────────────────┐
│  Spring Boot Services (Fargate)      │
│  - processor    (DDL/Flyway)         │
│  - workspace-api (dynamic CRUD)      │
│  - session      (session factory)    │
└────────┬─────────────────────────────┘


┌──────────────────┐
│  PostgreSQL      │
│  - dynamicdb     │
│  - workspace DBs │
└──────────────────┘

Messaging

All inter-service communication uses RabbitMQ (not SQS/SNS).

Transactional Outbox Pattern

Messages are not sent directly to RabbitMQ from within business transactions. Instead, they're persisted to the outbox_message table as part of the same database transaction, then published asynchronously by a poller.

Business Transaction

    ├── 1. DB writes (entity changes)
    ├── 2. OutboxService.queueMessage() → inserts OutboxMessage (PENDING)
    └── COMMIT

OutboxPoller (every 5s)
    ├── 3. Polls PENDING messages (batch of 100)
    ├── 4. Marks PROCESSING
    ├── 5. basicPublish() to RabbitMQ
    └── 6. Marks SENT (or FAILED after 5 retries)

OutboxMessage fields: messageId (UUID, dedup key), aggregateType + aggregateId, exchange, routingKey, payload (JSONB), status, retryCount.

OutboxPoller scheduled jobs:

  • Every 5s: poll and publish (batch of 100, max 5 retries per message)
  • Every 1m: reset stuck messages (PROCESSING > 5 minutes → back to PENDING)
  • Every 1h: cleanup old SENT messages (> 7 days)

Currently used for sync operation completions and task completion notifications via OutboxService.queueSyncCompletionMessage() and queueTaskCompletionMessage().

MessageBus (Idempotency + Audit)

For direct RabbitMQ publishing (non-outbox), the MessageBus provides a middleware chain inspired by Symfony Messenger:

  1. Idempotency check — queries message_audit table; skips if already COMPLETED or PROCESSING
  2. Audit record — persists a MessageAudit with PENDING status, full JSON payload, and optional metadata (workspaceId, entityType, entityId, operation)
  3. Send with retry — publishes via SmallRye emitter with up to 3 retries
  4. Audit update — marks COMPLETED on success, FAILED → DEAD_LETTER on exhausted retries

MessageAudit statuses: PENDINGPROCESSINGCOMPLETED | FAILED | DEAD_LETTER.

Exchanges

Key exchanges:

ExchangeTypeFlow
metadata-updatesdirectQuarkus REST → Spring Boot processor
metadata-updates-retrydirectProcessor retry with TTL
metadata-updates-dlxdirectProcessor dead letter exchange
task-completiondirectProcessor → Quarkus consumers
task-completion-retrydirectConsumer retry with TTL
task-completion-dlxdirectConsumer dead letter exchange
workspace-eventsdirectWorkspace event broadcasts
bulk-actionsdirectBulk operation events
sync-operationdirectSchema sync operations
email-exchangedirectEmail sending
member-events-exchangedirectOrganisation member change events

Consumers use single-active-consumer pattern with RabbitMQ broadcast mode for fan-out to SSE producers.

Infrastructure Dependencies

DependencyUsage
PostgreSQLPrimary database. dynamicdb for metadata/org data; per-workspace schemas created by processor
RabbitMQAll async messaging. Durable queues, dead-letter exchanges, retry with TTL
S3 / S3-compatibleFile storage (uploads, exports, mapped files). Per-workspace config with encrypted credentials. Required in production — local filesystem is dev-only

Not used (despite some docs mentioning it)

Redis is not in the codebase.

Resilience

The Spring Boot processor has Resilience4j as a dependency with configuration for:

  • Circuit breakersmigrationService (50% failure threshold, 10-call window) and workspaceConnection (50%, 5-call window)
  • Retry — max 3 attempts with exponential backoff for both services
  • Rate limitermigrationService at 10 calls/second

Configuration in processor-service/src/main/resources/application.properties.

WARNING

The Resilience4j dependency and configuration exist, but @CircuitBreaker, @Retry, and @RateLimiter annotations are not yet applied to service methods. The protection is configured but not active.

SSE (Server-Sent Events)

Real-time updates use SSE endpoints in Quarkus. REST API instances never broadcast directly — they publish WorkspaceEvent messages to RabbitMQ, and the consumer-worker instance routes them to the correct SSE streams based on organisationSlug and workspaceId fields.

Two SSE streams serve different frontends:

  • /sse/workspace/{id}/stream — data app (view/column changes, sync progress, task completions)
  • /sse/organization/{slug}/stream — admin app (workspace lifecycle, org settings, members, dashboard stats)

Events with both routing fields set are broadcast to both streams.

See SSE Broadcasting Architecture for the full flow diagram, routing table, and implementation details.

Email System

Email delivery is asynchronous via RabbitMQ. Three email types are supported:

TypeExpiryTrigger
VERIFICATION24 hoursUser registration
PASSWORD_RESET1 hourForgot password
INVITATION2 daysMember invitation

Flow: Service → EmailEventProducer → RabbitMQ (email-exchange) → EmailEventConsumerEmailService → AWS SES

The EmailService uses Quarkus ReactiveMailer backed by AWS SES SMTP in production. In dev mode (mailer.dev.enabled=true), all emails are redirected to a single address with a "[DEV REDIRECT]" subject prefix.

Email links point to the frontend: {app.frontend.url}/auth/verify-email?token={token}, {app.frontend.url}/auth/reset-password?token={token}, {app.frontend.url}/auth/accept-invitation?token={token}.

Subscription Tiers & Usage Tracking

Each organisation has a subscription tier that defines limits:

LimitExample
maxWorkspacesnull = unlimited
maxViewsPerWorkspace
maxMembers
monthlyRequestLimit
monthlyRowOperationLimit
storageLimitMb
rateLimitPerMinuteDefault 60

Usage is tracked per workspace per billing period via UsagePeriod (request count, row read/write counts, storage bytes). The UsageService aggregates usage across all workspaces for the current billing period and returns a UsageSummaryDTO.

SchemaStack Internal Developer Documentation