@renatorodrigues/cacheiro
Core library for the NX remote cache server. Implements the NX 20.8+ custom remote cache specification.
This package provides the server logic and the public API. It is not a runnable application on its own. To run it, pick one of:
- Cacheiro Instants: prebuilt Docker images for the filesystem, S3, GCS and Azure stores, published to GHCR.
- Cacheiro Runner: a reference runner you can run as-is, fork, or use as a foundation for implementing your own runner, built on the exported API.
Requirements
- Node.js 22+
API
new Cacheiro(store: CacheiroStore, config: CacheiroConfig, hooks?: CacheiroHooks)
Creates a server instance. Accepts a CacheiroStore implementation, a CacheiroConfig object, and an optional hooks object (sugar for .on() — see Hooks). Config is not validated at runtime — only TypeScript types are enforced. Validate before constructing using configSchema.
cacheiro.start(): Promise<FastifyInstance>
Builds the Fastify server and returns the Fastify instance. The server is not yet listening — use the returned instance to add custom routes, hooks, or plugins before calling listen().
cacheiro.listen(): Promise<void>
Binds the server to the configured host and port, then prints the startup banner.
cacheiro.stop(): Promise<void>
Drains open connections and shuts down the server gracefully.
import { Cacheiro } from '@renatorodrigues/cacheiro';
import { FileSystemStore } from '@renatorodrigues/cacheiro-store-fs';
const store = new FileSystemStore({
cacheDirectory: './cache',
ttlDays: 7,
sweepIntervalHours: 24,
});
const cacheiro = new Cacheiro(
store,
{
server: { port: 3000, host: '127.0.0.1', bodyLimitMb: 100, banner: true, infobox: true },
auth: { token: 'my-secret-token' },
},
{
onCacheHit: ({ hash, expired }) => console.log('hit', hash, expired),
},
);
const server = await cacheiro.start();
// add custom routes, hooks, or plugins here before listen()
await cacheiro.listen();CacheiroStore
Interface that all store implementations must satisfy. Import it to build a custom store:
import type { CacheiroStore } from '@renatorodrigues/cacheiro';CacheiroConfig
TypeScript interface describing the server configuration shape. Import it to type your own config loader:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
server.port | number | Yes | — | Port to listen on. |
server.host | string | Yes | — | Host to bind to (e.g. "127.0.0.1" or "0.0.0.0"). |
server.bodyLimitMb | number | No | 100 | Maximum request body size in megabytes. |
server.banner | boolean | No | true | Show the large ASCII art banner on startup. Compact header if false. |
server.infobox | boolean | No | true | Show the info box (version, URL, store details) on startup. |
server.logFormat | 'pretty' | 'json' | No | pretty | Log output format. json prints structured pino JSON lines; pretty prints human-readable lines. Defaults to pretty unless NODE_ENV=production. |
server.tls.certFile | string | No | — | Path to PEM certificate file. Enables HTTPS when set (with keyFile). |
server.tls.keyFile | string | No | — | Path to PEM private key file. |
server.tls.caFile | string | No | — | Path to CA certificate file (optional). |
auth.token | string | No | — | Bearer token required on all requests. Omit auth (or token) entirely to disable auth — passing '' still works but is deprecated and will be rejected in the next major version. |
auth.readOnlyToken | string | No | — | Optional bearer token granting read-only access (GET only). Requires auth.token to be set. Must differ from auth.token. |
import type { CacheiroConfig } from '@renatorodrigues/cacheiro';configSchema
JSON Schema (draft-07) for CacheiroConfig. Exported for runners and custom integrations to validate config before constructing Cacheiro:
import { configSchema } from '@renatorodrigues/cacheiro';
import { Ajv } from 'ajv';
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(configSchema);Hooks
Cacheiro exposes lifecycle events for metrics, logging, or custom side effects. Register them either via the third constructor argument (hooks object) or by calling .on() — both funnel into the same event bus, so you can mix and match, and .on() supports multiple listeners per event:
const cacheiro = new Cacheiro(store, config, {
onCacheSet: ({ hash }) => metrics.increment('cache.set'),
});
cacheiro.on('cacheHit', ({ hash, expired }) => {
if (!expired) metrics.increment('cache.hit_total');
});| Hook | Payload | Fires when |
|---|---|---|
onCacheHit | { hash, expired } | A GET finds and returns an artifact. expired: true means the artifact was past its TTL but is served one last time before removal — see the cacheiro-store-fs TTL docs. |
onCacheMiss | { hash } | A GET finds no artifact for the hash. |
onCacheSet | { hash } | A PUT successfully stores a new artifact. Cacheiro is a fixed-purpose NX remote cache — keys are content-addressed, so a hash is never legitimately overwritten (a repeat PUT gets a 409 instead), hence there is no onCacheUpdate. |
onServerStart | — | After cacheiro.listen() successfully binds. |
onServerStop | — | After cacheiro.stop() closes the server. |
onServerError | { error } | A request handler throws an uncaught error (mirrors the 500 response). |
Minimal skeleton wiring every hook, plus the cacheiro-store-fs-specific expired/swept events (registered directly on the store, not part of this hooks bus). Continuing the store/config from the example above:
const cacheiro = new Cacheiro(store, config, {
onCacheHit: ({ hash, expired }) => log.info('hit', { hash, expired }),
onCacheMiss: ({ hash }) => log.info('miss', { hash }),
onCacheSet: ({ hash }) => log.info('set', { hash }),
onServerStart: () => log.info('server started'),
onServerStop: () => log.info('server stopped'),
onServerError: ({ error }) => log.error('server error', error),
});
// same hooks are also registrable via .on() — useful for multiple listeners per
// event, or wiring after construction: cacheiro.on('cacheHit', ({ hash }) => ...)
store.on('expired', ({ hash }) => log.debug('evicted', { hash }));
store.on('swept', ({ count }) => log.debug('sweep removed', { count }));HTTP API
Defined by swagger.json (OpenAPI 3.0).
| Method | Path | Auth required | Description |
|---|---|---|---|
PUT | /v1/cache/:hash | Yes | Upload a task artifact |
GET | /v1/cache/:hash | Yes | Download a task artifact |
GET | /health | No | Health check — returns 200 OK |
/v1/cache/* endpoints require an Authorization: Bearer <token> header when auth.token is set. If auth.readOnlyToken is also set, requests bearing it may GET but get a 403 on PUT — hand it to untrusted CI (e.g. PR builds) so it can use the cache without poisoning it (CVE-2025-36852 / CREEP).
Stores
Store packages implement the CacheiroStore interface and are independent of this package. Instantiate the store of your choice and pass it to the Cacheiro constructor.
See @renatorodrigues/cacheiro-types for the full list of available stores and instructions on implementing a custom store.
Development
npm run watch # tsc --watch (hot rebuild)
npm run build # compile TypeScript
npm test # run tests
npm run test:watch # watch mode
npm run lint # oxlint
npm run fmt # oxfmt