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
| Path | Role |
|---|---|
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/adapter/in/web/HomepageController.java | Public and authenticated homepage endpoints |
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageQueryService.java | Current-tenant reads and public customId lookup with RLS re-entry |
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/application/service/HomepageCommandService.java | Upsert and publish/unpublish commands |
lumie-backend/modules/homepage/src/main/java/com/lumie/homepage/domain/entity/HomepageConfig.java | Single homepage config aggregate per tenant |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | Table creation and RLS for homepage_config |
lumie-backend/app/src/main/resources/db/migration/public/V46__fix_homepage_config_per_tenant_singleton.sql | Follow-up migration that repairs the singleton-per-tenant contract |
Public Surface
| Endpoint | Purpose |
|---|---|
GET /v1/homepage | Return the current tenant's homepage config, or a transient default config if none has been saved yet |
PUT /v1/homepage | Upsert homepage config for the authenticated tenant; AuthorizationGuard.requireOwner() enforces OWNER access |
POST /v1/homepage/publish | Toggle 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
| Dependency | Role |
|---|---|
TenantLookupPort | Resolves 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 |
AuthorizationGuard | Restricts 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
| Aggregate | Notes |
|---|---|
HomepageConfig | Tenant-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/homepagereturns a default config when no row exists; - anonymous
GET /v1/homepage/public/by-custom-id/{customId}returns the saved config if one exists, regardless ofpublished; - anonymous reads return
404only 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 bindapp.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 checkingpublished, which matches the example contract on this page even thoughHomepageControllercomments and tests still describe a published-only path.