Documentation

Get started in five minutes

Cachely is a remote cache for Nx, Lerna, Turborepo, Gradle, and Bazel builds. The first machine to build a task uploads the result; every other machine and CI runner downloads it and skips the work.

Cachely speaks the Nx self-hosted remote cache protocol, which also powers Lerna caching, plus the Turborepo (Vercel Remote Cache), Gradle HTTP Build Cache, and Bazel HTTP remote cache protocols.

Getting started

  1. Create an account and a workspace. Sign in with Google and create a workspace. Your cache is stored on fully managed Cloudflare R2 - zero configuration.
  2. Generate an API token. Open the workspace and create a token with a descriptive name (e.g. github-actions-prod or alice-laptop). Copy the raw value immediately - it is shown once and never again.
  3. Set two environment variables in your Nx or Lerna repo (and in your CI secrets), before running tasks:
    export NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://remote.cachely.dev
    export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=<your-token>
  4. Build something. Run nx build my-app or npx lerna run build. The first run uploads the result (a put); every later run on any machine using a token in the same workspace downloads it instantly (a hit).

CI setup (GitHub Actions)

Store the token as a repository secret (e.g. CACHELY_TOKEN) and expose the two variables at the workflow level - every nx invocation in the job picks them up automatically:

# .github/workflows/ci.yml
env:
  NX_SELF_HOSTED_REMOTE_CACHE_SERVER: https://remote.cachely.dev
  NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.CACHELY_TOKEN }}

Give pull_request workflows a read-only token (a separate secret) so untrusted branches can use the cache but never write to it - that is what closes the CVE-2025-36852 cache-poisoning attack. Any other CI provider works the same way: set the two environment variables from your secret store.

Requirements

  • An Nx version with self-hosted remote cache support (Nx 19+). All CI runners and developers must share the same Nx version - cross-version task hashes will not match, so they will not share cache.
  • NX_SELF_HOSTED_REMOTE_CACHE_SERVER must be the bare host (https://remote.cachely.dev) with no path. Nx appends /v1/cache/<hash> itself.
  • Each token is scoped to a single workspace. Issue one per CI pipeline, repo, or developer so you can revoke any of them without breaking the rest.

Storage

Managed (R2). Cachely stores cache objects in Cloudflare R2 on your behalf. Zero configuration, included in the subscription - nothing to provision, secure, or patch.

Lerna

Modern Lerna uses Nx for task scheduling and caching, so it connects to Cachely through the existing Nx protocol. Enable caching for the tasks you run in nx.json:

{
  "targetDefaults": {
    "build": { "cache": true },
    "test": { "cache": true }
  }
}

Set the same NX_SELF_HOSTED_REMOTE_CACHE_SERVER and NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN variables shown above, then run npx lerna run build. Cachely validates Lerna 9.0.7 with Nx 22.7.5; Lerna 9 supports Nx versions from 21.5.3 up to, but not including, 23.

Turborepo

Cachely also speaks the Turborepo remote cache (Vercel Remote Cache) protocol. Point Turborepo at it with three environment variables, then run your tasks as usual:

export TURBO_API=https://remote.cachely.dev
export TURBO_TOKEN=<your-token>
export TURBO_TEAM=<any-non-empty-value>

The same workspace token works for both Nx and Turborepo. TURBO_TEAM is required by the Turborepo CLI but its value is ignored - the token alone identifies your workspace.

Gradle

Cachely also speaks the Gradle HTTP Build Cache protocol. Enable the remote build cache in settings.gradle.kts (Kotlin DSL) or settings.gradle (Groovy DSL), using your workspace token as the HTTP Basic password (the username is ignored - leave it empty):

// settings.gradle.kts (Kotlin DSL)
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://remote.cachely.dev")
        isPush = System.getenv("CI") != null
        credentials {
            username = "" // unused; the token identifies you
            password = System.getenv("GRADLE_CACHE_TOKEN") // your Cachely token
        }
    }
}
// settings.gradle (Groovy DSL)
buildCache {
    remote(HttpBuildCache) {
        url = 'https://remote.cachely.dev'
        push = System.getenv('CI') != null
        credentials {
            username = '' // unused; the token identifies you
            password = System.getenv('GRADLE_CACHE_TOKEN') // your Cachely token
        }
    }
}

Turn the build cache on for every invocation in gradle.properties (or pass --build-cache per run):

# gradle.properties
org.gradle.caching=true

Then export the token before building - locally from your shell, in CI from your secret store:

export GRADLE_CACHE_TOKEN=<your-token>
gradle build

The same workspace token works for Nx, Turborepo, and Gradle. Keep isPush limited to trusted builds and pair it with a read-only token for pull requests - Gradle reads from the cache either way, but only trusted builds can populate it.

Bazel

Cachely also speaks the Bazel HTTP remote cache protocol (action cache and CAS). Point --remote_cache at the bare host in your committed .bazelrc - Bazel appends /ac/ and /cas/ itself - and keep the token in a gitignored user.bazelrc pulled in via try-import (Bazel cannot read .env files, so this is its dotenv equivalent):

# .bazelrc (committed)
build --remote_cache=https://remote.cachely.dev
try-import %workspace%/user.bazelrc
# user.bazelrc (gitignored - carries the raw token)
build --remote_header="Authorization=Bearer <YOUR_TOKEN>"

In CI, skip user.bazelrc and pass the header on the command line from your secret store, e.g. bazel build //... --remote_header="Authorization=Bearer $CACHELY_TOKEN". HTTP Basic via .netrc also works - use the token as the password (the username is ignored).

The same workspace token works for Nx, Lerna, Turborepo, Gradle, and Bazel. Uploads are on by default; give untrusted pull-request builds a read-only token and add build --noremote_upload_local_results so they can read the cache but never populate it.

Security

  • Tokens are stored hashed (SHA-256). The raw value is shown once at creation and never again.
  • Every cache request is authenticated - an unknown or revoked token is rejected with a 401 before any storage is touched.
  • Managed artifacts are content-addressed and immutable - an existing cache key cannot be overwritten (the API returns 409), so a known good build can never be silently replaced.
  • Tokens are scoped read-only or read-write. Give pull-request and fork builds a read-only token so they can use the cache but never write to it - this closes the CVE-2025-36852 (CREEP) cache-poisoning attack.
  • Workspaces are isolated per owner; one workspace can never read another's artifacts.

Troubleshooting

Builds never hit the cache
Confirm NX_SELF_HOSTED_REMOTE_CACHE_SERVER is the bare host (https://remote.cachely.dev) with no trailing path - adding the path yourself doubles it and every request 404s. Check the token has not been revoked, and verify every runner uses the same Nx version.
401 from Nx
The token is invalid, revoked, or scoped to a different workspace. Generate a new one in the dashboard under your workspace's tokens.
Lerna runs but never hits the remote cache
Confirm the task has "cache": true in nx.json, both Nx environment variables are visible to the lerna process, and every runner resolves the same Nx version. Lerna delegates cache reads and writes to Nx.
403 on upload
Your token is read-only. Cache reads work but writes are rejected - use a read-write token for the builds that should populate the cache (typically your protected branches, not pull requests).
Dashboard shows no activity
No cache requests have arrived in the selected time range yet, or the token used belongs to a different workspace than the one you are viewing.
Gradle never stores or loads entries
Confirm the URL is https://remote.cachely.dev - Gradle appends the cache key to it directly, so no path is needed. Check org.gradle.caching=true is set (or pass --build-cache), the task is cacheable, and GRADLE_CACHE_TOKEN is exported in the environment Gradle runs in. A 401 in --info output means the token is wrong or revoked; loads working while stores are skipped means isPush is false or the token is read-only.
Bazel never hits the cache
Confirm --remote_cache is the bare host (https://remote.cachely.dev) with no path - Bazel appends /ac/ and /cas/ itself. Check the --remote_header token is present and not revoked; a 401 means it is wrong or revoked. A 404 is just a miss - Bazel executes the action locally and (with upload enabled) populates the cache for next time.

Ready to speed up your builds?

Create a workspace, drop in a token, and watch your hit rate climb. Free tier - no credit card.