# 26.x → 27.0.0, entry by entry

**Two entries** — `CHANGELOG.md`'s 27.0.0 breaking entries in their order, grouped by
package, lowest tier first. No legacy path, no codemod, no shim. **Entry 2 can 404 a read your app
relies on**: find every un-scoped key a page links before the deploy (upgrade step 2). A later
slice appends its rows below the last one and never renumbers; each row's `CHANGELOG.md` line names
it as `(#N)` after the dash.

### The upgrade, top to bottom

| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every `@ultimat3/*` to the one new version, `bun install` | nothing yet — a mixed install is untested | — |
| 2 | **before the deploy:** `find apps -type f -exec grep -nE '/_storage/\|/media/\|signedUrl\(' {} +` — for each key that is NOT built with `scopedKey(orgId, …)` (no `org/<id>/` prefix) and that readers must still fetch, add its prefix to `defineStorage({ shared: { <disk>: ['<prefix>/'] } })`; leave everything else unshared | `404 X_STORAGE_KEY_UNSHARED` on that link | 2 |
| 3 | `bun run typecheck` — a hand-written `AsyncContext` or `Storage` object | TS2741 naming `exit` or `isShared` | 1, 2 |
| 4 | `x verify` | green, or a finding whose `fix:` is the edit | — |

### Entry by entry

Tier 0 — `@ultimat3/core` (1). Tier 1 — `@ultimat3/storage`, with `@ultimat3/cli`'s routes (2).

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `AsyncContext.exit(fn)` | you implement `AsyncContext` yourself rather than calling `asyncContext()`: TS2741. Add `exit: (fn) => fn()` for a context with no store, or your store's own `exit` |
| 2 | `GET /_storage/:disk/*key`, `GET /media/*key`, `StorageConfig.shared`, `Storage.isShared` | a page, island or client reads a key outside `org/<id>/` through either route — a brand asset on a disk, a shared upload. 26.x served it to every holder of `storage:read`; 27.0.0 answers `404 X_STORAGE_KEY_UNSHARED` unless `defineStorage({ disks, shared: { uploads: ['brand/'] } })` lists its prefix for that disk (whole segments ending in `/`, never `org/`, never the whole disk — `X_CONFIG_INVALID` at boot otherwise). Or move a truly public file to `apps/web/site/assets/` and name it with `asset()`. A hand-built `Storage` adds `isShared(disk, key)` (TS2741). Tenant-scoped reads are unchanged |
