docs(flags): drop the webhook — the seven-day premise was wrong
Next uses the LOWEST revalidate among a route's fetches, not the segment value. School pages fetch school details at 300s and place pages fetch national averages at 3600s, so the effective ISR period is five minutes and one hour respectively — not the seven days the segment declares. A flag flip therefore propagates on its own, well inside the monthly, by-hand cadence these flags are for. That deletes two webhook integrations, a revalidate route, a secret-in-query-string scheme, an idempotency requirement, and the rule that every fetch carry a cache tag — which was the part most likely to rot as fetches are added. Two constraints survive: a flag must never gate content on a force-static page, because app/admissions never revalidates; and a route-family flag must rebuild the sitemap, deferred with the route case since no flag in scope touches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This commit is contained in:
1 parent
c2364bf09e
commit
e2ca3d79f9
1 file changed
+39
-27
@@ -37,8 +37,9 @@ to handle them:
|
||||
|
||||
1. **Flag state lives outside the repository.** `main` is no longer the whole
|
||||
truth about what is switched on. The registry in §2 exists to bound that.
|
||||
2. **A flag can change without a deploy**, so nothing else invalidates the
|
||||
caches that a deploy would have cleared. §4 is that mechanism.
|
||||
2. **A flag can change without a deploy**, so nothing else clears the caches
|
||||
that a deploy would have cleared. §4 establishes how long a flip takes to
|
||||
become visible, and why that is short enough to need no extra mechanism.
|
||||
|
||||
Also considered: Flagsmith (heavier — Django, Postgres and Redis), GrowthBook
|
||||
(requires MongoDB), and Flipt v2 (the closest conceptual fit, git-native, but
|
||||
@@ -140,38 +141,49 @@ named constant with a comment saying what belongs on it.
|
||||
|
||||
## 4. Propagation
|
||||
|
||||
School and place pages carry `revalidate = 604800`. A flag value consulted
|
||||
during render is baked into the cached HTML, so **polling alone changes
|
||||
nothing** — the page was rendered days ago. Propagation is push, not pull.
|
||||
**Time-based revalidation is sufficient. There is no webhook.**
|
||||
|
||||
Two webhook integrations in Unleash, both firing on feature-environment
|
||||
enable/disable. The webhook cannot set custom headers, so each is authenticated
|
||||
by a shared secret in the query string.
|
||||
An earlier draft of this section specified two Unleash webhooks and a
|
||||
`revalidateTag('flags')` purge, on the premise that pages cache for seven days.
|
||||
That premise was wrong, and checking it removed the most complex part of the
|
||||
design.
|
||||
|
||||
1. → `POST /api/admin/flags-changed` on FastAPI. Refreshes the SDK cache, and
|
||||
rebuilds the sitemap — a route-family flag changes which URLs exist, and the
|
||||
sitemap is held in memory.
|
||||
2. → `POST /api/revalidate-flags` on Next. Calls `revalidateTag('flags')`.
|
||||
Next uses the **lowest** `revalidate` among a route's fetches to set the
|
||||
revalidation frequency of the whole route — the segment-level
|
||||
`export const revalidate` does not override a lower value inside it. Measured
|
||||
against this codebase:
|
||||
|
||||
Unleash retries once and can deliver duplicate or out-of-order events, so both
|
||||
handlers are idempotent: they re-read current state rather than applying a
|
||||
delta from the payload body.
|
||||
| Page family | Segment | Lowest fetch | Effective |
|
||||
|---|---|---|---|
|
||||
| `/school/[slug]` | 604800 | `fetchSchoolDetails` at 300 | **5 minutes** |
|
||||
| `/schools/*` | 604800 | `fetchNationalAverages` at 3600 | **1 hour** |
|
||||
|
||||
### Cache tagging: coarse, deliberately
|
||||
The Unleash SDK polls every 15 seconds, so a flip reaches school pages within
|
||||
about five minutes and place pages within the hour, unaided. Flags flip
|
||||
monthly, by hand, deliberately. That is fast enough.
|
||||
|
||||
**Every server-side fetch in `nextjs-app/lib/` carries the `flags` tag**, not
|
||||
only the fetch of `/api/flags` itself.
|
||||
What this removes: two webhook integrations, a `/api/revalidate-flags` route, a
|
||||
shared-secret-in-a-query-string scheme, an idempotency requirement against
|
||||
duplicate and out-of-order delivery, and a rule that every fetch in
|
||||
`nextjs-app/lib/` carry a cache tag. None of it has to be built, maintained, or
|
||||
kept correct as new fetches are added.
|
||||
|
||||
The tempting rule — tag only those fetches whose response shape a flag can
|
||||
change — is wrong in a way that fails silently. The first consumer proves it:
|
||||
`admission_distance` changes the response of `/api/schools/{urn}`, not of
|
||||
`/api/flags`, so a narrowly-tagged purge would leave ~25,000 school pages
|
||||
serving the pre-flip render for up to seven days. The failure is invisible
|
||||
locally and visible to Google.
|
||||
**If instant flips are ever wanted**, the webhook is the way to add them, and it
|
||||
is purely additive — nothing in this design has to change first.
|
||||
|
||||
Flips are rare and Next serves stale-while-revalidate, so a full purge costs a
|
||||
gradual re-render rather than a cliff. Correctness is worth more here than
|
||||
precision.
|
||||
### Two constraints this leaves behind
|
||||
|
||||
**Never flag content on a `force-static` page.** `app/admissions/page.tsx`
|
||||
declares `export const dynamic = 'force-static'`, so it is baked at build time
|
||||
and never revalidates. A flag gating anything on such a page would not take
|
||||
effect until the next deploy, silently. If a flag ever needs to reach one, that
|
||||
page must first move to ISR.
|
||||
|
||||
**A route-family flag still needs the sitemap rebuilt.** The sitemap is held in
|
||||
memory and rebuilt only at startup or via `POST /api/admin/regenerate-sitemap`.
|
||||
No flag in scope touches the sitemap (§6), so this is deferred with the route
|
||||
case rather than solved now — but a route flag must not ship without it, or the
|
||||
sitemap will advertise URLs that `notFound()`.
|
||||
|
||||
## 5. What "off" means, per surface
|
||||
|
||||
|
||||
Reference in new issue
Block a user