Store Service

Store service owns the store entity and all store-level configuration. It is the authority for store existence, status, settings, domains, locales, currencies, and feature flags. All other services treat the store UUID as an opaque foreign key — none of them own store metadata.

Store service is the only publisher of store.provisioned and store.suspended. All services consume these events to initialise or freeze their store-scoped data.

Schema#

CREATE TABLE store (
  id             UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  name           VARCHAR(255) NOT NULL,
  status         VARCHAR(16)  NOT NULL DEFAULT 'ACTIVE'
                   CHECK (status IN ('ACTIVE', 'SUSPENDED', 'DEPROVISIONED')),
  plan           VARCHAR(32)  NOT NULL,
  created_at     TIMESTAMPTZ  NOT NULL DEFAULT now(),
  updated_at     TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Supported locales per store (BCP 47)
CREATE TABLE store_locale (
  store_id    UUID    NOT NULL REFERENCES store(id) ON DELETE CASCADE,
  locale      CHAR(5) NOT NULL,
  is_default  BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY (store_id, locale)
);

-- Supported currencies per store (ISO 4217)
CREATE TABLE store_currency (
  store_id    UUID    NOT NULL REFERENCES store(id) ON DELETE CASCADE,
  currency    CHAR(3) NOT NULL,
  is_default  BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY (store_id, currency)
);

-- All other store-level config in typed JSONB columns
CREATE TABLE store_settings (
  store_id    UUID PRIMARY KEY REFERENCES store(id) ON DELETE CASCADE,
  timezone    VARCHAR(64)  NOT NULL DEFAULT 'UTC',
  contact     JSONB,   -- { email, phone }
  business    JSONB,   -- { legal_name, tax_id, address }
  storefront  JSONB,   -- { title, description, logo_s3_key, favicon_s3_key, colors }
  checkout    JSONB,   -- { guest_checkout_enabled, require_phone, ... }
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Custom domains
CREATE TABLE store_domain (
  id          UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  store_id    UUID         NOT NULL REFERENCES store(id) ON DELETE CASCADE,
  domain      VARCHAR(255) NOT NULL UNIQUE,
  is_primary  BOOLEAN      NOT NULL DEFAULT FALSE,
  ssl_status  VARCHAR(16)  NOT NULL DEFAULT 'PENDING'
                CHECK (ssl_status IN ('PENDING', 'ACTIVE', 'FAILED')),
  created_at  TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Feature flags (seeded from plan matrix at provisioning; overridable per store)
CREATE TABLE store_feature_flag (
  store_id  UUID         NOT NULL REFERENCES store(id) ON DELETE CASCADE,
  flag      VARCHAR(128) NOT NULL,
  enabled   BOOLEAN      NOT NULL DEFAULT TRUE,
  config    JSONB,
  PRIMARY KEY (store_id, flag)
);

gRPC Interface#

Internal services call store-service to read store context. Callers should cache responses with a short TTL (30–60 s) and invalidate on store.settings_updated.

service StoreReaderService {
  rpc GetStore(GetStoreRequest) returns (StoreResponse);
  rpc GetStoreSettings(GetStoreSettingsRequest) returns (StoreSettingsResponse);
  rpc IsFeatureEnabled(IsFeatureEnabledRequest) returns (IsFeatureEnabledResponse);
  rpc GetFeatureFlags(GetFeatureFlagsRequest) returns (FeatureFlagsResponse);

  // Domain-based store resolution — called by gateway-service on every storefront request
  rpc GetStoreByDomain(GetStoreByDomainRequest) returns (StoreResponse);
}
MethodCalled by
GetStorestorefront-service, tax-service, shipping-service
GetStoreSettingscatalog-service (locales, currencies), communication-service (timezone)
IsFeatureEnabledany service gating a feature at request time
GetFeatureFlagsany service warming a local feature flag cache at startup
GetStoreByDomaingateway-service — resolves Host header to store_id on every storefront request

Provisioning#

POST /stores is the single entry point for creating a store. Store service creates the entity, seeds feature flags from the plan matrix, and publishes store.provisioned. No other service is called directly — all dependent initialisation is event-driven.

POST /stores
{ "name": "...", "plan": "ENTERPRISE", "timezone": "America/Chicago",
  "defaultLocale": "en-US", "defaultCurrency": "USD" }

→ insert store + store_settings + locales + currencies + feature flags
→ publish store.provisioned
→ 201 { storeId, status: "ACTIVE" }

REST API#

MethodRouteDescription
POST/storesProvision new store
GET/stores/{id}Get store
PATCH/stores/{id}Update name or plan
POST/stores/{id}/suspendSuspend store
POST/stores/{id}/reinstateReinstate store
GET/stores/{id}/settingsGet settings
PATCH/stores/{id}/settingsUpdate settings
GET/stores/{id}/localesList locales
POST/stores/{id}/localesAdd locale
PATCH/stores/{id}/locales/{locale}Set as default
DELETE/stores/{id}/locales/{locale}Remove locale
GET/stores/{id}/currenciesList currencies
POST/stores/{id}/currenciesAdd currency
PATCH/stores/{id}/currencies/{currency}Set as default
DELETE/stores/{id}/currencies/{currency}Remove currency
GET/stores/{id}/domainsList domains
POST/stores/{id}/domainsAdd custom domain
DELETE/stores/{id}/domains/{domainId}Remove domain
GET/stores/{id}/feature-flagsList feature flags
PATCH/stores/{id}/feature-flags/{flag}Update flag

Custom Domains#

Custom domains are owned by store-service via the store_domain table. The gateway uses GetStoreByDomain to resolve incoming Host headers to store UUIDs on every storefront request.

SSL Provisioning Flow#

1. Admin: POST /stores/{id}/domains { "domain": "shop.example.com" }
2. store-service: insert store_domain (ssl_status: PENDING)
3. store-service: request ACM certificate for the domain
4. ACM: return DNS CNAME validation record
5. store-service: return CNAME to admin for DNS configuration
6. Admin: add CNAME to DNS at their registrar
7. ACM: validates CNAME, issues certificate (ssl_status: ACTIVE)
8. store-service: update CloudFront distribution to serve domain with ACM cert
9. store-service: publish store.domain_activated event

SSL status transitions: PENDINGACTIVE (on ACM validation) or FAILED (on validation timeout/error).

Domain Resolution#

The gateway caches domain → store_id mappings with a 60-second TTL backed by Redis. On cache miss, it calls GetStoreByDomain. Domain removal takes effect within the TTL window — no active cache invalidation.

Kafka Events#

TopicTriggerPayload
store.provisionedPOST /stores{ storeId, name, plan, defaultLocale, defaultCurrency, provisionedAt }
store.suspendedPOST /stores/{id}/suspend{ storeId, reason, suspendedAt }
store.reinstatedPOST /stores/{id}/reinstate{ storeId, reinstatedAt }
store.plan_changedPATCH /stores/{id} (plan field){ storeId, previousPlan, newPlan, changedAt }
store.settings_updatedPATCH /stores/{id}/settings{ storeId, changedKeys[], updatedAt }

store.settings_updated is the cache invalidation signal for storefront-engine and any service caching store settings locally.

Vault Configuration#

Key pathPurpose
ss3/kv/store-service/db.urlPostgreSQL JDBC URL
ss3/kv/store-service/db.usernameDatabase username
ss3/kv/store-service/db.passwordDatabase password
ss3/kv/shared/Shared Kafka bootstrap and OTel config