Config API
Social links configuration endpoints
Config API
Social Links
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
1. Admin: Get Social Links
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"
}
}2. Admin: Create Social Links
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)
3. Admin: Update Social Links
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)
4. Public: Get Social Links
GET /api/config/social-links
- Auth: public
Response behavior:
- returns configured values if present
- returns
nullvalues 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 keywith checkid = 1(singleton)facebook_url varchar(512) nullableinstagram_url varchar(512) nullablewhatsapp_url varchar(512) nullablecreated_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.