#53 puts the directory and /p/[handle] behind Next's cache. That is correct for a read-heavy public directory, but it means a revoked badge or a withdrawn profile can keep being served from a cache after the database says otherwise — the failure AGENTS.md describes as silent, fails-open and indistinguishable from a working directory. This issue decides how the application invalidates a cached page when the data behind it changes.
#53 names the tags — practitioner:<id> and practitioners — and does nothing else with them, because it has no write path to purge from. Everything below is what #14 needs before its writes are correct.
What the Next 16 docs settle, and what they leave open
Established by reading node_modules/next/dist/docs, because most published advice on this predates 16:
updateTag(tag) expires immediately. The next request waits for fresh data. Server Actions only — not Route Handlers, not Client Components.
revalidateTag(tag, "max") marks stale and serves stale-while-revalidate — the old content goes out once more while fresh data is fetched behind it. Callable from Server Functions and Route Handlers.
- Bare
revalidateTag(tag) with no second argument is deprecated.
- Tags must be attached to the cached data first, either with
fetch(url, { next: { tags: [...] } }) or with cacheTag() inside a 'use cache' function.
unstable_cache has been replaced by use cache in 16.
The difference between the two purges is load-bearing here, not a tuning choice. Stale-while-revalidate is right for a bio edit and wrong for revocation: it serves the pulled badge or the withdrawn profile exactly one more time. So revocation wants updateTag, which constrains it to a Server Action — consistent with #72, which already requires every admin action to go through a Server Action calling the RPC rather than a client writing to PostgREST.
The decision
How do tags get attached to a Supabase query? We query through supabase-js, so there is no fetch call of ours to hang next.tags on.
- Enable
cacheComponents: true and use 'use cache' with cacheTag(). The sanctioned Next 16 route. Costs a config change with application-wide caching semantics, which is why it is a decision and not a detail.
- Wrap the client's
fetch — createClient accepts global.fetch — and inject next.tags. No config change, but varying tags per query through a shared client is awkward and easy to get subtly wrong.
Pick one and record why. Whichever wins, revalidate stays as the backstop clock rather than the freshness mechanism.
Also decide: does a practitioner's own edit purge?
#53 specifies purging on status and verified changes, both Bluehex-owned. It says nothing about a practitioner rewriting their bio, which falls through to the clock — so they edit, see no change, and reasonably conclude it is broken. This is the read-your-own-writes case updateTag exists for. It is cheap to include and easy to forget.
The gap this does not close by itself
Anything changing data outside the application — psql, the Supabase dashboard, a future background job — purges nothing, and the cache goes stale with no signal. Closing it needs a Supabase database webhook calling a Route Handler that calls revalidateTag (a Route Handler cannot use updateTag), authenticated with a shared secret so it is not an open purge endpoint anyone can use to stampede the cache.
Worth deciding whether to close it now or to write down that the application is the only sanctioned writer.
Done when
- The tagging mechanism is chosen, implemented, and the reasoning recorded.
- Revocation expires immediately rather than serving one more stale response, and there is a test that would fail if someone swapped it for stale-while-revalidate.
- It is settled whether a practitioner's own edit purges their page.
- The out-of-band gap is either closed with an authenticated webhook or documented as accepted.
Related: #53 (defines the tag names), #14 (the writes that need this), #72 (already requires admin actions to be Server Actions).
#53 puts the directory and
/p/[handle]behind Next's cache. That is correct for a read-heavy public directory, but it means a revoked badge or a withdrawn profile can keep being served from a cache after the database says otherwise — the failureAGENTS.mddescribes as silent, fails-open and indistinguishable from a working directory. This issue decides how the application invalidates a cached page when the data behind it changes.#53 names the tags —
practitioner:<id>andpractitioners— and does nothing else with them, because it has no write path to purge from. Everything below is what #14 needs before its writes are correct.What the Next 16 docs settle, and what they leave open
Established by reading
node_modules/next/dist/docs, because most published advice on this predates 16:updateTag(tag)expires immediately. The next request waits for fresh data. Server Actions only — not Route Handlers, not Client Components.revalidateTag(tag, "max")marks stale and serves stale-while-revalidate — the old content goes out once more while fresh data is fetched behind it. Callable from Server Functions and Route Handlers.revalidateTag(tag)with no second argument is deprecated.fetch(url, { next: { tags: [...] } })or withcacheTag()inside a'use cache'function.unstable_cachehas been replaced byuse cachein 16.The difference between the two purges is load-bearing here, not a tuning choice. Stale-while-revalidate is right for a bio edit and wrong for revocation: it serves the pulled badge or the withdrawn profile exactly one more time. So revocation wants
updateTag, which constrains it to a Server Action — consistent with #72, which already requires every admin action to go through a Server Action calling the RPC rather than a client writing to PostgREST.The decision
How do tags get attached to a Supabase query? We query through
supabase-js, so there is nofetchcall of ours to hangnext.tagson.cacheComponents: trueand use'use cache'withcacheTag(). The sanctioned Next 16 route. Costs a config change with application-wide caching semantics, which is why it is a decision and not a detail.fetch—createClientacceptsglobal.fetch— and injectnext.tags. No config change, but varying tags per query through a shared client is awkward and easy to get subtly wrong.Pick one and record why. Whichever wins,
revalidatestays as the backstop clock rather than the freshness mechanism.Also decide: does a practitioner's own edit purge?
#53 specifies purging on
statusandverifiedchanges, both Bluehex-owned. It says nothing about a practitioner rewriting their bio, which falls through to the clock — so they edit, see no change, and reasonably conclude it is broken. This is the read-your-own-writes caseupdateTagexists for. It is cheap to include and easy to forget.The gap this does not close by itself
Anything changing data outside the application —
psql, the Supabase dashboard, a future background job — purges nothing, and the cache goes stale with no signal. Closing it needs a Supabase database webhook calling a Route Handler that callsrevalidateTag(a Route Handler cannot useupdateTag), authenticated with a shared secret so it is not an open purge endpoint anyone can use to stampede the cache.Worth deciding whether to close it now or to write down that the application is the only sanctioned writer.
Done when
Related: #53 (defines the tag names), #14 (the writes that need this), #72 (already requires admin actions to be Server Actions).