QUESTPIE

Tag invalidation

Group KV keys under a tag and drop the whole group in one call. Every built-in adapter implements it, but the methods sit on the adapter rather than on ctx.kv, so reaching them takes one deliberate step.

View markdown

Cache a rendered page under page:/posts/hello and its OG image under og:/posts/hello, tag both post:123, and one call after an edit drops both. That is the whole feature. It saves you from tracking, by hand, which derived keys a record fed.

The three methods

They are optional members of KVAdapter. All three built-in adapters implement all three.

MethodWhat it does
setWithTags(key, value, tags, ttl?)Writes the value and files the key under each tag.
invalidateByTag(tag)Deletes every key filed under that tag, then the tag.
invalidateByTags(tags)The same for several tags at once.

ttl is in seconds, like everywhere else in KV.

Reaching the adapter

KVService exposes get, set, delete, has and clear and nothing more, so ctx.kv.setWithTags does not exist. The tag methods live one level down. The reliable way to reach them is to keep the reference you built.

src/questpie/server/questpie.config.ts
import { runtimeConfig } from "questpie/app";
import { redisKVAdapter } from "questpie/adapters/redis-kv";

// getRedis returns a connected client, as on the Key-value store page
export const kvAdapter = redisKVAdapter({ client: getRedis });

export default runtimeConfig({ kv: { adapter: kvAdapter } });
import { kvAdapter } from "@/questpie/server/questpie.config";

await kvAdapter.setWithTags?.("page:/posts/hello", html, ["post:123"], 3600);
await kvAdapter.setWithTags?.("og:/posts/hello", ogImage, ["post:123"]);

// After an edit, one call drops both
await kvAdapter.invalidateByTag?.("post:123");

app.config.kv?.adapter reads back that same instance, since app.config is your raw config object. The ?. before each call is not defensive style, TypeScript requires it because the three members are optional.

On the zero-config default there is no adapter to read

Omit kv.adapter and KVService constructs MemoryKVAdapter internally, never writing it back. So app.config.kv?.adapter is undefined and every ?. tag call above silently does nothing, even though MemoryKVAdapter implements all three methods. Construct the adapter yourself to use tags.

What each backend stores

On Redis and Cloudflare the index is a second set of keys beside your values, written under tagIndexPrefix ("__questpie_tag:" by default) after keyPrefix. MemoryKVAdapter takes no options at all, so it keeps its index off to one side instead.

AdapterTag index
MemoryKVAdapterA Map of tag to a Set of keys
redisKVAdapterOne Redis SET per tag
cloudflareKVAdapterA JSON array per tag, since Cloudflare KV has no SET type

delete() keeps the index honest on all three, dropping the key from every tag that held it. On Redis that means a SCAN across the tag-index keys per delete, which is worth knowing before you delete in a tight loop.

Tag indexes do not expire

No adapter puts a TTL on the index itself, and nothing prunes it when a value expires. A tag keeps listing keys that are already gone, so invalidating it still issues a delete for each one before dropping the tag. clear() removes the index along with the values.

What tags do not do

defaultTtl does not apply. It is a KVService fallback, and setWithTags is an adapter call that never passes through the service, so a setWithTags without a ttl writes a value that never expires.

Neither is invalidation transactional. The deletes and the index cleanup go out as separate commands with nothing rolling them back, so a failure part way leaves some keys gone and some present. Treat a tag drop as a cache hint rather than a guarantee, and let the next read recompute.

  • Key-value store, the service, the adapters and the swap.
  • Hooks, where to invalidate after a committed write.

On this page