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.