diff --git a/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md b/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md
new file mode 100644
index 0000000..805d91b
--- /dev/null
+++ b/.planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md
@@ -0,0 +1,159 @@
+# Phase 5: Data layer full fidelity - Context
+
+**Gathered:** 2026-09-18
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Phase 5 ports the whole Płytarium data layer: all 25 models of `golem15.fonoteka` (plus the `golem15.user` tables they depend on) with matching table names, columns, indexes, defaults, relations, casts and lifecycle hooks, and a Go migration set whose final schema equals PHP's. Requirements DATA-03 to DATA-11 and CLI-03. Roadmap mode: mvp. The phase is security-load-bearing: mass-assignment boundaries and encrypted-at-rest credentials are named invariants in the PHP source, so the security-review agent runs on it and the DTO-vs-model convention applies from the first model.
+
+Two repos. `summercms.go` grows `lagoon` with what the models pull in: lifecycle hooks and transactional soft-delete cascade, relation helpers with ordered results and pivot models, `Fill` with the fillable allow-list, the hidden discipline, rule-string validation with the Laravel-shaped 422 map, casts (jsonable, money as fixed 4-decimal string, `lagoon.Encrypted`), the `system_files` attachment model with blob storage and thumbnails, the pagination envelope, the GORM callback registry for cross-plugin extension, and `migrate:rollback --plugin` hardening (CLI-03). It still knows nothing about Płytarium. `fonoteka.go` restructures its two flat Phase 3 plugins into the Winter directory layout and receives the models, the migration set, the write services' fill boundary and the tests.
+
+Not in this phase: any HTTP route, including the photo-upload endpoint (Phase 12, API-02), the guarded outbound fetch for cover URLs (HTTP-07), Discogs cover import (Phase 14), River jobs, Centrifugo broadcasting and Typesense indexing behind `Album`'s PHP traits (Phase 11), the user plugin's auth flows (Phase 7), the OAuth provider storage implementation (Phase 8; only the OAuth *models and tables* land here), admin form schemas (Phase 9), and the production data import (Phase 15).
+
+Known drift to correct in planning: the roadmap says "27 migrations"; `updates/version.yaml` now lists 38 PHP migration files across 26 versions. The count is not an acceptance number (see D-01).
+
+
+
+
+## Implementation Decisions
+
+### Migration granularity
+- **D-01:** Go migrations stay squashed per final-state table (P3 D-16 stands). One create migration per table, or per small tightly-coupled table group (e.g. OAuth tables, CSV import tables, the two Discogs credential tables), in FK order, each carrying a comment naming the PHP update files it folds. The PHP rename/flatten history (areas, items, locations, tags, item_categories) is not replayed. Success criterion 1 is read as: every Go migration runs up and down individually, and the final schema matches PHP's. The roadmap's "27" is not a target; the planner should fix the wording in ROADMAP.md/REQUIREMENTS.md DATA-09 when the phase is planned.
+- **D-02:** Schema equality is proven by an automated diff, not by reading. A normalized snapshot of the PHP app's final schema on Postgres (tables, columns, types, nullability, defaults, indexes, unique constraints, FKs) is produced once and committed in `fonoteka.go`; a testcontainers test migrates the Go sets up and diffs `information_schema`/`pg_catalog` against it. Intended differences live in a small, commented allow-list in the test. How the snapshot is produced (running the PHP migrations against a scratch Postgres vs. another route) is research; if the PHP migrations do not run cleanly on Postgres, research reports that before planning.
+- **D-03:** The Phase 3 minimal tables are widened by one appended ALTER migration per table (`widen_users`, `widen_collections`, `widen_albums`) placed right after the two shipped Phase 3 migrations, followed by the new create-table migrations. Shipped migrations are never edited (P3 D-17). Each widen migration has a real Rollback.
+- **D-04:** Reference data in the PHP history (the various-artist taxonomy seed, any settings defaults) is ported as idempotent data migrations inside the plugin's set, keyed by natural key, with a Rollback that removes them, same as the genres seed (P3 D-18). No seeder command.
+
+### Model DX: fillable, hidden, rules
+- **D-05:** Each model declares `Fillable() []string`, a mechanical copy of PHP `$fillable`. `lagoon.Fill` copies onto the model only names that are both requested by the caller and in the model's fillable list. Write services pass their own narrower list exactly as PHP does (`AlbumWriteService::FILL_FIELDS` is a strict subset of `Album::$fillable`: no `collection_id`, no `market_price_source`). The model list is the backstop, the service list is the real boundary. `json.Unmarshal` into a GORM model is never used on a write path (PITFALLS Pitfall 3).
+- **D-06:** Non-fillable and unknown keys are silently ignored, as Eloquent does; they are never persisted and never produce a 422. In non-production environments the dropped keys are logged once per call site.
+- **D-07:** Criterion 3 is proven at service level in this phase: the fill boundary of the PHP write services is ported now (at minimum Album, Collection and the four credential models) and fuzzed against real Postgres with random extra and server-owned keys, asserting nothing outside the list is persisted. `lagoon.Fill` has its own framework-level fuzz test on a fixture model. The HTTP-level fuzz over request DTOs is Phase 12's job; criterion 3's "every write endpoint" clause moves there and the planner notes it in the roadmap.
+- **D-08:** Hidden columns carry `json:"-"` on the GORM model so an accidental marshal cannot leak them, and models also declare `Hidden() []string` (for the Phase 9 admin schema and for a lagoon test that marshals every registered model and asserts hidden names are absent). API payloads are not produced by marshaling models: they come from ported `Serialize*` functions, as PHP's `SerializesFonoteka` does. The "explicit per-call override" is a named, greppable function (`Reveal`-style), not a serializer option.
+- **D-09:** Models declare `Rules() map[string]string` with the PHP rule strings copied verbatim. `lagoon` translates the Laravel rule grammar Płytarium uses (required, nullable, integer, numeric, between, min, max, in, unique:table, plus whatever research finds in the 25 models) onto go-playground/validator, runs it in a before-save callback, and returns the Laravel-shaped 422 error map with messages translated through `phrasebook`. `unique:table` is a DB check in the same step and must respect soft deletes the way Winter's does. An untranslatable rule fails loudly at boot or test time, never silently passes.
+
+### Encrypted cast
+- **D-10:** The Go cast reads and writes only its own AES-256-GCM format. Laravel CBC payloads are never on the live read path. Phase 5 ships a small, tested helper that decrypts a Laravel `encrypted` payload (base64 JSON with iv/value/mac, AES-256-CBC + HMAC under `APP_KEY`) for the Phase 15 cutover import to re-encrypt; it is not wired into the cast.
+- **D-11:** The key is `app.key` in config (`SUMMER_APP__KEY`), 32 bytes base64. Missing, short or undecodable key fails boot with no default, same rule as the JWT secret (P3 D-11). The column-encryption key is derived from it with HKDF and a fixed label so `app.key` can serve other purposes later without key reuse. A `summer key:generate` command prints a fresh key.
+- **D-12:** Ciphertext is versioned: a format/key-id prefix, nonce, ciphertext+tag. Config accepts `app.previous_keys` as decrypt-only keys (Laravel's `APP_PREVIOUS_KEYS` equivalent). No re-encrypt command in this phase.
+- **D-13:** Encrypted columns are typed `lagoon.Encrypted` on the model: Scanner/Valuer decrypt on read and encrypt on write; `MarshalJSON`, `String()` and `GoString()` always emit a redaction; plaintext is only reachable through an explicit `.Reveal()`. Applies to `api_key` on `UserAiCredential` and `OrgAiCredential` and to the secret columns of `UserDiscogsCredential` and `OrgDiscogsCredential`; research lists the exact columns from the PHP `$casts`.
+
+### Attachments (DATA-08)
+- **D-14:** Attachments use Winter's `system_files` table shape and name (`disk_name`, `file_name`, `file_size`, `content_type`, `title`, `description`, `field`, `attachment_id`, `attachment_type`, `is_public`, `sort_order`, timestamps). `attachment_type` keeps the PHP class string (`Golem15\Fonoteka\Models\Album`) through a per-model morph name, so cutover copies rows and files verbatim and later Winter ports reuse it. The table and `File` model are framework-owned (`lagoon`) with their own migration set that runs before plugin sets.
+- **D-15:** Full storage scope lands in this phase (user chose the largest option over the recommended table-only cut): `gocloud.dev/blob` is wired now, files are stored and deleted through it, and thumbnails are generated. Only the HTTP upload endpoint stays in Phase 12. Planner should size the phase accordingly.
+- **D-16:** v1 bucket is `fileblob` rooted at the same `storage/app/uploads` tree PHP uses, with bucket URL and public path prefix from a `storage.uploads.*` config section mirroring `cms.php`'s `storage.uploads`. Winter's `disk_name` partition path (`public/xxx/yyy/zzz/`) and the relative public URL shape (`relativeMediaUrl`) are reproduced exactly. The framework serves public files with a static handler at the configured path; S3/GCS is a config change later. `memblob` in unit tests.
+- **D-17:** `Thumb(w, h, mode)` reproduces Winter's thumb filename and location (next to the original, `thumb____0_0_.`; research confirms the exact rule from Winter Storm's `File` model) and generates lazily on first call through blob when the thumb is missing. Thumbs copied at cutover are reused, and `thumb_url` stays byte-identical for the parity diff. Pixel equality with PHP GD is not a goal.
+- **D-18:** The resize library is research's pick, recorded in RESEARCH.md as the phase decision naming the dependency: pure Go, maintained, no cgo/libvips. stdlib `image/*` plus `golang.org/x/image/draw` is the baseline to beat.
+- **D-19:** Deletion follows Winter: soft-deleting an owner keeps `system_files` rows and blobs; force delete removes rows inside the delete transaction and removes originals and thumbs from blob after commit. This runs through the same lifecycle-callback mechanism as the other cascades (DATA-03), not a special case.
+
+### Claude's Discretion
+- Lifecycle hook naming and wiring (Winter names `BeforeValidate`/`BeforeCreate`/`BeforeSave`/`BeforeDelete`/`AfterDelete` as optional interfaces vs. GORM's own hook names), the base model embed, and how soft-delete cascade runs in one transaction.
+- Pivot model shape for `album_artists.sort_order` and `CollectionEditor` (GORM `SetupJoinTable` vs explicit has-many-through), as long as the 3+ artist ordering round-trip and the business columns hold.
+- Money cast type and API: a named string-backed type in `models/` (or `lagoon` if generic) that reproduces `MarketPriceCast::get` (null for null/blank/non-numeric, else 4-decimal string) and leaves normalisation to the `Album` before-save hook, as PHP does. Never `float64` in the JSON path.
+- Jsonable cast shape, including `[]` vs `null` behavior per column (Pitfall 4).
+- Pagination helper API; the envelope is fixed: `{data, meta{current_page,last_page,per_page,total}}`, no `links`.
+- Callback-registry API surface (ARCHITECTURE.md Pattern 3b) and which plugin/model pair demonstrates criterion 5's cross-plugin extension plus companion migration; a fixture plugin in tests is acceptable if no real Płytarium case fits.
+- Soft-delete + unique strategy (partial unique indexes vs. matching PHP's actual constraints); D-02's schema diff decides what "matching" means, the delete-then-recreate test must pass for every soft-deletable uniquely-keyed table.
+- Which `Serialize*` functions are ported now (only those needed by this phase's tests) vs. Phase 12.
+- `Album`'s service-calling hook (`ArtistResolver` in `beforeSave`) becomes a callback registered from `classes/`, per the layout note; Scout/Broadcastable traits are left as seams for Phase 11.
+- CLI-03 details beyond what Phase 3 shipped.
+- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan" rules. Given D-15, expect more than the usual number of plans.
+
+### Folded Todos
+- **Verify the models-leaf layout rule against keios.eu and a jz/pxpx plugin before Phase 5** (`.planning/todos/pending/verify-models-leaf-rule.md`). Runs as the first task of the phase, before 24 models are laid out under the rule: repeat the `models/ → classes/` (and any sibling) `use`-edge probe on one keios.eu stack plugin with models and services (`/media/nvme/dev/golem15/keios.eu/plugins/golem15/user` or `paymentgateway`), one jz plugin and one pxpx plugin; classify each edge as cast / service-calling hook / other. Any "other" edge means `.planning/notes/plugin-layout-winter-directories.md` is amended before the bulk port proceeds.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### The PHP data layer being ported
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/*.php` — the 25 models: `$table`, `$fillable`, `$guarded`, `$hidden`, `$casts`, `$jsonable`, `$dates`, `$rules`, relations, hooks. `Album.php` is the densest (constants, money cast, jsonable, soft delete, belongsToMany artists, attachMany photos, `beforeSave` with `ArtistResolver`).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/version.yaml` and `updates/v*/` — 38 migration files across 26 versions; the history D-01 squashes and the seeds D-04 ports.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/casts/MarketPriceCast.php` — money cast semantics and the docblock explaining the blank-string case.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/AlbumWriteService.php` — `FILL_FIELDS` and the fill/fillEmpty/overwrite boundary that D-05 and D-07 port and fuzz.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/UserAiCredential.php`, `OrgAiCredential.php`, `UserDiscogsCredential.php`, `OrgDiscogsCredential.php` — `encrypted` casts and `$hidden` (D-10 to D-13).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php` — payload builders; lines ~190–215 for the photo payload, `getThumb(200, 200, ['mode' => 'crop'])` and `relativeMediaUrl` (D-08, D-16, D-17).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Collection.php` lines ~91–107 — `attachMany photos` (ordered) and public `attachOne image`.
+- `/media/nvme/dev/golem15/fonoteka/config/cms.php` lines ~300–335 — `storage.uploads` / `media` / `resized` disks and paths (D-16).
+- Winter Storm `File` model and `Winter\Storm\Database\Attach\File` in `/media/nvme/dev/golem15/fonoteka/vendor/winter/storm` — `disk_name` partitioning, `getPath`, `getThumb` naming, delete behavior (D-14, D-17, D-19).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/user/models/` and its `updates/` — the user-plugin tables that `widen_users` must match.
+
+### Go code this phase builds on
+- `lagoon/connection.go`, `lagoon/migrations.go`, `lagoon/commands.go`, `lagoon/order.go` — shared `*sql.DB`, per-plugin gormigrate runner, `migrate:*` commands, Polish order helper.
+- `pact/capabilities.go` — `HasModels`/`HasMigrations` and friends.
+- `../fonoteka.go/plugins/golem15/fonoteka/migrations.go` and `../fonoteka.go/plugins/golem15/user/migrations.go` — the shipped, never-edited Phase 3 migrations (`202609170001_create_schema`, `202609170002_seed_genres`) that D-03 appends to.
+- `../fonoteka.go/plugins/golem15/fonoteka/*.go` and `../fonoteka.go/plugins/golem15/user/*.go` — the flat Phase 3 plugins to restructure into the Winter layout.
+- `internal/build/` — the `models/`-is-a-leaf enforcement shipped in Phase 4 (P4 D-11) and the `make:model`/`make:migration` stubs that should match what this phase hand-writes.
+
+### Layout and architecture decisions
+- `.planning/notes/plugin-layout-winter-directories.md` — directory mapping and the models-leaf rule (casts into `models/`, service-calling hooks out to `classes/`).
+- `.planning/todos/pending/verify-models-leaf-rule.md` — the folded todo with its probe commands.
+- `.planning/research/ARCHITECTURE.md` §Pattern 3 (3a events, 3b GORM callback registry, 3c companion structs over shared tables), §Migration Flow, §Anti-Pattern 4. Text that says goose is superseded by gormigrate.
+- `.planning/research/PITFALLS.md` §Pitfall 3 (DTO vs model), §Pitfall 4 (nil vs `[]`), §Pitfall 5 (float vs fixed-decimal string), §Pitfall 7 (tri-state bool), §Pitfall 11 (soft delete + unique), §Pitfall 12 (pivot columns), §Pitfall 13 (no AutoMigrate), §Security Mistakes, §Integration Gotchas row on gocloud.dev/blob URL shape.
+- `.planning/research/STACK.md` §GORM: plain structs, §Migration tooling, §Version Compatibility (gorm v1.31.2, validator v10.30.4, gocloud.dev v0.46.0, testcontainers v0.44.0).
+- `.planning/research/FEATURES.md` — model-feature inventory from the PHP source.
+
+### Prior phase decisions that bind this phase
+- `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md` D-02, D-06, D-11, D-16, D-17, D-18, D-19.
+- `.planning/phases/04-cli-scaffolding-i18n-and-mail/04-CONTEXT.md` D-10, D-11, D-12, D-13.
+- `.planning/phases/01-framework-kernel-foundation/01-CONTEXT.md` D-06, D-08, D-10 (config namespaces, env mapping, fail-fast boot).
+- `.planning/PROJECT.md` §Constraints, §Key Decisions; `.planning/REQUIREMENTS.md` DATA-03 to DATA-11, CLI-03; `.planning/ROADMAP.md` Phase 5 section.
+- `CLAUDE.md` (repo root) §GSD workflow rules.
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `lagoon`: connection on the shared pgx-stdlib `*sql.DB`, per-plugin gormigrate runner with per-plugin history tables, `migrate`, `migrate:rollback --plugin`, `migrate:status`, the `pl-PL` ICU order helper and its testcontainers Postgres test harness.
+- `phrasebook` (Phase 4) for translated validation messages; `compass` for `app.key`, `app.previous_keys` and `storage.uploads.*`; `backpack` to publish the blob bucket and file service; `bonfire` for `key:generate`.
+- `internal/build` leaf-import check and `make:model`/`make:migration` stubs.
+- `fonoteka.go/parity` testcontainers `TestMain`: the place the schema-diff test and the service-level fuzz tests can share a Postgres with.
+
+### Established Patterns
+- No AutoMigrate anywhere, tests included; shipped migrations are never edited; each plugin's set rolls back without touching another's history.
+- Fail boot loudly on misconfiguration (now also: bad `app.key`, untranslatable rule string, unconfigured uploads bucket).
+- Response DTOs and explicit serializers, never the GORM model; `[]` not `null`; no generic envelope.
+- Framework never imports the app; anything Płytarium-specific (money cast semantics, write services, morph names) lives in `fonoteka.go`.
+- New dependencies need naming: `gocloud.dev` is already decided; the image resize library is named by this phase's research (D-18). Nothing else without a decision note.
+
+### Integration Points
+- The Phase 3 plugins in `fonoteka.go` are flat single packages; this phase moves them to `models/`, `classes/`, `controllers/`, `middleware/`, `updates/` with `plugin.go`/`routes.go` at the root, keeping the genres route and its parity fixture green throughout.
+- The framework's `system_files` migration set must order before every plugin set.
+- `Album`'s Scout and Broadcastable behavior are seams only; Phase 11 attaches them through the callback registry built here.
+- OAuth models and tables land here; the zitadel/oidc storage that uses them is Phase 8.
+
+
+
+
+## Specific Ideas
+
+- PHP's real mass-assignment boundary is two-layered: `$fillable` as backstop, a narrower per-service list as the working boundary. The Go port keeps both layers rather than collapsing them into DTO shape alone, so an agent can diff `Fillable()` against the PHP array line by line.
+- Rule strings, fillable lists and hidden lists are copied verbatim from PHP so conversion stays mechanical; translation to Go idiom happens once, inside `lagoon`.
+- Secrets should be unable to leak by accident: `json:"-"`, a redacting `lagoon.Encrypted`, and explicit `Reveal` calls the security review can grep.
+- `system_files` and the uploads tree stay Winter-shaped so cutover is a copy, and so the next Winter project port gets attachments for free.
+- The user chose to take blob storage and thumbnails in this phase despite the smaller recommended cut; treat it as a deliberate choice, not scope creep, and plan capacity for it.
+
+
+
+
+## Deferred Ideas
+
+- HTTP-level fuzz of request DTOs on every write endpoint — Phase 12/13 when the routes exist (second half of criterion 3).
+- Photo upload endpoint, manual cover URL and Discogs cover import — Phase 12 (API-02), HTTP-07, Phase 14.
+- Re-encrypt command for key rotation — after v1; the versioned format (D-12) keeps it possible.
+- Cutover import that decrypts Laravel payloads and re-encrypts, and copies `system_files` rows and the uploads tree — Phase 15, using the helper from D-10.
+- S3/GCS bucket for uploads — config change after cutover.
+- Typesense indexing and Centrifugo broadcasting hooks on `Album` — Phase 11.
+- Correcting the "27 migrations" wording in ROADMAP.md and REQUIREMENTS.md DATA-09, and moving criterion 3's endpoint clause — do at plan time for this phase.
+
+
+
+---
+
+*Phase: 5-Data layer full fidelity*
+*Context gathered: 2026-09-18*
diff --git a/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md b/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md
new file mode 100644
index 0000000..ce02123
--- /dev/null
+++ b/.planning/phases/05-data-layer-full-fidelity/05-DISCUSSION-LOG.md
@@ -0,0 +1,194 @@
+# Phase 5: Data layer full fidelity - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-09-18
+**Phase:** 5-Data layer full fidelity
+**Areas discussed:** Todo folding, Migration granularity, Model DX (fillable, hidden, rules), Encrypted cast compatibility, Attachments depth (DATA-08)
+
+---
+
+## Todo folding
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Fold in as first task | Run the models-leaf probe on keios.eu, jz and pxpx plugins at the start of Phase 5; amend the note if an "other" edge appears | ✓ |
+| Keep it separate | Leave as a pending todo; apply the rule on Fonoteka evidence only | |
+
+**User's choice:** Fold in as first task
+
+---
+
+## Migration granularity
+
+Context raised: P3 D-16 (squash per table) conflicts with roadmap criterion 1 ("all 27 migrations run up and down individually"); `version.yaml` actually lists 38 files across 26 versions.
+
+### Relation to PHP history
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Squash per table | Keep P3 D-16; one create migration per final-state table, commented with folded PHP files; reword criterion 1 | ✓ |
+| Mirror PHP one-to-one | One Go migration per PHP file including renames and the items-to-albums flatten | |
+| Squash per PHP version | One Go migration per version.yaml entry with a final-state effect | |
+
+### Proving schema equality
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Schema-diff test vs PHP dump | Committed normalized snapshot of PHP's final Postgres schema; testcontainers test diffs information_schema with a documented allow-list | ✓ |
+| Hand-audited checklist | Reviewer reads migration comments against PHP | |
+| You decide | Researcher picks after checking PHP migrations on Postgres | |
+
+### Widening the Phase 3 tables
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| One widen migration per table | widen_albums, widen_collections, widen_users after the Phase 3 pair | ✓ |
+| Single "phase 5 widen" migration | One ALTER migration for all three | |
+| You decide | Planner orders by FK needs | |
+
+### Data seeds
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Idempotent data migrations | Same as genres seed (P3 D-18) | ✓ |
+| Separate seed command | Schema-only migrations plus `fonoteka:seed` | |
+
+---
+
+## Model DX: fillable, hidden, rules
+
+Context raised: PHP API never serializes models via `toArray()` (`SerializesFonoteka` builds payloads); `AlbumWriteService::FILL_FIELDS` is a strict subset of `Album::$fillable`.
+
+### Declaring fillable
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Declared list + lagoon.Fill | `Fillable() []string` copied from PHP; `lagoon.Fill` intersects requested and fillable names; services pass narrower lists | ✓ |
+| Struct tags | `summer:"fillable"` tags read by reflection | |
+| Hand-written DTO copy only | No framework concept; DTO shape is the only boundary | |
+
+### Unknown / non-fillable keys
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Silently ignored, as PHP | Dropped, never persisted; logged once in non-production | ✓ |
+| Reject with 422 | Stricter than PHP; changes the contract | |
+
+### Proving criterion 3 without write routes
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Service-level fuzz now | Port write services' fill boundary and fuzz against Postgres; HTTP fuzz in Phase 12 | ✓ |
+| One real write route now | Pull a write route forward as the DTO reference | |
+| Framework-only test | Fuzz lagoon.Fill on a fixture model only | |
+
+### Hidden
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| json:"-" + explicit serializers | `json:"-"` on secrets, `Hidden()` list, marshal test, named Reveal override | ✓ |
+| Generic lagoon.Serialize(model, opts) | Reflection serializer with WithVisible option | |
+
+### Rules
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Rules() map on model, validated on save | PHP strings verbatim; lagoon translates to validator; Laravel-shaped 422 | ✓ |
+| validate struct tags | Hand-translated go-playground tags per field | |
+| You decide | Researcher evaluates grammar coverage first | |
+
+---
+
+## Encrypted cast compatibility
+
+### Existing Laravel ciphertext
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| GCM only; re-encrypt at import | Cast handles only AES-GCM; ship a Laravel decrypt helper for the Phase 15 import | ✓ |
+| Cast reads both, writes GCM | CBC decrypt on the live read path, lazy re-encrypt | |
+| No migration of secrets | Users re-enter keys after cutover | |
+
+### Key source
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| app.key, fail boot if missing | 32-byte base64, HKDF-derived column key, `summer key:generate` | ✓ |
+| Dedicated database.encryption_key | Separate secret for column encryption | |
+
+### Rotation
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Versioned format + previous keys | Key-id prefix; `app.previous_keys` decrypt-only | ✓ |
+| Single key, no version prefix | Simplest; rotation needs a format change later | |
+
+### Go type
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| lagoon.Encrypted type | Scanner/Valuer; redacting MarshalJSON/String; explicit Reveal() | ✓ |
+| Plain string + serializer tag | `gorm:"serializer:encrypted"` on a string | |
+
+---
+
+## Attachments depth (DATA-08)
+
+### Table
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Winter's system_files shape | Same table and columns, PHP class strings as morph names, framework-owned | ✓ |
+| New clean table | Go-native `summer_files` | |
+
+### Storage depth
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Table, relations and path/URL math only | Recommended cut; blob and thumbs later | |
+| Full blob storage now | gocloud.dev/blob in this phase, thumbs later | |
+| Everything incl. thumbnails | Blob storage plus 200x200 crop thumbs | ✓ |
+
+**Notes:** User picked the largest scope over the recommendation. HTTP upload endpoint remains Phase 12.
+
+### Thumb shape
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Same name and path, lazy on first call | Winter's thumb filename next to the original, generated when missing | ✓ |
+| Same name, eager at attach time | Generate declared sizes on attach | |
+| You decide | Researcher confirms Winter's rules first | |
+
+### Image library
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| stdlib image + x/image/draw | No cgo, quasi-stdlib | |
+| disintegration/imaging | Convenient, unmaintained since 2020 | |
+| Researcher picks | Maintained pure-Go option recorded in RESEARCH.md; no cgo | ✓ |
+
+### Bucket and serving
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| fileblob at storage/app, config-driven URL, Go static handler | Mirrors cms.php storage.uploads; memblob in tests | ✓ |
+| Same, no Go static handler | Reverse proxy serves files | |
+
+### Delete behavior
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| As Winter | Soft delete keeps files; force delete removes rows in-tx and blobs after commit | ✓ |
+| Never delete blobs automatically | Prune command later | |
+
+---
+
+## Claude's Discretion
+
+Hook naming and base model embed; pivot model shape; money cast type; jsonable cast shape; pagination helper API; callback-registry API and the cross-plugin extension demo pair; soft-delete + unique index strategy; which Serialize* functions are ported now; CLI-03 details; plan count and split.
+
+## Deferred Ideas
+
+HTTP-level DTO fuzz (Phase 12/13); upload endpoint, manual cover URL, Discogs cover import (Phase 12/14); re-encrypt command; cutover import of secrets and files (Phase 15); S3/GCS bucket; Typesense and Centrifugo hooks on Album (Phase 11); roadmap wording fixes ("27 migrations", criterion 3 endpoint clause) at plan time.