plugins/lisa-harper-fabric-agy/skills/harper-caching/SKILL.md
This skill should be used when implementing Harper (HarperDB/Fabric) caching tables, external-source caches, TTL expiration, stale revalidation, invalidation, and cache verification. Use it when caching an upstream API, database, or expensive computation in Harper instead of hand-rolling in-memory maps. Pairs with harper-resources, harper-schema-graphql, harper-rest-queries, and harper-operations.
npx skillsauth add codyswanngt/lisa harper-cachingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Harper can use a local table as a durable cache for an external source. The table stores records, enforces schema/index behavior, exposes normal REST/GraphQL routes, and calls the source Resource only when a record is missing, expired, or explicitly invalidated.
Use a caching table when the cached data should survive process restarts,
participate in Harper queries, or stay coherent across Fabric nodes. Do not keep
API responses in a module-level Map unless the data is deliberately
process-local, disposable, and not part of the application data model.
Cross-check the table declaration in [[harper-schema-graphql]] and the Resource method conventions in [[harper-resources]] before editing. If cached records are queried by filters, sort keys, or relationships, also align indexes and query shape with [[harper-rest-queries]].
Declare a normal @table and expose it with @export when callers should reach
the cache over REST or GraphQL. Use the expiration argument on @table for the
default time-to-live, in seconds:
type WeatherSnapshot @table(expiration: 300) @export(name: "weather") {
locationId: String @primaryKey
city: String @indexed
temperatureF: Float
conditions: String
sourceUpdatedAt: Long
fetchedAt: Long
}
Guidance:
city, tenantId, category, status)
so callers do not fetch broad collections and filter in JavaScript.sourceUpdatedAt, etag, or
fetchedAt when verification, debugging, or conditional revalidation needs it.sourcedFromImplement a source Resource that fetches the upstream record, then attach it to
the table with sourcedFrom. In Lisa template projects, write TypeScript under
src/ and rebuild the generated Harper assets; do not edit resources.js by
hand.
export class WeatherApiSource extends Resource {
static loadAsInstance = false;
async get(target) {
const id = target.id;
const response = await fetch(
`https://api.example.com/weather/${encodeURIComponent(id)}`,
{
headers: { Accept: 'application/json' },
},
);
if (response.status === 404) {
const error = new Error(`Weather location not found: ${id}`);
error.statusCode = 404;
throw error;
}
if (!response.ok) {
const error = new Error(`Weather upstream failed: ${response.status}`);
error.statusCode = 502;
throw error;
}
const data = await response.json();
return {
locationId: id,
city: data.city,
temperatureF: data.temperatureF,
conditions: data.conditions,
sourceUpdatedAt: Date.parse(data.updatedAt),
fetchedAt: Date.now(),
};
}
}
tables.WeatherSnapshot.sourcedFrom(WeatherApiSource);
When a requested record is missing or stale, Harper calls the source get() and
caches the returned record in the local table. Concurrent requests for the same
missing or stale record share the same upstream load, which avoids a cache
stampede for a single key.
Use expiration for the TTL: after that many seconds, the cached entry is stale
and the next read reloads it from the source. If a project cannot express the
default in schema, sourcedFrom can also receive options:
tables.WeatherSnapshot.sourcedFrom(WeatherApiSource, {
expiration: 300,
eviction: 3600,
scanInterval: 60,
});
Option meanings:
| Option | Use |
| --- | --- |
| expiration | Default TTL in seconds before the cached record is stale. |
| eviction | Seconds after expiration before Harper may physically remove the record. |
| scanInterval | Seconds between scans for expired records. |
Prefer schema directives for stable application behavior because the data model
and default freshness policy stay together. Use sourcedFrom options when the
cache attachment is intentionally dynamic or the downstream Harper version lacks
the needed schema directive.
When the upstream provides ETag, Last-Modified, or Cache-Control, pass that
freshness model through the source Resource instead of overwriting good cached
data with every revalidation.
export class WeatherApiSource extends Resource {
static loadAsInstance = false;
async get(target) {
const context = this.getContext();
const headers = new Headers({ Accept: 'application/json' });
if (context.replacingVersion) {
headers.set(
'If-Modified-Since',
new Date(context.replacingVersion).toUTCString(),
);
}
const response = await fetch(
`https://api.example.com/weather/${encodeURIComponent(target.id)}`,
{ headers },
);
if (response.status === 304) {
return context.replacingRecord;
}
const maxAge = response.headers
.get('Cache-Control')
?.match(/max-age=(\d+)/)?.[1];
if (maxAge) {
context.expiresAt = Date.now() + Number(maxAge) * 1000;
}
context.lastModified = response.headers.get('Last-Modified');
return response.json();
}
}
Use allowStaleWhileRevalidate(entry, id) on the cached table when stale data is
acceptable during refresh:
export class WeatherSnapshot extends tables.WeatherSnapshot {
static loadAsInstance = false;
allowStaleWhileRevalidate(entry, id) {
return Date.now() - entry.expiresAt < 60_000;
}
}
Return false or omit the method when callers must wait for a fresh value.
Use invalidation when an admin action, webhook, scheduled refresh, or upstream write makes the cached record stale before its TTL.
await tables.WeatherSnapshot.invalidate('nyc');
The next get() for that id reloads from the source. If the project Harper
version or resource shape does not expose invalidate(), use an explicit delete
or overwrite path and document the behavior:
await tables.WeatherSnapshot.delete('nyc');
await tables.WeatherSnapshot.get('nyc');
For write-through caches, implement put, patch, or delete on the source
Resource only when those operations should update the upstream system. Otherwise,
reject writes or keep cache mutation behind an operator-only Resource so clients
do not accidentally diverge from the source of truth.
In a Fabric deployment, treat every node as capable of serving a read while cache state is replicating:
get() deterministic and idempotent. A second node may reload the same
stale key.Prove both cache fill and cache hit behavior against a running Harper app:
bun run build, then the project Harper dev command).curl -sS http://localhost:9926/weather/nyc | jq .
curl -sS http://localhost:9926/weather/nyc | jq .
fetchedAt or equivalent freshness evidence.curl -sS -X DELETE http://localhost:9926/weather/nyc
curl -sS http://localhost:9926/weather/nyc | jq .
expiration in a local-only test fixture,
wait past the TTL, and verify either blocking refresh or stale-while-revalidate
behavior matches the Resource implementation.tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.