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
|
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.
|
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
|
2. **A flag can change without a deploy**, so nothing else clears the caches
|
||||||
caches that a deploy would have cleared. §4 is that mechanism.
|
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
|
Also considered: Flagsmith (heavier — Django, Postgres and Redis), GrowthBook
|
||||||
(requires MongoDB), and Flipt v2 (the closest conceptual fit, git-native, but
|
(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
|
## 4. Propagation
|
||||||
|
|
||||||
School and place pages carry `revalidate = 604800`. A flag value consulted
|
**Time-based revalidation is sufficient. There is no webhook.**
|
||||||
during render is baked into the cached HTML, so **polling alone changes
|
|
||||||
nothing** — the page was rendered days ago. Propagation is push, not pull.
|
|
||||||
|
|
||||||
Two webhook integrations in Unleash, both firing on feature-environment
|
An earlier draft of this section specified two Unleash webhooks and a
|
||||||
enable/disable. The webhook cannot set custom headers, so each is authenticated
|
`revalidateTag('flags')` purge, on the premise that pages cache for seven days.
|
||||||
by a shared secret in the query string.
|
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
|
Next uses the **lowest** `revalidate` among a route's fetches to set the
|
||||||
rebuilds the sitemap — a route-family flag changes which URLs exist, and the
|
revalidation frequency of the whole route — the segment-level
|
||||||
sitemap is held in memory.
|
`export const revalidate` does not override a lower value inside it. Measured
|
||||||
2. → `POST /api/revalidate-flags` on Next. Calls `revalidateTag('flags')`.
|
against this codebase:
|
||||||
|
|
||||||
Unleash retries once and can deliver duplicate or out-of-order events, so both
|
| Page family | Segment | Lowest fetch | Effective |
|
||||||
handlers are idempotent: they re-read current state rather than applying a
|
|---|---|---|---|
|
||||||
delta from the payload body.
|
| `/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
|
What this removes: two webhook integrations, a `/api/revalidate-flags` route, a
|
||||||
only the fetch of `/api/flags` itself.
|
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
|
**If instant flips are ever wanted**, the webhook is the way to add them, and it
|
||||||
change — is wrong in a way that fails silently. The first consumer proves it:
|
is purely additive — nothing in this design has to change first.
|
||||||
`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.
|
|
||||||
|
|
||||||
Flips are rare and Next serves stale-while-revalidate, so a full purge costs a
|
### Two constraints this leaves behind
|
||||||
gradual re-render rather than a cliff. Correctness is worth more here than
|
|
||||||
precision.
|
**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
|
## 5. What "off" means, per surface
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user