---
title: upload
description: Uploads a built APK to AppFlight. This is the primary command.
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.page/mhmzdev/appflight-cli-docs/llms.txt
> Use this file to discover all available pages before exploring further.

Uploads a built APK to AppFlight. This is the primary command.

```bash
appflight upload [--flavor <name>] [options]
```

## Flags

| Flag | Description |
|------|-------------|
| `--flavor`, `-f` | Flavor to upload. **Required** when the project has multiple flavors configured. |
| `--file` | Path to the APK. Overrides `apkPath` from `appflight.json`. |
| `--version` | Version string. Overrides the version read from your project files. |
| `--build-number` | Build number. Overrides the build number from your project files. |
| `--dry-run` | Print what would be uploaded without actually uploading. |
| `--ci` | CI mode: prefix log lines with `[appflight]`, disable progress bar. |

## Usage

**Flavored app** — `--flavor` is required:

```bash
appflight upload --flavor stage
appflight upload --flavor prod
```

**No-flavor app:**

```bash
appflight upload
```

**Dry run first:**

```bash
appflight upload --flavor stage --dry-run
```

**CI mode:**

```bash
appflight upload --flavor stage --ci
```

Also auto-detected when the `CI` environment variable is set.

## Version resolution

The CLI reads the version automatically from your project files:

- **Flutter** — reads `version` from `pubspec.yaml` (e.g. `1.2.3+45`)
- **React Native** — reads `versionName` + `versionCode` from `android/app/build.gradle` and combines them as `1.2.3+45`
- **Kotlin** — reads `versionName` + `versionCode` from root `app/build.gradle` (no `android/` wrapper) and combines them the same way

Override either at upload time:

```bash
appflight upload --flavor stage --version 1.2.3
appflight upload --flavor stage --build-number 99
```

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | Upload succeeded |
| `2` | `appflight.json` not found — run `init` first |
| `3` | Not logged in — run `login` or set `APPFLIGHT_API_KEY` |
| `4` | APK file not found — build the APK first |
| `5` | Version already exists — bump the version |
| `6` | API key invalid or revoked — run `login` again |
| `7` | Storage upload failed |
| `8` | APK limit reached — upgrade your plan |
| `9` | Forbidden — your role may not upload to this app |

## Plan limits

AppFlight enforces per-app APK limits based on your subscription:

| Plan | APK limit per app | On limit hit |
|------|-------------------|--------------|
| Economy (free) | 5 (individual) · 10 (org) | Upload blocked — CLI exits `8` |
| First Class | 15 (individual) · 20 (org) | **Rollover** — oldest build auto-rotates, upload succeeds |

### Economy — blocked at cap

When a free-tier user hits the cap, the upload is blocked and the CLI exits with code `8`:

```
✗ Upload blocked — you've reached your APK limit for this app.
✗ Free plan: 5 APKs per app. Upgrade in the AppFlight mobile app.
✗ Settings → Subscription → Upgrade to First Class
```

Run with `--verbose` to see the full server response:

```bash
appflight upload --flavor stage --verbose
```

### First Class — rollover at cap

When a pro user is at the APK cap, the new build is registered and the oldest existing build is auto-removed (FIFO by upload time). The upload succeeds (exit code `0`) and the CLI prints:

```
✓ v1.2.3 of com.your.app uploaded successfully.
Rolled over oldest build (v1.0.0) to stay at your plan cap.
```

Version uniqueness is still enforced — rollover never bypasses the duplicate-version check (exit code `5`). Rollover runs best-effort on the underlying storage blob: if the Storage delete fails, the Firestore record is still updated and a weekly sweep cleans up any orphans.

## How it works internally

1. `POST /v1/cli/upload-url` — requests a signed write URL from AppFlight
2. `PUT <signedUrl>` — streams the APK directly to Firebase Storage
3. `POST /v1/upload-apk` — registers metadata; AppFlight notifies testers

No APK bytes pass through the AppFlight server, so there is no file size ceiling.
