What has changed

This is v2.20.0 — Report a comment, or stop hearing from whoever wrote it.

v2.20.0 — Report a comment, or stop hearing from whoever wrote it

Released: September 16, 2026

Nothing on the website's comment controls changes in this release. It is the app: the two controls the website gained on 12 September are now in the app as well.

On a comment somebody else wrote, the app now offers "Report or block." Open it and there are two things you can do, each with a sentence saying what will happen before you press.

Report this comment takes the comment down straight away, for everyone, and somebody reads it before deciding whether it stays down. Nobody promises when that reading happens, and the app does not say otherwise. If a comment of yours is reported, you still see it on the book, marked as hidden from other readers — it has not been deleted, and you are not told who reported it.

Block whoever wrote this means you will not see anything else that reader writes, from now on. What is already on the page stays where it is. That is deliberate rather than a limitation: a block that reached backwards would let anybody watch which comments vanished together and so work out which of them shared a writer, which is exactly the thing shared comments are built never to tell you. So after you block somebody the page looks almost unchanged — the one comment you acted on goes — and the app says so, because otherwise it looks as though nothing happened.

Your blocks are listed on the Account screen, by date and nothing else, and each can be ended there. Ending one lets that reader's next comments reach you again; nothing that was hidden while the block stood comes back. The website does not list blocks yet — the app is the first place they can be seen or ended.


v2.19.1 — A long thread, and a search you can page through

Released: September 12, 2026

Nothing on the website changes in this release. It is the app again.

A book with a great many shared comments on it used to build every one of them before it drew anything. Three comments is nothing; two hundred — a book several reading groups have been through — is two hundred cards and all their controls, made at once, on a phone. The comments are now drawn as you reach them, the way the books on your main screen already were.

How much of a long thread anybody should be shown is a separate question and is deliberately not answered here. Nothing is hidden from you in this release; the app has only stopped doing all the work at the start.

Searching, then turning the page, has been checked. It was correct and nobody had ever proved it: every test of paging was of an unfiltered shelf, and every test of searching was of a search with one result. So there was no test that would have noticed the app forgetting your search when you asked for the second page. There is now, for a search and for a chosen shelf, and the behaviour itself was found to be right all along.

No behaviour changed on that second point — which is worth saying plainly. Being sure of something that was already true is not the same as fixing it, and the two should not be reported as if they were.


v2.19.0 — Choosing a shelf, when there are forty of them

Released: September 12, 2026

Nothing on the website changes in this release. It is the app, which is still being built and is not yet in the App Store.

Both screens listed every shelf you have, all at once. On the main screen that was a row of names above your books; on the scan form it was every shelf laid out under the field, to be found by eye and tapped. That is a reasonable design for five shelves and not one for forty, and forty is how many there are.

There is now one control, used in both places: a button saying which shelf is chosen, and behind it a list with a search box at the top. Type part of a name and the list narrows to it. If nothing matches, it says so rather than showing you an empty space and leaving you to guess whether the search worked.

The Shelf field on the scan form stays exactly where it was. The button sits beside it, not in place of it. Filing a book onto a shelf that does not exist yet is not an edge case — it is how a shelf comes to exist — and a release that quietly took that away would have looked tidier and been worse.

On the main screen the button says "Every shelf" until you choose one, which is also the way back out of a filter, and says which shelf you are looking at afterwards. That was previously only legible as an underline on one name in a row, which is not the same as being told.

The three sentences added in the previous release are still there, and the button is not drawn over them: if the list of shelves could not be loaded, or you have no shelves at all, the screen says which — rather than offering a button that opens onto nothing.


v2.18.0 — What came with the book, and which shelves there are

Released: September 12, 2026

Nothing on the website changes in this release. It is the app again, which is still being built and is not yet in the App Store.

A book scanned on the phone was arriving with less on it than the same book typed on the web. The catalogues answer eight things about a book — its title, author, publisher, year and ISBN, and also its subjects, a short description and a cover. The app was keeping the first five and throwing the last three away. Nothing failed and nothing said so; the book simply had no cover, and searching for a phrase from its description would never find it, because the description was never there.

All three now come across, and the form says in one line that the description and the cover came with the book, so nobody has to wonder where they came from. A book scanned and a book typed are now the same book, and there is a test whose whole job is to go red the day they stop being.

The Shelf field on the scan form was an empty box. No names under it, and nothing on the screen saying whether that meant this archive has no shelves or the list had simply not arrived. Those are two different things and the app said neither. It now says which — and if the list cannot be fetched, it says so and still lets a shelf be typed, because a shelf has never needed to exist before a book is put on it.

"Your shelf" is now "Your archive". The whole library is an archive; a shelf is one thing inside it that a book is filed onto. The app had one word for both, which is how the two lists of shelves came to be confused with each other in the first place. Everything that really is about a shelf — the filter, the picker, the field on the form — still says shelf.


v2.17.0 — Staying signed in

Released: September 12, 2026

Nothing on the website changes in this release. It is the first note here about the Scriptorium app, which is being built and is not yet in the App Store — and it is written down because the app takes its version number from this file, so a release note is how a build gets one.

The app could sign you in and then, about an hour later, stop working. Every part of it would report that authentication was required, and there was no way to sign out and back in again, because there was no way to sign out at all.

Two things fixed. The app now signs itself back in quietly while you are using it, so a session lasts as long as you keep using the app rather than as long as one token happens to last. And there is a Sign out, on the account screen, above the option to delete — your Scriptorium stays exactly as it is and signing back in needs a new code from your email.

When a session really has ended — you have been away long enough, or you signed out somewhere else — the app asks you to sign in again, in those words. It no longer shows you the sentence the server says to a program.

If you use Scriptorium on the web, none of this has ever applied to you: a browser session works as it always has.


v2.16.2 — Two clocks, told apart

Released: September 11, 2026

Adding a second address to your Scriptorium used to tell you the sign-in link lasted a week. It does not, and never did.

There are two different things running down at once when you add an address, and the message ran them together. The invitation — the thing that makes the new address one this Scriptorium will admit — lasts seven days, and that part was true. The link in the email is much shorter-lived than that, as every sign-in link here has always been.

So somebody who added an address in the evening, read the message the next morning and clicked a link they had been told was good for a week found that it was not. Nothing was lost — the invitation was still there, and a fresh link could be had from the ordinary sign-in page — but the screen had said something untrue and then the product appeared to break.

The message now says what is true of each: use the link soon, as it expires shortly, and the invitation itself stands for seven days.

No duration is named for the link, deliberately, and for the same reason the note before this one gave about the code: how long it lasts is a setting rather than a fact about Scriptorium, it can be changed in a minute by somebody with a browser, and a sentence here claiming to know it would go wrong the next time it moved — silently, in the one flow where being wrong means somebody cannot get in. The email itself has always said "expires shortly" and says it still.

Nothing else about adding an address changes.


v2.16.1 — How long is the code? Read the email

Released: September 11, 2026

A correction. The note for v2.14.0 said your sign-in email carries a six-digit code. It does not necessarily carry six, and this Scriptorium's does not.

The length of that code is a setting rather than a fact about Scriptorium. It lives in the dashboard of the service that sends the message, it can be anything from six digits to ten, and somebody with a browser can change it in a minute without a word of this software changing. Nobody had counted the digits in a real email; the number came from a line of documentation describing what happens if you leave the setting alone.

So the correction is not a different number. It is that there is no number to give you: the email is the thing to read. Type what it says, however many digits that turns out to be.

Nothing about signing in has changed, and nothing you do is different. The message has always carried whatever code was generated for it, and the link beside it still works the way it always has. What changed is that this release note, the app's screen and every comment in the source have stopped claiming to know something only that dashboard knows.

The earlier note stands where it is, wrong in that one word, because a release note that gets quietly edited afterwards is worth less than one that gets corrected in the open.


v2.16.0 — Leaving from a phone, and one answer for every address

Released: September 10, 2026

Two things the Scriptorium app cannot do without, built on the server before there is an app. Nothing on the web looks different today.

Deleting your account no longer needs a browser. Until now the only way to delete a Scriptorium was the form on the Account page. That form is unchanged and still the way to do it from a computer; what is new is that the same deletion can be asked for from somewhere else, so that an app can offer it from inside the app rather than telling you to go and find a website.

It is emphatically the same deletion. Not a second one written to look like it — the same comments withdrawn from every book, the same ratings removed, the same archive written down as owed an erasure, in the same order and with the same refusal when a second sign-in address still reaches your Scriptorium. It asks you to type delete either way.

Asking for a sign-in code now gives the same answer for every address. When a sign-in message is requested, Scriptorium replies the same way whether or not it has ever heard of the address: if that address has a Scriptorium, a message is on its way to it.

That sentence is deliberately incurious. An answer that said "no account here" would let anybody find out who has a Scriptorium simply by typing addresses in, and the rest of this product has been careful not to answer that question — a book in somebody else's library and a book that never existed have said the same thing since the day that door was built. This is the same decision arriving at the front door.

Two honest limits, said rather than glossed. If the sign-in service itself cannot be reached you are told so plainly, because "a message is on its way" while nothing can send one is worse than a failure you can see. And an address that is not the right shape is still refused at once, since telling somebody a message is coming to a typo helps nobody.


v2.15.1 — Deleting is your own account, and nobody else's

Released: September 10, 2026

If a second sign-in address reaches your Scriptorium, deleting now asks you to remove it first.

Everything that deletion touches belongs to the archive rather than to the address you signed in with: the shared comments are the archive's, the ratings are the archive's, and so is the access. There is no way to take away only one address's share of them, because nothing in Scriptorium has ever recorded which address wrote what.

That is fine when one person holds an archive, which is what a Scriptorium is meant to be. It is not fine when two addresses reach one — because then deleting would quietly withdraw the other reader's comments and remove their ratings too, from a button about your own account.

So it stops and says so, names the address, and points at the Account page where you can remove it. Then deleting deletes your Scriptorium and nobody else's, which is what the previous release said it did.

The sentence in v2.15.0 about all your addresses stopping at once still holds — an archive with one address has one to stop.


v2.15.0 — Delete my account

Released: September 10, 2026

You can now delete your Scriptorium yourself, from the Account page.

The Terms have promised this for weeks and the way to ask was to write an email. Now it is a page, and the page tells you what will happen before you confirm it — in your archive's own numbers, not in general terms.

What happens the moment you confirm:

Then your books, your notes and your own writing are erased. That part happens shortly afterwards rather than in the same instant, because taking an archive apart for real is done deliberately and not by a web page. The Terms say deletion means deleted and not hidden, and this is how that promise is kept rather than approximated.

A few things worth saying plainly.

Your own writing was always private. No one else has ever read your sermons, your notes or your research. What leaves other people's screens is the shared comments.

It cannot be undone, by you or by us, which is why the page asks you to type the word rather than press a second button.

Nothing stands between you and leaving. If the Terms have just changed, you are not asked to accept them first — being made to agree to a new contract in order to be allowed to go is the wrong way round.

And if you come back to the sign-in page afterwards, it tells you what you did, rather than that your account is suspended.


v2.14.0 — A code in the sign-in email

Released: September 10, 2026

Your sign-in email now carries a six-digit code as well as the link.

The link is still the way in, and for anybody signing in from a browser nothing has changed — click it as before. The code is there for signing in from an app, where a link that opens a web browser is the wrong door. It is labelled for that, so you can see at a glance when it isn't for you, and it expires when the link does.

Nobody from Scriptorium will ever ask you to read that code aloud.

Two things worth saying about what this is not.

It is not a new way to make an account. The code only ever appears in the message sent to an address Scriptorium already knows. If you have never signed in before, your welcome email is exactly what it was — because joining means reading and accepting the Terms and the Privacy Policy on the join page, and an account that came into being without that record is not something a shortcut should be able to create.

And it is not a second credential to look after. The code and the link are two forms of the same thing, both already generated for every sign-in; until today the message only showed you one of them.

Nothing else about signing in changes. There is still no password.


v2.13.0 — The API can read your library back

Released: September 10, 2026

Eight new endpoints, and what they add up to is that a program can now show you your shelf.

The API could write a library and could not read one. It could add a book, list your shelf names and look up an ISBN — which is exactly what a phone standing at a bookshelf needs, and exactly nothing of what an app needs to show you what you own. No endpoint returned a book, a note, a comment or a cover.

Now: your books, one page at a time, with the same search and the same order the library page uses; one book in full, with its cover and your own private note; your note, to change; and the shared comments on a book — reading, posting, editing, withdrawing and rating.

Everything here answers through the same code the website answers through. That is the whole design and it is worth saying why. The library listing was lifted out of the library page so that the page and the API are two callers of one query rather than two queries that agree today. Who may read a shared comment is decided in one place, and both surfaces ask it rather than each deciding. A second front door is a permanent cost, and this is what keeps it from becoming a second set of rules about who sees what.

Your private note stays private. It is the one thing in Scriptorium nobody has ever agreed to share — not with a friend, not with anyone — and the shelf listing does not carry it, another archive cannot reach it, and the endpoint that writes it writes that one field and refuses anything else.

Nothing about the website changes. Nothing about who can see what changes. A token still reaches one archive, its own.

This is the first of the steps toward a Scriptorium you could hold in your hand, and it is useful on its own whether or not that gets built.


v2.12.2 — A strip across the middle

Released: September 10, 2026

The scanner now reads a band across the middle of the picture, at the camera's full width, instead of shrinking the whole frame.

A barcode's information runs entirely sideways. The bars are the message; the height is the same message repeated hundreds of times, and a reader needs only a few rows of it. The scanner had been doing the opposite of what that implies — shrinking every frame by a fifth, losing detail on the one axis that carries the meaning, in order to keep hundreds of rows it had no use for. A band across the middle is thirty percent less picture to work through and a quarter more detail where the detail counts.

This came out of the line v2.12.1 added. Two people, the same book, one scanning and one not, and the line said both cameras were sending the same 1280×720. So the picture size was never the difference — and the theory that it was, published here nine days ago, was wrong. What actually differs is the reader: Chrome brings its own barcode reader on a Mac and on Android and does not on Windows, so the Windows machine falls back to the one Scriptorium carries, and that one had less to work with. Now it has more.

There is a bright band drawn on the preview, because otherwise the change would be a rule nobody could see. Hold the barcode inside it. The scanner also says so in words, for anyone using a screen reader, and it still checks the whole picture every fourth look — so a barcode held off to one side is still found, just a second or so later.

None of this makes a modest laptop camera into a good one, and it may not be enough on its own. The digits printed under the barcode still work on every machine, and always have.


v2.12.1 — The scanner says what it has

Released: September 10, 2026

The scanner now says what camera it got. One line under the status, from the moment the camera opens: the camera's own name, the resolution it actually negotiated, front or rear, and which of the two barcode readers is running.

Using FaceTime HD Camera · 1280×720 · front · ZXing

It is there every time, not behind a debug setting. The person who needs it is standing in an office being unable to scan a book, not a developer reproducing it afterwards — and on 10 September that was the whole difficulty. The same book scanned in a flash on one machine and not at all on another, three explanations were argued from first principles, and all three were wrong, because the scanner reported nothing about what it was doing.

And when the picture is small, it says so. The camera is asked for a 1280-wide frame, but that is a preference rather than a requirement, and a camera that cannot manage it quietly sends less. A barcode is a row of about ninety-five bars; in a 1280-wide frame the reader gets roughly four pixels per bar, and in a 640-wide frame roughly two, which is the edge of legible. Where the frame comes back small, the advice already on screen now explains that before offering the alternatives.

The first hint arrives at five seconds instead of twelve. Twelve seconds is a long time to hold a book at arm's length with nothing on screen but "Looking for a barcode…" in small grey type. The cost of a hint somebody did not need is a sentence; the cost of a hint that comes too late is the person giving up first.

None of this makes a small camera into a large one. It makes the next failure answerable in five seconds rather than an afternoon: if a camera turns out to be 640 wide, the answer is still a phone or the digits printed under the barcode — but it is an answer instead of a theory.


v2.12.0 — Looking up a book from a phone

Released: September 9, 2026

A new endpoint, GET /api/v1/lookup. Scanning a barcode and creating a book were both possible from an app already; fetching the book in between was not, because the lookup lived on a web route that wants a browser session. This is the same lookup, reachable with the same bearer token as the rest of the API.

It answers exactly as the website does — the same catalogues, the same merged result, and the same distinction between an ISBN that is not one, a book neither catalogue has, and a moment when the catalogues cannot be reached. It also says whether the book is already on your shelves, so a scanner can tell you that without asking twice.

Nothing about the website changes, and nothing about who can see what. A token reaches one archive — its own — exactly as before.

This is useful on its own whether or not an app is ever built.


v2.11.4 — Your friend does not need one already


v2.11.3 — A thumb you can see


v2.11.2 — A saved comment looks saved


v2.11.1 — The invitation letter says what friends can see

Released: September 8, 2026

The letter we send when inviting someone to Scriptorium said "Those notes are yours and stay that way." It was written before friends existed. Since v2.9.0 a friend you accept can read your notes on a book, so the sentence had stopped being true — in the one piece of writing a person reads before they have an account at all.

It now says:

Those notes are yours. Friends you choose can read them; nobody else can, and nothing is shared unless you say so.

Nothing about who can see what has changed. This is the last of the sentences that went stale when friends shipped: the two inside Scriptorium were corrected in v2.9.3, and this one lived in a document no code reads, so nothing could go red about it. A check now reads it along with the pages, and the old wording cannot come back.


v2.11.0 — Since you were last here

Released: September 8, 2026

A small gold dot appears beside Friends when something has happened. Open the Friends page and you will find what it was, listed newest first: who accepted your request, who has shelved books, who has asked to be friends. The dot goes when you look. There is no number on it and there never will be.

Home carries one line of the same thing, under the hero, when there is something to say. When there is not, both pages look exactly as they always have.

What it tells you, and what it does not

It tells you: a request of yours was accepted, a friend shelved books (all of them in one line, not one line per book), and somebody has asked to be friends.

It does not tell you: that a friend hid a book, that a friend ended a friendship, that a request you sent was declined, or anything at all about somebody who is not your friend. Those are quiet by design and stay quiet.

Nobody is told when you look. That sentence is on the page because it is true: looking at your friends writes one thing, on your own account, recording that you looked — and no page anywhere shows it to anyone else.

No email and no push notification, now or later. The dot exists to tell you something happened, once, not to bring you back.

What is not here

We wanted a line saying a friend had finished a book and how they rated it. Scriptorium does not record when a book's status or rating changed — only that it did — so there is no honest way to say "since you were last here" about it. Rather than guess, it is left out. If it arrives later it will be because the archive started keeping that date, not because we estimated one.

Nothing about who can see what has changed

Everything in the strip is something you could already see by visiting that friend's library. Hidden books are absent here exactly as they are there. No new permission of any kind was added for this release.


v2.10.3 — Joining through a friend's link makes the friendship

Released: September 8, 2026

Since v2.4.0, following a friend's invitation link has never actually made the friendship. You would be asked, you would open the link, the page would tell you that you and that person were about to be friends, you would sign up — and you would arrive with a Scriptorium and no friend. The request stayed in place, addressed to nobody, and appeared on no page either of you could reach.

Nobody saw an error, because there wasn't one. The step that was supposed to make the friendship looked for the request and could not see it — the archive only lets you read a request that names your Scriptorium, and a request sent to somebody who does not have one yet names nothing. Finding nothing looks exactly the same as there being nothing to find, which is why this went two days without anybody noticing. It could not have finished even if it had found the request: the part of Scriptorium that serves your pages is not allowed to write friendships at all, which is deliberate and stays that way.

It was found when Charles Adams joined on 8 September and was not made friends with the person who had asked him.

What happens now

If you arrive through a friend's link, you are friends when you arrive — which is what the page told you would happen.

If you sign up the ordinary way and somebody has asked to be friends with you, the request is waiting for you on your Friends page, to accept or not. It is not accepted for you: you never saw a page that promised anything, so the choice stays yours.

Charles

His friendship has been made, and John's request to him is marked accepted. He was told on the join page that it would happen, and holding him to a promise we failed to keep would have been the wrong way round. Nobody else was affected — his was the only request in this state.

If you have a request waiting from before today

It is attached to your Scriptorium now and will appear under "Waiting for you". Nothing has been accepted on your behalf.


v2.10.2 — Twenty friend requests a day, and asking again is free

Released: September 8, 2026

The limit on friend requests is now twenty a day, and asking again about somebody you have already asked no longer counts against it.

Ten a day was set before anyone had used the feature. The first person to reach it was reaching it while testing, which is usually how a number chosen in advance announces itself.

Asking again was being charged for

If your request to someone had not arrived — or you simply wanted it sent again — asking a second time re-sends the message and creates nothing new. It was still counted against your day's allowance, so the more you had used the feature, the more likely you were to be refused the one thing that costs nothing. That is fixed: a re-ask is free, and it stays free whether or not you are at the limit.

The refusal tells you when

It used to say "Try again tomorrow", which was wrong in two ways: the limit is a rolling twenty-four hours rather than a calendar day, and "tomorrow" is not something you can plan around. It now says the actual time:

You have sent 20 requests in the last day, which is as many as Scriptorium allows. You can send another after 1:16 pm on Wednesday.

Times are shown in Central time, which is where Scriptorium runs. It does not know where you are.

Why there is a limit at all

Worth saying plainly, since it is the thing standing in your way when you meet it. A friend request puts our email address in front of somebody who did not ask for it. Too much unexpected mail from one address and the large mail providers stop trusting it — for everybody, including the sign-in links that are the only way into your archive. A limit that is occasionally inconvenient is cheaper than a sign-in link in a spam folder.

Nothing about who can see what has changed in this release.


v2.10.1 — The friend request finally sends an email

Released: September 8, 2026

If somebody already had a Scriptorium, a friend request reached them silently. The request appeared on their Friends page and nothing told them it was there. Whether they ever saw it depended on their signing in and looking.

This was fixed on 7 September for everybody who asked after 11:12 that morning — but the five requests sent to existing users before then produced no message at all. One of them was accepted anyway, by somebody who happened to sign in and find it. Another is still waiting.

The message that now goes

{name} would like to share libraries with you on Scriptorium

{name} ({address}) has asked to be friends with you on Scriptorium.

If you accept, you will each see the other's books — the titles, covers and shelves, what has been read and how it was rated, and the notes, summaries and saved quotes on them. Never each other's writing. Either of you can hide a book from friends, or end the friendship, at any time.

Open your Friends page

Nothing happens until you choose. If you would rather not, ignore this; {name} is not told either way.

The message it replaces was correct and vague: it said "the notes and quotes you have saved" and left you to work out whether that meant notes on a book or the writing you keep here. It says now what crosses, and then says what never does.

It shows the asker's address beside their name, which is what the Terms promise: a request always shows who is asking. If they have not filled in a name, the address stands on its own.

It arrives in the same shape as your sign-in email — a plain-text version and a formatted one with a button, the link readable underneath it either way.

Nothing about who can see what has changed

A friend request has always been an invitation you answer. This is only the message telling you one arrived.


v2.10.0 — Comments on a friend's book

Released: September 8, 2026

You can now read the comments on a book a friend has. Open a friend's book and, under it, you will find what other readers wrote about that book — the same cards a person who owns it sees, with no name on any of them.

Until today a comment could only be read by somebody who had the same book. Scriptorium is small, and that meant most comments were read by nobody: of 349 editions, two are held by more than one person. A friend of somebody who has the book is a real reader — they can go and find the book, which is what a comment with a page number is for.

What has not changed

Nothing you wrote before today became more visible. Comments already in Scriptorium keep the audience they were written under: people who have the book, and nobody else. That is not caution, it is what the Terms you accepted said would happen — "it will apply only to comments written after the change, and only ones you opt in individually." If you would like your own earlier comments to be readable by friends of the people who have the book, write to support@spritzsolutions.com and we will widen yours, and only yours.

A hidden book hides its comments. If you have ticked "Hide from friends" on a book, your friends see nothing on it — not the book, not your notes, not your quotes, and now not the comments either. That last one is new: until today hiding a book had no effect on comments, because the only people who could read them already had the book themselves.

No name, still. A friend reading a comment through your copy sees exactly what a holder sees. They do not learn who wrote it, and neither do you.

Marking is still for people who have the book. A friend can read the comments and cannot mark them up or down.

Your writing is still private, always. Sermons, reflections, papers and letters are not shared with friends and never have been.

The Terms and the Privacy Policy have changed

Both moved to 8 September 2026, and you will be asked to accept them the next time you sign in. What changed: the paragraph saying who can read a shared comment, one sentence in the Friends section, and one clause in "Who can see it". The version you accepted before is still there to read beside the new one.

The box you write a comment in now says the wider audience before you write, and what it says when you type is what is recorded for that comment.

Something that was open for a day, and is closed

Between 7 and 8 September, a comment could reach further than anyone had decided it should. If a friend of yours had a book, and somebody had written a comment on that book, that comment could appear to you in a Scripture passage search — even though you did not have the book yourself and nobody had ruled that you could.

Nothing was misconfigured and nobody made a mistake in the ordinary sense. When friends arrived on 7 September, "a reader who has this book" was worked out by asking which books you were allowed to look at, and from that day that included your friends' books. The sentence did not change; what it meant changed underneath it.

It was found by a check written for this release — one whose whole job was to confirm that a friend could not already read these comments before the new feature gave them permission. It could, so the check failed, which is what a check is for.

It is closed. "A reader who has this book" now means your own copy, said in those words rather than inferred. The wider reading you have from today is the one described above: on a friend's book page, from comments whose authors wrote them under the new terms, with hidden books excluded.

The new Terms took effect only after that was closed, and not before. The sentence promising that comments written before today keep their old audience would not have been true otherwise, and a promise published on a day it is not true is the thing this release spent most of its effort on.

Seven comments exist in Scriptorium and three people wrote them. If you are one of them and would like to know exactly what was reachable, write to support@spritzsolutions.com and you will be told plainly.

Searching a friend's library

Comments do not appear in search results yet — only on the book's own page. That is deliberate and is its own decision: putting other people's words behind a search URL is a different question, and it will be answered on its own rather than as a side effect of this.


v2.9.3 — A page number is a locator, and two sentences that were not true

Released: September 8, 2026

Page numbers take the form you actually write

A page number could only be a single whole page. Typing 9-10 for a passage that runs across a break was refused with "A page number is a number, or leave it blank" — and so were xii for front matter, 243n4 for a footnote, and 9–11 typed with the dash a phone produces. Only 9 got through.

The refusal was true about how the number was stored and wrong about how people cite. A page number is now whatever you write down to find the place again, kept and shown exactly as you typed it — spaces, dashes, roman numerals and all. Page numbers already saved are unchanged and read the same as they always did.

The only things refused now are a page number spread over more than one line, and one longer than twenty-four characters, which is a paragraph in the wrong box.

A friend's quote now shows its page

Searching your friends' libraries turns up the quotes and ideas they have saved. The page each one came from was being left off — you could read the quote and not find it. It is shown now, in the same form it takes on their book page.

Two pages said your writing was private when it is not

Friends arrived in v2.9.0, and a friend you accept can read your notes, your summary, and the quotes and ideas you have saved on a book — unless you have hidden that book from them. The Terms and the Privacy Policy were rewritten to say so on the day it shipped.

Two sentences inside Scriptorium were not. The Notes panel on a book said "Nobody outside your Scriptorium can read these", and the review screen — the one built for deciding what a friend will see — said "Nobody can see your library yet". Both had been true when they were written and neither was true when you read it.

The Notes panel now says who reads them:

Friends you've added can read your notes on this book, unless you hide the book from them. Your writing is never shared.

And the review screen says that friends you have accepted can see everything not marked hidden.

Nothing about who can see what has changed in this release. What changed is that two screens now say what was already the case. Your sermons, reflections, papers and letters are not shared with friends and never have been.

A test now reads the pages themselves and fails if any of them promises a field is private, so a sentence cannot go on being true-when-written after the thing it describes has moved.


v2.9.2 — The sign-up page tells the truth

Released: September 8, 2026

Signing up could tell you a link had been sent when none had. If you had been invited and then went to create your Scriptorium, the page could answer "A link was sent to that address a moment ago. Check your inbox", and make you wait before trying again — when nothing had been sent to you at all and waiting would not have helped.

The cause: the wait between links was started when your invitation was created, not when a sign-in link was sent. Those are different pieces of mail, and only one of them is a link you can use. The wait now begins when a sign-in link has actually gone out, and following an invitation you have just received works straight away, as it always should have.

If the sign-in service does refuse an address, the page says so and says plainly that nothing was sent, instead of claiming something is on its way.

After you ask for a link

The page you land on is now an answer instead of a blank form. It tells you the address the link went to, that it comes from scriptorium@spritzsolutions.com, where to look if it has not arrived, and that there is nothing else for you to fill in.

Addresses with a plus sign are fine

A draft of this release was going to stop accepting addresses like you+notes@gmail.com, on the belief that they could not be signed in. That belief was wrong and no such change shipped. They work, they always have, and they still do.

Nothing about who can see what has changed in this release.


v2.9.1 — Deleting an account, for real

Released: September 8, 2026

If you ask us to delete your Scriptorium, there is now something that does it. The Privacy Policy and the Terms have promised this since they were first published; until today the promise was kept by hand and nothing carried it out in full.

Write to support@ and ask. Everything you kept goes — your books and the notes, quotes and ideas saved on them, your shelves, your writing — and so do the comments you shared on books, which disappear from other readers' view as well as your own. Deleted means removed, not hidden and not left behind with your name taken off. You will get a reply saying in plain words what was deleted, with the counts underneath it.

If you would rather delete only part of it, you can: your archive and not your shared comments, or your shared comments and not your archive. Ask for what you want.

Your sign-in itself is removed separately, by a person, looking at it. Nothing automatic can delete the way you get into Scriptorium.

If you have friends, ending your account ends those friendships. They are not told why.

Addresses that end in a full stop

An email address with a full stop on the end was being accepted, and mail sent to it never arrived. Typing you@example.com. — one keystroke too many — got as far as an invitation that could not be delivered, and nothing said so.

Addresses are now checked in one place instead of five, and the database refuses the bad shape as well, so a typo cannot reach an invitation by another route. Addresses with a + in them still work; they are real addresses and always were.

The one undeliverable invitation that had already been written has been removed. Nobody was waiting on it — it was never accepted and could not have been.

Nothing about who can see what has changed in this release.


v2.9.0 — How many books are in Scriptorium

Released: September 8, 2026

The Home page now shows the total number of books across every archive, in large figures at the top right, with "and counting" underneath. It is a real count, never rounded, and it goes up when anyone adds a book.

Nobody can see anyone else's books because of this. The number is answered by a single question the database will answer — "how many books are there" — and that question returns one number and nothing else. It cannot be asked whose they are, or what they are called.

Books you have hidden from friends are counted. Hiding a book keeps it off a friend's page; it does not mean the book is not on your shelf.

A migration from 29 August had never reached production

The two functions that answer "how many books" and "how many people" were written on 29 August and never applied to the live database. Nothing was broken by it and nobody could have noticed: no page had ever asked either question, so there was nothing to fail. It came to light only when the Home page needed the first of them.

The gate that applies these changes now checks that no earlier one is missing before it runs, and refuses if any is. It was the missing piece in a set of checks that could tell whether the database matched the code, but not whether the code had ever been applied.


v2.8.1 — Sign-in emails have a button

Released: September 8, 2026

The sign-in email was a long web address and not much else. It now has a button — "Open your Scriptorium" — with the address underneath it in small type, for the mail programs that strip buttons out and for anyone who would rather copy it.

The words are the same, and so is everything else. If your mail program shows plain text rather than formatting, you get exactly what you got before.


v2.8.0 — Every email comes from Scriptorium

Released: September 8, 2026

Sign-in emails are now written and sent by Scriptorium, like the friend messages already were, instead of being composed elsewhere and relayed. Nothing changes about how you sign in: you ask for a link, a link arrives, you use it.

What this makes possible is that the words in those emails now live in the same place as everything else, where they can be read and checked. The welcome email gets its own subject line for the first time — "Welcome to Scriptorium" rather than the transactional one it had to share.

If you are already signed in, you will notice nothing at all.


v2.7.4 — Asking again sends the message again

Released: September 8, 2026

If you asked somebody to share libraries and the message did not reach them, asking again did nothing at all — the page said your request had been sent, and nothing was sent. The request was already waiting, and the software took that to mean there was nothing to do.

Asking again now sends the message again, which is what the message itself promises: if the link lapses, they can send another.

Declining is unchanged and still quiet. If somebody declined you, asking again still sends them nothing, and they are still not told either way.


v2.7.3 — Two corrections to the invitation

Released: September 8, 2026

The message sent to somebody who does not have Scriptorium yet no longer carries stray asterisks around a word, and now ends the way the other message does: if you would rather not, ignore it, and the person who asked is not told either way.


v2.7.2 — The friend messages actually send

Released: September 8, 2026

The messages introduced yesterday were being refused before they reached the mail service, so nothing arrived and the person asking was told the send had failed. Fixed. Asking again sends the message.


v2.7.1 — The invitation says what Scriptorium is

Released: September 8, 2026

The message sent to somebody who does not have Scriptorium yet now explains what it is before asking them to join: what you keep in it, what you can search, that it costs nothing, that there is no password, and that your writing is private and stays that way. It still says who invited them and what sharing a library means, and it still carries the link and how long it lasts.


v2.7.0 — The friend messages are the ones that arrive

Released: September 8, 2026

Asking somebody to share libraries now sends them a message that says who asked and what it means.

Until today it did not. Somebody who already had a Scriptorium was sent a sign-in link — no mention of a request, nothing to say one was waiting. And somebody who did not have one was sent the generic "Welcome to Scriptorium", with no mention of who had invited them or why. The two messages that were written for this had never once been sent.

They are sent now, by Scriptorium itself, from scriptorium@spritzsolutions.com — and a reply goes to support@spritzsolutions.com, where a person reads it.

If you asked somebody to share libraries in the last few days and never heard back, this is likely why: what arrived did not tell them there was anything to answer. Asking again will send them the right message.

Nothing changed about who can see what, or about signing in.


v2.6.0 — Friends, as people

Released: September 7, 2026

The Friends page and a friend's library have been redesigned. Nothing about who can see what has changed.

The Friends page used to open with a form before it showed a single person, and a friend was a line of prose with a heavy button at the end of it. It now leads with the people: each friend is a card with their initials, how many books and shelves they share, and the covers of the five they shelved most recently, newest first — so somebody who has just been reading is at the top, and anyone with nothing shared yet is at the end. Requests waiting for you come first, since they are the thing that needs you; requests you have sent sit quietly at the bottom.

A friend's library is now their page rather than a copy of yours with a warning on it: their name and initials at the top with three numbers — books shared, shelves, reading now — then their shelves as buttons you can filter by, then their books shown cover-first. Empty fields simply are not there any more; the page used to print "Status: —" and "Rating: —" as though not knowing something were worth saying.

A book with no cover now shows its spine with the title on it, everywhere covers appear — your library, a friend's, and Search. Some books were showing a tiny dot instead, because one of the catalogues answers "I don't have that cover" with a picture of nothing rather than with an error.

A friend with no name of their own no longer has their email address printed twice. It appears once, and their initial comes from it.

All three pages work on a phone.

Nothing here changed the Terms, the Privacy Policy, or who may see what. A friend sees exactly what a friend saw yesterday: your books that you have not hidden, your notes and quotes on them, and never your writing.


v2.5.3 — Adding a book by ISBN works again

Released: September 7, 2026

Since 5 September, adding a book by ISBN has not worked at all. On the Add Book page, pressing Lookup did nothing and pressing Scan Barcode did nothing — no camera, no message, no sign that the button had been pressed. Typing the title, author and the rest by hand and saving worked throughout, which is how books kept being added and how this went unnoticed for two days.

The cause was a single character in the page's code, which stopped the whole of that page's JavaScript from running. Editing an existing book was never affected, and neither was anything else.

Fixed, and now checked: every page the tests render has its JavaScript parsed, so a page that looks fine and is dead cannot pass again.

If you added books by hand during those two days, nothing is wrong with them — they are ordinary books and a lookup will fill in anything missing.


v2.5.2 — The scanner stops pretending

Released: September 7, 2026

If the camera could not read a barcode, nothing ever said so. The preview stayed live and the words stayed "Looking for a barcode…" for as long as you were willing to hold the phone there. There was no way to tell "keep going" from "this will never work".

After about twelve seconds it now says it is still looking, and what usually helps: more light, and holding the barcode about a hand's width away. After forty it says plainly that some barcodes are too worn or too small for a phone to read, and that the digits printed underneath can always be typed in. The camera stays open through both — you might be a second away.

Taking a photo of the barcode is back, in the scanner window where you need it rather than as a third button on the form. A still photo often reads when a live view will not hold focus.

Two smaller things. If the barcode reader itself failed to load, the message said the camera could not be opened — it now says which one actually failed. And every message the scanner shows is now announced to a screen reader, which it should have been from the start.


v2.5.1 — The lookup says what is actually wrong

Released: September 7, 2026

If new Terms were waiting to be read, pressing Lookup on a book form said "Could not reach the book lookup. Check your connection and try again." Your connection was fine. There were documents waiting, and the button had no way to say so — it asked the server, the server answered with the acceptance page, and the form could not read a page where it expected a book.

It now says there are new Terms and a Privacy Policy to read, with a link straight to them. Accepting takes you back, and the lookup works.

Nothing else changes: typing an address into the bar still takes you to the acceptance page as it always did.


v2.5.0 — Search finds your friends' books too

Released: September 7, 2026

Search now looks in your friends' libraries as well as your own.

Your own results come first and are exactly what they were. Below them, if anything matched, a separate section headed "From your friends' libraries" — never mixed in with yours. Each result says whose shelves it is on, and opens on that friend's page rather than anywhere in your own library. Their quotes and ideas on those books are found the same way, and labelled the same way.

If nothing of your friends' matched, the section is not there. If you have no friends yet, the page is exactly as it always was.

Books you have hidden are not found by any friend's search. Your own search finds all of your own books, hidden or not — hiding is about friends, not about you. Your writing is never searched by a friend, and neither is who you have lent a book to. Ending a friendship stops this at once, in the same moment it stops everything else.

The Terms do not change, and that is the point. Nothing new is shared — only found. Every book this can reach was already open to that friend at their library page; this is a second way of arriving at the same place.


v2.4.2 — Ending a friendship asks first

Released: September 7, 2026

Two changes to the Friends page, both about which action the page puts in front of you.

Reading a friend's library is the point of the row. Their name links to it, and there is now a "View library" button that says so. Their email address is shown beside the name, as text.

Ending a friendship asks first. It used to be a button in the row, one mis-click from something you cannot undo on your own — the other person has to ask again, and they are not told it happened. It is now a quiet text link to a page that tells you what ending it does before you do it. Nothing else has changed about it: either of you can still end it at any time, and it still stops both directions at once.


v2.4.1 — The friend policies are switched on

Released: September 7, 2026

A defect found by the confirmation run immediately after v2.4.0's migration was applied. The two new friend tables were created with row-level security forced but never enabled, which are two different flags — so their access policies were present, correct, and enforcing nothing. No row was ever exposed: both tables were empty, no browser-facing role could reach them, and the application code that reads them had not yet been deployed. Enabled by migration 202609070033, and every check that was reading the wrong flag now reads both.


v2.4.0 — Your friends' shelves

What a friend never sees

Your writing. Sermons, reflections, papers and letters are not shared by Scriptorium and there is no setting that shares them.

Whom you have lent a book to. That names somebody who never agreed to anything.

Anything you have hidden, and anything you wrote on it.

They read as a reader: nothing on those pages can be changed by them, and you are not told when they look.

The Terms and the Privacy Policy change

They have to, and this is the sentence that changed. The Terms said your books, notes and shelves were private. That is no longer the whole truth, and a document that says it while a friend is reading your notes would be worse than the feature is good. They now say they are seen by you and by friends you have accepted, and by nobody else — not through an address you have not added, and not through a friend you have not accepted.

You will be asked to accept the new versions the next time you sign in. The old ones are not edited; what you agreed to before is still on record, exactly as it was.

Notes

Declining is quiet. Nobody is told they were declined, and the person who asked simply stops seeing the request.

The friendship is enforced by the database, not by the pages. A friend's books are reachable only through a view that has no column for lending and no column for anything hidden, behind a rule the database applies to every query — so a page that forgot to check would still be refused.


v2.3.1 — The hide tick moves to where you edit a book


v2.3.0 — A book you can keep to yourself

Why the tick arrives before the friends do

Nothing can see your library today. Nothing could see it yesterday. Sharing with friends is the next release, and this one exists so that there is a gap between the two.

Your notes were written under a promise that they were private, and the Terms say that nothing you have already written becomes shared because a feature was added. Shipping the tick alongside the friends would have kept that promise on paper: you would have been told about sharing and about the way to prevent it in the same breath, with a library built under a different expectation and no time to go through it. So the control comes first, and the thing it controls comes after.

Notes

Nothing else changes. No page you use behaves differently, no book moves, and every book is visible-by-default because there is nobody to be visible to. A tick you make today is a decision recorded in advance of the thing it decides, which is the only order in which the decision is really yours.

The Terms do not change in this release. Hiding something from a feature that does not exist changes no promise. They change when friends do.


v2.2.0 — Nothing to pay, for now

What this is not

It is not a promise that Scriptorium will always be free. That promise has not been made, and the Terms do not make it. What they say is that there is nothing to pay today, that there is no plan to change that, and — the sentence that has been there since the very first version — that if a paid tier ever exists you will be told before it applies to you, and nothing you have written will be locked away because you stopped paying.

Those two things are different, and the difference is the point. A price that is no longer intended is wrong. A promise of permanence that nobody has made would be worse.

Notes

The minor number moves because a Terms version is something a reader meets: you will see the new version and be asked to accept it.

Nothing about how Scriptorium works has changed, and there was never any billing to remove — no card was ever asked for and no payment was ever taken.


v2.1.2 — A cover that is a blank pixel

About the covers already saved

This fixes what gets written from now on. It does not repair the roughly 120 books already carrying a blank pixel — those are rows in the database, and repairing them is a separate, deliberate act with its own report to read first. Nothing on those pages will look different yet.

Note

Google Books is not probed and deliberately so: it returns a cover link only when it has one, so its silence was already truthful. This was one provider's behaviour and the fix stays that size.


v2.1.1 — A book is not a duplicate of itself


v2.1.0 — Something other than a browser can reach your archive

Scriptorium now has a small JSON API, and this release is the whole of it: your shelves can be listed, and a book can be added, by something that is not a web browser.

Nothing on the website looks or behaves any differently. There is no new page, no new button, and nothing you need to do.

Why it exists

The question was whether Scriptorium could have a phone app. The useful answer turned out to be that the one thing a phone does that a laptop cannot is be in your hand at a bookshelf — scanning a book onto a shelf in two seconds, in a bookshop or a friend's study, with a camera that works properly. Reading a sermon or searching a passage already works in a phone browser and would gain nothing from being rebuilt.

Everything such an app would need was already here — signing in, looking up an ISBN, telling you that you already own the book, saying when the lookup itself is broken — except two things. This is those two things.

It does not commit anyone to building the app. Whether that happens is a decision for later, and it is a better decision to make with this in place.

How it keeps your archive yours

The phone route was the tempting one and it was refused. A mobile app can usually talk to the database directly, and doing that here would have meant building every rule that protects a person's archive a second time, for a second identity, on the device most likely to be lost or stolen — and then keeping the two in agreement forever, where disagreement means somebody's private archive.

So this talks to Scriptorium itself. Same checks, same scoping, same tests. Reaching it takes the same verified sign-in the website takes, and the request is confined to the archive that sign-in belongs to.

Notes

A new capability rather than a fix, so the minor number moves — the first time this product has had a way in that is not a browser.

The one part worth writing down: a book added this way gets its edition recorded, exactly as one added through the form does. That is what lets a book take part in shared comments, and it would have been the easy thing to lose, because this was built for a barcode scanner and editions are about comments. Both ways of adding a book now go through the same piece of code, so there is no longer a second place it could be forgotten.

Not authenticated by the browser's sign-in cookie, deliberately. That cookie's protection against a hostile website depends on a browser behaviour, and a new way in should not inherit a dependence sized for a different surface.


v2.0.4 — Looking up a book you already have


v2.0.3 — The lookup says it is working


v2.0.2 — Three small things


v2.0.1 — The Terms are readable

The links to the Terms of Service and the Privacy Policy on the sign-up form asked you to sign in before you could read them — the two documents you were being asked to accept, behind the door you were trying to come through. Both now open to anybody.

And the sign-up form refused one of its own two answers. Choosing Not a student produced "Please say whether or not you are a student" — the form asking again for something you had just answered — and there was no way past it. Only students could complete a sign-up.

And it described shared comments wrongly, under the name fields. It said comments carry your first name and last initial. They carry no name at all, and have since the day the feature was designed — so the one sentence on that page explaining why a last name is asked for was telling people the opposite of what happens. It now says what is true: your name is how we write to you, and it is never shown to other readers.

All three were found by people walking through the form; registration was closed while they were fixed.


v2.0.0 — Comments, and a Door of Your Own

A major rather than a minor, and the reasoning is in the Notes.

Since 25 August. This is the release where Scriptorium stops being an archive one person keeps and becomes something other people can be in.

Comments on books

You can write about a book and let other readers see it. Not a review — the thing you would have written in the margin, with the page number, and the passage it is about.

Who sees it is decided by one rule: the people who have the same book. Somebody who does not have it — searching a passage, which is the only place you meet a book that is not yours — sees that comments exist and how many, and nothing else: not the words, not the citation.

A page number says which printing it came from. Scriptorium knows the difference between two printings of a book, so a citation on a comment says which one it is counted in — "p. 47" means nothing on its own.

Comments are grouped by that printing, which means two people holding different printings of the same title do not yet meet. Showing them to each other, in a labelled group, is decided and not built.

Anyone can sign up

There is a sign-up page. A name, an address, whether or not you are a student, and optionally your school and the year you expect to finish. A link arrives; opening it makes the Scriptorium. No invitation and no waiting for somebody to let you in.

The first email is a welcome now rather than a bare confirmation, and it says what Scriptorium is for. If you add a second sign-in address later, that one is not welcomed again — you are not new.

The research section is gone

Research entries have been removed, and what they did has moved into comments, which carry the same page number and the same Scripture reference and can be shared with other readers of the book.

Four research entries existed. All four were John's — nobody else had ever written one — and they were handed back as readable text before the surface came down. Nothing was deleted.

The book page

The panels no longer choose their own column. Identity, summary and lending sit on one side; comments, notes and Scripture Connections on the other, so the thing you came to the page to write in is not below the fold.

Scripture Connections is always there now, even when a book has none, and it says what a connection is for — how a book turns up when you search a passage — with a link to add one. It says "most books do not have one", because most books do not.

Terms and privacy

Two new versions, on 30 and 31 August, and you will be asked to accept them. They say what shared comments show and to whom, that marks are recorded and never shown to the author, and what happens to what you type into the sign-up form if you never open the link. The word anonymous appears in neither.

Notes

Shipped and visible were different dates. The tables, the policies and the routes went to production over several days behind a switch that was off, and the feature was rehearsed against a restored copy of the real archive rather than against invented data. The switch was thrown on 30 August, and shared comments have been visible to everyone since that afternoon.

It is worth saying because a changelog dates a feature to the release that contained it, and for a fortnight the honest answer to when did comments appear was neither the date of the commit nor the date of this entry. It was an afternoon in between.

On the number. The scheme so far has been feature-named minors, and this is a major instead: a surface was removed rather than added, the centre of the product moved to something other people take part in, and a stranger can now create an account without being let in by anybody.

Nothing here is a compatibility break in the usual sense — there is no API and one deployment — so the number is doing the only job it can do in a product like this, which is to tell a reader that this release is different in kind from the twelve before it. Said plainly because a reader in a year will otherwise wonder what broke.


v1.10.1 – v1.10.3 — three tagged releases with no entry

Written 30 August, five days late. All three were tagged on 25 August and none was written up. Recorded here rather than skipped: a changelog with a hole in it is a record that has stopped being one, and the gap was found by looking for the last entry before writing the next.

v1.10.1 — the legal text reaches the server. The Terms and the Privacy Policy are markdown files read at request time, and the deployment was not shipping them. /agreements would have failed on production while working perfectly everywhere else. The files are now listed as something the deployment must contain, and a test fails if they are not.

v1.10.2 — consent controls that cannot be mis-ticked. Each checkbox sits against the sentence it belongs to. Two boxes and two documents, and it had been possible to read one and tick the other.

v1.10.3 — an option group names the slot its readings fill. Lectionary housekeeping: where a Sunday offers a choice of readings, the group now says which slot in the service it is for.


v1.10.0 — Terms, a Privacy Policy, and a Record of Who Agreed

Released: August 26, 2026

Scriptorium has written terms and a privacy policy, versioned by date, and a record of who accepted which version and when.

Added

Versioned agreements

legal/terms/<date>.md and legal/privacy/<date>.md, one file per published version, front matter carrying title, version and effective date. The .md files are the single source and are rendered at request time — a generated HTML copy would be a second artifact that can drift, and drift is what this project keeps paying for. markdown is the third dependency this project has, after Flask and psycopg.

app/agreements.py:CURRENT_AGREEMENTS is the one place a current version is named. A test walks every .py file under app/ to prove nothing else names one.

A published version is never edited, and that is asserted from both sides:

agreement_acceptances

Which vault, which document, which version, which sign-in address, when — and the SHA-256 of the exact bytes accepted, so the text a person agreed to can be produced years later rather than merely asserted.

Append-only, enforced twice. REVOKE binds here in a way it does not for row-level security: the application owns every table but is not a superuser, so privileges revoked from it are genuinely withheld. It is still an owner and can grant itself back, so a trigger refuses UPDATE outright and refuses DELETE while the vault exists. DELETE is allowed only as the cascade from a vault that has already gone — a blanket block would have made every vault undeletable, exactly as the first version of vault_keeps_at_least_one_address did.

Acceptances are written in the same transaction as the vault, in resolve_or_create_vault(). A Scriptorium that exists without them is one whose owner never agreed to anything, and there is no later moment at which that can be fixed truthfully.

The request gate

A before_request check in the shape of the login gate, redirecting to /agreements when a new version is published, with the agreements routes, the auth routes and static exempt so it cannot loop.

Its cached "yes" is keyed on the versions themselves rather than a clock. Publishing changes the fingerprint, every session's pass stops matching at once, and nobody carries a stale one for up to a window. The "no" is never cached.

The page distinguishes new from changed, per document rather than per page: a vault can have accepted the terms last year while the privacy policy is new. On the day this ships every existing vault is in the first case, and telling those people a document "changed since you last accepted it" would have been false for all of them.

Notes

Four things the page rehearsal caught on the new blueprint before any of it shipped: /agreements returned 500 when the acceptance table was absent, two routes had no sample values, /join was never rendered because its flag defaults off, and the harness itself was rendering against a schema older than the code. It now brings the restored cluster up to the current migration set first and asserts by name that the tables the code reads are present.

No attorney has reviewed either document. That review lands as a new version, not an edit — which is the case this whole structure was built to handle, so its first real use will be the one that matters most. Recorded in TODO.MD.


v1.9.1 — A Local Archive That Can Run Multi-Tenant Mode

Released: August 25, 2026

Nothing user-facing. /account and everything beside it could not render against a local SQLite archive, so the only place to see multi-tenant mode work was production.

Fixed

init_db() also adds vault_id to the eight tables 202607300002 scopes. Every vault-scoped query filters on it; five new tables without it would still have produced a local archive that could not answer a page.

Added

Running multi-tenant mode locally

python -m scripts.seed_local_vault
SCRIPTORIUM_MULTI_TENANT_ENABLED=1 python run.py
# then open http://127.0.0.1:5050/dev/sign-in

The seed adopts the rows already in the archive, without which switching a local database into multi-tenant mode makes every book disappear — which looks exactly like a bug.

/dev/sign-in cannot reach production, by construction rather than by care: run.py registers it only inside if __name__ == "__main__", and Vercel's entrypoint is run:app. A subprocess test takes Vercel's exact path and asserts the route is absent. The login gate lets it through via AUTH_EXEMPT_ENDPOINTS, a declared set empty everywhere else, so nothing in app/ knows a development route exists.

A third rule in the rehearsal harnesses

An observation is not a test. A verification that ran before a later change is stale, however carefully it was watched at the time.

Learned here. The seed script was run end to end, worked, and then broke on every first run: vaults.id was TEXT PRIMARY KEY when it was watched and INTEGER AUTOINCREMENT forty minutes later. No branch went unexercised and nothing was overlooked — the evidence simply had a timestamp on it, and the timestamp was older than the schema.

It joins the two already there: a check that pipes through anything swallowing a non-zero exit is not a check, and a check that cannot fail proves nothing. All three are in rehearse_migrations.sh and echoed by the three harnesses beside it.

Notes

Three times this week a comment was correct while the code beneath it was wrong — classify() said roles come from the book while returning them by position, the holy_day_role note described a rule its own code contradicted, and the vaults.id comment named a helper the script did not call. A comment that is right while the code is wrong is the failure mode where reading the file makes you more confident, not less.

202608260009 is still not applied.


v1.9.0 — The Scripture Module

Released: August 25, 2026

Browse by book, by chapter, and by day of the church year. The surface that makes v1.7.1 and v1.8.0 visible — 232 occasions, 110 days, 1,533 lectionary references, and 31 Holy Days that nobody had yet seen rendered.

It was also the first real inspection of that import, and rendering the data asked questions of it that no query had. Four of the answers were defects.

Added

Two trees that meet at a passage

Scripture — /scripture, /scripture/luke, /scripture/luke/15. All 80 books in canonical order, including the quiet ones: a canon with sixty empty books is a map of where thirty years of reading has actually gone, and hiding them would turn the page into a list of recent enthusiasms.

The church year — /lectionary/day, /lectionary/day/all-saints-day. 110 days grouped by season in church-year order. A year-varying day renders Year A, B and C as sections and a fixed day renders one, on the same code path. Below them, every sermon linked to any of that day's reading sets in any year — ten All Saints sermons across thirty years, in one place.

A sermon appears there only through writing_lectionary_contexts, never because its Scripture happens to overlap. The test seeds a sermon whose passage is exactly the day's gospel and asserts it stays off the page.

One predicate, one renderer

app/passages.py is now the only place that asks what the archive holds about a passage, and _passage_results.html the only place that renders it. Scripture Search was rewired through both rather than keeping its own copies.

This is the point of the release, not housekeeping. Until v1.7.0 the passage-overlap predicate existed twice, byte for byte, and both copies had the same bug for months. The test compares what the two pages render, not what two queries return — a shared query that each page filtered differently afterwards would pass the weaker test.

Standing checks

Rehearsal harnesses

Fixed

python return "old_testament" if is_first else "epistle"

77 readings carried a value their own citations contradict — 53 under old_testament (35 Acts, 17 Apocrypha, 1 Revelation) and 24 under epistle (14 Acts, 10 Revelation). The live Scripture Search page printed the role directly, so it labelled Revelation 7:9-17 "Old Testament" under All Saints while calling the same passage "Epistle" under the Fourth Sunday of Easter.

202608260009 moves all 1,001 non-gospel readings to the four slot names — first_reading, response, second_reading, gospel — and option_group's epistle with them, so one vocabulary exists rather than two agreeing on half their values.

Changed

The column could not answer this. It disagrees with the citations in seven places: four rows marked canticle that are not Prayer Book canticles, two marked psalm that are not psalms, and Canticle 9 marked psalm.

Migrations

202608260009_one_vocabulary_for_the_slots — not applied. This release is the expand step.

The rule is apply-then-merge, and it inverts here for a reason the rule implies: this migration rewrites values the live page renders, so applying first would print first_reading and response as raw strings on a page people are using. Neither order is safe alone, so the change is split — expand (a display that accepts both vocabularies, this release), migrate (apply against a site that already understands its output), then contract (delete the old keys, recorded in TODO.MD).

Both knowing inversions of that rule are recorded in ARCHITECTURE.md beside the rule itself.

Deferred

Recorded in TODO.MD with their reasons: the fixed-date Holy Days have no calendar date — it is in the source manifest for 27 of the 31 and the converter drops it, so they sort by name for now, which is the one thing this module otherwise refuses to do. And 28 of the 31 list their psalm after the gospel in reading_order; whether that record should be renumbered is a liturgical judgment before it is a data one.


v1.8.1 — The Account Page Renders Its Dates

Released: August 25, 2026

A fix release, on its own, ahead of v1.9. /account returned 500 for every user from the moment v1.8.0 deployed.

Fixed

SQLite returns every timestamp as a string, so strptime raised ValueError, the filter degraded to the raw value, and 310 tests passed. PostgreSQL returns datetime objects for timestamptz, and strptime raises TypeError on one of those — uncaught, 500.

Every other dateformat call site renders writings.date_preached, which is text in both databases. The Account page renders vault_memberships.created_at and vault_invitations.created_at, both timestamptz. It is the first page in this application to render a real timestamp, and it broke the day it shipped.

The rule now lives in app/dates.py and is total by construction: it accepts date and datetime objects directly, catches TypeError alongside ValueError, and returns anything it cannot read unchanged — so the next type that arrives looks wrong instead of taking a page down.

Added

A page-render pass against real PostgreSQL

scripts/rehearse_pages.sh. This is the fourth time SQLite and PostgreSQL have diverged in production — after INSERT OR IGNORE, an int passed into a boolean column, and LIKE needing to become ILIKE — and the suite structurally cannot catch the class, because every test renders against SQLite.

So it restores the schema and data backups the gate already takes, signs in as a real membership, and requests every registered route:

  34 rendered, 4 skipped, 0 failed

Proved both ways. With the broken filter restored, it reports FAIL /account/ raised TypeError in seconds. With a filter that degrades silently instead of raising — a 200 with a raw timestamp on the page — it reports no formatted date — a timestamp rendered raw, because a status code alone would not have caught that.

Two things it needed before it could prove anything. The gate's data backup is public-only, deliberately, and public.vaults.created_by references auth.users, so a straight restore failed every row in the vault-owned chain — 296 books, 24 writings, 5 memberships — leaving a cluster that looked restored and held nothing to render. And last_signed_in_at was null on every row in the backup, which was taken minutes after the column was created, so the newest branch on the newest page would have been skipped in silence; the harness stamps one row rather than hoping.


v1.8.0 — Your Name, and a Second Way In

Released: August 25, 2026

Imagine you are a student at Sewanee and you graduate and they are about to cut off your email. How do you not lose access?

That question is the whole release. Scriptorium is meant to hold decades of reading and preaching, and until today the only key to a vault was one email address — usually one issued by an institution the person will eventually leave. An archive that outlives a diocese, a parish and a seminary cannot be reachable only through an address a registrar controls.

Added

The Account page

Your name. First name and last name. That is the whole section, and the heading is the whole prompt: the page does not explain what the name will be used for later. That sentence is true in the compose flow, where a comment is about to carry it, and it arrives there with comments.

How you sign in. Every address on the vault, with when it was added and when it was last used. Add one. Remove one.

account_profiles

One name per Scriptorium, first_name and last_name, keyed on vault_id rather than on the auth user. Two sign-in addresses are two auth accounts for one person: keyed on the account, the name would have to be typed twice and attribution would depend on which address happened to sign in — one person appearing as two readers. This is the failure D4 and E.4 describe, and it holds because one vault is one person; multi-member vaults would have to revisit the key alongside the comment policies.

D4 is unchanged in effect. The shared comment view still exposes a first name and one initial; the initial is derived at read time as left(btrim(last_name), 1) rather than stored, so there is no second copy to disagree with the surname it came from. The seven-field allowlist and its column-set test are untouched.

Not named public_profiles: the table holds a surname, and a surname never leaves the vault.

last_signed_in_at

Written on the vault-resolution path, which is the one path every sign-in takes. It is what makes a stale institutional address visible while it still works.

The operator recovery script

scripts/grant_sign_in_address.py. Self-service cannot help someone with no working address — they cannot receive the link. With ten users that is a conversation rather than a system, but a documented one, so it is a script rather than an operator improvising SQL at a prompt. It also updates vault_memberships.email when an auth account has drifted from its membership, which is why the login gate is not widened: widening it would mean trusting a typed address before anything had verified it.

Fixed

It is now a deferred constraint trigger that asks about the vault rather than about the row. rehearse_migrations.sh deletes five things to prove which two are refused: the only address, and the auth account left holding it. The fourth case is the one worth reading — an auth account can only strand a vault if it holds the last membership and did not create it, because vaults.created_by is a plain foreign key and PostgreSQL already refuses to delete a creator.

Where it would otherwise have surfaced is the point. That script runs after a migration commits and the gate fails if it fails, so the false alarm would have arrived about a change that had already happened. function_is_recorded() and trigger_is_recorded() are now named, and the checks and the tests call them rather than each carrying a pattern.

Deferred

Recorded in TODO.MD rather than dropped, each designed and written down before being held back:

Migrations


v1.7.1 — The Holy Days, and a Gate to Put Them Through

Released: August 25, 2026

The lectionary was missing its fixed-date calendar. v1.2 claimed the Revised Common Lectionary was imported and v1.4 corrected that to Years A, B and C only — the sanctoral calendar, the days that fall on a date rather than in a cycle, was never converted. Christmas Day, the Epiphany, Holy Cross Day and twenty-seven others were absent from an archive that exists to hold sermons preached on them.

They are in. Getting them in meant first fixing what the calendar already held, and then building something to run a content change through, because this release started by applying an index to production by accident.

Added

The fixed-date Holy Days

A gate, and one path to production

Standing checks

scripts/verify_schema_baseline.py grows from six checks to eleven. The five added here are about the lectionary rather than about privileges:

Fixed

Withdrawn

Changed

Notes

Two rules came out of this release and are written into the rehearsal harnesses' own docstrings so the next script inherits them:

  1. apply_migration always takes both backups. It is never taught to classify a migration as content-touching or not, because that is a judgment call, and judgment is what failed.
  2. No verification step pipes through anything that can swallow a non-zero exit, and every restore or verify asserts on a count rather than on the absence of visible errors.

v1.7.0 — Scripture Search

Released: August 25, 2026

Scripture Search was a v1.2 goal. /scripture-search has answered "coming soon" ever since, while the Home page and the Search page both linked to it. It answers now.

Finding it required fixing the thing that would have made the answers wrong.

Fixed

So a reference spanning chapters could be stored and then never found again. Matthew 5-7 did not answer a search for Matthew 6. John 3:16-4:2 did not answer John 4:1. Both are forms this project documents as supported, and both go in through a parser built to make sure a reference is never dropped in silence. The archive could lose a relationship on the way out instead of on the way in.

The live archive holds 39 references that span chapters. All are lectionary readings, so no personal relationship was lost — but they include the Good Friday Passion (John 18:1-19:42) and the Servant Song (Isaiah 52:13-53:12), which are not obscure things to search for.

Both sides are now normalised to one integer per endpoint, chapter * 1000 + verse, so the question is a single overlap test rather than a tree of IS NULL branches. The predicate lives once, in app/scripture.py, with the table as a parameter.

Resolution is now by auth user id alone. The address is a label, refreshed on sign-in; if it collides with one another vault holds, the old label stays and the person is let in regardless. Failing a sign-in over a display value would trade a cosmetic problem for a serious one.

.env.example already described the two variables as separate, calling MIGRATION_DATABASE_URL "used only for schema migrations", so this was a correctness fix rather than a preference. The fallback is gone; every script in scripts/ already named that variable explicitly and none relied on it.

Added

Scripture Search

Sign-in addresses

Guards

Changed


v1.6.2 — Close the Schema Drift

Released: August 24, 2026

Nothing in this release is visible in the application. It closes the gap between what supabase/migrations/ says the database is and what the database actually was, and withdraws two sets of privileges nobody meant to grant.

The gap had been open since July 30 and nothing had noticed, because nothing ever asked.

Fixed

Nothing was exposed: row-level security denied every one of those operations, and no advisor flagged them. But the membership table is what every RLS policy joins against and what decides which archive a person sees, and the only thing standing between the public internet and TRUNCATE public.vault_memberships was the continued presence and correctness of those policies. One permissive policy added carelessly, or one disable row level security typed while debugging and not typed back, and there was no second layer.

202607290001 revoked these grants on the twelve tables it created. 202607300002 created three more and revoked nothing, so they kept Supabase's defaults. anon now holds no privilege anywhere in public.

Functions cannot be closed this way. PostgreSQL hard-wires EXECUTE TO PUBLIC on every new function and ALTER DEFAULT PRIVILEGES does not remove it — measured on 17.10, on a clean cluster and on a Supabase-configured one. Writing the statement anyway would have read as protection that does not exist, so the migration records the rule instead: every function this project creates carries its own explicit revoke, and the baseline verifier fails if one does not.

Added

It also records, plainly, that every row-level security policy in this project is dormant for the application. The Flask server connects as postgres, which bypasses RLS, owns every table, and has relforcerowsecurity = false everywhere. Tenant isolation in production is the AND vault_id = ? that app/tenancy.py appends to each query, and nothing else. ARCHITECTURE.md and MULTI_TENANT_PLAN.md both describe RLS as part of the isolation model; a reader should not have to discover the difference by querying pg_roles.

It earned its place twice: it caught a fixture error that turned out to confirm vault_memberships.unique(user_id), and it caught the EXECUTE TO PUBLIC behaviour that made one line of the intended fix a silent no-op.

Deferred

Recorded in TODO.MD rather than left implicit:


Next

Version 0.9 will focus on completing and polishing the Library module before beginning the Writing module in Version 1.0.

Added

Library

Unreleased

Added

Changed

Changelog

[1.0.1] - In Development

Added

Changed

Changelog

v1.1.0 — Scripture Relationship Engine

Added

Changed

Notes

This release introduces the core relationship engine that will become one of the central architectural components of Scriptorium.

v1.2.0 — Relationships & Lectionary Foundation

Added

Books

Lectionary

v1.3.0 — Book Text Capture

Released: July 6, 2026

Added

v1.4.0 — Complete the Revised Common Lectionary (Years B & C)

Released: July 6, 2026

Added

Changed

Fixed

Notes

The fixed-date Holy Days (sanctoral) calendar — St. Andrew, All Saints, Thanksgiving, etc. — was scoped for this release but deferred; see ROADMAP.md.

v1.5.0 — Expanded Writing

Released: July 6, 2026

Added

Changed

v1.6.0 — Scriptorium Online

Released: July 30, 2026

Scriptorium moved from a single laptop to the web. The application is the same Flask application in this repository; only where it runs and where its data lives changed. See "Online Deployment" in ARCHITECTURE.md for how the online version is assembled from this folder.

Added

Changed

Notes

The production cutover of July 30, 2026 is recorded in production-cutover-report.json: 194 books, 22 writings, 12 shelves, 4 research entries, and 14 sermon lectionary contexts were preserved, with row counts and field fingerprints matched before and after.

v1.6.1 — Scriptorium on a Phone

Released: August 17, 2026

The archive was only fully usable from a desktop. Cataloguing happens at the shelf and looking a book up happens in conversation, so the phone had to work.

Fixed

Added

Removed

Note: photographing a barcode on the Add Book page is unrelated to this removal and stays. That reads a barcode in the browser and never sends the photograph anywhere.


v0.8.0 — Library & Research Foundation

Released: June 30, 2026

Added

Library

Research

Search

Development


Changelog

All notable changes to Scriptorium will be documented in this file.