Reference

The Room, As Built

The essays describe the room going up, one decision at a time. This page is the room as it currently stands: the databases, what each one holds, how they join, and which of them the model may write to.

Everything below runs on one Postgres instance on hardware in my house. None of it is reachable from the internet, which is why it can be described here in full. There is nothing to protect by being vague about the shape of it.

The data plane

The writing room data plane Three layers. At the top, the things that ask questions: interactive writing sessions, scheduled jobs, and site builds. In the middle, an MCP access layer that exposes reads and gated writes. At the bottom, one Postgres instance holding four databases: canon, search, substack and shared. WHO ASKS Writing sessions interactive, a human in the chair Scheduled jobs nightly, unattended Site builds read-only, at publish time ACCESS MCP servers pg_read — SELECT only · pg_write — gated · memory_search · book_search ONE POSTGRES INSTANCE, NOT PUBLICLY REACHABLE canon canon.* — the objective board synthesis.* — N relative truths vot_notes.* — working notes search book_chunks — prose, embedded memory — recall, embedded the second index, by meaning substack substack.* — pieces and sources analytics.* — how they performed the publishing side of the house shared homelab telemetry, comms, household same instance, different concern not part of the writing room
Three layers. The model reaches storage only through the access layer, never directly.

canon — the objective board

This is the record of what is true in the world regardless of who is looking. Nineteen books, 521 chapters, 125 characters, 48 locations, plus the graphs that connect them: which characters appear in which chapter, who sired whom, which coterie holds which territory, which mortals are attached to which vampire.

Two details in here are the load-bearing ones, and both of them are the subject of an essay in the series. canon.characters carries aiimageprompt, aidialogueprompt and aivoiceprompt — the contract fields, so a character looks, talks and sounds the same in book six as in book one. And canon.locations carries state_end_book1, state_end_book2 and changes_notes, because a location record has to store what a place has become, not only what it is right now.

synthesis — the relative truths

Beside the board sits a second schema holding what each character individually knows, believes and is carrying at a given point in the story. synthesis.character_state is the centre of it: per character, per chapter, it records psychological_state, demon_pressure, humanity_note, last_fed and chapter_exit_state. Around it, open_threads tracks what has been raised and not yet paid off, relationship_delta tracks what changed between two people and when, and arc_markers tracks where a character sits on their arc.

One objective board, N relative truths. A chapter renders one character's relative truth, and the model is not permitted to narrate from the board it can see.

search — the second index

Canon answers questions when you already know the word to type. search answers them when you only know what you meant. Both of its tables carry an embedding column: book_chunks holds the prose itself, chunked, and memory holds recall across sessions — scoped, typed, with a JSON side-car for metadata. A keyword index and a semantic index over the same material, doing different jobs.

Every table

Twenty-two tables and one view, across three schemas in the canon database, plus two in search. Row counts are exact as of the date above, and will drift from the day this is published onward.

canon — the board

TableRowsHolds
books19One row per book: display name, whether it is publishable, and the date its serialisation starts.
book_dependencies17Which books have to come before which. Two columns, book and depends_on, and that is the whole reading order.
chapters521The spine. Book, chapter and part, POV character, location, status, word count, qr, summary, and must_mention — what this chapter is obliged to land.
chapter_cast1,210Who appears in a chapter and in what capacity. The largest table in canon, because presence is a per-chapter fact.
chapter_links129Typed links between chapters — callbacks, setups, the places one scene answers another.
characters125Twenty-seven columns each. Identity and lineage via sire_id, the psychological apparatus (ambition, convictions, humanity, predatortype, enneagram, archetype), and the three AI contract fields.
kindred_relationships397The relationship graph between vampires: source, target, type, description. Directed, so A's read on B is a different row from B's read on A.
coterie19The groups: name, leader, territory, description.
character_coterie93Which characters belong to which group.
coterie_relationships0How groups stand toward other groups and toward individuals. Modelled, not yet populated — the shape exists ahead of the need.
locations48Twenty-three columns per place. Sensory signature, sun exposure, wards, and crucially state_end_book1 / state_end_book2 — what the place has become, not only what it is.
location_characters106Who is attached to a place, and in what role.
touchstones147The mortal anchors holding a character's humanity in place. One of the most consequential small tables in the schema.
thralls_and_tools176The people and objects a character has at their disposal, typed by kind.
v_coterie_overmembership viewAn integrity guard: characters carrying more coterie memberships than they ought to. With no foreign keys to lean on, checks like this are how the board catches itself drifting.

synthesis — the relative truths

Every table here carries book, chapter and part, and most carry character_name as text. They are observations recorded against a moment in the story rather than rows in a normalised model, and they are keyed accordingly.

TableRowsHolds
character_state481Per character, per chapter-part: outfit, physical condition, psychological state, demon_pressure, humanity_note, last_fed, and the state they exit the chapter in.
relationship_delta419What changed between two named characters at a point in the story, with a cumulative_note carrying the running total of where they now stand.
arc_markers302Where a character sits on their arc at a given chapter: marker type, label, detail.
open_threads291What has been promised and not yet paid off. Typed, with a status and the chapter that closed it — an unresolved promise is a book-level gate failure, so this table has teeth.
outfit_log73What a character was wearing, and what changed since the last time they were on the page.
chapter_xrefs50Typed cross-references between two chapter-parts, each with a note. Chapter links seen from the synthesis side rather than the board's.
character_aliases41The names a character is called by, scoped to a book and a chapter range. This is the bridge between canon's integer ids and synthesis's plain names, and the reason the two keying styles can coexist.
TableRowsHolds
vot_notes.notes68Working notes tied to a character, a chapter or a book. Categorised, prioritised, and able to point back at the note that spawned them.
search.book_chunks213The prose itself, chunked by book/chapter/part, each chunk carrying an embedding.
search.memory3,357Cross-session recall: source, scope, kind, the text, a JSON side-car for metadata, and an embedding. By some distance the largest table in the room.

The shape of that last row is worth sitting with. There are 521 chapters and 125 characters, and there are 3,357 rows of memory — the room remembers considerably more about how it works than it has ever published.

How they actually join

There are no declared foreign keys anywhere in this database. Every join is by convention, and the two schemas do not even use the same convention.

That seam is deliberate, and it is also the sharpest edge in the whole design. The board is normalised because it has to stay internally consistent. The observations are keyed the way they get written down, because something has to be cheap to append mid-draft or it will not get written down at all. The cost is that nothing at the database level stops a character_name in synthesis from drifting out of step with a name in canon — which is exactly why synthesis.character_aliases exists.

Who reads, and who writes

Reads are broad and writes are narrow, on purpose. Interactive sessions read across all of it. Scheduled jobs read and write their own lanes. Site builds only ever read, and only at publish time — every site in the stack, this one included, is static output, so nothing a reader does reaches a database.

The rule that matters: canon is the thing the model consults, not the thing it edits. Corrections to the board go through a path a human runs deliberately. And that is not a policy the model is asked to respect — it is a permission it does not hold. The next section is where that gets enforced.

Confidentiality, integrity, availability

The next three sections are one argument, and the frame I actually think in is the one I use at work. Security people call it the CIA triad. It applies to an unpublished novel series exactly as well as it applies to a federal information system, and it is a more useful way to talk about this than a list of features.

PropertyHow it is actually met
C Confidentiality The whole thing sits in my house, and nothing on the internet can reach in to it. That is not a firewall rule I am trusting — there is no inbound route to trust. What does leave, leaves encrypted, and the storage provider holds ciphertext it cannot read.
I Integrity The AI holds no write permission on the critical fields — canon and the notes database are read-only to it at the database-role level. Under that: hourly snapshots and nightly backups, so a mistake by something that does have write access is recoverable rather than permanent. Under that: a three-drive array whose filesystem checksums what it stores and repairs corruption from parity, so the bytes that come back are the bytes that went in.
A Availability It lives on an always-on NAS rather than a workstation — which is also why the scheduled layer works: the machine holding the data is the machine running the jobs, and it does not sleep when I do. A UPS carries it through power loss and shuts it down cleanly if the outage outlasts the battery. Remote access is over a private mesh network, so I can reach it from anywhere without the internet gaining a way in.

Writers are not usually handed this vocabulary, and it is worth borrowing. Most advice about protecting your manuscript stops at keep a backup, which is one third of one of these three.

The access layer

This is the part that matters most, and it is the part that is easiest to get wrong by writing a very firm sentence in a prompt and believing it. The model cannot corrupt canon. Not because it has been told not to. Because it does not have the permission.

Two servers, two postures

Access is split across two MCP servers with deliberately different powers. The production server offers a read tool that accepts SELECT and WITH and nothing else, a schema-inspection tool, and a write tool. The development server, pointed at a separate set of databases, offers full SQL — inserts, updates, deletes, and DDL — as a single transaction that commits on success and rolls back on any error.

Everything experimental happens on the second one. Schema changes are made against dev and promoted deliberately; there is no path by which a session exploring an idea reshapes the live board.

The boundary is a role, not a promise

The production write tool will accept a statement aimed at canon. It submits it. The database refuses it. canon and the notes database are read-only at the database-role level, so a denied write comes back as a permission error from Postgres, not as a polite refusal from a tool that could have chosen otherwise.

That distinction is the whole design. An instruction not to edit canon is a request, and a model misreading a request is an ordinary Tuesday. A grant that was never issued is not a request — there is no phrasing, no misunderstanding, and no clever framing that turns it into a successful write. DDL is not available on production at all, from any tool, so nothing can quietly add a column or drop a table either.

Every service runs in a container

Nothing in the stack runs as a loose process on the host. Each service — the database included — runs in its own container, holding only what it was explicitly handed: its own filesystem view, its own network, its own environment. A service that misbehaves is standing in a room with the doors it was given and no others, so a fault in one part of the stack does not become a fault in the machine.

I want to be accurate about this rather than oversell it: a container is not the same isolation guarantee as a separate machine, and anyone telling you it is has something to sell. It is defence in depth. It shrinks the blast radius of a mistake, and combined with a database role that grants no write on canon, the two failures you would need to line up to corrupt the board stop being one careless afternoon.

The reproducibility is the other half of the benefit, and it is what entry No. 14 was about: the arrangement of the whole room is a file. It can be read, diffed, and stood back up.

Search runs locally

The two semantic search tools embed the query on the machine itself, with a local embedding model, then rank by vector similarity inside the database. The manuscript is never shipped to a third-party embedding service in order to be searched. Semantic recall over unpublished fiction and a hosted vector API are two things that should not be combined, and here they are not.

The backup layer

Access control is prevention. Backups are recovery, and the room needs both, because the access layer only defends against one category of accident. It does nothing about a bad migration I run myself, a disk that dies, or a decision at three in the morning that looked correct at the time.

The whole cluster, including the permissions

The database backup is a full cluster dump — every database, plus the global objects: roles, grants, tablespaces. That last part is easy to skip and important not to. The thing protecting canon is the role model, so a backup that restored the data without the grants would hand back a canon that anything could write to. The safeguard is inside the thing it safeguards.

Encrypted before it leaves

The order of operations is the point. The backup is taken first, on my own hardware. It is encrypted second, still on my own hardware. Only then is it offloaded to Azure. The cloud is the last step in that chain and it never holds anything but ciphertext, because the encryption happened before the upload rather than as a setting on the bucket.

The key is ours, not theirs. That single fact is the whole distinction. The keypair was generated here, the private half is escrowed here, and it has never been in Azure — so there is no version of this where the provider decrypts these archives, whether asked politely, compelled, or breached.

Provider-side encryption is a different arrangement wearing the same word. There, the provider holds the key and undertakes not to look, and you are trusting a policy. Here there is no policy to trust, because the capability does not exist on their side. Azure is storage, not a party to the work: it is renting me space it cannot read.

Split by how fast things change

Manuscripts change every session and are small, so they go daily. Media is a hundred times the size and almost never changes, so it goes weekly. Backing both up on the same schedule would either waste most of the transfer or leave a day's writing unprotected, and there is no reason to accept either when the two have such different shapes.

Versioning and a soft-delete window sit under all of it, which covers the failure mode people usually discover too late: a backup job that runs perfectly and overwrites a good copy with a broken one.

Snapshots, backups and off-site copies are three different things

These get talked about as one thing at three intervals. They are not. They answer three different questions, and a plan missing any one of them has a hole exactly the shape of the question it skipped.

LayerAnswersCadence
Snapshot I deleted the wrong thing forty minutes ago. Same storage, restored in seconds, no decryption and no archive to pull. hourly
Backup This is corrupt and I need last night's version. A separate copy, restorable independently of the live database — including the roles and grants. nightly
Off-site copy The building burned down. And only this one answers that. A flawless nightly backup sitting in the same house as the original is not a plan for losing the house. daily / weekly

Stated as exposure rather than as reassurance: an hour of my own mistakes, a night of corruption, and — for the one failure where the machine and everything near it is simply gone — whatever has left the building since the last upload.

The filesystem checks its own work

Underneath all of the above sits the storage itself. The box runs TrueNAS, which means the filesystem is ZFS, and that choice is doing more work than any other single decision in this section. Three drives with a drive's worth of parity, so one can fail without the room noticing — that part is ordinary, and any RAID does it.

What is not ordinary is that ZFS checksums every block it writes and verifies that checksum on every read. A block that comes back wrong is detected as wrong, repaired from parity, and rewritten — silently, while the file is being read. A periodic scrub walks the entire pool doing the same thing to data nobody has touched in months, which is exactly where rot hides. Conventional RAID cannot do this: it knows the drives disagree but not which one is telling the truth. ZFS knows, because it wrote down what the answer was supposed to be.

The hourly snapshots come from the same place. ZFS is copy-on-write, so a snapshot is not a copy of anything — it is a note saying which blocks were current at that moment. That is why they are effectively free to take and why hourly is a reasonable cadence rather than an extravagant one.

This is not redundant with the backups, and it is the layer people most often leave out. A backup is a faithful copy, including of damage. If a block rots and nothing detects it, tonight's backup copies the rot, and so does tomorrow's, and so does every one after that until the last good version ages out of retention. Corruption you cannot detect is corruption you will eventually back up, everywhere, and then restore with confidence.

Checksumming is what stops the backup chain from laundering bad data into every copy of itself. Without it the other three layers are faster ways to propagate a problem.

The availability layer

Availability is the property writers are least likely to think of as a security concern, and it is the one that bites soonest. Work you cannot reach is work you do not have, and a manuscript locked inside a machine that will not boot is indistinguishable, on the day you need it, from a manuscript that was never written.

Power, and shutting down on purpose

The machine sits on a UPS. Short outages — which is nearly all of them — it simply rides through. When one runs long enough to drain the battery, the box shuts itself down cleanly on its own rather than being dropped at whatever instant the power finally goes.

That second half is doing more than protecting uptime. An unclean shutdown partway through a write is one of the more dependable ways to corrupt a database, so a graceful power-down is as much an integrity control as an availability one. The UPS is not there to keep the room running through a blackout. It is there to buy enough time to stop properly.

Remote access without an inbound route

The ordinary way to reach your own machine from somewhere else is to open a port and forward it, which quietly spends the confidentiality claim at the top of this page. Instead the room is on a private mesh network — Tailscale — where my devices authenticate to each other and route between themselves directly.

The result is worth stating precisely, because it sounds like a contradiction and is not: I can reach the room from anywhere, and the internet still has no way in. There is no listening service published to the world, no port forwarded, and nothing to find by scanning the address my house happens to have today. Remote access and zero inbound exposure are not a tradeoff to be balanced — that is a habit of thought left over from an older era of networking, and it is worth putting down.

The scheduled layer

The middle box in the diagram. Nothing in this section involves a person being awake for it, which is the entire reason it exists: the room has to keep working on the days I do not open it.

These are cron jobs, and they run on the same Network Attached Storage (NAS) machine as my database — not on my desk. That distinction is the whole difference between a set of scripts and a system. My workstation can be off, I can be away for a week, and a chapter still goes live on the morning it was scheduled for, because nothing in the publishing path is waiting on me to open a laptop. The very first piece in this series claimed the writing room keeps running between sessions. This is the part that makes that true rather than aspirational.

Publishing is a date field and a nightly build

Every scheduled entry — chapter, note, article — carries a date, the real-world day it is meant to appear. A filter runs at build time and drops anything whose date has not arrived yet. Not hidden behind a check, not rendered with a coming soon banner: excluded from the build entirely, so no page is generated and there is no URL to leak early.

The nightly build is what makes the date real. Write the thing whenever it gets written, stamp the day it should land, and the first build that runs on or after that date publishes it. It compares by calendar day rather than exact time, so an entry goes live from midnight regardless of what hour the build actually fires.

One field, three consumers — the version that bit

That same date ended up driving three separate mechanisms. It decides whether a page exists at all. It decides whether the chapter is subscriber-only, via a seven-day window measured from the same date, after which the chapter opens to everyone. And it decides when subscribers get the email saying a new chapter is live.

The imageprompt field elsewhere in this system is also one field with three consumers, and that is a good design because it was built that way deliberately. This one arrived at three consumers by accretion, one mechanism at a time, each reasonably. The difference is not cosmetic — a field that three things depend on and nobody declared as shared is a field where a sensible change to one consumer misfires the other two. That is exactly what happened here, and the fix was guardrails rather than a rewrite: an idempotency ledger so a chapter can never be announced twice, and a hard cap of one chapter announced per run, so even if several come due at once nobody's inbox takes the pile.

The notes are designed to forget

Working notes carry a priority, and the priority sets a time to live — low-priority notes have days, high-priority notes have months. A weekly job deletes what has aged out. Left there, that would be a mechanism for quietly losing things that turned out to matter, so a second job runs first: it scans for everything the prune is about to take and reports it, with the SQL to rescue anything worth keeping already written out.

Deliberate forgetting with a review step in front of it. A notes table that only ever grows stops being a working surface and becomes an archive, and the room already has an archive.

Nothing runs unwatched

Every scheduled job on the box checks in to an external dead-man's switch when it finishes. A job that fails is a problem; a job that silently stops running is a worse one, because nothing alerts and everything looks fine until someone notices that a chapter did not go out. Silence past the expected window raises the alarm on its own, and failures go to a central log rather than to a console nobody is reading.

The gates

The schema says what the room knows. The gates say what it is allowed to ship. They run in a fixed order, and the order is the point: content before craft, craft before scoring. Nothing downstream runs if something upstream failed.

Pre-gates

These run first and block everything after them. They are not craft judgements and they are not negotiable.

GateRequirement
G0 Content Standards Gate. A fixed four-question test: is the character a minor at the depicted event; is this trafficking testimony; is the character a vampire in-scene whose body cannot respond; has a specific passage been named against a specific test. Content, not craft. The author's answer is final. Runs before everything, every session.
SR Scene Rendering. Load-bearing scenes — confrontation, revelation, decision, power shift, relationship texture — must be rendered in scene, not summarised into narration. A summary that costs the reader the exchange is a rendering failure.

Hard gates

Binary. Each violation deducts from the craft total, which is what keeps the Quality Rating from being an opinion — a gate either tripped or it did not, and the arithmetic is the same every time.

GateRequirementPenalty
G1Em dashes and multi-hyphen sequences. Zero em dashes, zero double or triple hyphens in prose. A single legitimate hyphen only, never a lone hyphen as punctuation.−2.1
G2IP compliance. Zero World of Darkness terminology in prose; the series uses its own replacements throughout.−2.1
G3POV, tense, narrator neutrality. Third person past, limited POV, one POV character per chapter. The narrator is a camera, not a judge — no moral verdicts in narrator prose.−1.6
G4Banned AI patterns. No a beat, for a long moment, silence stretched, the world narrowed to; no purple prose; no banned emotional-tell constructions.deduction
G5Phrase repetition. No phrase longer than five words repeated within a chapter-part or between consecutive chapters.−1.1
G6Said-construct monotony. No three or more back-to-back dialogue lines tagged said or asked, or untagged. Vary with action beats and clear-speaker no-tag lines.−1.1
G7Emotional tell ban. Zero banned physiological tells. Vampire-physiology-aware: negating the response in vampire POV is correct, not a violation; living humans and ghouls are exempt for literal response.−1.1 ea.
G8Beast voice. Every Beast tag is second-person imperative only. The Beast is never named in narrator prose; the narrator names the sensation only.−1.6
G9Author fingerprint fidelity. Both Tier 1 fingerprints present — moral weight through character voice, specificity over generic darkness — plus two of three Tier 2 where the scene has pressure.−1.5
G10Character voice fidelity. Each speaking character's dialogue is checked against their canonical aidialogueprompt. Two or more auditable violations fails that character.−1.6 / char

G10 is where the gates and the schema meet. The check is not against a style guide, it is against canon.characters.aidialogueprompt — the same field the data plane above carries. The contract is written once into the record and then enforced against every line that character speaks, in every book.

Canary metrics

Statistical thresholds calibrated for the genre (in this case horror). All of them must pass before a Quality Rating is scored at all.

GateMetricThreshold
CG1Passive voice rate< 30 / 100 sent.
CG2Emotion tell rate< 25% of sent.
CG3Weak adverb rate< 10.0 / 1k words
CG4Sentence variety (stddev / 2)>= 5.5
CG5Complex paragraphs< 20%
CG6Slow-pacing paragraphs< 38%
CG7Performance tag rate< 25% of tags
CG8Adverb-tagged dialogue< 12% of tags
CG9Voice consistency (top tag share)>= 50% of tags
CG10Compressed-interaction rate< 3% of paras

The Quality Rating

Chapter QR is craft dimensions C1 through C5 summed on a 0.0 to 10.0 scale, minus the sum of every hard-gate penalty above. It measures execution: did this scene do its job well. It is a decimal by design, and it is never projected or estimated — the only way to produce one is to run the scorer against the actual file.

Book QR is the architecture pass, run against the whole object rather than a scene: book-level gates BG1 to BG5 — unresolved promise, series discontinuity, arc absence, theme collapse, structural fracture — against craft dimensions BC1 to BC5 — arc architecture, character completion, thematic coherence, pacing distribution, series integration.

The score does not stay in the report. canon.chapters carries a qr column typed real, so every chapter's rating lives on the chapter record next to its word count and status. That closes the loop between the two halves of this page: the gates produce a number, the number goes back into the board, and the board is what the next session reads before it writes anything.

The last gate

AO Authorizing Official

I read it over, and I approve it, before it ships in any form. Not a chapter, not an excerpt, not a note, not an image. Every gate above this one is automatable and all of them run before I ever see the file. None of them decide anything.

A 10.0 is not permission. It means the scorer found nothing left to deduct for, which is a statement about the scorer. The gates can tell me a piece is clean. They cannot tell me it is good, and they cannot tell me it is mine.

I do Risk Management Framework work for a living. In that world the Authorizing Official is the person who reads the whole package, accepts the risk, and signs — and when it fails, it fails on their name. Writing has the same seat. No scanner can occupy it, because the chair is not made of keystrokes. It is made of accountability, and accountability does not show up in a percentage.

The seats

The data plane is what the room knows. The gates are what it will allow. The seats are who does the work — and in a real writing room these are jobs that people hold. Every one of them below is a named entry point that loads its own context and does one job, and all six are automated.

SeatThe jobTouches
planningTurns a thin chapter summary into a beat-by-beat brief before anyone drafts a word — the document that sits between the outline and the draft.reads
drafterWrites the chapter against that brief, the POV character's voice contract, and the cast who are actually in the room.reads
continuity editorReads the finished chapter and writes back what changed: state, relationship deltas, arc markers, threads opened and closed, what everyone was wearing.writes
line editorRuns the manuscript against the hard gates and fixes what it can — em dashes, repetition, said-monotony, banned tells.reads
quality controlScores the file. Runs the canary metrics, applies the penalties, produces the number.reads
watchdogStands guard against drift — the check that what was just written still agrees with the board.reads

Note the third column, because it is the access layer showing up in the staffing. Exactly one seat writes anything, and what it writes is synthesis — the observational half. The continuity editor records what happened. It does not get to revise what is true. Canon stays read-only to every seat at the table, including that one.

The loop, and where it stops

A draft goes round: written, extracted, polished, scored. If it fails the bar it goes back and round again. Clearing the bar does not publish it — clearing the bar only qualifies it to be looked at. It arrives at one chair, and that chair signs or rejects. Rejected, it becomes a draft again and the loop resumes. Signed, and only then, it ships.

Six seats automated, one not. That ratio is the honest summary of this entire site: nearly all of the labour of running a writing room can be handed to a system built carefully enough, and the one job that cannot be handed over is not a craft skill at all. It is being the person whose name is on it.

Diagram of the writing room: the automatable seats (planning, drafter, continuity editor, line editor, quality control, canon watchdog), the data plane beneath them, and the human Authorizing Official who signs off on what ships.
The seats, the data plane, and the one chair that does not get automated.
All entries

All writing on this site contains elements of both human and AI produced material. This author uses all resources at his disposal.