---
title: "Protected Tracks"
description: "Require a second person's approval before a patch reaches a track, and block production changes during deployment freeze windows"
---

On Enterprise, you can control who puts a patch in front of production users, and when:

* A **protected track** takes no patch until an admin or owner approves it. See [Protect a track](#protect-a-track).
* A **deployment freeze window** blocks production changes for a period you set, such as a holiday or a weekend. See [Deployment freezes](#deployment-freezes).

Shorebird’s servers enforce both, so they hold whether you ship from the console, the CLI, CI or a service account.

## Protect a track

In an organization that’s part of an enterprise, an admin or owner, or an app collaborator with the Admin role, can protect a [track](/code-push/tracks/) so that no patch reaches it without a second person’s approval. Open the app’s [**Tracks** tab](/code-push/tracks/#managing-tracks-in-the-console) in the Shorebird console and turn on **Protected** next to the track, usually `stable`.

Turning protection off is always possible, including after an organization leaves its enterprise. Requests already pending stay pending when protection is turned off: approve, reject, or cancel each one. An organization that leaves keeps its protected tracks, and patches sent to them still need approval. The console shows the **Approvals** tab only while the organization’s plan is Enterprise. Without the tab, reach the approvals page from the **Waiting for approval** banner or the link the CLI prints.

A protected track can’t be deleted until its protection is turned off.

## Send a patch to a protected track

Publishing to a protected track, promoting a patch to it, or rolling forward a patch on it doesn’t ship the patch. Shorebird files an approval request instead, and the CLI prints a link to the app’s approvals page and exits with an error, so a CI job stops there:

```
Track stable is protected. Approval requested for patch 4: https://console.shorebird.dev/apps/<app-id>/approvals
```

A rollforward prints `Approval requested to roll forward patch 4` in its place.

`shorebird patch` still uploads the patch, and a promoted patch stays on its current track until the request is approved. Each release has at most one pending request per track: a newer request, such as a rerun of the CI job, replaces the older one. Approval is only needed to put a patch on a protected track: rolling back a patch, or moving it off the track, never needs it. A patch waiting for approval, or already on a protected track, takes no new artifacts, so adding a platform or retrying an upload to it fails with `patch_artifacts_create_patch_under_approval`. Create a new patch instead.

In CI, the failed step’s error code is `patch_approval_required` ([`error.api_code`](/code-push/ci/scripting/#errors) in `--json` output), which tells a waiting patch apart from a failed one. Run CI with a [service account](/account/service-accounts/) rather than a person’s API key: a request sent with your own key is your own request, which another admin or owner has to approve, unless you’re an owner and give a reason.

While requests are pending, the app’s pages in the console show a **Waiting for approval** banner, and the release page marks the waiting patch.

## Approve a request

When a request is filed, every admin and owner of the organization except whoever asked gets an email linking to it. The app’s **Approvals** tab lists pending requests with the patch, release, track, and who asked. Admins and owners can **Approve** or **Reject** each one, with an optional comment. Approving puts the patch on the track at once; as with any patch, devices get it if it’s the highest-numbered patch there that isn’t rolled back. Approving fails, and the request stays pending, if:

* the organization’s enterprise has since started [requiring signed patches](/enterprise/patch-signing/) and the patch isn’t signed,
* a rollforward’s patch has since moved off the track, or
* a [deployment freeze window](#deployment-freezes) is active. Approve the request once the window ends.

Reject or cancel a request that can’t be approved. Rejecting leaves the patch where it is. Whoever asked can **Cancel request** instead, which also leaves the patch where it is.

App collaborators with the Admin role can approve and reject requests and protect tracks too, but they aren’t emailed when a request is filed, and they can’t approve their own.

* You can’t approve your own request, unless you’re an owner and give a reason. The request records that you approved it yourself, and the organization’s other admins and owners get an email with your reason.
* Only people approve. No API key, service account, or CI token can approve or reject a request, or turn a track’s protection on or off, whatever its permissions.
* Decided requests stay listed under **Decided** with who decided and their comment, and every request and decision is in the [audit log](/enterprise/audit-logs/).

## Deployment freezes

A deployment freeze window is a period when no patch can be put in front of production users, such as a holiday change freeze or every weekend. While a window is active, Shorebird refuses:

* **On a protected track:** approving a request. A patch sent to the track still files its request, which waits for the window to end.
* **On `stable`, when it isn’t protected:** promoting a patch to it, including `shorebird patch --track=stable`, rolling a patch forward on it, raising a patch’s rollout percentage on it, and setting a rollout schedule on it.

A timed rollout schedule on `stable` or a protected track holds at its current step while a window is active, and moves on once it ends.

Other changes still work during a freeze, so an incident can always be undone: rolling a patch back, moving a patch off the track, and patches on tracks that are neither `stable` nor protected, such as `staging`.

A refused change fails with the error code `deployment_freeze_active`. Its message names the window and when it ends:

```
Deployment freeze "Holiday freeze" is active until 2026-12-26T00:00:00.000Z.
```

To ship during a freeze, wait for the window to end, remove it, or [override it](#override-a-freeze).

A refused `shorebird patch` has still uploaded the patch. Once the window ends, [promote that patch](/code-push/tracks/#promoting-a-patch-between-tracks) instead of running `shorebird patch` again, which would create a new one. It’s the newest patch marked `[no track]` in `shorebird patches list --release-version <version>`.

### Add a freeze

A window covers either every app in your enterprise, or one app. Both are managed on a **Deployment freeze windows** card in the [Shorebird console](https://console.shorebird.dev):

* **Enterprise-wide windows** apply to every app in every organization of the enterprise. Enterprise Owners and Admins manage them: open **Enterprise** in the sidebar, then its [**Security** tab](https://console.shorebird.dev/enterprise/security).
* **App windows** apply to one app. Open the app’s **Settings** tab. Anyone who can protect its tracks can add and remove them: an admin or owner of the organization, or an app collaborator with the Admin role. The card also lists the enterprise-wide windows that apply to the app, marked **Enterprise**; those can only be removed from the enterprise’s **Security** tab.

Windows apply only while the organization is part of the enterprise. An organization that leaves keeps its protected tracks, but its freeze windows stop applying. On other plans, the app’s card has no **Add freeze window** button.

To add a window, click **Add freeze window**, give it a **Name**, and pick a **Schedule**:

* **Fixed window**: **Starts at (UTC)** and **Ends at (UTC)**, as ISO 8601 timestamps such as `2026-12-24T00:00Z` and `2026-12-26T00:00Z`. The end must be after the start.
* **Weekly recurring**: a **Day of week**, a **Start time (24-hour)** in `HH:MM`, a **Duration (minutes)**, and a **Timezone**, an IANA name such as `UTC` or `America/New_York`. The start time is local wall-clock time in that zone, so the window starts at the same local time across daylight saving changes. A window can last up to 10,079 minutes, one minute short of a week, and can run past the end of the week: a window starting Friday at 17:00 for 3,840 minutes runs until Monday at 09:00.

Each window on the card shows its name, a **Fixed** or **Weekly** badge, and its schedule. To remove one, open its options menu, choose **Remove**, and confirm with **Remove**. Production changes are no longer blocked during that window.

### Override a freeze

An owner of the app’s organization can ship through an active window by giving a reason of at least 10 characters. So can an enterprise Owner or Admin who can also promote patches to `stable` in the app. It has to be their own sign-in, from `shorebird login` or the console. An API key or a service account can’t override a freeze, whatever its permissions.

The console and the CLI’s own commands have no option for it yet. Send the request with [`shorebird api`](/account/api/) and add `break_glass_reason` to the request body. The public API’s promote, roll-forward and rollout endpoints accept it for a patch going to `stable`, or for raising its rollout percentage there, when `stable` isn’t protected:

```
shorebird api -X POST /apps/APP_ID/patches/promote \
  -F patch_id=PATCH_ID -F channel_id=CHANNEL_ID \
  -f break_glass_reason="Hotfix for the checkout crash"
shorebird api -X POST /apps/APP_ID/releases/RELEASE_ID/patches/PATCH_ID/rollforward \
  -f break_glass_reason="Hotfix for the checkout crash"
shorebird api -X PUT /apps/APP_ID/releases/RELEASE_ID/patches/PATCH_ID/rollout \
  -F rollout_percentage=PERCENTAGE \
  -f break_glass_reason="Hotfix for the checkout crash"
```

`PATCH_ID`, `CHANNEL_ID` and `RELEASE_ID` are IDs, not the patch number or the track’s name. List them with `shorebird api /apps/APP_ID/channels`, `shorebird api /apps/APP_ID/releases` and `shorebird api /apps/APP_ID/releases/RELEASE_ID/patches`.

`PERCENTAGE` can’t be below the patch’s current percentage, since rollouts only go up, and only a higher one needs the reason. Setting a percentage ends any timed rollout schedule the patch was on. `-F` sends only a whole number as a number. For a fraction such as `12.5`, pass the whole body as JSON with `--input`, reason included: with `--input`, `-f` and `-F` fields go in the URL, where the reason isn’t read.

```
echo '{"rollout_percentage": 12.5, "break_glass_reason": "Hotfix for the checkout crash"}' |
  shorebird api -X PUT /apps/APP_ID/releases/RELEASE_ID/patches/PATCH_ID/rollout --input -
```

The rollout call sets one platform’s patch, the one in the path, so repeat it with each platform’s `PATCH_ID`.

On a protected track, a patch or a higher rollout percentage goes out only through an approval request, and approving one can’t override a freeze from the console, the CLI’s own commands or the public API.

The organization’s other admins and owners get an email about each override.

Adding, removing, and overriding a window are recorded in the [audit log](/enterprise/audit-logs/#deployment-freezes).
