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);
}| Method | Called by |
|---|---|
GetStore | storefront-service, tax-service, shipping-service |
GetStoreSettings | catalog-service (locales, currencies), communication-service (timezone) |
IsFeatureEnabled | any service gating a feature at request time |
GetFeatureFlags | any service warming a local feature flag cache at startup |
GetStoreByDomain | gateway-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#
| Method | Route | Description |
|---|---|---|
POST | /stores | Provision new store |
GET | /stores/{id} | Get store |
PATCH | /stores/{id} | Update name or plan |
POST | /stores/{id}/suspend | Suspend store |
POST | /stores/{id}/reinstate | Reinstate store |
GET | /stores/{id}/settings | Get settings |
PATCH | /stores/{id}/settings | Update settings |
GET | /stores/{id}/locales | List locales |
POST | /stores/{id}/locales | Add locale |
PATCH | /stores/{id}/locales/{locale} | Set as default |
DELETE | /stores/{id}/locales/{locale} | Remove locale |
GET | /stores/{id}/currencies | List currencies |
POST | /stores/{id}/currencies | Add currency |
PATCH | /stores/{id}/currencies/{currency} | Set as default |
DELETE | /stores/{id}/currencies/{currency} | Remove currency |
GET | /stores/{id}/domains | List domains |
POST | /stores/{id}/domains | Add custom domain |
DELETE | /stores/{id}/domains/{domainId} | Remove domain |
GET | /stores/{id}/feature-flags | List 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 eventSSL status transitions: PENDING → ACTIVE (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#
| Topic | Trigger | Payload |
|---|---|---|
store.provisioned | POST /stores | { storeId, name, plan, defaultLocale, defaultCurrency, provisionedAt } |
store.suspended | POST /stores/{id}/suspend | { storeId, reason, suspendedAt } |
store.reinstated | POST /stores/{id}/reinstate | { storeId, reinstatedAt } |
store.plan_changed | PATCH /stores/{id} (plan field) | { storeId, previousPlan, newPlan, changedAt } |
store.settings_updated | PATCH /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 path | Purpose |
|---|---|
ss3/kv/store-service/db.url | PostgreSQL JDBC URL |
ss3/kv/store-service/db.username | Database username |
ss3/kv/store-service/db.password | Database password |
ss3/kv/shared/ | Shared Kafka bootstrap and OTel config |