🏎️💨 Turbocharge your builds with a Turborepo remote cache in a single edge function
Tags: turborepo, webdev, nextjs
Published: 2023-11-15 13:55:08

Monorepos are a great tool for organizing projects, but they can lead to slow build particularly in complex projects with lots of interdependent packages. Turborepo is a great solution for improving your build times which has become particularly popular for Next.js sites. It uses lots of clever tricks to ensure you only build the absolute minimum needed, using lots of caching and dependency tracking. I'm going to share how I built [a custom Turborepo remote cache](https://github.com/ascorbic/turbofan) in a single edge function.

## The secrets of Full Turbo

One of Turborepo's neatest tricks is a global remote cache, which lets you share build artifacts between your local development environment and production builds and between different members of your team. It works in the background to speed up your builds everywhere.

Turborepo is owned by Vercel, so understandably they want you to use their cache service. However Turborepo does support custom remote caching, and there is already a great open source [Turborepo remote cache](https://github.com/ducktors/turborepo-remote-cache) project, but I had a feeling I could build something simpler. 

## A cache server in 100 lines of code

While the remote cache server API is undocumented, it is fortunately quite simple. I had early access to the new Netlify Blobs product, which seemed perfect for this. With an Edge Function handling the API and Netlify Blobs for the storage, I implemented a Turborepo remote cache in under 100 lines of code.

If you just want to start speeding up your builds, you can [check out the repo](https://github.com/ascorbic/turbofan) or just click the button: [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg?q)](https://app.netlify.com/start/deploy?repository=https://github.com/ascorbic/turbofan-server) 
If you want to see how I built it, read on.

## The Turborepo remote cache API

The docs for the Turborepo custom cache currently consists of [a link to the Go source of the client](https://turbo.build/repo/docs/core-concepts/remote-caching#custom-remote-caches), but the API is fortunately very simple. A Turborepo remote cache needs an endpoint at `/v8/artifacts/:hash` that accepts `PUT` requests to add an item, and `GET` requests to return it. Authentication is via a shared bearer token, and the `teamId` is passed as a query param. This is super simple to set up as a Netlify edge function:

```ts
import type { Config } from "@netlify/edge-functions";

export default async function handler(request: Request, context: Context) {
   // Do cool stuff here
}

export const config: Config = {
	method: ["GET", "PUT"],
	path: "/v8/artifacts/:hash",
	// This lets us handle our own cache rules
	cache: "manual",
};

```

This gives us a function that runs on the right path, with the right methods.

Now add some auth. Turborepo sends the token in an `Authorization` header, which we can store in a shared environment variable. You can use anything for the token value. I like to use a UUID, which you can generate on a Mac by running `uuidgen` in the terminal. You'll need to store the same token on the server site and any repos that use it.

```ts
// snip
export default async function handler(request: Request, context: Context) {
  const bearerHeader = request.headers.get("authorization");
  // Remove the leading "Bearer " from the token
  const token = bearerHeader?.replace("Bearer ", "");
  // Compare it with the stored value, and return a 401 if it doesn't match
  if (!token || token !== Netlify.env.get("TURBO_TOKEN")) {
    console.log("Unauthorized");
    return new Response("Unauthorized", { status: 401 });
  }
}
```

We're going to use [Netlify Blobs](https://docs.netlify.com/blobs/overview/) to store our artifacts. These don't need any setup - you can just start using them. We'll use a store based on the team ID:

```ts
import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/edge-functions";

export default async function handler(request: Request, context: Context) {
  // Auth stuff goes here...

  const url = new URL(request.url);

  const teamId = url.searchParams.get("teamId") ?? "team_default";

  // Get a store
  const store = getStore(`artifacts-${encodeURIComponent(teamId)}`);

```

Now we just need to handle getting and putting the artifacts. First we generate the cache key for the object and then handle uploads:

```ts
  const hash = context.params?.hash || url.pathname.split("/").pop();

  if (!hash) {
    console.log("Missing hash");
    return new Response("Not found", { status: 404 });
  }

  const key = encodeURIComponent(hash);

  if (request.method === "PUT") {
    // Get the uploaded file as binary
    const blob = await request.arrayBuffer();
    if (!blob) {
      console.log("No content");
      return new Response("No content", { status: 400 });
    }
    // ...then put it in the store. That's all!
    await store.set(key, blob);
    return new Response("OK");
  }
```

Now we've implemented uploads, we can implement downloads.

```ts
  try {
    // Try to get the blob from the store as a binary buffer
    const blob = await store.get(key, {
      type: "arrayBuffer",
    });
    if (!blob) {
      // Oh no, return a 404
      return new Response(`Artifact ${hash} not found`, { status: 404 });
    }

    const headers = new Headers();
    // This content-type is required by Turborepo
    headers.set("Content-Type", "application/octet-stream");

    headers.set("Content-Length", blob.byteLength.toString());

    // We can set some useful cache headers, using Netlify's new cache features
    headers.set(
      "Netlify-CDN-Cache-Control",
      "public, s-maxage=31536000, immutable"
    );
    headers.set("Netlify-Vary", "header=Authorization,query=teamId");

    return new Response(blob, { headers });
  } catch (e) {
    // Catch any errors and return a 500
    console.log(e);
    return new Response(e.message, { status: 500 });
  }
```

That's it! 

If you add that to your site at `netlify/edge-functions/turbofan.ts` then it will run and handle requests to the cache URL. 

## Easy mode

Instead of manually writing or copying the code, you can just use the version I've published to deno.land:

```ts
import type { Config } from "@netlify/edge-functions";

export { handleRequest as default } from "https://deno.land/x/turbofan/mod.ts";

export const config: Config = {
	method: ["GET", "PUT"],
	path: "/v8/artifacts/:hash",
	cache: "manual",
};

```

That's all you need. Make sure you've set the `TURBO_TOKEN` env var.

Or, even easier, just click this button to deploy a standalone remote cache: 
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg?q)](https://app.netlify.com/start/deploy?repository=https://github.com/ascorbic/turbofan-server) 

## Setting up your repo
You now need to tell `turborepo` to use the remote cache. You can do this by creating a config file. This is _not_ the `turbo.json` in the root of your repo! You create it at `.turbo/config.json`, which may have been excluded in your `.gitignore` You will need to change that. Here's what you need to create:

```json
{
    "teamid": "team_anything_you_want_here",
    "apiurl": "https://your-turboprop-server-name-here.netlify.app"
}
```

If you create that and set the `TURBO_TOKEN` env var, Turborepo will know to use the remote cache.

You can see it's working because Turborepo will print "Remote caching enabled" in the logs.

Have fun, and may all your builds be FULL TURBO.