ADR-0015: A journey chapter owns its passage¶
Status¶
Accepted
Date¶
2026-10-05
Context¶
Editors build a journey chapter around one passage. They write that passage's interpretation for the journey's theme, and they would rather write a new passage than search for an existing one, even when the same excerpt is already in the library. The client also wants readers to meet a journey's passages inside the journey, not on Home or a theme page first.
Until CSL-490 a passage was one shared row. Any chapter could link any passage, the same passage
could sit in several journeys, and every discovery surface (the passage list, the Home quote,
theme pages, related quotes) showed it. The app's API had to stay as it was: a chapter links to
/api/passages/{id}/, and bookmarks and reflections point at a Passage.
Decision Drivers¶
- Must keep a chapter's passage out of the main collection, both in the CMS and in the app, whatever state its journey is in.
- Must stop a chapter's passage being reused by another chapter or journey.
- Must keep the app API, bookmarks, reflections and the Audio Studio working unchanged.
- Should hold for writers that skip form validation, such as the journey importer.
Considered Options¶
Option A: hide a passage once a published journey uses it¶
The first plan for CSL-490: work it out at query time from published chapters.
- Pros: no new column, and the CMS stays as it is.
- Cons: the passage is still shared and reusable, and a draft journey's passages still show in the app. Product dropped this plan once editors said they make a new passage per chapter anyway.
Option B: work out "journey-only" from chapter links¶
- Pros: no new column.
- Cons: a live journey's unsaved chapter edits exist only in its latest revision, so a passage shows in the app until the journey is published. The chooser saves a passage before its chapter exists. A passage whose chapter is removed would come back into the app.
Option C: a separate journey-passage model¶
- Pros: the two kinds of passage can never be confused.
- Cons: it would duplicate everything a
Passagehas: revisions, the Audio Studio, the API payload, and the bookmark and reflection targets. That breaks the "API unchanged" driver.
Option D: a stored flag, plus database rules on the chapter¶
- Pros: one cheap filter on every discovery surface, and the same rows and endpoints as before. The database refuses a second chapter on a passage, whoever writes it.
- Cons: one more column to keep right. It is set once, when the passage is created, and a revision must never reset it.
Decision¶
We will use Option D:
Passage.journey_onlyis set when a passage is made for a chapter: by the chapter's create-only chooser, or by the journey importer. Editors cannot change it.JourneyChapter.passageaccepts journey passages only (limit_choices_to, checked by forms andfull_clean), and a database unique constraint lets one chapter at most hold each passage.Passage.with_content_jsonkeeps the row'sjourney_only. Revisions saved before the flag existed lack it, and loading one would otherwise reset it to False.- Discovery reads
readers.passages.discoverable_passages(). Passage detail still readspublished_passages(), so a chapter keeps opening its passage. - The CMS Passages list, the shared passage chooser, the duplicate-text check and the Book passage count cover the main collection only.
Writings and articles are not affected. They stay shareable between chapters.
Consequences¶
Positive¶
- The app API is unchanged. Bookmarks, reflections and audio stay on the same rows.
- The importer has no passage
linkand no duplicate decision step, which removed a whole flow.
Negative¶
- The same excerpt can exist twice: once in the main collection and once per chapter that uses it. That is the product intent.
- A passage whose chapter is removed or replaced stays in the database, hidden and unused. Nothing in the CMS lists these.
- Migration
0042copies a passage that several chapters shared, one copy per extra chapter. A copy has no Audio Studio narration, related quotes or revisions. Prod had no shared passages on 2026-10-05.
Risks & Mitigations¶
- Risk: a new discovery surface reads
published_passages()and leaks journey passages. → Mitigation:discoverable_passages()is the documented entry point, and the tests cover each surface. - Risk: a writer that skips validation puts a main-collection passage in a chapter. → Mitigation: the only such writer, the journey importer, always creates journey passages.
Related¶
- ADR-0008: content as snippets, and the three nullable piece FKs on
JourneyChapter - CSL-490, CSL-242 (journey import), CSL-247 (duplicate-text warning)