Frameworks & Hosting8 min read

How to Integrate Fly.io with Your Headless CMS

Connect Fly.io to structured content so every publish can refresh cached pages, rebuild static routes, or update a Fly-hosted app without waiting for a manual deploy.

Published April 29, 2026
01Overview

What is Fly.io?

Fly.io is an application hosting platform that runs Dockerized apps, full-stack frameworks, APIs, and workers on Firecracker microVMs in regions close to users. Teams use it when they want app servers, background jobs, private networking, volumes, and global deployment controlled through flyctl, GitHub Actions, and the Machines API.


02The case for integration

Why integrate Fly.io with a headless CMS?

If your app runs on Fly.io, the content workflow can’t stop at “publish.” A new pricing page, product guide, or homepage banner needs to reach the Fly-hosted app, invalidate the right cache entries, and show up for users without someone running fly deploy from a laptop.

A headless CMS integration solves that handoff. With Sanity’s AI Content Operating System, editors publish structured content into the Content Lake, a webhook fires on that publish event, GROQ selects only the fields your Fly app needs, and a Sanity Function or webhook handler sends the payload to Fly.io. For dynamic apps, that might update an internal cache. For static routes, it might trigger a rebuild endpoint or restart Fly Machines after a cache refresh.

The disconnected version is slower and easier to break. Editors publish, developers get pinged in Slack, a deploy runs later, and stale content can sit in memory, Redis, or generated HTML until someone notices. The trade-off is that you need to design your invalidation path carefully. Restarting Machines on every small edit is simple, but it’s usually wrong for high-volume publishing. A targeted route or tag-based cache update is better when content changes many times per hour.


03Architecture

Architecture overview

Content starts in Sanity’s Content Lake as typed JSON. When an editor publishes, updates, or deletes a document, a Sanity webhook fires with the document ID and mutation details. A Sanity Function can receive that event, run a GROQ query such as *[_id == $id][0]{title, "slug": slug.current, modules[]{...}}, and shape the result into the exact payload your Fly-hosted app expects. From there, the Function can POST the content payload to a private endpoint on your Fly.io app, for example https://your-app.fly.dev/internal/content-sync, using a shared secret header. The Fly app can write the content into an in-memory cache, Redis, SQLite on a Fly Volume, LiteFS, or any storage layer you already run there. If the app pre-renders pages or keeps long-lived process caches, the same Function can call the Fly.io Machines API with Authorization: Bearer FLY_API_TOKEN. A common flow is GET /v1/apps/{app_name}/machines to list running Machines, then POST /v1/apps/{app_name}/machines/{machine_id}/restart after the sync endpoint confirms the new content is available. End users then receive fresh content from the Fly region closest to them.


04Use cases

Common use cases

🌍

Global content refreshes

Publish in Sanity, then push route-level updates to a Fly.io app running in regions such as ord, fra, and nrt.

Static page rebuild triggers

Use webhooks to tell a Fly-hosted Next.js, Remix, or Astro app which slugs changed, instead of rebuilding the whole site.

🧠

Content-fed app workers

Send product docs, guides, or pricing rules from the Content Lake to Fly.io workers that generate search indexes, feeds, or notifications.

🗺️

Region-aware experiences

Model market-specific content in Sanity, then let Fly.io serve the right copy from app instances close to each audience.


05Implementation

Step-by-step integration

  1. 1

    Create and deploy your Fly.io app

    Install flyctl, run fly auth signup or fly auth login, then run fly launch in your app directory. Choose your organization, app name, primary region, and Dockerfile or framework preset. Deploy once with fly deploy so you have a live https://your-app.fly.dev URL.

  2. 2

    Create a Fly.io API token

    For local testing, fly auth token can give you a token tied to your account. For automation, create a scoped token with Fly.io’s token commands and store it as FLY_API_TOKEN in Sanity or your webhook runtime. Your handler will use it with the Machines API at https://api.machines.dev.

  3. 3

    Model the content your Fly app needs

    In Sanity Studio, define schemas for the routes your app renders. A typical page schema includes title, slug, seoTitle, description, locale, cacheTags, and modules. Keep app-specific fields explicit so GROQ can fetch them without parsing HTML or page blobs.

  4. 4

    Create the sync trigger

    Add a Sanity webhook for create, update, and delete mutations on publish, or use a Sanity Function if you want the server-side logic to run inside Sanity. Filter events with GROQ so only relevant document types, such as page, product, or article, trigger the Fly.io sync.

  5. 5

    Connect the handler to Fly.io

    Have the handler fetch the latest document with @sanity/client, POST the shaped JSON to an internal route on the Fly app, and call the Fly.io Machines API only when a restart or cache reset is needed. Keep the internal route protected with a secret header.

  6. 6

    Test the end-user path

    Publish a draft in Sanity Studio, confirm the webhook fires, inspect the Fly.io app logs with fly logs, and load the affected route from at least two regions if you run multiple Machines. Test delete events too, since removed slugs often break integrations first.


06Code

Code example

typescriptsanity-to-fly.ts
import {createClient} from '@sanity/client';

const sanity = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: process.env.SANITY_DATASET!,
  apiVersion: '2025-01-01',
  token: process.env.SANITY_TOKEN,
  useCdn: false,
});

async function fly(path: string, init: RequestInit = {}) {
  return fetch(`https://api.machines.dev/v1/apps/${process.env.FLY_APP_NAME}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${process.env.FLY_API_TOKEN}`,
      'content-type': 'application/json',
      ...(init.headers || {}),
    },
  });
}

export default async function handler(req: Request) {
  const event = await req.json();

  const page = await sanity.fetch(
    `*[_id == $id][0]{_id,_type,title,"slug":slug.current,"updatedAt":_updatedAt,cacheTags}`,
    {id: event._id}
  );

  if (!page) return Response.json({skipped: event._id}, {status: 202});

  await fetch(`https://${process.env.FLY_APP_NAME}.fly.dev/internal/content-sync`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-sync-secret': process.env.FLY_SYNC_SECRET!,
    },
    body: JSON.stringify(page),
  });

  const machines = await (await fly('/machines')).json();
  await Promise.all(
    machines.map((m: {id: string}) => fly(`/machines/${m.id}/restart`, {method: 'POST'}))
  );

  return Response.json({synced: page.slug, machines: machines.length});
}

07Why Sanity

How Sanity + Fly.io works

Build your Fly.io integration on Sanity

Sanity’s AI Content Operating System gives you the structured content foundation, real-time event system, and flexible APIs to connect Fly.io to the way your team ships.

Start building free →

08Comparison

CMS approaches to Fly.io

CapabilityTraditional CMSSanity
Content shape for Fly-hosted appsOften sends rendered HTML or page-shaped blobs that your app has to parse before it can cache or render.Structures content as typed JSON in the Content Lake, with references you can join in one GROQ query.
Publish-to-Fly update pathCommonly depends on manual deploys, scheduled exports, or plugin-specific webhooks.Uses webhooks or Functions to run sync logic on content events, then call Fly.io endpoints or the Machines API.
Cache invalidation controlOften clears too much cache because content fields aren’t tied cleanly to routes or tags.Uses GROQ to return slugs, cacheTags, locales, and referenced content in one payload.
Static and dynamic renderingWorks best when the platform controls rendering, which can limit custom Fly.io app patterns.Supports dynamic reads, preview flows, Content Releases, and route-specific sync for Fly-hosted frameworks.
Operational trade-offsSimple for small sites, but harder when app servers, workers, and multiple regions need fresh content.Can reduce extra infrastructure with Functions, though high-volume publishing still needs a careful cache and restart strategy.

09Next steps

Keep building

Explore related integrations to complete your content stack.

Ready to try Sanity?

See how Sanity's Content Operating System powers integrations with Fly.io and 200+ other tools.