Site

A site is what a tenant shows the public: a name and a tagline, a home page, a colour scheme and a navigation. The module owns that data and none of the rendering — a theme reads a title, a navigation and a colour and decides what to do with them — so a deployment can replace the theme without touching what a tenant configured. There is one site per tenant, and it has settings from the moment the tenant exists: reading them before anybody has saved anything answers the defaults, not a 404. The module is modules/site; this page states nothing that tree does not.

The map

Open the map full screen · source features/site in the record

What it promises

Entity

SiteSettings in contracts/site.go: a title and a tagline; a home slug, the content served at /, a slug and not an id and empty until somebody chooses one; a theme (light, dark, system, and empty means system); a primary colour, #rrggbb lower-cased and #2563eb when none was chosen; a logo file id with no foreign key behind it; and a navigation, Nav, of at most 12 NavItem links, each a label of at most 40 characters and a path within the site. Validate trims, lower-cases and counts characters rather than bytes, and refuses a colour that is not #rrggbb, a theme that is not one of the three, a link that leaves the site (httpx.LocalPath) and a home slug that is not a slug. One row per tenant or none, kept by the unique index in migrations/000018_site.up.sql. Nothing is Immutable: a PUT replaces the whole row. Public is the visitor’s half — the title, the navigation and the theme.

Events

site.settings_updated in contracts/events.go, from Service.Save on a tenant’s first save and on any change; saving what is already stored publishes nothing. There is no created and no deleted. The payload SettingsUpdated carries the settings id, the title, the home slug, the theme and the time, so a subscriber that renders the site need not read the row back.

Permissions

site:manage in contracts/permissions.go — the read, the PUT, the generated screens and the navigation entry. There is no site:read, and the public route asks for nothing.

Service

contracts.Service: Settings, Save; both take the caller’s transaction, so the change and its event commit together. The fake and the conformance suite live in contracts/sitetest/.

Routes and screens

Method Path Does Authorization Publishes

GET

/api/v1/site/settings

Read this tenant’s settings

site:manage

PUT

/api/v1/site/settings

Save this tenant’s settings

site:manage

site.settings_updated

GET

/api/v1/site/settings/public

Read this settings as a visitor sees it

public

  • GET /api/v1/site/settings and PUT /api/v1/site/settings — the two routes rest.Singleton.Mount registers (kit/rest/singleton.go, declared in module.go). There is no list, no POST and no DELETE, because a tenant has one site whether or not anybody has saved it. The read answers the defaults rather than a 404; the PUT replaces the whole row, crud.Reset drops whatever a body claims about its id and timestamps, and a value Validate refuses is a 422.

  • GET /api/v1/site/settings/public — the public face, behind httpx.Public(): the title, the navigation and the theme and nothing else, an empty navigation rather than a null, and a 404 at a host that resolves to no tenant.

  • Commands: none.

  • Screens: the singleton has a Write permission, so Mount registers it as a resource and the admin generates its seven pages under /admin/site/settings: the list shows the one row, the detail and the edit form read and save it through Settings and Save, and creating or deleting through them answers with a conflict, "a tenant has one settings, and it is neither created nor removed". The navigation entry "Site" is shown to anyone with site:manage. Hand-written pages: none — there is no HTML in the module, so the theme can be replaced.

Authorization

Authorization Who passes Routes

site:manage

a member of the tenant

settings-read, settings-save

none: public

anyone

settings-public

Events, jobs and subscriptions

Event Published by Handled by

site.settings_updated

settings-save

audit (every event)

  • Publishes: site.settings_updated, from Save in internal/service.go, through events.Publish inside the caller’s transaction; the Singleton’s `Event field declares it on the PUT so the boot gate can check it.

  • Jobs: none, written out as a decision in module.go: nothing about a site happens because time passed.

  • Subscriptions: none.

What it needs

Deps field

Interface

Supplied by

none

site.Deps{} is empty

apps/platformkit/modules.go calls site.Module(site.Deps{}); a site belongs to a tenant by carrying its id, which row-level security matches on. The private application’s registry builds it the same way, with no requirements.

Configuration: none.

Who uses it

  • No module takes its contracts: nothing in the public repository or its private consumers imports modules/site/contracts, and no client storefront reads the site settings. The event’s payload is written for whatever renders the public site; nothing subscribes to it yet.

  • The admin and the native shell reach it through httpx.Resource, not through contracts/.

Verification

  • go test ./modules/site/…​ — the fake and the real service pass one conformance suite (conformance.go): the defaults for a tenant that has configured nothing, a save that reads back in the order it was written, silence on an identical save, one event on a change, and crud.ErrInvalid for a colour, a theme, a link, a bound or a slug that is not one. The internal tests (internal/service_test.go) prove what the fake cannot: one row per tenant whatever a body says about its id, the event in the caller’s transaction with a trimmed title, and a rollback that leaves neither. The module tests (module_test.go) mount the routes: read and PUT and nothing else, a lower-cased colour, a 422, a public face that carries the title, navigation and theme and none of the tagline, home slug, colour or timestamps, a 403 on an anonymous read of the settings, a navigation that cannot leave the site, and lengths counted in characters.

  • No browser journey: make e2e runs e2e/admin-tasks.spec.ts, which does not touch the site.

  • Not proven: the generated screens over the singleton, because no test mounts the admin over site; a subscriber to site.settings_updated, because none exists; the public route at a host that resolves to no tenant.