Skip to content

Fix the serious findings from the full app review (batch 1) - #24

Merged
EmotiveImpact merged 39 commits into
mainfrom
claude/project-thread-evovgx
Oct 12, 2026
Merged

EmotiveImpact merged 39 commits into
mainfrom
claude/project-thread-evovgx

Conversation

@EmotiveImpact

@EmotiveImpact EmotiveImpact commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Before: the full app review on 9 October found 14 serious problems. An old app backup in the repository still carried a Google API key. The hosted app had stopped sending its security headers, the privacy and terms pages did not exist, and reminder emails promised a setting nobody could find. Members saw conflicting balances. Every name lookup read every account on the platform, so a busy task list took more than 30 seconds to load. Notes and tasks could be written straight to the database, so anyone could mark a task done or delete anything. A removed admin's invitations still worked, a retried save could create a second expense, fund, pledge or contribution, and anyone could raise someone else's pledge. Recurring bills could not be paused or archived safely, an edit could save over a newer one, and the week view showed only the first late task.

After: each of those is fixed, and each has a test that fails on the old code. The database refuses the unsafe changes itself, so the hosted client and the local API behave the same way.

What changed, by finding

Finding Change
1 Removed the old app backup that held the Google API key.
2, 4, 53 Recurring bills have versions, so a stale edit is refused and the editor offers to keep your changes or take the latest. They gain their own Pause, Resume, Archive and Restore commands, and resuming is blocked while someone in the bill has left. Each card only shows the buttons the server would accept for that person.
3 Notes and tasks change only through six database commands with request ids and versions. Removal is a soft delete by the creator or an admin, and hides the note's earlier versions and the task's history too. Caps of 500 notes and 1,000 tasks per commune.
5 One net balance per member, worked out in packages/core.
6 The week view lists every late task, yours first, with "N days late".
7 Leaving, removal or demotion withdraws an admin's open invitations, and accepting checks the sender is still an admin. Members shows who sent each invitation.
8, 9 Account lookups use the uuid key through one helper, and the task list resolves its names once per load (37 s down to about 0.1 s on 20,000 accounts).
10 New expenses, funds, pledges, contributions, recurring bills, notes and tasks carry a request id per open form, so a retry returns the first record, and only while it still matches what was sent.
11 Only the pledger can raise a pledge or bring it forward. Others can lower it, move it later or cancel it, and the pledger is told. An edit or cancel names the pledge it was based on, so it cannot undo someone else's change.
12 The email reminders setting, with one-click unsubscribe.
13 Privacy and terms pages that work signed out.
14 Security headers from the hosted app again.

Second review

A separate review of each part found 11 more points, all fixed here with tests that fail without them:

  • Recurring bills. A retried add is refused if the request or the bill has changed since; the same add sent twice at once waits for the first; an edit must say which version it was based on; and after a save with no reply, the conflict view says that save may have gone through.
  • Notes and tasks. Removed records keep their history hidden; a late retry of an add that has since been removed is refused; editing or removing a task locks the commune first, as a member leaving does, so the two no longer deadlock; and a task edit whose reply was lost is recognised as the person's own save.
  • Pledges and retries. Each pledge request is kept on its own, so a retry after a cancel and a new pledge is refused rather than answered with the wrong pledge; stale pledge edits and cancels are refused; and once an attempt may have saved, a later refusal no longer says nothing was saved.

The older task commands (complete, skip, hand over, undo) still lock the task before the commune. A later batch brings them into the same order.

Releasing

Nothing hosted was touched. Migrations 0026 to 0030 are local only, like 0012 to 0025. When releasing, apply them in order with the earlier ones, refresh the Data API schema cache, then deploy the client. Three need care, recorded in infra/neon/README.md:

  • 0027 removes direct note and task writes, so the client must ship with it. An older open tab can still read notes and tasks but cannot change them until it reloads.
  • 0028 must be applied before the new client is deployed.
  • The reminder worker should be deployed after 0030, with UNSUBSCRIBE_SECRET and WORKER_PUBLIC_URL set.

Needs the owner

  • Revoke the old Google API key in Google Cloud. Removing the file does not remove it from git history.
  • Approve the legal wording in docs/legal/privacy-draft.md and add the operator's details. The live pages give a short factual summary until then.
  • Check that support@ourcommune.io receives mail.
  • The Vercel preview fails within seconds, as it has on every new branch since 6 October; see the comment below.

How it was checked

On the final branch, after the second review fixes:

  • 16 SQL test files on fresh databases, plus a lock order test that forces the departure and edit race.
  • 13 local API suites on a disposable database and port.
  • 334 web tests.
  • The reminder worker's 5 suites, with no real email sent.
  • Typechecks for the web app, local API and worker, and a production build.

A browser walk as an admin and a member at desktop and phone widths covered the first round; the second review's screen changes are covered by the web tests.

How

Each fix lives in a SECURITY DEFINER function with REVOKE ALL ... FROM PUBLIC and a fixed search_path. Existing functions are patched with pg_get_functiondef and replace(), and the migration stops if a patch no longer matches. Every migration starts with BEGIN; and has one COMMIT;, ready for the automatic migration step in #21. The recurring bill, note and task, and invitation work was built in parallel and merged here. The remaining findings follow in later batches.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS

claude added 27 commits October 9, 2026 09:58
.migration-backup/ held 556 files of the replaced Supabase and Mantine app,
including docs/COMMUNE-BUILD-BIBLE.md with a Gemini API key that had been
scrubbed in June and came back with the backup on 22 September. Nothing in
the current app, build or tooling reads the folder; the old files stay in
git history. Removing them does not make the key safe: it still has to be
revoked in Google Cloud.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Members showed only one direction of money between you and each person:
"Owes you" hid anything you owed them, and your own row showed what you owe
and hid what you are owed. In a trip where Jordan owed €42.17 on one bill
and you owed Jordan €150 on another, Members said "Owes you €42.17" while
the breakdown said "You owe Jordan Reid €107.83".

netBalances in packages/core now offsets open shares in both directions into
one figure per person. Members and the breakdown's statement headline both
use it, and your own row shows the net across everyone ("Owe €75.67
overall"). A test checks the figure equals the statement's closing balance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
vercel.json only had the page rewrite, so the app sent no Content Security
Policy and any site could frame it, which allowed clickjacking of money and
membership actions. The headers block was dropped in May.

The response now carries a CSP (scripts only from the app, styles and fonts
from the app and Google Fonts, images from the app, data URLs and Unsplash,
frame-ancestors 'none', no plugins, no base or form redirection),
X-Frame-Options DENY, nosniff, a referrer policy and a permissions policy.
The header allows connections to any HTTPS host so a preview built for a
new Neon branch cannot break on its host name; a build that knows its Neon
endpoints adds a meta policy naming exactly those origins, and browsers
enforce both.

Checked against a production build with a mocked Neon: eleven signed-in
screens, sign-in, sign-up and password reset raise no violations, a fetch to
another host is refused and the app will not render in a frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The app linked to ourcommune.io/privacy and /terms and the website linked
back to app.ourcommune.io/privacy and /terms, but neither page existed, so
the links ended on sign-in or Home, and sign-up gave no privacy information.

/privacy and /terms are now app routes that work signed in or out. Until the
fuller notice in docs/legal/privacy-draft.md is approved, they give a short,
factual summary of what the app does today: what it keeps, what stays in the
browser, the services that handle data (Neon, Vercel, Resend, Google,
Unsplash), that it does not move money, the emails it sends, and
support@ourcommune.io for requests, with the ICO for complaints. Sign-up
links to the privacy page, and the sign-in pages and Settings link to both.
Nothing here names the operator or sets retention: those wait for the
owner's decisions listed in the draft.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Every reminder email said reminder emails could be changed in Commune
settings, but Settings only had the on-device Notices switch, and the emails
had no one-click unsubscribe.

Settings now has an Email reminders switch saved to the account. It reads
the existing preference row and saves through set_email_reminders
(migration 0029), because the table's column grants cannot express an
upsert of your own row through the Data API. The local API uses the same
function.

When UNSUBSCRIBE_SECRET and WORKER_PUBLIC_URL are set, the email worker
adds a signed /unsubscribe link to each reminder email and List-Unsubscribe
and List-Unsubscribe-Post headers (RFC 8058). Opening the link only asks,
since mail apps open links to check them; the POST turns reminder emails
off. A link altered to name someone else is refused. Without the settings,
emails point to Settings as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A Pause or Edit sent from a screen opened before someone else recorded a
period used to write the whole bill back, putting the old amount and due
date in place. Every change to a bill now makes a new version; an edit
must name the version it was based on and a stale one is refused. Pause,
resume, archive and restore are separate commands that change nothing
else, are safe to repeat, and write their own history line.

- Resuming names anyone in the payer or split who has left.
- An edit cannot move the next due date onto, or before, a period that
  was already recorded or skipped, and a new date must fall within a
  year back and two years ahead.
- Archived bills are always paused, so they never block a member leaving,
  and record, skip, pause and edit refuse them until they are restored.
- Adding a bill carries a request id, so a retry returns the same bill.
- A skip reaches at most one period ahead, and one bill can have at most
  12 periods skipped in a day.
- Index recurring_bill_occurrences(expense_id) for the snapshot's
  per-expense period check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Migration 0027 moves every note and task write into SECURITY DEFINER
functions, as announcements already are, and drops the client write
grants and policies on both tables.

- create_note / create_operation carry a request id; a retry with the
  same content returns the same row, a reused id with other content is
  refused. Content is checked with plain messages.
- update_note / update_operation name the version the edit started
  from. Every task change, occurrence commands included, moves the
  task's version, so an edit opened before a completion is refused.
  An edit cannot set status, completion or timestamps.
- remove_note / remove_operation are soft deletes for the creator or an
  admin, with one activity line; history rows stay. Removed rows leave
  the snapshot, search, the read policy and departures.
- A commune holds up to 500 notes and 1,000 tasks and events.

shared_content.sql passes again; grants, people and handover tests
use the commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
…make creates retry-safe

Database part of review findings 7, 10 and 11 (migration 0028).

- Invitations: when an admin leaves, is removed or becomes a member, the
  invitations they sent that are still open are revoked. Accepting or
  previewing an invitation needs its sender to still be an active admin,
  and the migration revokes open invitations that already fail that rule.
  list_commune_invites also returns the sender's id and name.
- Retries: expenses, funds, pledges and contributions are created through
  commands that take a request id. A repeat returns the original record
  while it still stands as requested; the same id for a different change is
  refused. The older RPCs keep their signatures and run the same rules.
- Pledges: someone other than the pledger may only lower a pledge (never
  below what is recorded), move it later or cancel it, and the pledger gets
  a notice naming who changed what. Cancelling twice changes nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The save route now takes the version an edit was based on (a stale one is
a 409) and a request id for an add, and no longer pauses or resumes.
POST /api/recurring/:id/pause and /archive call the new commands and
return the bill's state with duplicate: true for a repeat.

The recurring HTTP tests now pick their month-end dates relative to
today, since a new bill's first due date must fall within the last year.
A new recurring-bills test covers a stale edit, the pause command,
archive and restore, resuming after a member left, and the skip limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The local API now calls create_note, update_note and remove_note, and
create_operation, update_operation and remove_operation, instead of
writing the tables. Adds need a requestId and edits an expectedVersion;
a stale edit is a 409. Task edits no longer carry status or completion.

Adds note-and-task-commands.mjs and updates the other HTTP tests and the
lifecycle fixture, which now also seeds a late task for someone else and
a late task with nobody on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Commune keeps account ids as text while Managed Neon Auth keys accounts
by uuid. Every name and email lookup compared u.id::text with an id, so
each one read the whole account table, and the task list made one per
name. With 20,000 accounts a household with 300 rotating tasks took 37
seconds to load.

Migration 0030 adds commune_private.accounts, which compares uuid with
uuid behind a validity check (a malformed id still matches no one), and
moves every lookup onto it. The task list now resolves its names once
per snapshot. The same household loads in about 0.1 seconds, and the
task list output is unchanged. The invitation email sender uses the same
lookup, so the worker deploys after this migration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Both backends now create expenses, funds, pledges and contributions through
create_expense_command, create_fund_command, pledge_fund_command and
record_fund_contribution_command, passing the form's request id and
answering {id, duplicate}. The local API answers 201 for a new record and
200 for a repeat, and gives older callers without a request id a fresh one.
The invitation list carries invitedBy and invitedByName.

The Neon contract test pins the new function names, arguments and results,
the reused-request-id rejection (400), the pledge-limit refusal (403) and
the invite sender mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Add expense and the fund, pledge and contribution dialogs keep one request
id while they are open. After an unconfirmed attempt the fields lock and
the form says that trying again will not record it twice; a refusal says
nothing was saved, and a reused id says an earlier attempt may already
have been saved.

On someone else's pledge the editor offers only what the database
accepts: a lower amount (not below what is recorded) or a later date,
with copy that names who can raise it or bring it forward and says they
will be told. The cancel dialog says the same. Pledge change and
cancellation notices name who did it and open the fund.

Members shows who sent each invitation, and a demotion, removal or leave
says which open invitations will be withdrawn.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The local API test runs four accounts through 0028: invitations list who
sent them, a removed or demoted admin's invitations stop working, two
simultaneous attempts at one expense record it once, repeats of a fund,
pledge or contribution return the first record, a reused request id with
different details is refused, an admin cannot raise or bring forward
someone else's pledge, and the pledger is told about a change or a
cancellation once. It refuses port 3002 and any non-loopback address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The production client no longer writes commune_notes or
commune_operations through the Data API. Adds, edits and removals call
create_note, update_note, remove_note, create_operation,
update_operation and remove_operation, and return {id, version,
duplicate}. A missing version or request id is passed as null so the
database refuses it plainly. Task edits never send status or completion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Each open add form keeps one request id, so a retry after an
unconfirmed save is recognised instead of adding a second copy. Task
edits send the version the form opened with; a completion, skip or
handover since makes the edit stale. The form then says the task
changed, keeps Save off and offers to load the latest details. The note
editor detects a conflict by status rather than by message text.

Failures use commandFailure: a refusal shows the database's reason, an
unknown outcome says a retry is safe. Remove is offered only to the
person who added the task or an admin, and waits while a change saves.
Status and completion are no longer part of an add or an edit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The week showed only the oldest late task; any other late task appeared
nowhere. Now each late task has its card, yours first and then the
oldest. The Yours card includes your late tasks, so it no longer says
you are clear while one is waiting.

Copy reads "1 day late" or "3 days late" instead of "Late · 1 days",
and an unassigned task says "Nobody has this yet." The late cards' Skip
and Done buttons wait while a change is saving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The recurring bills screen now uses the new commands. An edit sends the
version its editor loaded, an add sends one request id per open form, and
Pause, Resume, Archive and Restore call their own commands instead of
saving the whole bill. Each action is shown only to the people its
command allows.

When a bill changed while it was being edited, the editor says so, lists
what changed and saves nothing until the person keeps their edits on top
of the newer version or takes the latest one. A save whose reply was lost
is recognised as the person's own. People who have left are taken out of
the split when the editor opens, and the editor says who; a payer who has
left must be replaced. Resuming such a bill opens the editor, and someone
who cannot edit it is told why it cannot resume yet. A period that was
already recorded or skipped says so plainly, and conflicts refresh the
bill before the message appears.

Archived bills leave the active list and appear under an Archived filter,
where they can be restored, still paused. Skip appears only once the next
period can be skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The membership dialog chose between "Your" and "Their" by action, so an
admin demoting themselves read "Their open invitation will be withdrawn".
It now follows whose membership changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
The database refuses to skip a one-off task, so the button always
failed. The task window already hides it; the late card now does too.
With every late task listed, one-off late tasks are more often on show.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS

# Conflicts:
#	apps/web/tests/neon-lifecycle-contract.test.tsx
…and 11)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS

# Conflicts:
#	apps/local-api/README.md
The merged recurring bill and invitation changes each added a lookup that
compared account ids as text. Both now go through commune_private.accounts,
and the lookup test covers them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
commune-web Error Error Oct 9, 2026 11:54am UTC

claude added 2 commits October 9, 2026 11:34
A repeated add now gets the existing bill back only when that bill is
still at version 1 and its name, amount, payer, split, frequency, next due
date and paused state all match the request. A different request, or a
bill that has changed since, is refused as already used for a different
bill instead of silently keeping the old one.

The add takes a transaction lock on the person and request id before it
looks the request up, so the same request sent at once to two communes
waits for the first and is refused, instead of reaching the unique index.
An edit that names no version is refused as a mistake in the request,
not reported as a conflict.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
When the editor's conflict view follows a save that got no reply, and the
newer bill is not recognisably that save, it now says the earlier save may
have gone through and asks the person to check the bill below, instead of
saying nothing of theirs was saved. An outright refusal of a later attempt
settles it, and taking the latest version starts afresh.

An add refused because its request was already used now tells the person
an earlier attempt may already have been saved, and to check Recurring
before adding it again, as the expense form does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
claude added 10 commits October 9, 2026 11:35
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A pledge can be cancelled and made again, so its request id no longer lives
on the pledge row. fund_pledge_requests keeps one row per (user, request)
with the pledge it made and what it asked for; clients have no grants on it.
A repeat replays only while the pledge still stands as that request left
it (not cancelled, unchanged, and not made again by a later request);
otherwise it is refused as a reused request. A late retry can no longer
bring back a cancelled pledge, or read as "you already have a pledge" when
it did save. fund_commitments.request_id is dropped (0028 is unreleased).

update_fund_pledge gains p_expected_amount_minor and p_expected_due_on
(DEFAULT NULL, same grants). When given and the pledge no longer matches,
any editor is refused with P0001 and nothing is saved, so an edit made from
an older screen cannot undo someone else's change.

SQL test blocks 4 and 5 fail without these changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
PATCH /api/commitments/:id accepts expectedAmountMinor and expectedDueOn
and passes them to update_fund_pledge; a pledge changed since is a 409.

The HTTP test covers both review points: a retry of an earlier pledge
request after cancel and re-pledge is a 400 reused request (it was a 409
"already have a pledge", or revived the cancelled pledge), and stale edits
or cancellations are a 409 with nothing saved and no notice sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Pledge edits and cancellations send the amount and date the dialog opened
with, through the local API and the hosted client. On a 409 the fund is
reloaded before the message shows, and the dialog starts again from the
pledge as it is now.

Once an expense, fund, pledge or contribution form has had an attempt with
an unknown outcome, a later refusal (a 403 after a role change, a 401) no
longer says "Nothing was saved."; it says an earlier attempt may already
have been saved and where to check before adding it again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Removing a note left its earlier versions readable by any member, someone
who joined later included, through the revision route and the Data API, so
an old door code stayed one request away. The revision and occurrence read
policies now also require the note or task to be there. The rows are kept
and the owner still sees them. The local route and the hosted client answer
404 "Revision not found", as for an id that never existed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A retried add found its earlier row by request id and returned it as a
duplicate even after it had been removed, handing back a record nobody can
see. create_note now raises P0001 "This note has been removed" and
create_operation "This task has been removed" (or event), which the local API
and the hosted client report as 409.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
A member leaving locks the commune row and then that member's tasks, while
update_operation and remove_operation locked the task first and then needed
the commune row for their activity line. Run together, each waited on the
other until PostgreSQL cancelled one as a deadlock. Both now read the task's
commune without a lock, lock the commune, then lock the task. An unknown id
is still refused with 42501.

task_lock_order.mjs holds a departure between its two locks, starts an edit
or a removal, then lets both run: before this change each deadlocked (40P01)
every time; now the edit is refused as stale and the removal goes through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
When an edit saved but its reply was lost, the next refresh brought in the
next version from that same save, and the form called it someone else's
change: "nothing here has been saved", with Save disabled. The form now
remembers what it last sent. It is stale only when the latest record differs
from that; when it matches, it says "Your last save went through." and the
next save names that version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6P7aTcMBccooYYr4xYnVS

Copy link
Copy Markdown
Owner Author

The Vercel preview for commune-web shows Error on this PR. It isn't caused by this diff: vite build in apps/web passes on the head commit (88d1c06), and previews have failed within seconds of "Building" on every new branch since 6 October (#20, #22, #23), before any of the code runs. The likeliest cause is the project or its Neon integration, not the build. Nothing in this PR can fix it, so I'm leaving it for whoever has the Vercel login to check a failed deployment's log.


Generated by Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T02:01:21.841079Z 88d1c06 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch had an error being deployed

1 failed deployment
Preview — 88d1c06d Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants