# Clean up the Fly.io preview apps your team left behind

> See every leftover pull request preview app on Fly.io next to the pull request that created it, and retire the dead ones safely.

- Workflow type: app
- Services: Fly.io, GitHub
- Categories: Engineering, Operations
- Published: 2026-08-21

## What it does

- Shows a board of every Fly.io preview app that matches your review app naming pattern, each row joined to the pull request that created it: number, title, whether it is open, merged or closed, who opened it, and how old the app is.
- Separates Machines from volumes, because a volume keeps costing money by the hour whether or not its Machine is running, so an app with nothing running can still be on your bill.
- Floats the worst offenders to the top: preview apps whose pull request closed weeks ago but that still have Machines running or volumes sitting there.
- Gives every row stop, snapshot, delete volume and delete app actions, with deletes behind a confirmation and the option to snapshot first.
- Adds a Review stale environments button that sends an agent across the flagged apps to write a destroy, keep or ask the owner recommendation with a one line reason, so a person approves the sweep.

## What you'll need

- A Fly.io account, connected with an organization wide access token so the board can see every app in your org (an app only token can see just one app).
- A GitHub account with access to the repositories your preview apps are built from.
- Preview apps that follow a predictable naming pattern, such as pr-123-your-org-your-repo. If yours are named differently you can set your own pattern inside the app.

## Prompt

Build me an app for governing the Fly.io preview environments my team leaves behind. Every pull request spins up a review app named something like pr-123-my-org-my-repo, and cleanup depends on a GitHub Action firing when the pull request closes, which misses often enough that we pay for dead environments for months. I want one screen that shows every leftover review app, ranked worst first, with the actions to retire it safely.

The main screen is a board of my Fly.io apps. Use List Apps, filtered to my organization slug, and keep only the apps whose names match a review app naming pattern. That pattern is a setting, defaulting to pr-{number}-{repo_org}-{repo_name}, and parsing it yields the pull request number and the repository for each app. Teams name review apps differently, so let me edit the pattern in settings and show a live preview of which of my current app names match before I save it.

Each row joins the Fly app to its GitHub pull request. Resolve the pull request with Search Issues and Pull Requests, scoped to the repository and number parsed out of the app name, then call Get a Pull Request for the authoritative details: number, title, whether it is open, merged or closed, who opened it, and when it was merged or closed. When several apps come from the same repository, List Pull Requests is a cheaper way to pull that repository's recent pull requests in one call instead of one lookup per row. Show the pull request number and title as a link, its state, the author, and how long ago it closed.

For each app also show how old it is, how many Machines it has and how many of those are actually running, using List Machines for each Machine's state, region and created time, and which volumes are still attached, using List Volumes for size, region, whether the volume is attached to a Machine, and created time. If the app record carries no creation timestamp, derive the app age from its oldest Machine. Keep Machines and volumes in separate columns: a volume keeps billing by the hour whether or not its Machine is running, so an app with zero running Machines and two live volumes is still costing money and must never look clean.

Rank rows so the worst offenders float to the top, meaning apps whose pull request was merged or closed more than a chosen number of days ago, a setting defaulting to seven, but which still have running Machines or live volumes. Sort by how much is still live and how long it has been dead: running Machine count, total volume size, and days since the pull request closed. Give each row a clear badge such as Stale, Watch or Active, and let me filter to just the flagged rows, filter by repository, and search by app name.

Every row gets actions: Stop Machine for a single Machine or for every running Machine on the app, Create Snapshot for a volume, Destroy Volume, and Destroy App. Stopping is a single click with a clear result toast. Anything destructive sits behind a confirmation dialog that names exactly what is about to disappear, the app and its Machines and its volumes, and requires typing the app name to proceed. That dialog carries a snapshot volumes first checkbox which runs Create Snapshot on each volume before the destroy runs. Snapshots are not instant, so show the snapshot as pending and let me re-check it with List Snapshots rather than pretending it finished. The confirmation should also warn that managed add ons attached to an app are not necessarily removed when the app is deleted and are worth checking separately.

Add a Review stale environments button that starts a background agent across every flagged app. For each one the agent checks the pull request state with Get a Pull Request, looks at the Machine's recent activity with List Events, and reviews the volumes and any existing snapshots with List Volumes and List Snapshots, then writes a per app recommendation of destroy, keep, or ask the owner, with a one line reason such as merged 24 days ago, no Machine events since, two volumes still live. Write those recommendations back into the app so each row shows the verdict, the reason and when it was generated, and show progress while the agent works. The agent itself never deletes anything. A human reads the recommendations and approves the sweep, so also let me select the rows recommended for destroy and run those destroys as a batch, still behind the same confirmation.

Persist a few things in the app: the naming pattern, the organization slug, the staleness threshold in days and the default for snapshot before destroy; the latest agent recommendation per app; a keep until snooze so an environment I have deliberately spared drops off the flagged list until that date, along with the reason and who set it; and an action log recording every stop, snapshot and destroy with who did it and when, so the sweep is auditable afterwards.

Two practical notes. Fly's API is rate limited to roughly one request per second per action and GitHub search allows about thirty requests a minute, so load the board in batches, cache what you fetch for the session, render rows as they resolve instead of blocking on the whole set, and let me refresh a single row on demand. And handle the ordinary messy cases: an app whose name matches the pattern but has no matching pull request should appear as unmatched rather than silently disappear, a repository I cannot see should say so, an app with no Machines at all should still show its volumes, and a destroy that fails because a volume is still attached should tell me exactly that.

## How to customize

- Change the naming pattern the board uses to recognise a preview app and read the pull request number out of its name.
- Set how many days after a pull request closes an environment counts as stale, and whether apps with no Machines still show up.
- Decide whether snapshot before delete is ticked by default, and limit the board to certain repositories or a single organization.

## FAQ

### Will it delete anything automatically?

No. Nothing is removed unless a person clicks it and confirms. Even the review agent only writes recommendations into the board, so a human reads them and approves the sweep.

### Our review apps are not named pr-123-org-repo. Does it still work?

Yes. The naming pattern is a setting. Edit it to match how your team names review apps, and the board shows you which app names currently match before you save.

### Why does it show volumes separately from Machines?

Because a volume carries on billing by the hour whether or not the Machine using it is running. An app with zero running Machines can still be quietly costing you money, so volumes get their own column instead of being hidden behind a green status.

### Can I keep the data before deleting an environment?

Yes. The delete confirmation has a snapshot first option that takes a snapshot of each volume before anything is removed. Snapshots are not instant, so the board keeps showing the request until it completes.

### What does the review agent actually look at?

For each flagged app it checks the current state of the pull request, the recent activity on the app's Machines, and which volumes and snapshots exist, then writes destroy, keep or ask the owner plus a one line reason such as merged 24 days ago with no activity since.

### Does deleting the app get rid of everything it was costing me?

It removes the app and the resources that belong to it, but managed add ons attached to the app are not always removed with it, so the confirmation reminds you to check those separately.

Use this prompt in General Input: https://www.generalinput.com/prompts/clean-up-the-flyio-preview-apps-your-team-left-behind