0008. items_flat regeneration: kds-owned, triggered per-outlet at publish¶
- Status: Accepted
- Date: 2026-06-12
- Deciders: POS Integration team
Context¶
items_flat is a denormalized, per-store read cache that the consumer
storefront (store-service) reads — its V2 product query
(GetProductByStoreIdV2) INNER JOINs it, and it carries the flattened
categories, modifiers, tags, allergens, prices and image URL for each item so the
storefront can serve a menu with a single SELECT. It is owned by
kds-management-service (schema + the regeneration SQL in database/item.go).
Before this decision, items_flat was regenerated in two unrelated places:
kds-management-serviceregenerated it only as part of the Deliverect menu sync — so menus from other adapters (iSeller, Revel, Cata) never populated it.pos-integration-servicecarried a local SQL port of the same denormalization and ran it aftersync_productsand inside the publish repo.
Two copies of the same complex INSERT…SELECT drift over time, and coverage was
inconsistent across adapters. We needed a single owner and a single, reliable
trigger. The repo already had the building blocks: a Cloud Tasks client with a
goroutine fallback (pkg/cloudtasks), the internal-endpoint pattern
(InternalExecuteJob), and kds had just exposed an H2H endpoint to regenerate
items_flat (POST /v1/items-flat/regenerate, kds PR #503 / INTR-744).
Decision¶
kds-management-service is the single owner of items_flat regeneration, and
pos-integration-service triggers it through one standardized gate: after a
menu publish (draft → live), once per published outlet.
- The publish flow (
MenuDraftService.BatchPublishDrafts) callsItemsFlatRegenService.Submit(tenant, outletUuid)for each outlet after the publish transaction commits — fire-and-forget; a regen failure never fails the publish (it is a read cache; the next publish repairs it). - In production it enqueues a Cloud Task on a dedicated, env-prefixed queue
(
{env}-items-flat-regen) whose target is the kds H2H endpoint; in local dev (no Cloud Tasks client) it falls back to a direct HTTP call in a goroutine. - The task calls
POST {KDS_MANAGEMENT_URI}/v1/items-flat/regenerate. kds is per-tenant subdomain-routed, soKDS_MANAGEMENT_URIis a template (https://{subdomain}.…/service/kds); the{subdomain}is resolved from the tenant (central.domains, viaProviderRepository.GetTenantDomain). - Auth is the per-tenant
KDS_API_KEYfromtenantdb.settings, sent as theapikeyheader (+x-tenant-id); kds's grpc-gateway maps these to gRPC metadatafwd-apikey/fwd-x-tenant-idand validates the key for thekdsrole against the middleware. No OIDC. sync_productsno longer regeneratesitems_flat; the local SQL port (MySQLProductRepository.RegenerateItemsFlat) is markedDeprecatedand is no longer called. The catalog sync only updatesitems/item_store; the next publish projects the changes intoitems_flat.
Consequences¶
Positive
- One source of truth for the denormalization SQL (kds) — no drift.
- All adapters populate
items_flat, not just Deliverect. - Publish is not blocked by the (heavy) regen; per-outlet tasks retry independently and are idempotent (full delete + reinsert per store).
- Reuses the existing Cloud Tasks + goroutine-fallback pattern.
Negative / costs
- A cross-service runtime dependency at publish (pos-integration → kds H2H),
plus the auth (
KDS_API_KEYwithkdsrole per tenant) and a dedicated queue to provision per environment (Terraform). items_flatis a cache: catalog edits made viasync_productsare only reflected after the next publish, and there is a brief async window after publish before it is refreshed.- The H2H call is authenticated only at the app layer (apikey), not OIDC — see the broader internal-endpoint hardening tracked in INTR-777.
Alternatives considered¶
- Keep the local SQL port and run it from pos-integration — rejected: two copies of the denormalization SQL drift, and kds (Deliverect) already owns one.
- Regenerate on every
sync_products— rejected: heavy, couples the cache to catalog sync rather than to what is actually published, and still needs the cross-service call for non-Deliverect adapters. - OIDC + Cloud Run IAM for the call — deferred: the per-tenant apikey is the established H2H mechanism; OIDC hardening is tracked separately (INTR-777).
References¶
- PRs #129 (one-gate regen via kds H2H), #130 (resolve
{subdomain}), #132/#133. - kds PR #503 / INTR-744 — the H2H
RegenerateItemsFlatendpoint. - Code:
internal/service/items_flat_regen_service.go,pkg/cloudtasks/client.go(EnqueueItemsFlatRegen),internal/repository/mysql_menu_draft_repository.go(publish trigger). - Related: ADR 0007 (image pipeline reuses the same Cloud Tasks pattern), ADR 0009 (the sibling publish-time projection into the V1 category model).