Skip to main content

Homepage Service

This is the reference page for lumie-backend/modules/homepage, the tenant-scoped module that stores landing-page configuration, publish state, and public-by-customId lookup.

Source Paths

PathRole
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/adapter/in/web/HomepageController.javaPublic and authenticated homepage endpoints
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageQueryService.javaCurrent-tenant reads and public customId lookup with RLS re-entry
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageCommandService.javaUpsert and publish/unpublish commands
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/domain/entity/HomepageConfig.javaSingle homepage config aggregate per tenant
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlTable creation and RLS for homepage_config
lumie-backend/app/src/main/resources/db/migration/public/V46__fix_homepage_config_per_tenant_singleton.sqlFollow-up migration that repairs the singleton-per-tenant contract

Public Surface

EndpointPurpose
GET /v1/homepageReturn the current tenant's homepage config, or a transient default config if none has been saved yet
PUT /v1/homepageUpsert homepage config for the authenticated tenant; AuthorizationGuard.requireOwner() enforces OWNER access
POST /v1/homepage/publishToggle the published flag for the saved config; OWNER only
GET /v1/homepage/public/by-custom-id/{customId}Anonymous public lookup by tenant customId

Module Boundaries

DependencyRole
TenantLookupPortResolves a tenant from customId before the module can re-enter tenant context
TenantContextHolder.withinContext(...)Restores both slug and tenant ID so PostgreSQL RLS can see the correct homepage row
AuthorizationGuardRestricts homepage writes and publish toggles to OWNER users

The homepage module stores JSON sections and publish state only. It does not own file binaries directly; image IDs and URLs are payload fields inside the homepage JSON document.

Aggregate And Table

AggregateNotes
HomepageConfigTenant-scoped singleton with templateId, templateVersion, sections, and published

HomepageConfig.defaultConfig() is not a persisted row. It is a transient fallback object returned only for authenticated GET /v1/homepage when the tenant has never saved configuration.

Runtime Flow

Contract Notes

The public read path does not currently enforce the published flag, even though the controller comment still says it should.

// lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageQueryService.java
@Transactional(readOnly = true)
public Optional<HomepageConfigResponse> findCurrent() {
// The homepage is always public once saved — no `published` gate.
return homepageConfigPersistencePort.find()
.map(config -> HomepageConfigResponse.from(config, objectMapper));
}

That means the runtime contract is:

  • authenticated GET /v1/homepage returns a default config when no row exists;
  • anonymous GET /v1/homepage/public/by-custom-id/{customId} returns the saved config if one exists, regardless of published;
  • anonymous reads return 404 only when the tenant lookup fails or no homepage row exists.

Example Contracts

These examples come directly from HomepageController, UpdateHomepageConfigRequest, PublishHomepageRequest, HomepageConfigResponse, and HomepageQueryService.

Save Homepage Config

PUT /v1/homepage
Content-Type: application/json

{
"templateId": "solo-instructor",
"templateVersion": 1,
"sections": {
"hero": {
"headline": "Learn with Lumie"
}
}
}
HTTP/1.1 200 OK

{
"id": 1,
"templateId": "solo-instructor",
"templateVersion": 1,
"published": false,
"sections": {
"hero": {
"headline": "Learn with Lumie"
}
},
"updatedAt": "<timestamp>"
}

Public Homepage Response

GET /v1/homepage/public/by-custom-id/acme
HTTP/1.1 200 OK

{
"id": 1,
"templateId": "solo-instructor",
"templateVersion": 1,
"published": false,
"sections": {
"hero": {
"headline": "Learn with Lumie"
}
},
"updatedAt": "<timestamp>"
}

That published: false public example is intentional. HomepageQueryService.Tx.findCurrent() removed the publish gate, so the saved row is public as soon as it exists.

Failure, Retry, And Observability

  • HomepageCommandService.publish(...) refuses to publish blank JSON such as {} or [].
  • Public reads must cross a real Spring proxy transaction boundary through HomepageQueryService.Tx; otherwise the RLS aspect would not bind app.tenant_id.
  • The controller comment and older summaries still describe a published-only public read, but the current application service explicitly removed that gate.
  • The module mainly exposes state changes through application logs; there is no queue, retry worker, or external callback path here.

Verification

cd lumie-backend
./gradlew :modules:homepage:test
./gradlew :modules:homepage:test --tests '*Homepage*'

Expected success signals:

  • Gradle exits with BUILD SUCCESSFUL, and the homepage module tests still cover current-config, publish, and public-read paths.
  • HomepageQueryService.Tx.findCurrent() still returns any saved row without checking published, which matches the example contract on this page even though HomepageController comments and tests still describe a published-only path.