Skip to content

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:

  1. Accounts ship in V1; no monetization in V1. User identity exists for saves / Soul Map; the tier field ships now but tier-gated reads stay Phase 2.
  2. Tag axes are load-bearing. Never drop tag dimensions without a recorded ADR — traversal queries depend on them.
  3. AI tagging is build-time only. review_status enforces 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 draftreviewed (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 draftreviewed

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 draftreviewed

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 draftreviewed

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

  1. Ingest a piece (corpus excerpt, letter, essay, or editorial).
  2. AI drafts tags across all tag dimensions + themes.
  3. Editor QAs tags in CMS; review_status flips draft → reviewed.
  4. Optional: editor attaches a thought-provoker Prompt (or one is AI-generated).
  5. 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