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:
- Every shared comment you have written is removed from every book. Other readers stop seeing them straight away, and they cannot be brought back.
- Every rating you have given someone else's comment is removed. Their counts will shift a little; nothing else of theirs is touched.
- Your sign-in addresses stop working — all of them, not only the one you happened to use.
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
- The Friends page now says what happens to somebody who is not here yet. Typing an address raised a question the form did not answer — whether it had to be a person who already had a Scriptorium. It does not: if they have not got one they are invited to make one, and the invitation tells them it came from you.
v2.11.3 — A thumb you can see
- The Helpful and Not helpful marks were invisible. v2.11.2 made them quiet outlined buttons and did not notice that buttons in Scriptorium are always white lettering — so the words were white on an unfilled pill, and only appeared when the pointer passed over one and repainted it. They are now a thumb up and a thumb down: small, legible without hovering, and filled in when the mark is one you have already given.
v2.11.2 — A saved comment looks saved
- Shared comments were showing your own comments twice — once to read, and again inside an editing box that was always open. Several comments of your own meant a panel twice as tall as it needed to be, with nothing to say which copy was the real one. A comment now reads as a comment, and editing is behind an Edit control on the ones you wrote.
- Its page, edition and passage are one line instead of three, so the Scripture reference — the part you use to find a comment again — sits beside the citation rather than under it.
- Helpful and Not helpful are quieter. They were solid buttons, heavier on the page than the writing they were rating. The one you chose still fills in, because that is the only way to see what you already said.
- Withdrawing a comment moved inside the editor, rather than sitting on the face of the card at the same weight as Save.
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
- You can share your library with friends you choose. Add a friend by email address; if they accept, each of you can see the other's books — what is on your shelves, whether you have read them, what you thought of them, and the notes and quotes you have saved.
- Both people have to agree, and either of you can end it at any time, with immediate effect. Nothing is shared by asking or by being asked. Only by accepting.
- Anything ticked "Hide from friends" stays hidden — the book, its notes and its quotes, and it is not counted in any total a friend is shown. That tick arrived last release so there was time to use it first.
- Before you accept, you are told what it shares: how many books, on how many shelves, and how many you have already hidden — with a link to go through your library before you answer.
- Your friends' libraries have their own pages, with their own search. Your library and your search are unchanged; nothing is merged.
- A friend who has no Scriptorium yet is invited to one. If they join through that invitation, the two of you are friends from the start.
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
- "Hide from friends" is now a checkbox on the edit-book form, saved with everything else when you save the book. It was on the book page and on every row of your library, which put a control you cannot use yet in front of you on every screen; it moved after seeing it in place.
- A book's page says "Hidden from friends" only when it is, and says nothing when it is not.
- Nothing about what hidden means has changed, and no book's setting changed. Sharing with friends still arrives in the next release.
v2.3.0 — A book you can keep to yourself
- Every book now has a "Hide from friends" tick, on its own page and on each row of your library.
- Next release, you will be able to share your library with friends you choose. Anything you tick now stays hidden from them.
- A review mode, at Library →
?review=friends, which shows how many of your books would be shown and how many are hidden, and puts the ticks in front of you so you can go through them in one sitting.
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
- There is no charge for Scriptorium, and no plan to charge for it. The Terms named a yearly cost for clergy after a first free year. That is withdrawn. Nothing is being asked of anybody, and nothing is planned.
- A new version of the Terms says so, and you will be asked to accept it the next time you sign in. The old version is not edited — what you accepted before is still on record, exactly as it was.
- The withdrawn price is named in the new Terms rather than quietly deleted. Two people had already accepted terms that stated it, and removing it without a word would leave them wondering whether it still applied to them.
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
- Books looked up from Open Library no longer arrive with an invisible cover. The lookup was building the cover's web address out of the ISBN and never asking whether a cover was actually there — and Open Library answers a book it has no cover for with a single transparent dot rather than saying no. Nothing could tell the two apart: the image loaded, the page reported no error, and what you saw was a blank space. The lookup now asks first, and says nothing rather than something that is not there.
- When Open Library has no cover, Google's is used. Previously Open Library's pixel won, because a pixel is an answer. A book whose cover only Google has now gets it.
- A cover Open Library does have is kept, unchanged. The address it is stored under is now one that will fail visibly if the cover is ever withdrawn, instead of quietly becoming a dot again.
- A slow or unreachable cover does not cost you the lookup. The title, author and publisher still come back; only the cover is left out.
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
- Pressing Lookup on a book you already have no longer warns that you already have it. The check that stops you entering the same book twice was searching your whole library, found the very book you were editing, and offered you a button to the page you were already on. It now leaves that one book out — and still tells you if you change an ISBN to one you own on a different book, which is a real duplicate.
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
- On an existing book, the lookup now fills what is empty and leaves what you wrote. Books added before Scriptorium fetched their details can be brought up to date by opening them and pressing Lookup — but until now that replaced everything, so a title you had trimmed or an author you had spelled properly was overwritten without a word. It now fills only the blanks, and says how many it filled. Adding a new book is unchanged: every field comes from the lookup, because there is nothing there to keep.
- And a lookup can no longer empty a field. Where the book service knew nothing, it used to write nothing — clearing a publisher or a year you had entered by hand. It now leaves what it does not know.
v2.0.3 — The lookup says it is working
- Looking up a book by its ISBN now tells you it is doing something. The Lookup button, the barcode scanner and the photograph all fetch the book from outside Scriptorium, which can take a few seconds — and until now nothing on the page changed while it did. The button reads Looking up… while it works, a line beneath says the same in words, and when the book arrives it names what was found.
- And it tells you when it fails. With no connection, clicking Lookup used to do nothing at all — no message, no matter how many times you clicked. It now says it could not reach the lookup.
- Clicking Lookup twice no longer sends two requests. The button is held until the first one answers, so a slow connection cannot fill the form from whichever request happens to come back last.
v2.0.2 — Three small things
- The sign-up form's two answers line up now. A student and Not a student each sat a line below their own radio button, in the small grey type meant for field labels rather than for an answer you read and pick.
- Your Account page says when your Scriptorium began.
- The button that reads a barcode from a photograph says so. It was labelled Use a photo, which reads as an invitation to add a picture of your book; it now reads Photograph the barcode. It never stored a photograph and does not now — the image is read for the number and discarded.
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.
- No name is attached. Not "anonymous" — Scriptorium is small, and a promise that nobody could work out who wrote something is one the software cannot keep at this size. What it does is not attach your name.
- A page number and a Scripture reference, both optional. The reference is what makes a comment findable: search a passage and the comments other readers have written about it come back with your own books.
- Marks, not scores. You can mark a comment up or down. Your own mark is visible to you immediately; totals appear only once a comment has three, so a single reader's opinion never reads as a verdict.
- You can edit or withdraw anything you wrote.
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:
- In the repository, every file under
legal/that exists inorigin/mainmust be byte-identical to it — with a second test that deliberately edits a published file to show the comparison can see it. - In production,
accepted_agreements_are_uneditedchecks that every version an acceptance row references still exists and still hashes to what was recorded when the person clicked.
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()created no vault tables.vaults,vault_memberships,vault_invitations,vault_entitlementsandaccount_profilesexisted only insupabase/migrations/, so multi-tenant mode existed only on PostgreSQL. Four test fixtures each built those five tables by hand — which is how a fixture comes to be the only description of a schema.
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.
-
ARCHITECTURE.md's rule that the two schemas move together is now asserted rather than remembered.tests/test_local_schema_matches_migrations.pycompares the column set of all 17 tables in both directions and names the column and the direction when they differ. Types are allowed to differ, and do: SQLite has nouuidand notimestamptz. -
seed_local_vault.pyfailed on every first run. It supplied a uuid forvaults.id, and SQLite'sINTEGER PRIMARY KEYis the one column type it enforces strictly. It usesinsert_and_get_id()now — the same helperapp/tenancy.py:215already used to create a vault, and the one the comment above the column already pointed at. -
init_db()creates its own parent directory. Thatmkdirlived increate_app(), so the application started happily against an emptyDATA_FOLDERwhile a script callinginit_db()directly failed on the same folder. A step every caller needs belongs in the thing they all call.
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
day_slugs_are_unique— 110 days, 110 slugs. A day is addressed by its name, so a slug collision is the naming problem wearing a different hat.a_psalm_is_never_also_a_canticle— 1,252 readings examined. The three-case display rule is unambiguous only while no reading is both, and that is a property of the data rather than of the code.
Rehearsal harnesses
scripts/rehearse_pages.sh— shipped in v1.8.1, now step 3 of the Release Workflow, unconditional.scripts/rehearse_value_migration.sh— a value migration changes no schema, so the schema fingerprint cannot see it. This restores the gate's own backups and proves the migration against 1,252 real readings.
Fixed
reading_rolenamed a genre it could not know. The converter assigns it by position, from a function that never consultsscripture_books.testament:
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.
-
The day page read in the order the source printed, not the order a service reads. Every Sunday page prints the psalm second and the Holy Days pages print it last, so 28 of the 31 Holy Days showed their psalm after the gospel.
reading_orderkeeps its meaning as the faithful record of the source's layout, and the page derives liturgical order from the slot. Palm Sunday's palms gospel still precedes its procession psalm, becausesection_orderoutranks both. -
The season ordering list had never met the Holy Days.
"Holy Days"was missing fromLECTIONARY_SEASON_ORDERand"All Saints"was in it — a season no occasion has ever carried. All 31 fell through to the unknown-season branch and 30 of them shared a single sort key, which is not an order at all. The day index is the first page that ever asked them to sort. -
SELECT DISTINCTordered by a column it did not select. SQLite permits it, PostgreSQL refuses, so every chapter page 500'd while 314 tests passed. Caught by the page rehearsal on its first real run, before it shipped. -
A conversion check that failed on success.
all_saints_is_three_rows_one_nameassertedexisting_ids == [68], so it passed only while All Saints existed in Year B alone and began reporting FAIL the moment the import it was written for succeeded.
Changed
- The response slot displays three ways, and the third is an answer rather than a fallback. Every citation from the Psalms is a Psalm; a label naming a canticle is a Canticle; neither gets no genre label at all — the citation alone. Six readings are in that third case: the Song of Hannah, the Song of Moses, Song of Solomon, Lamentations 3 and two from Wisdom, scriptural songs sung where the psalm is sung.
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
dateformatcould not read a timestamp. The filter calleddatetime.strptime(value, "%Y-%m-%d")and caughtValueErroralone.
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.
- Dates are shown in America/Chicago. A membership created at 8pm Central
is stored as 01:00 the next day in UTC, and reading back as the 26th for
something done on the 25th is the kind of wrong that makes an archive feel
untrustworthy. A plain day —
date_preached— is never shifted, because it is a day rather than an instant.
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.
- Adding writes a
vault_invitationsrow carrying this vault's id and sends a magic link to the new address only. Nothing is added until the link is opened, and the new address proves control of itself the same way this product proves anything — by mail arriving where it was addressed. No token is retained and nothing is trusted from the browser but the address to send to. - Removing takes effect immediately, and that is the point rather than a
side effect. Institutions recycle addresses: Sewanee will eventually
reassign
someone@sewanee.edu, and a retired address still on a vault is a working key to twenty years of sermons for whoever receives it next. No exploit, no bug — just mail arriving where it was addressed. - Never the last one. The interface refuses it and so does the database.
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
- The last-address guard would have made every vault undeletable. As
drafted it was a per-row
BEFORE DELETEtrigger, and two cascades reachvault_memberships— deleting a vault, and deleting an auth account. A per-row guard fires inside both, so a vault's own last membership refused to go and took the vault with it. Proved on a disposable cluster, both shapes side by side.
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.
- The standing verifier could not see a constraint trigger. It matched
the literal string
create trigger <name>, and a constraint trigger can only be declared ascreate constraint trigger <name>. The same shape as the abbreviation rule that was anchored to the start of a string: a check written against the one form the project happened to have used.
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.
-
The rehearsal rolled each migration back on its own, with every later migration still applied — a state that never occurs.
202608260008adds a column to the table202608260006creates, so rolling back0006in isolation destroys0008's work and re-applying0006alone can never restore it. The harness reported that as "0006is not reversible", which is false: what is not reversible is the order. It now takes the whole stack down from the newest, asserts each rollback changed something, brings it back up in order, and requires the fingerprint to return exactly. -
account_profileshadupdated_atand nocreated_at. An oversight rather than a decision.created_atis on fourteen of seventeen tables;updated_athas only ever been on three, so the shape to notice isupdated_atwithoutcreated_at— and the only other one,vault_entitlements, is deliberate. A profile row is written lazily, on the first save, sovaults.created_atdoes not answer when its owner was first willing to be named. Recorded inDATA_MODEL.mdunder Timestamps.
Deferred
Recorded in TODO.MD rather than dropped, each designed and written down
before being held back:
- Labels on an address. With two addresses a label is noise.
is_primary— it marks where Scriptorium would write if it could write anywhere, and there is no mail path. A marker with no consumer is the same mistake as a name with no consumer.- A transactional mail path. Every email Scriptorium sends is a Supabase magic link. The first thing to build on a real one: notify on every addition and removal, including when the notified address is itself the one changing.
vault_address_events, the address-change log — both its jobs were deferred with it.- Rate limits. Adding an address already requires a live authenticated session, which is the control that decides whether a stranger can do it at all; the limits bound the damage after that control has already failed.
- The single-address prompt. The more valuable place to ask is at signup,
and building it into a settings page and then again at the front door is
building it twice. See
BEFORE_PUBLIC_SIGNUP.md, now tracked.
Migrations
202608260006_a_vault_keeps_a_name_and_a_way_in— applied 2026-08-25 16:19 UTC, both backups taken, eleven checks passed.202608260008_a_profile_records_when_it_began— not applied. It addscreated_atand nothing in the application reads it. The number skips 0007, which was withdrawn before it was ever applied.
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
- 34 occasions, 34 reading sets, 154 readings and 167 Scripture references. The calendar goes from 198 occasions to 232, and from 1,366 to 1,533 lectionary Scripture references.
- 30 Holy Days, all 30 present, counted against the source manifest rather than against an expectation.
- The document offered 35 occasions and 34 were added. All Saints' Day Year B
was already in the calendar as occasion 68, and the import was required to
collide with it on all three key columns —
calendar_name,nameandlectionary_year— not on the two that had been looked at. Colliding on two would have inserted a second All Saints Year B, which is the exact duplicate class this release removed. - Two days vary by lectionary year and get three rows each: All Saints' Day and Thanksgiving Day. The rest are year "All" and get one. A day wrongly marked year-varying gets three pages; a year-varying day wrongly marked fixed loses two thirds of its readings without complaint, so both lists were read rather than counted.
A gate, and one path to production
scripts/production_gate.py— one gate for schema and content alike: a schema backup and a data backup, both always, each verified by a row count rather than by the absence of an error; the target hostname typed back by a person, with no flag to skip it; one transaction; the deltas the report promised enforced at commit; the standing verifier afterwards.scripts/apply_migration.py— the only path to production DDL. It refuses a file that is not a migration, refuses a migration with no paired rollback, and runs preconditions read-only before it takes a backup or prompts anyone.scripts/reconcile_rcl.py,scripts/convert_holy_days.pyandscripts/import_holy_days.pyall run behind the same gate.scripts/rehearse_import.sh— the second rehearsal layer.rehearse_migrations.shproves a migration on an empty cluster; this proves an import on a disposable cluster restored from the gate's own backups of production. An importer that only ever runs against the live database is one that can only be tested there.
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:
no_new_orphaned_references—scripture_referenceshas no foreign key to what it points at, so a deleted reading leaves a row behind that nothing will ever notice.canticle_numbers_are_on_the_reading— the number belongs onlectionary_readings.display_reference, where twenty existing rows already put it.occasions_are_canonically_named— askscanonical_occasion_name()rather than restating it, so the rule has one home.occasion_names_carry_no_abbreviation— no occasion name contains a full stop.occasion_names_group_cleanly— no two occasions in a calendar differ only by punctuation, spacing, or a leading "The". This is what the v1.9 day pages will group on.
Fixed
- The calendar held a day twice under two spellings. Reconciliation renamed 18 occasions to one convention, retired one duplicate, removed its reading set, collapsed a split reading and rewrote two display references: 199 occasions to 198, with seven readings and seven references removed and no sermon link disturbed.
- All Saints existed in Year B only, so two thirds of the day was missing. It is now three rows under one name — ids 68, 313 and 315 — with the original row kept.
- Canticle numbers were being written to the wrong field. The rule now
puts the number on the reading and the citation on the Scripture reference,
which is what the rows already carrying a number did. Three reading labels
and three reference citations were rewritten to match, with zero deltas. Canticle 9 sits under
reading_role = 'psalm'because the role is a liturgical function, not a claim about which book the text comes from. - Three defects in
extract_episcopal_rcl.py, fixed at the source rather than in the data: titles were being joined with a pipe, navigation labels were being read as occasion names, and markup remnants were surviving into titles. A re-extract cannot reintroduce them. - An abbreviation rule that was about a position rather than about the
abbreviation.
^St\.expanded "St. Barnabas" and left "Nativity of St. John the Baptist" alone — and the standing check inherited the blind spot exactly, because it asks the rule rather than restating it. Both rules are now position-independent.
Withdrawn
- A migration adding a unique constraint on
(calendar_name, name, lectionary_year). That constraint has existed since the initial schema on July 29, line 91. It was never the missing piece: it could not have caught the duplicate, because the two spellings differed. Withdrawn rather than applied.
Changed
INSERT OR IGNOREbecomesON CONFLICT DO NOTHINGthroughoutapp/rcl_importer.py— portable across SQLite 3.24+ and PostgreSQL, where the SQLite spelling is a syntax error.import_rcl_into()is separated fromimport_rcl_data()so the caller can own the transaction, which is what lets the gate enforce deltas at commit.SCHEMA_BASELINE.mdrecords anomaly A.4 — six orphaned Scripture references, accepted and counted — and what each kind of backup is for.DATA_MODEL.mdrecords the occasion naming convention, the canticle rule and whyreading_roleis a liturgical function, and the shape of the v1.8 Lectionary Day pages.
Notes
Two rules came out of this release and are written into the rehearsal harnesses' own docstrings so the next script inherits them:
apply_migrationalways 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.- 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
- The passage predicate never consulted
chapter_end. It lived inline inroutes.pytwice, byte for byte, and both copies checkedchapter_start,verse_startandverse_endand nothing else.
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.
-
A discontinuous query searched only its first passage.
/searchinterpreted the query withparse_reference(), soIsaiah 55:1-5, 10-13quietly searched1-5and dropped the rest. -
Vault resolution matched on email as well as user id, so the first person whose address changed fell through to the invitation branch, found none, and was refused entry to an archive sitting right there.
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.
python run.pyserved production.config.pyfell back toMIGRATION_DATABASE_URLwhenDATABASE_URLwas unset,.env.localsets it, andload_local_environment()put it back into the process even when the shell unset it — so no environment variable could prevent it. Every local run on a maintainer's machine was one careless POST away from writing to the live archive.
.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
- One input, one button, one friendly error. Results in four groups.
- Books, Writing and Research Notes are the archive's own, each carrying its vault scope separately from the passage predicate.
- In the Lectionary is kept apart, above a rule and labelled as shared reference data. It answers a different question — when does the church read this passage — from rows that belong to no vault. It is the first page where a shared answer and a private one appear together.
- A discontinuous reading is one card listing every segment that answered,
not one card per segment:
Luke 15:1-3; Luke 15:11b-32. - The query stays in the URL. A passage is worth bookmarking, sending, and going back to.
- A reference nobody could read is reported rather than answered with an empty archive. "No books connected to this passage" is a different statement from "I could not read that".
Sign-in addresses
vault_invitationsgains a nullablevault_id. Null still means "create a new vault", so every existing invitation keeps its meaning; set means "join this vault", which is the whole of adding a second sign-in address.- No interface yet. The resolution and the schema it needs are in place, so an archive can already outlive the address that opened it.
Guards
run.pyrefuses to start against a non-local database unlessSCRIPTORIUM_ALLOW_REMOTE_DATABASEis set, and names the host it refused. Removing the fallback is the fix; this is what catches the next variable that reintroduces it. The same rule was already inscripts/rehearse_multi_tenant_migration.py.scripts/rehearse_migrations.shnow takes every migration carrying a rollback down and back up, comparing a schema fingerprint either side, rather than only the one it was written for.
Changed
- The Writing page's search box was disabled and did nothing, which on a phone reads as broken rather than absent. It now hands its query to the search that already searches writing.
unrecognized_reference_message()takes aconsequence, so one builder serves both callers. Storing a reference fails to connect it; searching for one fails to look for it, and telling someone their search "was not connected" describes an intent they never had.DATA_MODEL.mdrecords that a research entry answers a passage search through the book it belongs to, and what that inherits.
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
anonheld full DML andTRUNCATEonvaults,vault_membershipsandvault_invitations— twenty-one grants, on the three tables the whole tenancy model rests on.anonis the unauthenticated public.
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.
-
Four
SECURITY DEFINERfunctions were executable byanonat/rest/v1/rpc/<name>. Three returntrigger, and PostgreSQL refuses to run a trigger function outside a trigger context, so they were hygiene rather than an exploitable weakness. The fourth returnsnow(). All four are revoked. Revoking does not affect the triggers that use them — PostgreSQL does not consult EXECUTE privilege when firing one. -
The default privileges that caused both are disarmed for tables and sequences, so a table added by later work is no longer born browser-reachable.
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
SCHEMA_BASELINE.md— the production schema as found on August 24, 2026: roles, policies, functions, triggers, grants, and five anomalies. The companion toMIGRATION_BASELINE.md, recording structure where that records data.
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.
-
scripts/verify_schema_baseline.py— six read-only checks that assert what that document claims: every function and trigger appears in a migration file, noSECURITY DEFINERfunction is executable byanon,anonholds nothing, new objects are born private, and the three tenancy tables carry no write grant. Emits JSON in the style ofproduction-cutover-report.json. Prose decays; this does not. -
scripts/rehearse_migrations.sh— applies every migration to a disposable PostgreSQL 17 cluster, then runs the new one forward, twice, back, and forward again, and confirms vault provisioning still works. It cannot be pointed at a remote database because it builds its own.
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.
-
tests/test_schema_drift_migration.py— thirteen assertions on what the migration says, including that it drops no trigger, thathas_active_vault_accesskeeps its grant, and that it writes no row. -
Before and after verifier reports in
migration_reports/.
Deferred
Recorded in TODO.MD rather than left implicit:
authenticatedstill holds twenty-four write grants on the eight tenant tables, granted deliberately in July for a browser path that was never built. They belong with the work that rewrites those policies.- Identical default privileges exist for
supabase_admin, which cannot be altered from here. Practical exposure is closed; the residual is reported on every verifier run. scriptorium_ops_ping()is a deletion candidate once whatever calls it is identified.
Next
Version 0.9 will focus on completing and polishing the Library module before beginning the Writing module in Version 1.0.
Added
Library
- Shelves can now be renamed.
- Renaming a shelf automatically updates all books on that shelf.
- Books can be marked as "Currently Reading."
- Home page now displays Currently Reading books.
- Currently Reading books show the number of research notes attached.
Unreleased
Added
- Began the Writing module.
- Added Writing navigation and landing pages.
- Added Add Writing workflow.
- Added initial Sermon archive page.
- Added Locations reference table.
- Added Occasions reference table.
- Added DATA_MODEL.md to document Scriptorium's architecture.
Changed
- Refined the Writing data model based on real ministry workflow.
- Replaced sermon titles with occasions.
- Replaced sermon summaries with ministry notes.
Changelog
[1.0.1] - In Development
Added
- Unified Search now searches across Books, Writing, and Research Notes.
- Writing added as a searchable collection.
- Search results are grouped by collection.
- Added Writing filter to Search.
- Added Guiding Principles document to guide long-term development.
Changed
- Search has begun evolving from a library search into a true Scriptorium search.
Changelog
v1.1.0 — Scripture Relationship Engine
Added
- Introduced a normalized Scripture relationship engine.
- Added
scripture_bookstable containing all canonical books of the Bible. - Added
scripture_referencestable for reusable Scripture relationships. - Added Scripture parsing service capable of understanding:
- Whole books
- Chapters
- Individual verses
- Verse ranges
- Added reusable Scripture service (
app/scripture.py). - Added support for multiple Scripture references on sermons (one per line).
- Added structured Scripture relationships to the Writing module.
- Added Scripture relationship retrieval for sermon detail pages.
- Added Scripture-aware editing of sermons.
- Added Scripture-aware search capable of matching overlapping references (for example, searching John 12:25 finds sermons tagged John 12:20–36).
Changed
- Writing module now uses structured Scripture relationships instead of relying solely on text fields.
- Scripture relationships are designed to be reusable across future modules including Books, Research, Reflections, Quotes, and Lectionary.
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
- Books now support structured Scripture Connections.
- Books participate in the shared Scripture relationship network.
- Scripture Connections use the shared Scripture service (
app/scripture.py). - Books can create, edit, and display structured Scripture relationships.
- Existing generic
scripture_referencesarchitecture required no database changes.
Lectionary
- Added the Lectionary schema:
lectionary_occasions,lectionary_reading_sets,lectionary_readings,writing_lectionary_contexts. - Lectionary readings reuse the shared
scripture_referencesarchitecture. - Added a validating, dry-run, transactional, idempotent RCL importer (
app/rcl_importer.py). - Imported the full Revised Common Lectionary Year A (Episcopal Church adaptation), including tracks and alternatives.
- Added Lectionary browsing by occasion and reading set.
- Sermons can link to a Lectionary reading set through
writing_lectionary_contexts, independent of Scripture focus.
v1.3.0 — Book Text Capture
Released: July 6, 2026
Added
- Added local OCR capture of book notes: photograph a page, margin note, or highlighted passage from the book detail page.
- OCR runs locally via
pytesseract/Tesseract (app/ocr.py); no cloud vision API. - Added HEIC/HEIF photo support (
pillow-heif) and EXIF-orientation correction for real phone photos. - Extracted text fills the existing Research note form for review and correction before saving.
- Captured notes save as ordinary Research entries (Quote/Idea), reusing the existing
research_entriesarchitecture — no new tables. - Captured notes are immediately findable through existing Unified Search.
v1.4.0 — Complete the Revised Common Lectionary (Years B & C)
Released: July 6, 2026
Added
- Imported the full Revised Common Lectionary Year B (63 occasions, 355 readings).
- Imported the full Revised Common Lectionary Year C (63 occasions, 359 readings).
- Sermon lectionary-context selector now orders by true church-year sequence (Advent through the numbered Propers) instead of alphabetically, and supports typing to filter the list.
Changed
extract_episcopal_rcl.pyandconvert_episcopal_rcl.pygeneralized from Year-A-only scripts into a shared, year-parametrized pipeline, reused for Years B and C.- Fixed several citation-parsing gaps the Year A data never exercised: Unicode en/em dashes in verse ranges, single-chapter books cited without a chapter number (Philemon, Jude, etc.), a discontinuous list nested inside an optional parenthetical group, and numbered-Canticle citations (e.g. "Canticle 9", "Canticle 15") resolved to their scripture equivalent.
- Several occasion shapes that were hardcoded by name for Year A specifically (alternative-track and Apocrypha-track Sundays) are now detected structurally, since the same shapes recur in Years B and C with different — sometimes non-Apocryphal — content.
Fixed
- Corrected 16 occasions (Ash Wednesday, Good Friday, Holy Week weekdays, Easter Week weekdays, Holy Saturday, Pentecost Vigil, Ascension Day, Easter Evening) that were mistagged
lectionary_year = "A"instead of"All". These are word-for-word identical across all three lectionary years; a sermon linked to one of them is now found regardless of which year it was preached, matching how Christmas Day already worked.
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
- Four new writing types: Reflection, Paper, Letter, and Miscellaneous — previously dead cards on the Add Writing page.
- Reflection: Title, Publication Date, Scripture Focus, Reflection Text.
- Paper: Title, Scripture Focus, Details, Paper Text.
- Letter: Subject, Recipient, Date Sent, Letter Text.
- Miscellaneous: Title, Date, Details, Content.
- Reflections and Papers participate in the shared Scripture relationship network via a Scripture Focus field, same as Sermons and Books.
- Sermon list, detail page, and search results now show the preached/written date as the title for all five writing types, not just Sermons.
- Redesigned the Lectionary page: 199 occasions now group into collapsible sections by season with a type-to-search box, instead of one long flat list.
Changed
- Added
titleandrecipientcolumns to the existingwritingstable. Every other field is reused from existing generic columns, the same way Sermons already reuseoccasion/occasion_type— no new tables. - Extracted the church-year sort logic (previously only used by the sermon lectionary-context picker) into a shared
app/lectionary.pymodule, now also used to group the Lectionary browse page by season.
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
- Scriptorium is deployed to Vercel and served at
https://scriptorium.spritzsolutions.com. - Sign-in through Supabase email links, restricted to invited addresses
(
app/auth.py). Authentication is required wheneverVERCEL_ENVisproduction. - Private vaults (
app/tenancy.py): every book, shelf, research entry, writing, and user-created Scripture reference belongs to exactly one vault. Scripture book names and the Revised Common Lectionary stay shared. - Vault entitlements, so a vault can be complimentary or paid and access can
be withdrawn without deleting the archive
(
supabase/migrations/202607300003_vault_entitlements.sql). - Supabase row-level security policies, so isolation holds even for a request that reaches the database outside the application.
- Migration and verification tooling under
scripts/, including a full rehearsal against a disposable database before production was touched.
Changed
app/database.pyspeaks both SQLite and PostgreSQL.DATABASE_URLselects PostgreSQL; without it, the local SQLite archive is used exactly as before, so local development did not change.- Every private query carries its vault, and every private insert records one.
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
- ISBN lookup reports a provider outage as an outage. When Google Books rate
limited the request and Open Library refused it, every scan and every
lookup answered "No book found for this ISBN" — indistinguishable from a
book genuinely absent from both catalogues. Failures are now logged with
the provider and reason, the message says which catalogue was unreachable,
and
/lookup-isbn/diagnosticsprobes both providers from wherever the application is running. - Open Library editions no longer catalogue without an author. The edition
record stores authors as references (
/authors/OL26320A) and carries no name, so the fast, reliable lookup path produced books with a title and a blank author; the names are now resolved separately. - Sermon, Reflection, and Paper Scripture Focus now saves the references a
lectionary reading actually uses. A discontinuous reading
(
Isaiah 55:1-5, 10-13), a verse suffix (Luke 15:1-3, 11b-32), a passage crossing a chapter (Genesis 1:1-2:4a), an abbreviation with a period (Matt. 5:1-12), a chapter-and-verse written with a period (Matthew 14.13-21), and short abbreviations (Mk,Jn) were all understood as book names of their own, matched no book in the canon, and were then discarded without a word. Only a plainBook 1:2-3survived. - A reference Scriptorium cannot understand is now reported back by name instead of disappearing. Nothing typed into a Scripture field is dropped in silence.
- The Scripture Focus box reopens with the text as it was typed. It previously rebuilt itself from stored relationships, so a reference that had been dropped came back empty and the next save erased it for good.
- Reflections and Papers now store their Scripture text alongside their relationships, the way Sermons always have.
- Barcode scanning works on a phone. It depended on the browser's Barcode
Detection API, which Safari does not implement — and every browser on
iPhone and iPad is Safari underneath, so the camera never opened there and
the message suggested using Chrome on a Mac. A vendored ZXing decoder
(
app/static/vendor/zxing.min.js) now reads the frames wherever the browser cannot, loaded only when the Add Book page needs it. - Camera failures explain themselves: a blocked permission, a camera in use, a device without a camera, and an insecure connection now say so, instead of one message about checking permissions.
- The navigation bar wraps on a narrow screen instead of scrolling sideways. Search and Sign out used to sit off the edge of a phone screen with nothing to suggest they were there.
- Search boxes turn off autocorrect, autocapitalisation, and spellcheck, and ask for a Search key on the keyboard. A phone keyboard was quietly rewriting author and title queries into other words.
- Book cards, the search row, and the scanner fit a phone screen: full-width search button, cards sized to their contents rather than a fixed height, and a scanner that fills the screen.
Added
- Vault access enforcement. A vault's entitlement is
complimentaryorpaidandactive,suspended, orcanceled(vault_entitlements); a vault without active access is refused at sign-in and on every request, so access can be withdrawn without deleting an archive. - The confirmed entitlement is remembered for the length of
SCRIPTORIUM_ACCESS_CACHE_SECONDS(default 900) instead of asking the database on every page and every stylesheet. Only a positive answer is cached, so granting access is instant and revoking it takes at most that long — the direction that fails safe. - "Use a photo" on the Add Book page. Photograph the barcode and Scriptorium reads it from the still image — the fallback when a browser refuses live camera access, and steadier in poor light.
tests/test_scripture.py: the reference forms above, the reporting of unreadable references, and the round trip from typing a sermon's Scripture Focus to reading it back.
Removed
- Book Text Capture, the OCR feature shipped in v1.3.0. The "Capture from
Photo" button is gone from the book detail page, along with
app/ocr.pyand the/books/<id>/research/ocr-captureroute. Research notes are typed, as they were before v1.3.0. Existing notes captured by OCR are ordinary research entries and are untouched. - The
Pillow,pytesseract, andpillow-heifdependencies, which nothing else used, and with them the system Tesseract binary the project needed installed. Scriptorium now depends on Flask and psycopg only.
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
- Book catalog
- Book detail pages
- Cover images
- Shelves
- Shelf detail pages
- Lending system
- Dashboard
- Fast Cataloging
- Duplicate detection
- Google Books lookup
- Open Library lookup
- ISBN lookup
- Barcode scanner
- Camera scanner
Research
- Quotes
- Ideas
- Tags
- Page numbers
- Research attached directly to books
Search
- Unified Search
- Book search
- Research search
- Search filters
- Contextual snippets
- Search highlighting
Development
- Git feature branch workflow
- First tagged release (
v0.8.0) - Improved
.gitignore - Removed Python cache files from Git tracking
Changelog
All notable changes to Scriptorium will be documented in this file.