# Upgrade

Source: https://docs.qiick.io/externa/upgrade

Stay on a newer Externa release after first install. **Code** comes from Git (or a Docker image tag). **`php artisan externa:upgrade`** runs migrate / permissions sync / cache clear by default — it does **not** pull code unless you pass **opt-in** `--sync-upstream`.

> **Related**
> [Packagist & create-project](https://docs.qiick.io/externa/packagist) · [Installation](https://docs.qiick.io/externa/installation) · [Quick Start](https://docs.qiick.io/externa/quick-start) · [Deploy with Docker](https://docs.qiick.io/externa/deploy-with-docker) · [Backup & restore](https://docs.qiick.io/externa/backup-restore) · [Deployment](https://docs.qiick.io/externa/deployment) · [Operations](https://docs.qiick.io/externa/operations) · [core README](https://github.com/qiick-io/externa-core#quick-start)

## Mental model (read this first)

| Fact                          | Meaning                                                                                                                                                                                              |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `composer create-project`     | **Install once.** Copies the app tree into a new directory. It is **not** how you receive later Externa releases.                                                                                    |
| Your git remote               | After you `git init` / push that tree to **your** repo, that remote tracks **your** commits — it does **not** auto-update from `qiick-io/externa-core`.                                              |
| Packagist `type: project`     | Same scaffold story: Composer does **not** replace your app on later `composer update`.                                                                                                              |
| Recommended update channel    | **Git:** clone (or add `upstream`) and checkout / merge a release tag `vX.Y.Z`.                                                                                                                      |
| `php artisan externa:upgrade` | Default: after code is already on the new release — `migrate --force`, `permissions:sync`, `optimize:clear`. **No** composer/npm. Opt-in: `--sync-upstream=vX.Y.Z` can merge the official tag first. |
| Docker                        | Pull / pin a newer **GHCR** image tag; keep named volumes; then migrate inside the container (or rely on `RUN_MIGRATIONS`).                                                                          |

> **create-project ≠ update channel**
> Operators who started with Packagist and then pushed to their own GitHub/GitLab still need a deliberate code update (git tag / `upstream` merge / `--sync-upstream`, or Docker image bump). Point them here from [Packagist](https://docs.qiick.io/externa/packagist), [Installation](https://docs.qiick.io/externa/installation), [Quick Start](https://docs.qiick.io/externa/quick-start), and the [core README](https://github.com/qiick-io/externa-core#quick-start).

> Upgrade the host PHP runtime to **8.4.27+** or **8.5.12+** (CVEs in PHP itself, not Externa — [GHSA-w8hw-w9fx-j79w](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w), [GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)). From externa-core **1.2.2**, Composer requires `php: ^8.4.27` ([#248](https://github.com/qiick-io/externa-core/pull/248)). After upgrading, **restart php-fpm** and verify CLI + FPM. Official `ghcr.io/qiick-io/externa-core:1.2.2` and `latest` ship PHP 8.4.27 with hardened `php.ini` — re-pull and recreate containers; source builds use `docker build --pull --no-cache`. Full notice: [discussion #247](https://github.com/qiick-io/externa-core/discussions/247). Keep `session.upload_progress` Off after patching — [Deployment — PHP runtime hardening](https://docs.qiick.io/externa/deployment#php-runtime-hardening).

## Version-specific notes

### 1.3.x → 1.4.0

> On PostgreSQL, 1.4.0 rebuilds the `collections_items_values` unique index with `NULLS NOT DISTINCT` ([#278](https://github.com/qiick-io/externa-core/issues/278)). That syntax needs **PostgreSQL 15 or newer**; on 14 the migration fails. Upgrade the database server first. The same migration **deletes older duplicate rows** for non-translatable values (it keeps the newest row per item / field / position), so take a backup before migrating. SQLite, MySQL, and MariaDB skip this migration. The bundled Compose files already use `postgres:16-alpine`.

#### Upgrade steps (source)

1. Back up the database and `storage/` — [Backup & restore](https://docs.qiick.io/externa/backup-restore).
2. Checkout / merge tag **`v1.4.0`** (or `php artisan externa:upgrade --sync-upstream=v1.4.0`).
3. `composer install --no-dev --optimize-autoloader` — new packages include `laravel/mcp`, `spatie/laravel-google-cloud-storage`, and `azure-oss/storage-blob-laravel`.
4. `npm ci && npm run build`
5. `php artisan externa:upgrade` (add `--force` in production) — runs `migrate --force`, `permissions:sync`, and `optimize:clear`.
6. Restart queue workers, scheduler, Horizon (and Reverb / Pulse if you run them). The scheduler now also runs `ai:prune-tool-audit` daily.
7. Review the behaviour changes below, then smoke-test login, one collection, the file manager, and AI chat.

**Docker:** pull `ghcr.io/qiick-io/externa-core:1.4.0`, recreate containers, and run `php artisan externa:upgrade --force` in the app container (or rely on `RUN_MIGRATIONS=true`, then run `php artisan permissions:sync`). See the `www-data` and `DB_PASSWORD` notes below.

#### New AI settings and removed packages

- **Assistant reasoning and context window** ([#570](https://github.com/qiick-io/externa-core/pull/570)): the Assistant now asks real providers for **medium** reasoning effort, which is slower and uses more tokens. Tune it or turn it off with `OPENAI_REASONING_EFFORT`, `ANTHROPIC_REASONING_EFFORT` and `GEMINI_REASONING_EFFORT` (`low` / `medium` / `high`; any other value or empty = off). `LOCAL_AI_REASONING_EFFORT` is unset (none) by default. `AI_ASSISTANT_PROVIDER` picks the Assistant's provider (default `local`), and `OPENAI_CONTEXT_WINDOW` / `ANTHROPIC_CONTEXT_WINDOW` / `GEMINI_CONTEXT_WINDOW` / `LOCAL_AI_CONTEXT_WINDOW` set the context size shown next to an answer's token usage — [AI configuration](https://docs.qiick.io/externa/ai-configuration#assistant-provider-reasoning-and-context-window-1-4-0). None of these need to be set; the defaults apply when they are missing.
- **Removed packages** ([#520](https://github.com/qiick-io/externa-core/pull/520), [#568](https://github.com/qiick-io/externa-core/pull/568)): `laravel/sail`, `@headlessui/react`, the unused `@radix-ui/react-*` packages (the UI imports from `radix-ui`) and the Wayfinder Vite plugin are gone. Run `composer install` and `npm ci` after pulling; if your fork imports any of them, add them back in your own `composer.json` / `package.json`.

#### What `externa:upgrade` now does for you

- **`files.effective_access` backfill** ([#186](https://github.com/qiick-io/externa-core/issues/186), [#434](https://github.com/qiick-io/externa-core/issues/434)): the migrations add and compute the denormalized access column for every existing file. No manual step. `php artisan files:backfill-effective-access` stays available and is safe to re-run. A file whose access was never computed is now **hidden** from roles without `read_private` instead of being treated as public.
- **API key owners** ([#215](https://github.com/qiick-io/externa-core/issues/215), [#439](https://github.com/qiick-io/externa-core/issues/439)): new `api_keys.owner_id`. Each existing key is owned by its creator when that user is still **active**; otherwise by the first active super-admin, then admin. Keys stay ownerless only when no admin exists. Effective access becomes key role ∩ key scopes ∩ owner permissions, so review keys that were re-owned. Keys are revoked when their owner is deactivated or deleted.
- **New admin permissions** ([#439](https://github.com/qiick-io/externa-core/issues/439)): `permissions:sync` creates `deploy.trigger` and `ai.prompts.manage` and grants **newly created** permissions to the built-in `admin` role. Permissions you removed from `admin` before stay removed. Grant them to other roles yourself — [Roles & permissions](https://docs.qiick.io/externa/roles-and-permissions#full-permission-enum-catalog).
- Other new tables and columns: `api_keys.files_scope` (existing keys → `all`), `api_keys.scopes` (null = no MCP access), `pending_tool_calls`, `ai_prompts`, and sort indexes on `collections_items`.

#### Behaviour changes

- **AI write tools are denied by default** ([#203](https://github.com/qiick-io/externa-core/issues/203)). With `AI_WRITE_TOOLS=false` (default), the assistant cannot create, update, delete, import, or roll back anything. To restore writes, set `AI_WRITE_TOOLS=true` and allowlist tool classes in `AI_TOOLS_WRITE_ALLOW` or **Settings → AI write tools**; each write then asks for approval in chat — [AI tool approvals & audit](https://docs.qiick.io/externa/ai-tool-approvals).
- **Chat keeps read actions** ([#439](https://github.com/qiick-io/externa-core/issues/439)): `ManageCollections`, `ManageCollectionItems`, `ManageFiles`, and the other `Manage*` tools still answer `list` / `get` / `search` in chat while writes are off. MCP lists `Manage*`-backed write tools only when the write gate allows them.
- **MCP server (opt-in)**: `/mcp` stays **404** until `AI_MCP_ENABLED=true`. Existing API keys have no `mcp:read` scope, so they cannot call MCP until you grant it — [MCP server](https://docs.qiick.io/externa/mcp-server).
- **Registration off by default** ([#272](https://github.com/qiick-io/externa-core/issues/272)): the `registration_enabled` default is now `false`. A value already saved in **Settings → Auth & Security** wins; installs that never saved it fall back to the new default and **close self-registration**. Re-enable it there if you rely on sign-up. Register, forgot-password, and reset-password are now throttled per IP (5 per minute), and the passkeys limiter is keyed on IP.
- **Webhook signatures and replay window** ([#273](https://github.com/qiick-io/externa-core/issues/273)): outbound webhooks add `X-Externa-Signature-V2` over `"{timestamp}.{body}"`; the legacy `X-Externa-Signature` stays for one minor — move receivers to V2 ([Outbound webhooks](https://docs.qiick.io/externa/webhooks)). **Breaking for inbound AI collection-import callers:** they must send `X-AI-Webhook-Timestamp` (within 300 s), sign `"{timestamp}.{body}"`, and never resend the same signature (replays are rejected) — [AI API](https://docs.qiick.io/externa/ai-api#webhook-hmac-auth).
- **Docker workers run as `www-data`** ([#320](https://github.com/qiick-io/externa-core/issues/320)): Horizon, Reverb, scheduler, Pulse, and queue workers drop from root via `setpriv`. Bind-mounted `storage/` or `bootstrap/cache` volumes created as root may need a one-time `chown -R www-data:www-data storage bootstrap/cache` (or a recreated volume). The entrypoint still `chown`s when it starts as root.
- **Docker refuses placeholder DB passwords in production** ([#272](https://github.com/qiick-io/externa-core/issues/272)): with `APP_ENV=production`, the entrypoint exits when `DB_PASSWORD` is empty, `change-me`, `changeme`, `password`, or `secret`.
- **Security headers on by default** ([#187](https://github.com/qiick-io/externa-core/issues/187)): admin pages now send `X-Frame-Options: DENY`, a CSP, `Referrer-Policy`, and `Permissions-Policy`. If you embed the admin in an iframe, load admin assets from another origin, or serve previews from a disk without a `url`, adjust `SECURITY_*` or set `SECURITY_CSP` — [Security headers](https://docs.qiick.io/externa/configuration#security-headers).
- **SSRF hardening** ([#274](https://github.com/qiick-io/externa-core/issues/274)): remote fetches deny CGNAT `100.64.0.0/10` and pin DNS; OIDC discovery and endpoints are now SSRF-checked. A lab IdP on a private or CGNAT host needs `OIDC_ALLOW_PRIVATE_HOSTS=true` — [SSO / OIDC](https://docs.qiick.io/externa/sso-oidc).
- **`APP_DEBUG=false`** is the new `.env.example` default. Existing `.env` files are not touched; keep it `false` in production.
- **API key `files_scope`** ([core#322](https://github.com/qiick-io/externa-core/issues/322)): new column `api_keys.files_scope` (`none` \| `read` \| `all`). Migration sets existing keys to **`all`** (same Files API behaviour as 1.3.0). New keys with `collection_slugs` default to **`none`**; unscoped keys default to `all` (or `read` when `read_only`). After upgrade, review scoped partner keys and tighten `files_scope` where least privilege is needed — [API abuse controls](https://docs.qiick.io/externa/api-abuse-controls#api-key-scopes).
- **Upload type policy** ([core#327](https://github.com/qiick-io/externa-core/issues/327) / [PR #362](https://github.com/qiick-io/externa-core/pull/362)): Settings → Files & Media gains optional extension/MIME allowlists and an **Allow SVG** opt-in (off by default). Empty allowlists keep the 1.3.0 `config/files.php` defaults. The hard floor (executables, HTML, SSI `.shtml`, etc.) still cannot be disabled. Review SVG only if you need logos/icons and accept XSS risk when SVG is served or inlined — [File manager — Upload security](https://docs.qiick.io/externa/file-manager#upload-security).

New in 1.4.0 (opt-in, no action needed): batch item writes, system-column and relational filters, `sort`, Live Preview `as_of`, deploy hooks, field conversion, GCS / Azure disks, theme presets. Env keys: [Environment variables](https://docs.qiick.io/externa/environment-variables). Full list: core `CHANGELOG.md`.

### 1.2.x → 1.3.0

#### Upgrade steps (source)

1. Checkout / merge tag **`v1.3.0`**.
2. `composer install`
3. `npm ci && npm run build`
4. `php artisan migrate` — adds `schedule_error` / `schedule_failed_at` on `collections_items` ([core PR #303](https://github.com/qiick-io/externa-core/pull/303))
5. Set **`AI_WEBHOOK_TOKEN`** (required — fail-closed in every environment)
6. `php artisan optimize:clear`
7. Restart queue workers / scheduler / Horizon (and Reverb / Pulse if you run them)

**Docker:** pull `ghcr.io/qiick-io/externa-core:1.3.0` (or `latest`). When building from a checkout, confirm `.dockerignore` excludes `/backups`, `/storage/app/*`, and agent tooling folders ([core PR #294](https://github.com/qiick-io/externa-core/pull/294)).

#### Behaviour / breaking notes

- **Breaking — outbound webhooks / Slack SSRF** ([core#118](https://github.com/qiick-io/externa-core/issues/118) / [PR #267](https://github.com/qiick-io/externa-core/pull/267)): loopback, RFC1918, link-local, and other reserved hosts are **rejected on save and at delivery** unless you opt in. Delivery also refuses HTTP redirects. For LAN receivers (e.g. n8n on a private IP), set `WEBHOOKS_ALLOW_PRIVATE_HOSTS=true` and refresh config cache. Public HTTPS endpoints need no change — [Outbound webhooks](https://docs.qiick.io/externa/webhooks#ssrf-guard).
- **Breaking — upload MIME allowlist** (same PR): after the extension denylist, uploads must sniff to a type in `config/files.php` `upload_allowed_mimes`, with magic-byte checks for claimed formats. **SVG, JSON, and XML are not allowed.** SSI/markup extensions (`shtml`, `xml`, …) are also denylisted ([PR #314](https://github.com/qiick-io/externa-core/pull/314)) — [File manager — Upload security](https://docs.qiick.io/externa/file-manager#upload-security).
- **AI webhook fail-closed + rotation** ([core#185](https://github.com/qiick-io/externa-core/issues/185) / [#207](https://github.com/qiick-io/externa-core/issues/207)): empty `AI_WEBHOOK_TOKEN` rejects requests in **every** environment (no local unsigned bypass). Rotate with `php artisan ai:rotate-webhook-token` (`AI_WEBHOOK_TOKEN_PREVIOUS` / `AI_WEBHOOK_TOKEN_PREVIOUS_EXPIRES_AT`); then `config:clear` / restart workers — [AI API](https://docs.qiick.io/externa/ai-api#rotate-the-webhook-token).
- **`TRUSTED_PROXIES` now applied** ([PR #295](https://github.com/qiick-io/externa-core/pull/295)): previously documented but unread. Set behind a reverse proxy and verify client IP / HTTPS / rate limits / API key IP allowlists — [Reverse proxy](https://docs.qiick.io/externa/reverse-proxy).
- **AuthZ** — only super-admins grant/manage super-admin principals ([PR #307](https://github.com/qiick-io/externa-core/pull/307)); collection grants fail closed for ACL-scoped admins ([PR #312](https://github.com/qiick-io/externa-core/pull/312)) — [AuthZ map](https://docs.qiick.io/externa/authz-map).
- **Partial item updates** — REST `PATCH` and GraphQL `updateItem` keep omitted fields/locales ([PR #293](https://github.com/qiick-io/externa-core/pull/293)).
- **OpenAPI in prod images** — `GET /api/v1/openapi.json` works with `composer install --no-dev` ([PR #310](https://github.com/qiick-io/externa-core/pull/310); `symfony/yaml` is a direct require).
- **UI bookmarks — project settings split** ([core#201](https://github.com/qiick-io/externa-core/issues/201) / [PR #304](https://github.com/qiick-io/externa-core/pull/304)): `/settings/project` is General only. Localization, Auth & Security (OIDC), Content (Live Preview), Files & Media, API, and Integrations each have their own path. Sidebar modules moved to `/settings/appearance/sidebar`. Deprecated test aliases (`project.webhook-test`, notifier tests) remain for one minor — prefer `project.integrations.*`. Guide: [Project settings](https://docs.qiick.io/externa/project-settings).
- Also in 1.3.0 (non-breaking defaults): optional API cost / GraphQL / payload caps ([core#113](https://github.com/qiick-io/externa-core/issues/113)), API key scopes including `?include=` / cache / Files `read_only` ([core#116](https://github.com/qiick-io/externa-core/issues/116) / [PR #321](https://github.com/qiick-io/externa-core/pull/321)), field type/translatable lock with stored values ([PR #265](https://github.com/qiick-io/externa-core/pull/265)). See core `CHANGELOG.md` `[1.3.0]`.

### 1.2.1 → 1.2.2

- Security patch ([#250](https://github.com/qiick-io/externa-core/pull/250)). Composer `php` constraint is now **`^8.4.27`** ([#248](https://github.com/qiick-io/externa-core/pull/248)). No new migrations. See the PHP security notice above and [discussion #247](https://github.com/qiick-io/externa-core/discussions/247) — do not treat older 8.4.x as enough.
- AI collection-import webhook: sign the **raw request body only** (`sha256=` + HMAC-SHA256). Invalid or missing signatures return **401** (was 403). Set `AI_WEBHOOK_TOKEN` in every non-local environment ([#214](https://github.com/qiick-io/externa-core/pull/214)) — [AI API](https://docs.qiick.io/externa/ai-api#webhook-hmac-auth).
- `externa:install` keeps an existing `APP_KEY` or generates one, runs the minimal profile without Redis, and can be re-run with `--force` ([#249](https://github.com/qiick-io/externa-core/pull/249)) — [Installation](https://docs.qiick.io/externa/installation#interactive-installer-externa-install).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.2`.

### 1.2.0 → 1.2.1

- Hotfix for field renames dropping values from pending drafts and older revisions ([#218](https://github.com/qiick-io/externa-core/issues/218), [#223](https://github.com/qiick-io/externa-core/pull/223)). No new migrations.
- **Right after deploying, before the scheduler runs or anyone publishes**, repair drafts/revisions that still hold a pre-rename key: `php artisan externa:repair-renamed-field-drafts` (dry run), then `--apply`. See [Schema changes](https://docs.qiick.io/externa/schema-changes#repairing-older-drafts).
- Run `npm ci && npm run build` (the `vite` range is now `^8.3.5`).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.1` (or jump straight to **1.2.2** if you are catching up).

### 1.0.x → 1.1.0

- **AI chat history schema** (`laravel/ai` ^1.0): conversations use a polymorphic participant, and messages store `steps` + `status` instead of `tool_calls` / `tool_results`. You **must** run `php artisan migrate` (included in `externa:upgrade`) before opening existing AI conversations. Back up first: [Backup & restore](https://docs.qiick.io/externa/backup-restore).
- Destructive AI tool actions (delete, force-delete, bulk delete, delete field, AI turn rollback) now wait for **Approve / Reject** in the chat.
- Scheduled publish: make sure `collections:process-schedules` runs on the scheduler (`php artisan schedule:work` or cron).

### 1.1.x → 1.2.0

- Run `composer install` (adds `laravel/socialite` `^5.31`), then `npm ci && npm run build`, **before** `externa:upgrade`.
- Migrations add editorial approval columns, `collections_items.board_sort` / `published_at`, and `users.oidc_sub`, and make **`users.password` nullable** (SSO-only users). Project settings also gain notifier mute list `notifier_event_types` (null = all events).
- Run `permissions:sync` (included in `externa:upgrade`) to create `can-submit-collections` / `can-approve-collections`.
- Restart Horizon and Reverb after deploy (presence, Meilisearch sync, and git-export jobs).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.0`.
- All new integrations (Meilisearch, git export, Slack/Telegram, OIDC) are off until configured — [Environment variables](https://docs.qiick.io/externa/environment-variables#optional-integrations-1-2-0).

## Before you start

1. **Backup** DB + `.env` + `storage/` — [Backup & restore](https://docs.qiick.io/externa/backup-restore).
2. Put the site in maintenance mode if operators share the host:

```bash
php artisan down
```

3. Note current version (`composer.json` → `version` or Git tag).

## Path A — Git tag / upstream (recommended)

### A1. You cloned `qiick-io/externa-core` (or keep `origin` = Externa)

```bash
cd /path/to/externa-core
git fetch --tags origin
git checkout v1.0.0   # example — use the target release tag
composer install --no-dev --optimize-autoloader
```

Then continue at [Shared steps](#shared-steps-after-code-is-on-the-new-release).

### A2. You create-project’d, then pushed to **your** remote

`origin` is yours. Add Externa as `upstream` and merge the release tag:

```bash
cd /path/to/my-externa
git remote add upstream https://github.com/qiick-io/externa-core.git   # once
git fetch --tags upstream
git merge v1.0.0   # example — resolve conflicts if you customized app code
composer install --no-dev --optimize-autoloader
```

Vanilla trees (few or no app edits) usually merge cleanly. Heavy forks: merge/rebase carefully, keep `.env` / `storage` / DB.

Or use the assisted CLI path below instead of the manual `git` steps.

Contributors on `develop`: merge/rebase onto the target tag or release branch, then the same composer / migrate / build steps.

## Assisted sync (`--sync-upstream`)

**Opt-in only.** Plain `externa:upgrade` still does **not** pull from GitHub. Use this when you want the Artisan command to fetch the official remote and merge a release tag, then run the usual migrate / permissions / cache steps.

### What it does

1. Requires a **git** working tree (fails clearly if not).
2. Ensures remote `upstream` → `https://github.com/qiick-io/externa-core.git` (adds it if missing).
3. `git fetch --tags upstream`, verifies `refs/tags/vX.Y.Z`.
4. Lists files that would change (`git diff --name-status HEAD vX.Y.Z`).
5. Asks for confirm (“N files will be overwritten / changed — proceed with caution?”) unless `--yes`.
6. `git merge --no-edit vX.Y.Z` — on conflict, **stops** (no migrate); resolve by hand.
7. On success (or if you skipped sync): migrate + `permissions:sync` + `optimize:clear`.

### Risks / when to use

- **May overwrite customized app files** and can fail on heavy forks. Prefer Path A by hand if you have large local diffs.
- Refuses a dirty tree unless `--force` (not recommended with sync).
- Does **not** run `composer` / `npm` — install deps after code lands.
- Prefer **dry-run first**.

```bash
# Preview only — no merge, no migrate
php artisan externa:upgrade --sync-upstream=v1.0.0 --dry-run

# Apply sync (confirm prompt) then migrate / permissions / cache
php artisan externa:upgrade --sync-upstream=v1.0.0

# Non-interactive apply (CI / scripts) — still fails on conflicts
php artisan externa:upgrade --sync-upstream=v1.0.0 --yes

# Production hosts
php artisan externa:upgrade --sync-upstream=v1.0.0 --force --yes
```

> **Warning**
> Assisted sync is a convenience for mostly-vanilla trees. Customized forks should merge carefully, keep `.env`, and never blind-commit conflict resolutions.

## AI-assisted upgrade

When merge conflicts or a large diff make a human review painful, paste context into your coding agent. **Keep `.env` and secrets out of commits.** Do **not** ask the agent to force-push or blind-commit without review.

### Template prompt (per version)

Copy and fill `vOLD` / `vNEW` (example: `v1.0.0` → `v1.1.0`):

```text
I am upgrading Externa (Laravel app from qiick-io/externa-core) from vOLD to vNEW.

Context to use (paste one or more):
1) GitHub compare: https://github.com/qiick-io/externa-core/compare/vOLD...vNEW
2) And/or CHANGELOG + release notes for vNEW
3) My conflicted / customized files (paste `git status` and the conflict hunks)

Goals:
- Resolve conflicts preserving my intentional customizations
- Keep .env, storage/, and local secrets untouched — never commit them
- Prefer upstream for framework/app defaults I did not customize
- After a clean tree: composer install, npm ci && npm run build, then
  `php artisan externa:upgrade` (or migrate + permissions:sync + optimize:clear)

Constraints:
- Do not blind-commit; show the plan and diffs first
- Do not rewrite git history or force-push
- Call out breaking changes from the compare/CHANGELOG
```

After the agent proposes a resolution: review, run tests / smoke checklist, then commit yourself.

## Path B — Replace tree (vanilla create-project, no useful git history)

When you never put the site under git, or history is disposable:

1. Backup `.env`, `storage/`, and the database.
2. `composer create-project qiick/externa-core:X.Y.Z /tmp/externa-new` (pin the target version).
3. Copy `.env` (and any local customizations) into the new tree; keep the same DB and storage volume/mount.
4. Run [Shared steps](#shared-steps-after-code-is-on-the-new-release) in the new tree, then cut over the web root.

Do **not** expect `composer update qiick/externa-core` to refresh application source — the package was copied once at create-project time.

## Path C — Docker (GHCR / Compose)

Keep named volumes (`externa_*_storage`, DB volumes). Bump the image tag, then bring the stack back up:

```bash
# Quick deploy sample — pin the release you want (example: 1.2.2)
EXTERNA_IMAGE=ghcr.io/qiick-io/externa-core:1.2.2 \
  docker compose -f compose.quick.yaml pull
EXTERNA_IMAGE=ghcr.io/qiick-io/externa-core:1.2.2 \
  docker compose -f compose.quick.yaml up -d
```

Build-from-source Compose: checkout / merge the matching tag in the repo that owns `compose.yaml`, then `docker compose … up --build -d`.

If `RUN_MIGRATIONS` is off, run migrate (+ permissions) in the app container:

```bash
docker compose exec app php artisan migrate --force
docker compose exec app php artisan permissions:sync
# or: docker compose exec app php artisan externa:upgrade --force
```

Details: [Deploy with Docker](https://docs.qiick.io/externa/deploy-with-docker) · volumes note below.

## Shared steps (after code is on the new release)

```bash
php artisan migrate --force
php artisan permissions:sync
# Only if PermissionEnum removed names and you intend cleanup:
# php artisan permissions:sync --prune

npm ci
npm run build

php artisan optimize:clear
# production hosts that cache config/routes:
# php artisan config:cache && php artisan route:cache && php artisan view:cache

php artisan up
```

Restart queue workers / Horizon / Reverb / `pulse:work` / scheduler after deploy so they load new code.

### `php artisan externa:upgrade`

Runs the **safe automated subset** by default: `migrate --force`, `permissions:sync`, `optimize:clear`.

```bash
php artisan externa:upgrade
# production or dirty git tree:
php artisan externa:upgrade --force
```

**Does not** (by default) fetch git tags, pull from GitHub, run `composer`, or run `npm`. Get the new code first (Path A/B/C), **or** use opt-in `--sync-upstream` above. Refuses `APP_ENV=production` and a dirty git working tree unless `--force`.

| Flag                                                        | Meaning                                                                                    |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `--sync-upstream=vX.Y.Z`                                    | Opt-in: fetch `upstream` + merge tag (see [Assisted sync](#assisted-sync---sync-upstream)) |
| `--dry-run`                                                 | With `--sync-upstream`: list files only; no merge / migrate                                |
| `--yes`                                                     | Skip confirm when applying `--sync-upstream`                                               |
| `--force`                                                   | Allow production and/or dirty tree                                                         |
| `--no-migrate` / `--no-permissions` / `--prune-permissions` | Tune the post-sync subset                                                                  |

## Docker volumes

With Compose (`compose.yaml` / `compose.prod.yaml` / `compose.quick.yaml`):

- Keep named volumes (`externa_storage` / `externa_prod_storage` / `externa_quick_*`, DB volumes).
- Rebuild or pull the app image after the tag change.
- Prefer `externa:upgrade --force` (or migrate + `permissions:sync`) inside the app container when entrypoint migrate is off.

## Smoke checklist

- [ ] `GET /health/ready` → 200
- [ ] Login as an operator
- [ ] Open one collection + one item
- [ ] Queue still processes (zip or a trivial job) if you rely on async features

## Non-goals

- Silent auto-pull as the **default** of `externa:upgrade` (opt-in `--sync-upstream` only)
- Remote zip overwrite without git
- Composer create-project auto-update magic
- Cross-engine DB moves (SQLite → Postgres) — dump/restore with engine tools separately
