Skip to content

Data model

This page explains the collections behind the book-sharing system and the role each one plays.

The model is intentionally split into catalog data, member activity, and interaction data. That separation reduces duplication and makes it easier to apply different rules to each layer.

Core collections

Collection Purpose Key fields Notes
authors Canonical author records name, slug, bio, photo, status Linked to books through book_authors
books Canonical book records title, slug, description, isbn, status Main public content target for reading journeys and book interactions
book_authors Junction table for book-author links books_id, authors_id, sort M2M bridge between books and authors
reading_journeys A member's longer-running reading record title, slug, status, visibility, book, user Tied to one book and one user
reading_shares A member's share post about reading activity slug, status, visibility, content, book, reading_journey, user Can exist with or without a linked journey
reading_share_images Junction table for share images reading_share_id, directus_files_id, sort M2M bridge between shares and uploaded files

Why the core model looks like this

  • authors and books are canonical records because the same book should not be recreated for every member.
  • book_authors exists as a junction because books can have multiple authors, and authors can write multiple books.
  • reading_journeys exists separately from reading_shares because long-lived progress tracking and short-form publishing are different behaviors.
  • reading_share_images is kept separate so a share can have many images without bloating the main share record.

Interaction collections

Collection Type Target field Owner field Main fields
reading_share_comments Comment reading_share user status, visibility, content
reading_share_reactions Reaction reading_share user date_created
reading_journey_comments Comment reading_journey user status, content
reading_journey_reactions Reaction reading_journey user date_created
book_comments Comment book user status, content
book_reactions Reaction book user date_created

Why interaction data is modeled separately

  • Comments are their own collections because they need moderation fields and their own ownership rules.
  • Reactions are their own collections because they are conceptually link rows, not embedded counters.
  • Book, journey, and share interactions are split by target type because each target has different visibility and write rules.

This is a little more verbose than a single polymorphic interaction table, but it is much easier to understand and safer to validate in Directus.

Relationship patterns

Pattern Example Why it matters
One book to many journeys reading_journeys.book -> books A journey is always about one book
One book to many shares reading_shares.book -> books A share always points back to one book
One journey to many shares reading_shares.reading_journey -> reading_journeys A share can be part of a longer reading journey
One target to many comments book_comments.book -> books Comments stay close to a single content target
One target to many reactions reading_share_reactions.reading_share -> reading_shares Reactions are simple link rows, not rich content
Many books to many authors books <-> authors via book_authors Supports co-authored works cleanly

These relationship patterns reflect how people actually use the system.

  • A member reads one specific book in one journey.
  • A share is usually about one reading moment.
  • Comments and reactions need an unambiguous target.

The model favors clarity over cleverness. That makes downstream queries, moderation, and UI work simpler.

Ownership and audit fields

There are two separate concepts in play.

Field family Meaning Used in
user Business owner of the content or interaction reading_journeys, reading_shares, comments, reactions
user_created, user_updated, date_created, date_updated Directus audit metadata Most editable content collections

For comments and reactions, user is the field the product actually cares about. Directus flows now protect that field from spoofing.

This is an important design choice. Audit fields tell us who created or updated a record inside Directus. The business user field tells us who the content belongs to in the product. Those are not always the same concern, so the model keeps them separate.

Entity view

erDiagram
    AUTHORS ||--o{ BOOK_AUTHORS : linked_by
    BOOKS ||--o{ BOOK_AUTHORS : linked_by
    BOOKS ||--o{ READING_JOURNEYS : has
    BOOKS ||--o{ READING_SHARES : has
    READING_JOURNEYS ||--o{ READING_SHARES : groups
    READING_SHARES ||--o{ READING_SHARE_COMMENTS : has
    READING_SHARES ||--o{ READING_SHARE_REACTIONS : has
    READING_JOURNEYS ||--o{ READING_JOURNEY_COMMENTS : has
    READING_JOURNEYS ||--o{ READING_JOURNEY_REACTIONS : has
    BOOKS ||--o{ BOOK_COMMENTS : has
    BOOKS ||--o{ BOOK_REACTIONS : has
    READING_SHARES ||--o{ READING_SHARE_IMAGES : has

Design choices worth noticing

  • Reaction collections are intentionally minimal. They are just target-plus-user rows with a timestamp.
  • Comment collections keep richer moderation fields like status, but reaction collections do not.
  • All interaction collections use UUID primary keys, even when the business rule is effectively a unique pair.
  • User relations on comments and reactions are nullable so content can survive user deletion.

The overall design is biased toward explicit models.

  • More collections, but simpler rules inside each one.
  • More relations, but cleaner query behavior.
  • More structure, but less ambiguity about what a row means.