No. 1 · Autumn 2026 Building the world of tomorrow, in public Est. 2019

The build log

Press

Everything on the log for Press – newest first.

Wednesday, October 14, 2026

  1. [kevin · press] Built #1142: Press: row-level security on every table in public, with the app's own policies — no table readable by a role without a policy saying so (0m)

    The “before” list is empty: all 83 tables in public already have row-level security on, so nothing needed enabling. The “after” list will be empty too. The README now has the convention line: “every new table: enable RLS, write its policies, and watch_ro_read if the watch reads it.” A new suite test fails any migration that adds a table without row-level security, a revenue_* table without the watch’s policy, or a revenue_* view that isn’t security_invoker. I checked that it fails when a policy is removed.

  2. [kevin · press] Built #1138: Press: a read policy for the watch_ro role on every RLS table the daily watch reads (revenue_*, books_series/works/variants), in a Press migration (11m)

    A Revenue migration adds the read policy to all eight revenue_* tables. That’s the card’s seven plus revenue_ad_set_targeting, so that every revenue table is covered. It also grants SELECT on the ten revenue_* views. A books-module migration does the same for books_series, books_works and books_variants. Both create watch_ro only if it’s missing, and both drop any same-named policy before recreating it, so they’re safe whether or not Todd’s one-off exists. All ten revenue views are security_invoker, and the only Books tables they join are those three. So once this is applied, every view works for the watch too. A suite test now fails any migration that adds a revenue_* table without the policy, or a revenue_* view that isn’t security_invoker. I checked that it really fails when a policy is removed. The README has a “Reading Press from outside” note with the rule.

  3. [kevin · press] Built #819: OpenAI legacy user API keys are deactivated Oct 22 — replace with project keys in anything live on Press (skip The System Server / translations, deprecated) (1m)

    Press doesn’t use OpenAI anywhere, so for Press there’s nothing to replace.

  4. [kevin · press] Built #836: Translations: a rights attestation checkbox the user must tick before a book can be uploaded (44m)

    Where the box appears (you chose both Books and Translations): Translations’ New edition step. Books’ “Upload a manuscript” on the Library page. Books’ “Add a version or variant” on a book’s page. What each ticket point became: The box: it shows the label verbatim with the muted line under it. The wording lives in one place, @oftomorrow/ui/rights, so every form shows exactly the same text. The button: it stays disabled until the box is ticked. The tick is never remembered: it clears after each upload, and in Translations also when you change the book or the language. The record: stored on each upload’s row. Books: books_versions. Each upload is a version. Translations: translations_editions. Column What it holds attested_at When. The database stamps it, so the browser can’t set or backdate it. attested_by The user id of the person who ticked. attestation_text The wording exactly as shown. attested_build The deployment’s commit; empty locally. A confirmation can’t be changed, withdrawn, or added to an older row afterwards. Every confirmation is also copied to a separate log: books_attestations and translations_attestations. Deleting the version or edition keeps its log entry, and nobody can edit or delete log entries, not even the admin key. They only go if their whole group is deleted. The server refuses without it: Books’ two upload actions and Translations’ POST /api/editions (answering 422) refuse a request without the confirmation, before anything is saved. They also refuse wording that has changed since the page was opened. Reopening an existing edition needs no new tick. Other paths: the old-app import script now refuses a writing run unless you pass --confirm-rights. It prints the wording first, and records the confirmation as given by the operator named in --created-by-email. The Books catalogue spreadsheet import creates books without manuscripts, so it doesn’t ask. Migrations: 34452004_books_rights_attestation and 34457005_translations_rights_attestation. Checks: All tests pass in Books, Translations, the shared UI package and the repo scripts (1,682), plus the conformance checks, including 28 Books and 76 Translations database tests. I walked it in headless Chrome as a local editor. In all three steps the button waited for the box, and forcing a submit without it got the server’s refusal. A real upload and a new edition each recorded who, when and the exact wording, both on the row and in the log.

Tuesday, October 13, 2026

  1. [kevin · press] Built #800: Press exposes POST /api/ads: secured, idempotent on request_id, never generates creative, emits ads.created with refs (7h 23m)

    Part 1 is live in both apps (22643e2), so ad events are now queued in Press. They won’t actually go to Signals until SIGNALS_API_KEY is set. Parts 2 and 3 are built and committed locally (27e8b60) but not pushed, because they need another migration first. Part 2: the route Signals calls to create an ad POST /api/ads turns a request into a one-ad Studio test, saves its Attribution tag, and pushes it paused, through the same code a person’s push uses. GET /api/ads/{ad_id} reports where that ad stands. The test, the Meta campaign and the Attribution campaign are all named after the request’s campaign. The ad’s Attribution ad group is {campaign}-a. Whoever makes the tag before the job runs needs to use those names, because that’s how Ad Desk matches the ad’s sales. CW should know this. Nothing is saved until everything checks out: the book and its storefront edition, the copy and image, the audience, the Facebook Page, and the tag. A failure lists the bad fields. Retries are safe. If the same request arrives again after its ad was made, Press returns the first answer. If Meta refused before anything that could spend existed, a retried job pushes the same test again. If a campaign was already made, Press refuses for good. Keys: a group admin mints and revokes them in Ad Studio → Settings. A key is shown once and Press keeps only a hash of it. Changes made through a key are logged in Meta’s change log under the name of the admin who minted it. Part 3: picking up jobs from Signals A new hourly Ad Studio job takes one job from ot.press:ads, reads the JSON block in its description as CW agreed, creates the ad, and marks the job done. If Meta refused, the job’s failure note points at the error event rather than raising a second ticket. The ad that’s created carries the job’s id, which is what CW’s #834 needs to approve the job automatically. The job does nothing until three things are in place: the Signals key, the account that owns Meta, and a live Press key. Revoking every Press key stops it. Testing: all tests across the suite pass and Ad Studio builds. A new hourly Ad Desk job checks for Press-made ad sets that have passed their end date in Meta within the last week. For each one, it sends Signals an ended event (reason “scheduled”, dated at the end time) for each ad. When all of a Studio test’s ad sets have stopped, it also marks the test ended. An ad that was already reported as ended, for example one Lis paused in Ad Desk, isn’t reported twice.

  2. [kevin · press] Built #799: Press emits ad lifecycle events to Signals (ads.created/published/budget_changed/ended/errored/attribution_linked) (40m)

    Ad Studio reports four things to Signals: an ad was created, when Todd pushes a test; an ad went live, when the test is launched, with its start and end dates; an ad ended, when the test is ended; Meta refused a step, when it does. Ad Desk reports an ad ended, or its budget changed, when Lis approves a pause, a budget change or the end of a test. Only ads Press made are reported, and practice runs report nothing. Each event is saved in Press first and then sent straight away. Anything that couldn’t be sent is retried by a new hourly job in Ad Desk. If Signals refuses an event outright, the job’s line on Ad Desk’s Jobs page names it. Each event contains only the fields Signals accepts, since it refuses anything it doesn’t recognise. All tests across the suite pass, and both apps build.