Happy House - Ecommerce Docs
Developer Resourcesconfig

Config API

Social links configuration endpoints

Config API

Social links are stored as a singleton config row and exposed through:

  • admin write/read endpoint for backoffice management
  • public read endpoint for website/mobile footer/header links

Endpoints

GET /api/admin/config/social-links

  • Auth: Bearer token
  • Guards: JwtAuthGuard, RoleGuard
  • Permission: Settings_READ

Response payload:

{
  "message": "Social links fetched successfully",
  "data": {
    "facebookUrl": "https://www.facebook.com/happyhouse",
    "instagramUrl": "https://www.instagram.com/happyhouse",
    "whatsappUrl": "https://wa.me/9779800000000",
    "updatedAt": "2026-04-26T12:00:00.000Z"
  }
}

POST /api/admin/config/social-links

  • Auth: Bearer token
  • Guards: JwtAuthGuard, RoleGuard
  • Permission: Settings_UPDATE

Request body:

{
  "facebookUrl": "https://www.facebook.com/happyhouse",
  "instagramUrl": "https://www.instagram.com/happyhouse",
  "whatsappUrl": "https://wa.me/9779800000000"
}

Notes:

  • endpoint creates singleton row with id = 1
  • if row already exists, returns conflict (use PATCH endpoint to edit)

PATCH /api/admin/config/social-links

  • Auth: Bearer token
  • Guards: JwtAuthGuard, RoleGuard
  • Permission: Settings_UPDATE

Request body:

{
  "instagramUrl": "https://www.instagram.com/happyhouse"
}

Notes:

  • partial updates allowed; only provided fields are changed
  • if row does not exist yet, returns not found (create first using POST)

GET /api/config/social-links

  • Auth: public

Response behavior:

  • returns configured values if present
  • returns null values when not configured yet

5. Mobile-Composed Public Route

GET /api/mobile/config/social-links

This is the same public customer controller mounted under /mobile via MobileModule composition routing.

Schema

Table: social_links

  • id serial primary key with check id = 1 (singleton)
  • facebook_url varchar(512) nullable
  • instagram_url varchar(512) nullable
  • whatsapp_url varchar(512) nullable
  • created_at timestamptz not null default now()
  • updated_at timestamptz not null default now()

Runtime settings and cache invalidation

PATCH /api/admin/config/runtime-settings upserts one managed key — the OAuth credentials, the Resend keys, the GTM / GA / Meta Pixel ids, and the site-wide SEO indexing switch.

The write emits the Next.js revalidation tag runtime-config. Before that, changing an analytics key in the admin purged nothing: the storefront holds the public runtime config for 180 seconds and no backend write emitted a tag for it, so the only way to see a new key take effect was to wait it out. There was no error and no log line — the write succeeded and the site stayed wrong.

The tag string is the whole contract. revalidateTag is matched by string equality across two repositories that share no type, so a typo purges nothing, throws nothing and looks exactly like working. It is asserted on the exact string in cache-invalidation.tags.spec.ts rather than left to a call site, and it was read from the consumer (happy-shop-frontend/src/lib/api/modules/runtime-config.ts:18) rather than recalled.

The runtime_config domain registers no Redis pattern, deliberately: the API does not cache runtime settings at all — RuntimeConfigService reads the row on every call — so a pattern here would only ever SCAN a keyspace that cannot match. Register one in the same change that adds the first cached runtime-config read. The faq domain records the same reasoning for itself.

SEO_INDEXING_ENABLED reaches further

That key is the site-wide indexing master switch, read by page-seo-customer.service.ts — flipping it changes robots.txt, the sitemap and every page's robots directives. So a write to it purges the page_seo domain as well as runtime_config. Clearing only the latter would leave the storefront serving an indexable site for a full TTL after somebody de-indexed it, which is the one direction of this switch that cannot be allowed to lag.

Which keys reach which domains is a per-key map in runtime-config-admin.service.ts, so the answer stays visible instead of becoming a conditional buried in a write path.

The page_seo trigger works: that domain now emits page-seo — the string the storefront actually subscribes to — alongside its own granular seo:sitemap / seo:robots / seo:page.

The alignment that made all of this take effect

Until this pass, almost no revalidation tag matched anything.

The backend emitted granular, well-named tags: page:blogs, seo:sitemap, catalog:categories, layout:footer. The storefront subscribes to eight coarse ones — blog, catalog, content, faq, page-seo, products, reviews, runtime-config — and its /api/revalidate route passes whatever it receives straight to revalidateTag() with no translation.

Only the product family overlapped. So blog posts, FAQ entries, catalog taxonomy, page SEO and every CMS content write revalidated nothing at all, and the storefront served stale pages until its own TTL lapsed. "The admin saves but the site does not change" was this.

The fix is additive: each domain now emits the consumer's coarse tag alongside its granular ones. A rename would have required both repositories to deploy together, and a half-done rename leaves nothing matching at all.

cache-invalidation.tags.spec.ts asserts the contract per domain, so a future edit that drops a coarse tag fails a test rather than silently stopping revalidation in production.

Four domains deliberately carry no coarse tag — banner, shipping, promotion, emi — because the storefront has no fetcher for them yet. Those are unconsumed surfaces, not mismatches, and inventing a string now would freeze a contract the consumer has not written. Adding one is a decision, and the spec names them so it stays a deliberate one.

Why the invalidation lives at the admin leaf

RuntimeConfigService is provided directly by six modules (auth, notifications, banners, page-seo and both config leaves), and exactly one of them writes — upsertValue has a single caller. Putting the cache invalidator on that shared service would drag CacheInvalidationSharedModule into all six to serve one write path, so RuntimeConfigAdminService owns the flow instead: validate the key, persist, purge what the write moved, assemble the response. The controller stays thin.

The invalidator is injected as a required dependency, never @Optional() — an optional one that silently resolves to undefined leaves stale data with no error anywhere, which is the failure this whole domain exists to prevent.