# State as Code: a 5-minute walkthrough

## What you'll do

In about 5 minutes: fetch the live App Store Connect state of an existing app into a YAML file, edit one field, preview the diff, and apply it. The example uses tideterm (`app.tideterm.ios`) throughout, but every command takes a bundle ID, substitute your own.

When you finish, you'll have a `state.yaml` that is the authoritative source of truth for your app's ASC configuration, diffable in git, and applicable idempotently.

---

## Prerequisites

**Flightline installed:**

```bash
go install github.com/ul0gic/flightline@latest
flightline --version
```

**ASC API key:**

1. In App Store Connect, go to Users and Access → Integrations → App Store Connect API.
2. Generate a key with App Manager role (or Admin for sales and finance reports). Download the `.p8` file.
3. Place it at `~/.appstoreconnect/AuthKey_<KEY_ID>.p8` with mode 600:

```bash
chmod 600 ~/.appstoreconnect/AuthKey_<KEY_ID>.p8
```

**Environment variables:**

```bash
export APP_STORE_CONNECT_KEY_ID=XXXXXXXXXX
export APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export APP_STORE_CONNECT_VENDOR_NUMBER=12345678
```

Add these to your shell profile or a `.envrc` (gitignored). Flightline reads them at startup; you can also pass `--key-id` and `--issuer-id` flags.

---

## 1. Fetch live state

Pull the current ASC state for your app into a file:

```bash
flightline fetch app.tideterm.ios -o state.yaml
```

Or capture to stdout and redirect:

```bash
flightline fetch app.tideterm.ios > state.yaml
```

Flightline writes to stderr when using `-o`:

```
flightline: wrote state.yaml (2847 bytes)
```

Open `state.yaml`. The top of the file will look like:

```yaml
# yaml-language-server: $schema=https://flightline.dev/schemas/v1alpha1/state.schema.json
#
# Generated by `flightline fetch`, edit, then `flightline plan` to preview
# the diff against live ASC, and `flightline apply --confirm` to write.

apiVersion: flightline.dev/v1alpha1
kind: AppState

metadata:
  bundleId: app.tideterm.ios
  version: "1.0.1"
  platform: IOS

spec:
  version:
    releaseType: AFTER_APPROVAL
    copyright: © 2026 Tideterm Labs

  build:
    number: "42"

  metadata:
    locales:
      en-US:
        name: .PassDMV: California
        subtitle: Pass the test, first try
        description: |
          California DMV practice tests with the latest official questions.
          Track your progress, focus on weak areas, and pass with confidence.
        keywords: DMV,driver,test,license,California,permit,practice
        promotionalText: Free updates as the DMV question bank evolves.
        marketingUrl: https://tideterm.app
        supportUrl: https://tideterm.app/support
        privacyPolicyUrl: https://tideterm.app/privacy
  ...
```

The `# yaml-language-server: $schema=...` line activates autocomplete in VS Code and any editor with the YAML language server. If you type `spec.` on a new line, your editor will offer completions.

**What got fetched.** Every surface Flightline supports is represented: version, build, metadata, screenshots, IAP, age rating, export compliance, reviewer demo, categories, pricing, TestFlight groups, and custom product pages. Surfaces with no live data are omitted (nil pointers mean "not managed").

---

## 2. Edit a field

Open `state.yaml` and change the `promotionalText` for `en-US`:

```yaml
spec:
  metadata:
    locales:
      en-US:
        promotionalText: "Now with updated 2026 question bank."
```

Promotional text is the only metadata field you can update on a live, approved version without a resubmission, so it's a good field to experiment with.

Save the file.

---

## 3. Plan the diff

Preview what Flightline would change, without writing anything:

```bash
flightline plan state.yaml
```

Expected output:

```
OP     PATH                                         FROM                                       TO
~      /spec/metadata/locales/en-US/promotionalText Free updates as the DMV question bank...   Now with updated 2026 question bank.
```

`~` means update. `+` means create. `-` means delete. Read-only: `flightline plan` never writes.

**JSON output for scripts:**

```bash
flightline plan state.yaml --output json
```

```json
{
  "bundleId": "app.tideterm.ios",
  "version": "1.0.1",
  "changes": [
    {
      "op": "~",
      "path": "/spec/metadata/locales/en-US/promotionalText",
      "from": "Free updates as the DMV question bank evolves.",
      "to": "Now with updated 2026 question bank."
    }
  ]
}
```

**CI use.** If you want a non-zero exit code when there are changes (useful in a CI gate):

```bash
flightline plan state.yaml --exit-on-changes
# exits 0 when no changes, 2 when changes exist, 1 on error
```

---

## 4. Apply

Write the changes to ASC:

```bash
flightline apply state.yaml --confirm
```

Without `--confirm`, `flightline apply` prints the plan and refuses to write, the safety gate. This matches Terraform's behavior: `apply` without `--confirm` is equivalent to `plan`.

Expected stderr output during apply:

```
flightline: applied ~ /spec/metadata/locales/en-US/promotionalText
```

The command exits 0 on success. If any individual change fails, Flightline continues with the remaining changes, surfaces the errors in the output, and checkpoints completed changes for a safe resume.

**Checkpoint and resume.** If an apply is interrupted (Ctrl-C, network drop, crash), Flightline stores a coordinate-scoped checkpoint in the user cache. It binds the checkpoint to the bundle ID, version, platform, and original plan. Resume with:

```bash
flightline apply state.yaml --confirm --resume
```

Flightline fetches live state again, verifies that the remaining diff matches the interrupted plan, and applies that residual diff. A mismatch stops before any write and asks you to start a fresh apply. A fully successful apply removes its checkpoint.

**Dry run.** Fetch live state and compute the full dispatch path without sending mutating requests:

```bash
flightline apply state.yaml --dry-run --output json
```

Dry-run still requires credentials and network access because it reads current ASC state. It sends no POST, PATCH, or DELETE requests.

---

## 5. Re-plan to confirm idempotency

Run plan again:

```bash
flightline plan state.yaml
```

Expected output:

```
OP      PATH    FROM    TO
(none)  no changes
```

Flightline fetched the live state, compared it to your file, and found no differences. The round-trip is clean. This is the contract: applying the same state file twice produces no second write.

---

## When you'd use which command

| Command | What it does | Writes? |
|---------|-------------|---------|
| `flightline fetch <bundleId>` | Snapshot live ASC state into YAML | No |
| `flightline fetch <bundleId> --version 1.0` | Fetch a specific version | No |
| `flightline fetch <bundleId> --output json` | Fetch as JSON (for piping, not editing) | No |
| `flightline plan state.yaml` | Preview what would change | No |
| `flightline plan state.yaml --exit-on-changes` | Plan + CI gate exit code | No |
| `flightline plan state.yaml --output json` | Machine-readable diff | No |
| `flightline apply state.yaml` | Plan only, refuses to write | No |
| `flightline apply state.yaml --confirm` | Write all changes | Yes |
| `flightline apply state.yaml --confirm --resume` | Resume after interruption | Yes |
| `flightline apply state.yaml --dry-run` | Read live state and compute dispatch, no mutations | No |

---

## Typical workflow

```
1. Edit       edit state.yaml
2. Lint       flightline lint state.yaml           # offline schema + format rules
3. Plan       flightline plan state.yaml           # read-only diff, no writes
4. Apply      flightline apply state.yaml --confirm
5. Preflight  flightline preflight <bundleId> -v <v>  # live rule check
6a. External TestFlight  flightline testflight beta-review submit <bundleId> --build <n>
6b. App Store release   attach the build and intended IAPs, then Submit for Review in ASC
```

Steps 1 to 5 are reversible. The two terminal paths are separate: beta review unlocks external TestFlight distribution and does not submit a production release. App Store Review submission remains a manual ASC action; run preflight immediately before it and verify the review submission contains the build and every intended IAP.

---

## Trial-gated paywalls: tell the reviewer where the purchase lives

ASC state cannot see in-app gating. If your paywall only appears after a free
trial expires, the reviewer's fresh install will never show it — and "we cannot
locate the In-App Purchases" rejections follow. Two rules of thumb:

- Your App Review notes MUST give the exact steps to reach the purchase
  (`flightline reviewer-demo` manages this surface; preflight's
  `review-details.completeness` rule enforces it once an IAP is in the submission).
- Expose a purchase entry point that is reachable at all times — a buy button in
  Settings during the trial costs nothing and removes the whole rejection class.

## Current limitations

### Screenshots and IAP review screenshots

`flightline apply` drives binary asset uploads for version screenshots, IAP review screenshots, and Custom Product Page screenshots. Asset paths are resolved relative to the state file. The diff engine compares local MD5 values with Apple's live checksums, so unchanged bytes are skipped:

```bash
flightline plan state.yaml
flightline apply state.yaml --confirm
```

For managed screenshot sets, files absent from the desired list are deleted during the confirmed apply. `--resume` also resumes interrupted multipart uploads after validating their file checksums. See [Uploading assets](./uploading-assets.md) for the full workflow and the optional one-off upload commands.

### Privacy nutrition labels

Privacy nutrition labels are not manageable via the App Store Connect API (v4.3). The API has no endpoint for `appPrivacyDetails`. Manage them in the App Store Connect web UI.

`flightline privacy-labels get <bundleId>` returns a diagnostic explaining this gap; it does not fail silently.

---

## Troubleshooting

**`asc: HTTP 401 Unauthorized`**

Your credentials are wrong or expired. Check:

```bash
# Confirm the env vars are set and non-empty
echo $APP_STORE_CONNECT_KEY_ID
echo $APP_STORE_CONNECT_ISSUER_ID

# Confirm the .p8 exists at the expected path
ls ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
```

JWTs are minted per-request and expire after 20 minutes, there's no token cache to invalidate.

**`asc: HTTP 403 Forbidden`**

Your key exists but lacks permission for this operation. Check the role on your API key in App Store Connect (Users and Access → Integrations). App Manager or Admin roles are required for most write operations.

**`config: chmod 600 ~/.appstoreconnect/AuthKey_<id>.p8`**

Your `.p8` file mode is too wide (world-readable or group-readable). Fix it:

```bash
chmod 600 ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
```

Flightline refuses to load a key file with permissions wider than 600.

**Strict YAML errors (`yaml: line N: cannot unmarshal !!int into string`)**

Flightline's strict loader rejects YAML type coercions. Common fixes:

```yaml
# Wrong, YAML parses these as non-strings
version: 1.0
number: 42
appPricePointId: FREE

# Right, quote them
version: "1.0"
number: "42"
appPricePointId: "FREE"
```

**`plan: state.yaml failed schema validation`**

The file has a field-level error. The diagnostic output includes `file:line:col` for YAML errors and a JSON Pointer path for schema errors. Fix the specific field and re-run.

**`asc: no version "1.0.2" on platform IOS`**

The version in `metadata.version` doesn't exist in ASC yet. Either create the version first (`flightline versions create`) or update `metadata.version` to match an existing editable version.

**`state: ageRating.seventeenPlus is derived by Apple; cannot be set directly`**

The `seventeenPlus` field in `spec.ageRating` is Apple-computed from your questionnaire answers. Remove it from the spec or leave it as read-only documentation. It has no write effect.

---

## See also

- [Field reference](../reference/state-yaml.md), every field, every constraint, every gotcha
- [Uploading assets](./uploading-assets.md), the screenshot and IAP review-screenshot upload workflow
- [Preflight in CI](./preflight-in-ci.md), run live release checks as a deployment gate
- Schema: `schemas/flightline.schema.json` (or `https://flightline.dev/schemas/v1alpha1/state.schema.json`)
- `flightline --help`, `flightline fetch --help`, `flightline plan --help`, `flightline apply --help`
