CMS Architecture¶
The content model is a tag graph split into three layers.
- The Content Library is what editors author — separate content types with distinct field shapes.
- The Editorial Structure is how editors curate — Themes as navigable worlds, Journeys as sequenced reading paths.
- The App Layer is what the backend computes on top — the tag-overlap graph, user data, and community reflections.
Bridging them is a polymorphic Content Item — any Passage / Letter / Essay / Podcast. This is what the graph traverses, what Journeys reference, and what users save. Separate storage, uniform traversal.
See the data-model plan for the design rationale and reconciliation narrative behind this structure. See the glossary for canonical term definitions.
Schema-level constraints:
- Accounts ship in V1; no monetization in V1. User identity exists for saves / Soul Map; the
tierfield ships now but tier-gated reads stay Phase 2.- Tag axes are load-bearing. Never drop tag dimensions without a recorded ADR — traversal queries depend on them.
- AI tagging is build-time only.
review_statusenforces editor QA on AI-drafted tags before publish.
Content Library¶
Content types are separate because their fields genuinely differ — plain text for audio sync (Passage), rich text with recipient/date (Letter), contributor + Substack source (Essay), external embed (Podcast). The Content Item bridge gives uniform traversal without merging storage.
Passage¶
Atomic unit. Always Lewis's words, always an excerpt. Primary interaction is cloned-voice audio. Body is plain text because word-by-word audio sync needs a clean string.
| Field | Type | Notes |
|---|---|---|
title |
string | Display label |
body |
plaintext | Lewis's exact words — no rich text (audio sync) |
content_type |
enum | letter / poem / nonfiction — distinguishes provenance within evergreen Lewis corpus |
source_context |
text | Contextual note about where in the source this excerpt comes from |
source |
ref → Work xor Letter/Essay | Source ladder anchor. Out-of-corpus → Work (purchase proxy); in-corpus → the Letter/Essay itself. Never both. |
themes |
refs → Theme · multi | Category axis (Imagination, Grief, Joy/Longing…) |
tags |
refs · multi | Faceted tags — dimensions TBD (candidate: motif, life-stage, register, tone-depth) |
thought_provoker |
ref → Prompt | Pre-reading lens; overrideable per Chapter |
audio_url |
url | ElevenLabs cloned-voice read-aloud (pre-generated, build-time). No Narnia audio (HarperCollins). Audio model TBD: stored URL vs stream-by-ID. |
featured_image |
media | AI-generated; client disclosure pending |
tier |
enum | free / premium — field ships now, gating logic Phase 2 |
front_door |
bool | Narnia entry-wedge flag |
review_status |
enum | draft → reviewed (AI drafts tags; editor QA before publish) |
Letter¶
Lewis's words, held in full. Can be the source of a Passage. In-app inclusion needs client confirmation (assumed yes, not validated).
| Field | Type | Notes |
|---|---|---|
title |
string | |
body |
richtext | Full text |
recipient |
string | Who Lewis wrote to |
original_date |
date | When Lewis wrote it |
cover_image |
media | |
themes |
refs → Theme · multi | |
tags |
refs · multi | |
thought_provoker |
ref → Prompt | |
tier |
enum | free / premium |
review_status |
enum | draft → reviewed |
Essay¶
Editorial piece by a named contemporary contributor — not Lewis. Sourced from Substack.
| Field | Type | Notes |
|---|---|---|
title |
string | |
subtitle |
string | |
body |
richtext | |
author |
ref → Author | Contributor (Malcolm Guite, etc.) |
featured_image |
media | |
publication_date |
date | |
substack_source_url |
url | Canonical Substack URL |
themes |
refs → Theme · multi | |
tags |
refs · multi | |
thought_provoker |
ref → Prompt | |
tier |
enum | free / premium |
review_status |
enum | draft → reviewed |
Podcast¶
Longer-form external audio — embedded, not CMS-hosted. Phase 1: schema stub only, not built.
| Field | Type | Notes |
|---|---|---|
title |
string | |
subtitle |
string | |
cover_image |
media | |
embed_link |
url | External audio source |
duration |
int | Seconds |
transcript |
text | Indexed for search |
themes |
refs → Theme · multi | |
tags |
refs · multi | |
thought_provoker |
ref → Prompt | |
review_status |
enum | draft → reviewed |
Work¶
Proxy for books not held in full. Drives the source ladder to purchase. Source reference only in V1 — no e-reader; cover, synopsis, publication date, future buy link.
| Field | Type | Notes |
|---|---|---|
title |
string | |
cover_image |
media | |
publication_year |
int | |
about |
richtext | Synopsis |
buy_link |
url | Bookstore affiliate handoff |
rights |
flag | e.g. HC audio excl — gates chapter audio availability |
in_app_readable |
bool | Always false at MVP |
Author¶
Standalone type for essay contributors. Photo drives the essay card. No detail page at MVP.
| Field | Type | Notes |
|---|---|---|
name |
string | |
photo |
media | |
bio |
text | Card-level; short |
Prompt¶
Standalone reflection/thought-provoker question. Reusable across pieces, cleanly overrideable per Chapter.
| Field | Type | Notes |
|---|---|---|
question_text |
text | The prompt shown to the user |
type |
enum | reflection / discussion / personal |
Worlds & Taxonomy¶
Theme¶
A navigable world — the portal. Each has its own landing page, bespoke colour palette, and portal imagery. Content pieces and Journeys are assigned to Themes via multi-select.
V1 working set (9 themes, may grow pre-launch): Imagination, Joy/Longing, Myth, Grief, Faith, Friendship, Wonder, Reason, Love. Architecture must support adding themes via CMS in v2 even if the v1 set is fixed.
| Field | Type | Notes |
|---|---|---|
title |
string | |
description |
richtext | |
portal_image |
ref → Portal Image | Bespoke portal imagery |
color_palette |
token | Design token — bespoke per theme; switching a theme recolours the UI |
content_blocks |
ordered refs | Editor-managed blocks driving the theme landing page |
featured |
bool | Surfaces on Home |
Portal Image¶
CMS-managed reusable image library. Editors pick one per Theme. Grows over time.
| Field | Type | Notes |
|---|---|---|
name |
string | Internal label |
image |
media |
Tags (deferred)¶
Granular, multi-dimensional taxonomy distinct from Themes. Powers fine-grained discovery + the graph. Facets TBD — candidate dimensions from the product model:
- Motif — cross-work, powers surprise jumps
- Life stage — youth → grief (single per piece)
- Register — bedtime / morning / anytime
- Tone depth — meaning (default) / theological (opt-in)
Tag taxonomy is an upstream content-design blocker: facets + controlled vocabulary must be locked before the CMS vocab tool, AI tagging, and the graph engine can be built.
Editorial Structure¶
Journey¶
Curated, sequenced reading experience. Belongs to one or more Themes. A Journey holds no content of its own — it's an ordered sequence of Chapters referencing existing library pieces. Same object as a "Portal" viewed as a doorway — don't model as two things.
| Field | Type | Notes |
|---|---|---|
name |
string | |
copy |
richtext | Description / overview |
colour |
token | |
icon |
media | |
portal_shape |
enum | From a preset list |
opening_question |
text | |
final_question |
text | |
featured |
bool | Surfaces on Home |
themes |
refs → Theme · multi | A Journey can span multiple Themes |
chapters |
ordered refs → Chapter |
Journey detail page shows: title, overview, chapter list, and length. Users can have multiple journeys in progress simultaneously; progress is gated behind account creation.
Chapter¶
One slice of a Journey. Groups pieces around a moment in the arc. References library content — holds no body of its own.
| Field | Type | Notes |
|---|---|---|
opening_question |
text | |
opening_passage |
ref → Content Item | Optional scene-setter |
primary_reading |
ref → Content Item | Required — the core piece |
supporting_content |
refs → Content Item · multi | Additional context |
prompt_override |
ref → Prompt | Overrides the piece's default thought provoker for this chapter |
order |
int | Sequence within the Journey |
Any content type can appear in a chapter.
Content Blocks¶
Editor-managed ordered blocks driving Theme landing pages and the Homepage. Not hardcoded layouts. Block types TBD — at minimum: featured Journey, featured piece, content grid.
Home is likely editorially curated (pending confirmation): hero slot always a passage/quote; below it, featured-theme + featured-journey carousels + a "What's new" module.
Content Item (polymorphic bridge)¶
The graph, Journeys, Saves, and Reflections all need to reference "any content piece" uniformly. Rather than merging storage (which breaks field-level type differences), a Content Item interface exposes the shared surface:
- Theme tags (multi)
- Faceted tags (multi)
- Thought provoker (Prompt ref)
- Bookmarking + reflections
- Review status
Implementation: multi-table inheritance, generic foreign key, or union view — decided at build time per the framework's strengths. The key invariant: one traversal interface, separate storage.
Tag-Overlap Graph (computed, not stored)¶
Nodes are Content Items; edges are computed from shared Tags + Themes. Two flavours:
- Further in — same thematic thread, going deeper
- Connects across — same theme in a different work or format
Tagging pipeline: AI drafts at ingest → editors QA before publish → graph recomputes on publish. No runtime AI in the content API path. No hand-built relations between pieces (except editor hero-pins).
At MVP, tag-overlap ranking runs on Postgres GIN array operators. A Ranker interface keeps the Meilisearch swap mechanical for Phase 2.
Related content surfacing is deferred (IA stand-up 2026-06-30) — the graph engine may still be built in V1, but its UI surface is deprioritized. Confirm scope before investing heavily.
User Data¶
User¶
Email/password account. Apple/Google Sign-In. Soft delete for App Store compliance (account deletion is a V1 deliverable). The app is fully usable as a guest; accounts persist Soul Map data.
Save (Soul Map entry)¶
| Field | Type | Notes |
|---|---|---|
user |
ref → User | |
content_item |
ref → Content Item | What was saved |
prompt_text |
text | Snapshot of the prompt shown — not a ref, because chapters can override the default prompt |
user_response |
text · nullable | The user's reflection |
saved_at |
datetime |
Journey Progress¶
| Field | Type | Notes |
|---|---|---|
user |
ref → User | |
journey |
ref → Journey | |
chapter |
ref → Chapter | Current chapter |
last_piece |
ref → Content Item | Resume point |
last_opened_at |
datetime |
Journey Completion¶
| Field | Type | Notes |
|---|---|---|
user |
ref → User | |
journey |
ref → Journey | |
completed_at |
datetime |
Reflection (community)¶
Moderated community note. "Publish, not post" — never appears as raw user input. Surfaced as ambient presence ("14 have sat with this"), not a comment thread.
| Field | Type | Notes |
|---|---|---|
content_item |
ref → Content Item | |
user |
ref → User · nullable | Nullable for pre-account reflections (if supported) |
body |
text | |
status |
enum | pending / published |
hearts |
int | Aggregate count |
Moderation queue UI is Phase 2.
Daily Drop¶
Scheduled "today's piece" pointer. Schema ships at MVP; scheduling UI is Phase 2.
| Field | Type | Notes |
|---|---|---|
drop_date |
date | |
content_item |
ref → Content Item | |
register |
enum | Time-of-day variant |
Relationships¶
| From | → To | Cardinality | Meaning |
|---|---|---|---|
| Passage | Work xor Letter/Essay | N → 1 | Source ladder (out-of-corpus → Work; in-corpus → source piece) |
| Essay | Author | N → 1 | Contributor |
| Content Item | Theme | N ↔ N | Multi-axis tagging |
| Content Item | Tags | N ↔ N | Faceted taxonomy |
| Content Item | Prompt | N → 1 | Thought provoker (overrideable per Chapter) |
| Content Item | Content Item | N ↔ N | "Related / go deeper" — computed from shared tags, not stored |
| Theme | Portal Image | N → 1 | Bespoke portal imagery |
| Theme | Content Blocks | 1 → N | Theme landing page layout |
| Journey | Theme | N ↔ N | A Journey can span multiple themes |
| Journey | Chapter | 1 → N | Ordered sequence |
| Chapter | Content Item | N → N | Opening passage + primary reading + supporting content |
| Chapter | Prompt | N → 1 | Prompt override (replaces piece's default) |
| Save | Content Item | N → 1 | Soul Map entry |
| Save | User | N → 1 | |
| Reflection | Content Item | N → 1 | Community note |
| Daily Drop | Content Item | N → 1 | Scheduled "today's piece" |
External Integrations¶
| System | Touches | Role |
|---|---|---|
| Substack | Essay.substack_source_url | Syndicates editorial into Essay via Celery polling |
| Bookstore (affiliate) | Work.buy_link | Purchase handoff from the source ladder |
| HarperCollins | Work.rights / Passage.audio_url | Rights holder; restricts Narnia audio |
| ElevenLabs | Passage.audio_url | Cloned-voice read-aloud (pre-generated at build time) |
Editor Workflow¶
- Ingest a piece (corpus excerpt, letter, essay, or editorial).
- AI drafts tags across all tag dimensions + themes.
- Editor QAs tags in CMS;
review_statusflipsdraft → reviewed. - Optional: editor attaches a thought-provoker Prompt (or one is AI-generated).
- Publish. Tag-overlap graph recomputes; the new node becomes traversable.
No runtime AI in the content API response path.
Open Items¶
- Tags faceted taxonomy — upstream content-design blocker; facets + controlled vocabulary must be locked before CMS build
- Passage audio model — stream-by-ID from voice provider vs stored pre-generated URL
- Separate types vs unified traversal — the Content Item synthesis (separate storage, uniform interface) is proposed but not formally blessed; see the data-model plan
- Content block library — block types for Theme landing pages + Homepage TBD
- Theme colour palette — CMS reference vs design-token enum
- Letter + Essay corpora — schemas are provisional until raw content is ingested and validated
- Letters in-app — needs client confirmation (assumed yes)
- Deeper meaning — likely deprioritized; client has no editorial team to QA AI interpretations
- AI-generated featured images — client AI-disclosure conversation pending
- How many Journeys ship at MVP — still open