Loving the conversation here (and in person and in the email thread!). I’ve been staying up to date and trying to wrap my head around it all. I’ve been working on this problem for the last 5 years on the Hardcover side, but this is the first anything I’ve done using Atmosphere and PDSs and that side of things, so please feel free to correct anything I say that feels off, or lacks understanding of that side of this.
I wanted to break down a few concepts that I’ve seen touched on, with some thoughts on how we’re solving them today, and touching on some ideas on how to solve them long-term in a decentralized way.
Books/Works vs Editions
I think this is one of the biggest concepts that if we can get right, it powers a ton of things across different sites (ex: View the Spanish version? / View the Audiobook version? / Is there a DRM free ebook for this book? Which audiobook edition has the highest rating? Which has a narrator that I like the most?).
To go back to @nickthesick ‘s mention of including all the useful data needed, the Edition has some of the relevant useful data to the user reading that edition. Without edition information you could say “I’m 50% of the way done with the book”. With the edition information you could say “I’m on page 256 out of 512 on the Hardcover edition of Book”, or “I’m 12 hours and 42 seconds out 20 hours and 5 minutes of the audiobook edition”. Also, different editions have different cover images, and we’ve found users really want to see the cover of the edition they have.
In order to see stats like listening time or pages read, edition information is required, and progress updates need to be linked to the edition. It can be an annoying step, both for data and for the user, but for full accuracy it’s needed.
All of that data lives in the relationship between a Book and an Edition. OpenLibrary calls these Works, which I think is a more universal term for an abstract “representation of a book”.
Important things to note about a Work/Book vs an Edition:
- An Edition AND a Book/Work can have identifiers back to other platforms. On Hardcover, we have
edition_id and book_id. OpenLibrary uses keys like /works/OL82563W (work/book) and /books/OL1439870M (Edition).
- Choosing a title for the “Book/Work” gets more difficult the broader it is (ex: different languages, or even HP and the [Sorcerers|Philosophers] Stone).
- Not all Editions have an identifier. Some editions don’t have ISBNs or ASINs (I’m looking at you Manga
.)
- Series data is best mapped to the Book/Work, not the edition. That way * The Hobbit is considered Book 0 of The Lord of the Rings, rather than all 519+ editions needing that data.
I think the takeaway here is that an ISBN identifies an edition, not a work. So if ISBN is the dedup key, community.lexicon.book.book is edition-shaped. But .review and .status are going to be written at whatever level users think in, which is usually the work. If I listen to the audiobook and you read the paperback, we’re both reading the same book and we want to see each other’s reviews, but we don’t share an ISBN.
Deciding where to draw the line for a work/book is tough. Here are some fun edge cases, and what decisions we made:
- If a work/book has editions in multiple languages, are those the same work? (Hardcover: all language editions are children of the same book/work, just in different languages)
- What about abridged vs non-abridged? (Hardcover: These are children editions of the same book, however we don’t current abridged separately other than in text fields).
- What about partial books (Hardcover: * The Way of Kings Part 1) (Hardcover: we call this a “partial edition”, which has a single parent book).
- What about book collections, ex: * The Adventures of Tom Sawyer and Adventures of Huckleberry Finn contains two separate books. (Hardcover: We track this as a different book than Sawyer/Huckberry Fin, with a “Collection” boolean, then track the “books” this book contains as an ordered array of book ids).
- How is non-english data about the Work/Book stored (title, description, etc)? (Hardcover: we do not do this today
).
- What about translations of an Edition? The translation of * The Odyssey by Emily Wilson was the first by a woman from the original source text, and has notably different meaning in some places to other translations. (Hardcover: we consider all translations to be editions of the same source, and all the same book).
One tricky thing I see is that different sites will decide what makes up a Book/Work is different. Some places might want to separate Work/Book by language to side step that last question. Or they might consider abridged a separate Work/Book because reviews would be notably different.
Here’s the visual representation of how this ends up working.
┌───────────────────────────┐
│ Book │ the work
│ title, authors, series │ "Project Hail Mary"
│ canonical edition │
│ dto json ◄───────────┼── cached json combining all edition dtos
└─────────────┬─────────────┘
│ 1..n
┌─────────────▼─────────────┐
│ Edition │ the manifestation
│ isbn10, isbn13, asin │ format: physical | ebook | audio
│ format, language │ pages OR audio duration
│ publisher, release date │
│ pages / audio seconds │
│ contributors (narrator, │
│ translator, editor) │
│ dto json ◄───────────┼── cached json combining all book mapping dtos
└─────────────┬─────────────┘
│ 1..n
┌─────────────▼─────────────┐
│ BookMapping │ external identity + snapshot
│ source (OpenLibrary, │
│ Google Books, etc) │
│ external_id │
│ payload text ◄───────────┼── cached snapshot of source
│ dto json ◄───────────┼── cached json representation of source
│ fetched_at │
└───────────────────────────┘
This mirrors how we store data on Hardcover, which I wrote more about here. We have a service we call a “prioritizer” which knows which fields to grab from different mappings (ex: use the Title from Goodreads, use the data and description from Google Books, use the cover from Google, use the edition information from Open Library, etc). That rolls up N BookMappings into a single Edition. Then we repeat that process to roll up N Editions into a Book, favoring the Editions in each format that has the most readers (ex: This Book is 294 pages or 12 hours 9 minutes based on Edition X and Y).
I could see a case for all of us collaborating on the Edition Level, but then with different concepts of what the “book/work” level is in different platforms. We could have both entities, and then use a composite key / snapshot both of those dto (data transfer objects / snaptshot / whatever we call it) objects into the users PDS to represent the book + edition.
In that case edition wouldn’t “belong” to a book, but would belong to many “books”. I’d love to figure out a structure that works for both, but I suspect some voting/linking might be needed there.
Librarians / Curators / Non-Author Editors
Hardcover maintains a book database with a community of volunteer librarians. That’s a role people apply for, and a Hardcover team member (usually one of 3 librarians, but could be any of the 15 of us working part time for equity + me full-ish time) will grant that permission. Librarians have resources in our docs to understand standards, as well as a channel in the Hardcover Discord that only Librarians can access by linking Discord with Hardcover.
Librarians can merge have an “access level”, and can merge books together that impact users below that threshold. This can be disruptive if done wrong, since now all users past history is now wrong.
Progress is Two Different Records
This one seems small and I don’t think it is. “I’m on page 200” is three separate facts:
-
A log entry. An event, at a timestamp, against a specific edition. Immutable, append-only, and it’s what reading history, stats, and streaks are built from.
-
Current position. State. Mutable, one per user per book, and it’s what the UI reads to show you where you are.
-
All reads. Log of the overall reads, edition and when. I started the audiobook edition on May 1 and finished it on May 8.
We store both. Collapsing them into one mutable record loses history, and I noticed BookHive ran into a version of this already (issue #41, where status changes on a mutated record all inherit the original createdAt).
It also matters for format: page 200 and minute 340 aren’t the same unit, and progress only makes sense against the edition you’re actually consuming. So the log entry needs an edition reference even if status lives at the work level.
(The above progress section was co-written with Claude, which did some research on my behalf).
Review Text Formatting
I think we should make plaintext field mandatory, and have content included in multiple formats. Maybe a plaintext version then an array of contentType versions provided by the platform?
Hardcover currently saves all reviews in plaintext (which is used to create the also cached length), and Slate in JSON custom format that supports spoiler tags, links to other entities in our system, etc). If we used something like @omg.jacky.wtf mentioned with contentType: “text/x-slate-hardcover-1 for this, each platform could own this format, while still having the plaintext. We’d save the HTML and Markdown versions in here as well for convenience, which are derived from the Slate document.
Claims and edit rights are two systems
Bibliograph’s claim record seems to do double duty: it establishes that a DID is the author or publisher, and it grants edit rights, with librarian status accruing from holding multiple verified claims.
Those probably want to come apart. Authors are subjects of the data. Librarians are curators of it. In our experience the overlap is small, and the incentives are different enough that merging them causes trouble.
We’re mid-design on author verification right now, and the thing that pushed us away from a single gate (ID upload, DNS record, that kind of thing) is that it excludes almost everyone who isn’t already well resourced. What we landed on instead is accumulating lightweight signals toward a two-tier badge: social cross-linking, publisher vouching, email domain, and ORCID, which is the one signal that’s fully automatable and which I haven’t seen mentioned here yet. Worth a look if the claim record is going to carry any weight.
Additional Review Metadata
I’m curious to learn more how about reviews in other platforms handle it when different services might add on additional fields as part of a review. We have a few other fields that we consider part of the “review”:
- Sponsored Content?
- Review contains spoilers?
- Canonical URL for this review
- Media URLs (posts to Instagram, TikTok, etc that contain the same content).
The last two are important for reshowing the review and linking back to the original. When we show a review that has a canonical tag, we add that canonical URL in the header so the original author URL gets the SEO credit. The media URLs allow for embedding other formats of the review alongside and for some other really neat future-features (ex: video feed of reviews by readers with similar taste to you for books we recommend for you).
Other sites might have other metadata they ask for too: tags, moods, genres, pace, character vs plot, spiciness, you name it.
Private and follower-only data is unaddressed
I’ve been following different people working on private and following only data on ATproto, but I think this is still something that’s being figured out. Is anyone familiar with those options and their status?
The MVP scope is read status and reviews, with no visibility model. Repos are world-readable and there aren’t private data primitives yet, so anything written on-protocol is public, full stop.
That’s fine for the MVP, but I’d like to see a visibility field in the record shape from day one even if the only supported value initially is public. It gives apps a defensible answer to “why isn’t all my data here,” and it means the lexicon doesn’t need a v2 the moment private primitives land. On our side, private entries just wouldn’t sync at all, driven by that field.
What Hardcover Can Bring
@nickthesick ‘s second takeaway is a that good book data is very hard to get and enrichment work benefits everyone, and I agree. Once we land on a shape of the models that works, we can help seed it. We currently don’t have ATProto integration on the site, but I’ve been looking into allowing users to bring their own PDS and we’ll save their data there. The private/following only side of Hardcover’s data has been the biggest blocker for me on getting started with any kind of integration there. I think if we can figure out how the entities map between systems and figure out private/following only data, then we’d be in a good spot to start something.
I suspect we’d create a bunch more entities in there specific to Hardcover as well (lists the user created, prompts they’ve answered, etc).
Count me in for the working group!