Skip to main content

Backend Overview

Lumie's backend is one Spring Boot deployable assembled from app, libs/*, and the Gradle subprojects included in lumie-backend/settings.gradle.kts. The entrypoint is lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java, which starts a single JVM with @SpringBootApplication(scanBasePackages = "com.lumie") and @EnableAsync.

This page is an overview document. It maps the deployable boundary, the major runtime flows, and the shared rules that every backend module lives under.

Source Paths

PathRole
lumie-backend/settings.gradle.ktsDeclares the subprojects that ship in the backend runtime
lumie-backend/app/build.gradle.ktsAssembles the deployable jar from app, libs/*, and included modules/*
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.javaSpring Boot entrypoint
lumie-backend/app/src/main/resources/application.yamlRuntime configuration for data sources, Flyway, Modulith, RabbitMQ, Redis, MinIO, CORS, rate limits, and worker URLs
lumie-backend/libs/commonShared tenant, auth, exception, idempotency, logging, and base-entity utilities
lumie-backend/libs/internal-apiIn-process module contracts and cross-module events
lumie-backend/libs/messagingRabbitMQ queue, exchange, and routing-key constants
lumie-backend/modules/*Product and platform modules loaded into the same JVM

Deployable Shape

What Ships In Process

app/build.gradle.kts wires these categories into the backend jar:

modules/activity-log exists in the repository tree but is not included in settings.gradle.kts, so it is not part of the current runtime.

Primary Runtime Flows

Authenticated product request

  1. A controller under modules/*/adapter/in/web receives /v1/**.
  2. JwtAuthenticationFilter accepts either Authorization: Bearer ... or the lumie_access_token cookie.
  3. Tenant and user context are written into TenantContextHolder and UserContextHolder.
  4. The owning application service executes inside a transaction.
  5. RlsTenantContextAspect binds app.tenant_id so PostgreSQL RLS can filter tenant-scoped rows.
  6. The module either commits synchronously or emits a post-commit event for follow-up work.

Internal worker or platform request

  1. /internal/** requests are authenticated by InternalHmacAuthFilter, not by end-user JWT.
  2. The filter verifies X-Tenant-Slug, X-Timestamp, and X-Signature, resolves the tenant ID, and grants synthetic ROLE_INTERNAL.
  3. Downstream code runs with the same tenant context and RLS enforcement as a normal request.

After-commit async follow-up

  1. A module publishes a Spring Modulith event inside the same database transaction as its domain write.
  2. The event is stored in public.event_publication.
  3. After commit, an @ApplicationModuleListener handles the event in-process or forwards it to RabbitMQ.
  4. If the listener or broker send fails, the publication remains incomplete and is retried on restart because spring.modulith.events.republish-outstanding-events-on-restart=true.

Shared Boundary Rules

  • The backend is a modular monolith, not a set of separately deployed Java services. Module names describe ownership boundaries inside one process.
  • Module structure follows domain, application, and adapter; see Architecture.
  • Synchronous module-to-module calls go through libs/internal-api interfaces, usually implemented under adapter/in/internal/.
  • Durable internal events go through Spring Modulith's JDBC outbox, not direct @TransactionalEventListener wiring.
  • External Python workers are integrations. They do not own tenant data tables; the monolith remains the source of truth for transactional writes and tenant-safe reads.

Failure Modes To Keep In Mind

  • Missing tenant ID in context means RLS-backed tables appear empty even if the slug is present.
  • Giving the runtime database role SUPERUSER or BYPASSRLS would silently defeat tenant isolation, so startup is blocked by RuntimeDbRoleGuard.
  • Async code that loses TenantContextHolder or UserContextHolder will fail or read the wrong scope unless it uses the task-decorator or explicit withinContext(...) restoration.
  • Queue-backed jobs are durable only when the module publishes through the outbox path; direct HTTP calls to workers do not get that durability.

Verification Commands

cd /Users/bluemayne/Projects/Lumie/lumie-backend
./gradlew test
./gradlew integrationTest
./gradlew :app:test
./gradlew :libs:common:test

Useful integration tests for this overview:

  • app/src/test/java/com/lumie/app/config/RoutingDataSourceIntegrationTest.java
  • app/src/test/java/com/lumie/app/migration/MigrationsRlsIntegrationTest.java
  • libs/common/src/test/java/com/lumie/common/tenant/RlsTenantContextAspectIntegrationTest.java