Skip to content

@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.

ts
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:

ts
import type { CacheiroStore } from '@renatorodrigues/cacheiro';

CacheiroConfig ​

TypeScript interface describing the server configuration shape. Import it to type your own config loader:

FieldTypeRequiredDefaultDescription
server.portnumberYes—Port to listen on.
server.hoststringYes—Host to bind to (e.g. "127.0.0.1" or "0.0.0.0").
server.bodyLimitMbnumberNo100Maximum request body size in megabytes.
server.bannerbooleanNotrueShow the large ASCII art banner on startup. Compact header if false.
server.infoboxbooleanNotrueShow the info box (version, URL, store details) on startup.
server.logFormat'pretty' | 'json'NoprettyLog output format. json prints structured pino JSON lines; pretty prints human-readable lines. Defaults to pretty unless NODE_ENV=production.
server.tls.certFilestringNo—Path to PEM certificate file. Enables HTTPS when set (with keyFile).
server.tls.keyFilestringNo—Path to PEM private key file.
server.tls.caFilestringNo—Path to CA certificate file (optional).
auth.tokenstringNo—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.readOnlyTokenstringNo—Optional bearer token granting read-only access (GET only). Requires auth.token to be set. Must differ from auth.token.
ts
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:

ts
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:

ts
const cacheiro = new Cacheiro(store, config, {
  onCacheSet: ({ hash }) => metrics.increment('cache.set'),
});

cacheiro.on('cacheHit', ({ hash, expired }) => {
  if (!expired) metrics.increment('cache.hit_total');
});
HookPayloadFires 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:

ts
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).

MethodPathAuth requiredDescription
PUT/v1/cache/:hashYesUpload a task artifact
GET/v1/cache/:hashYesDownload a task artifact
GET/healthNoHealth 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 ​

sh
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

Crafted with 🤍 by a 🇧🇷 human in 🇩🇪, for the humans of the 🌐