Skip to content

Public sites perf · Phase 2: built-in caching layer (cache components + shared Redis handler, docs cacheable) #72

Closed

Description

@clbotdev

Part of the public-site performance plan (master issue linked above). Can run alongside Phase 1. Design: docs/site-delivery.md §4.

Goal

Stop building public pages on demand from the API and database. FrontLit caches rendered pages and data itself and invalidates them by tag on publish. This must work with a plain docker compose up and no CDN.

Deployment Cache storage Extra infra
Single container Next's default in-memory/disk cache none
Multiple replicas Shared Redis cache handler with refreshTags() for invalidation across replicas Redis, already in docker-compose.yml (REDIS_URL, optional)

Decision recorded

Docs are no longer exempt from caching. docs-management.md §23.3's "zero staleness / no-store" rule is replaced by: docs HTML is cached like other public pages and invalidated through the signed outbox webhook when a publication goes live or a policy changes. Staleness is bounded by webhook delivery, normally under a second. Search, chat answers and the public docs API keep authoritative, uncached reads. If the origin is down, the last cached publication may be served.

Tasks

  • Remove request-header dependencies so routes can be cached:
    • x-frontlit-embed in apps/sites/app/sites/[teamId]/layout.tsx: move /embed into its own route group and layout.
    • x-frontlit-canonical-host in apps/sites/lib/request-host.ts: get the canonical host from cached team data (add it to public settings).
  • Turn on cache components (cacheComponents, "use cache: remote", cacheTag, cacheLife) and replace the fixed 60-second teamCache in apps/sites/lib/api.ts.
  • Tag shell and content separately:
    • shell:<team>: settings, theme, branding, nav/footer, chatbot settings, code injection.
    • docs:<team>, page:<team>:<path>, blog:<team>, changelog:<team> (sitemap and RSS follow their collection).
    • Shell changes clear shell:<team>; content publishes clear only their content tag.
  • Docs:
    • Cache getDocumentationSnapshot under docs:<team> instead of no-store.
    • Remove the /docs no-store override in apps/sites/proxy.ts.
    • Have app/api/webhooks/documentation/route.ts clear docs:<team> and the sitemap instead of the whole team.
  • Move all site invalidation onto the outbox. revalidateTeam in apps/api/src/services/revalidate.ts is a best-effort POST today, which is acceptable only because of the 60-second cache. Change the /api/webhooks/revalidate payload to { eventId, teamId, tags[], invalidateHosts? }, accept only known tag families for that team, and add replay protection. Event-to-tag mapping: docs/site-delivery.md §4.5.
  • Shared header/footer: page reads also carry shell:<team>, because the API hydrates the published header/footer into every page. A publish that promotes a changed header/footer draft invalidates shell:<team>.
  • Cross-replica invalidation: add a Redis cache handler (cacheHandlers.remote) implementing refreshTags(). Keep the in-memory default when REDIS_URL is unset.
  • Host resolution cache: move the per-pod hostCache in apps/sites/lib/host.ts to Redis when available, so domain-change invalidation reaches every replica.
  • Multi-instance basics from the Next self-hosting guide: consistent build ID / deploymentId, and a shared server-actions encryption key if applicable.
  • Leave uncached: /help/tickets/*, chat, search, forms and previews stay dynamic.
  • Update docs: docs/site-delivery.md (new), docs-management.md §23.3, architecture.md §12.2 and §13.5.

Done when

  • Repeat views of pages, blog and docs don't call the API.
  • Publishing updates every replica within about a second.
  • A single-container setup with no Redis still works.

Activity

  1. clbotdev commented on Oct 25, 2026

    @clbotdev
    ContributorAuthor

    Implemented in #143 (all tasks). Measured on the sample tenant, against main:

    • Repeat views don't call the API: 0 API calls per view on every page (docs was 1.0).
    • Publish → visible: 0.18–0.35 s for pages, blog and changelog; 0.43–0.54 s for docs.
    • Publishing reaches every replica: with Redis, a second replica that didn't get the webhook shows the publish in 0.33–0.48 s. Without Redis it was still stale after 15 s.
    • Single container without Redis still works. Pages also keep serving with Redis stopped mid-run or down at boot.
    • Throughput: 41→140 req/s (home), 56→181 (docs article). p50 under load 236→63 ms.

    Changes from the plan:

    • The pages:<team> tag was dropped: nothing reads page lists except the sitemap, which has its own tag.
    • Page invalidation is computed by diffing releases at activation.
    • The public site has no Server Actions, so no shared encryption key is needed; DEPLOYMENT_ID covers the build ID.
  2. added a commit that references this issue on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions