Relates to #19 and #18. Neither is closed — both are standing tickets.
The finding
Measured on production, 2026-09-07: 8 rows in push_tokens, 4 live devices.
user
device row exists
last written
(no user)
no
2026-08-29
(no user)
no
2026-08-31
fnreviewalpha
no
2026-09-01
fnreviewalpha
no
2026-09-02
(no user)
no
2026-09-05
(no user)
no
2026-09-05
fnreviewalpha
yes
2026-09-04
fnreviewalpha
yes
2026-09-05
Six of eight point at a device that is gone; four of those at an account that is gone. Oldest is nine days old and nothing would ever have collected it.
An APNs token is a stable per-install identifier Apple holds as well as we do. A row nobody can use is an identifier kept for no reason.
Why
All three paths that remove a device delete prekeys and envelopes and forget this table — revokeDevice, deleteAccount, and the retention expiry sweep. Each now takes the token with it. Account deletion filters by user rather than by its device list, so a token whose device row had already gone is collected too — the state four production rows were in.
The sweep collects what is already there. Compared against the device table, not swept by age: a push token is supposed to outlive its last use — a phone in a drawer for a month should still ring — so age says nothing about whether it is wanted. Having no device does. Counted as push_orphans beside the other sweep numbers.
There is a second cause this MR does not fix, because it is client-side: iOS signOut clears store.appPassword as step 1 (deliberately — it is what makes the keys left behind unable to fetch anything) and then calls forgetPushToken(), a try? call on an authenticated route. With a live cached relay token it works; once that has expired there is nothing left to mint another, it 401s, and the try? swallows it. Same shape as the attachment 401. Written up in the doc; needs its own ticket.
Falsified
sweep made unconditional → a token whose device is live survives goes red
sweep made to delete nothing → both go red
The second test is the one that matters more: a sweep that takes a working phone's token stops it ringing, and nothing on the phone would say why.
Two investigation passes, written up rather than acted on
Both tickets say investigate and document, do not change, so these are documents.
docs/METADATA-PASSES.md (#18) — new finding: length(payload) is visible and is not on /metadata. Fourteen envelopes, fourteen distinct lengths, clustering by kind (~6.3K ×2, ~9.4K ×6, ~13.9K ×4, ~16.7K ×2). Sealed sender removes attribution from the row; it does not remove size, timing or order, and a long enough window is made of those. Size-bucketing is proposed for its own ticket with numbers, since the trade has to be measured rather than asserted.
Two of the ticket's own bullets are stale, and the doc says so rather than repeating them: retention does sweep (in-process supervised loop, logged every cycle), and the unsealed fallback is measured — sends_sealed/sends_unsealed in the sweep line, and 14/14 sealed on live traffic. Fourteen envelopes from two accounts is not a sample to generalise from.
docs/IDENTIFYING-DATA.md (#19) — the edge-log retention timer runs hourly, exits 0, and has pruned 0 MB on every run since it was installed. Not because it is broken: edge-traefik-1 was recreated 13 hours ago for the obfs4 deploy and only .log and .log.1 exist, and the script starts at .2. "Exit 0" and "0 MB" together are not evidence, so the glob was checked against a fixture instead of assumed — it removes .2 upward including two-digit rotations and keeps the live file and one rotation.
Docker's own cap on that container is max-file 10 × 100m = about 3½ days of client IPs. Lowering max-file to 2 would put the bound at ~18 hours in Docker itself. Proposed, not done — that container fronts every site on the box.
Also stale in #19: users no longer has dead tuples (autovacuum has run); only envelopes does.
Relates to #19 and #18. Neither is closed — both are standing tickets.
## The finding
Measured on production, 2026-09-07: **8 rows in `push_tokens`, 4 live devices.**
| user | device row exists | last written |
|---|---|---|
| *(no user)* | no | 2026-08-29 |
| *(no user)* | no | 2026-08-31 |
| fnreviewalpha | no | 2026-09-01 |
| fnreviewalpha | no | 2026-09-02 |
| *(no user)* | no | 2026-09-05 |
| *(no user)* | no | 2026-09-05 |
| fnreviewalpha | **yes** | 2026-09-04 |
| fnreviewalpha | **yes** | 2026-09-05 |
Six of eight point at a device that is gone; four of those at an account that is gone. Oldest is nine days old and nothing would ever have collected it.
An APNs token is a stable per-install identifier **Apple holds as well as we do**. A row nobody can use is an identifier kept for no reason.
## Why
All three paths that remove a device delete prekeys and envelopes and forget this table — `revokeDevice`, `deleteAccount`, and the retention expiry sweep. Each now takes the token with it. Account deletion filters by *user* rather than by its device list, so a token whose device row had already gone is collected too — the state four production rows were in.
The sweep collects what is already there. **Compared against the device table, not swept by age**: a push token is *supposed* to outlive its last use — a phone in a drawer for a month should still ring — so age says nothing about whether it is wanted. Having no device does. Counted as `push_orphans` beside the other sweep numbers.
There is a second cause this MR does **not** fix, because it is client-side: iOS `signOut` clears `store.appPassword` as step 1 (deliberately — it is what makes the keys left behind unable to fetch anything) and then calls `forgetPushToken()`, a `try?` call on an authenticated route. With a live cached relay token it works; once that has expired there is nothing left to mint another, it 401s, and the `try?` swallows it. Same shape as the attachment `401`. Written up in the doc; needs its own ticket.
## Falsified
- sweep made unconditional → *a token whose device is live survives* goes red
- sweep made to delete nothing → **both** go red
The second test is the one that matters more: a sweep that takes a working phone's token stops it ringing, and nothing on the phone would say why.
## Two investigation passes, written up rather than acted on
Both tickets say investigate and document, do not change, so these are documents.
**`docs/METADATA-PASSES.md` (#18)** — new finding: **`length(payload)` is visible and is not on `/metadata`.** Fourteen envelopes, fourteen distinct lengths, clustering by kind (~6.3K ×2, ~9.4K ×6, ~13.9K ×4, ~16.7K ×2). Sealed sender removes attribution from the row; it does not remove size, timing or order, and a long enough window is made of those. Size-bucketing is proposed for its own ticket with numbers, since the trade has to be measured rather than asserted.
Two of the ticket's own bullets are **stale**, and the doc says so rather than repeating them: retention *does* sweep (in-process supervised loop, logged every cycle), and the unsealed fallback *is* measured — `sends_sealed`/`sends_unsealed` in the sweep line, and **14/14 sealed** on live traffic. Fourteen envelopes from two accounts is not a sample to generalise from.
**`docs/IDENTIFYING-DATA.md` (#19)** — the edge-log retention timer runs hourly, exits 0, and has **pruned 0 MB on every run since it was installed.** Not because it is broken: `edge-traefik-1` was recreated 13 hours ago for the obfs4 deploy and only `.log` and `.log.1` exist, and the script starts at `.2`. "Exit 0" and "0 MB" together are not evidence, so the glob was checked against a fixture instead of assumed — it removes `.2` upward including two-digit rotations and keeps the live file and one rotation.
Docker's own cap on that container is `max-file 10 × 100m` = **about 3½ days of client IPs**. Lowering `max-file` to `2` would put the bound at ~18 hours in Docker itself. Proposed, **not done** — that container fronts every site on the box.
Also stale in #19: `users` no longer has dead tuples (autovacuum has run); only `envelopes` does.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Relates to #19 and #18. Neither is closed — both are standing tickets.
The finding
Measured on production, 2026-09-07: 8 rows in
push_tokens, 4 live devices.Six of eight point at a device that is gone; four of those at an account that is gone. Oldest is nine days old and nothing would ever have collected it.
An APNs token is a stable per-install identifier Apple holds as well as we do. A row nobody can use is an identifier kept for no reason.
Why
All three paths that remove a device delete prekeys and envelopes and forget this table —
revokeDevice,deleteAccount, and the retention expiry sweep. Each now takes the token with it. Account deletion filters by user rather than by its device list, so a token whose device row had already gone is collected too — the state four production rows were in.The sweep collects what is already there. Compared against the device table, not swept by age: a push token is supposed to outlive its last use — a phone in a drawer for a month should still ring — so age says nothing about whether it is wanted. Having no device does. Counted as
push_orphansbeside the other sweep numbers.There is a second cause this MR does not fix, because it is client-side: iOS
signOutclearsstore.appPasswordas step 1 (deliberately — it is what makes the keys left behind unable to fetch anything) and then callsforgetPushToken(), atry?call on an authenticated route. With a live cached relay token it works; once that has expired there is nothing left to mint another, it 401s, and thetry?swallows it. Same shape as the attachment401. Written up in the doc; needs its own ticket.Falsified
The second test is the one that matters more: a sweep that takes a working phone's token stops it ringing, and nothing on the phone would say why.
Two investigation passes, written up rather than acted on
Both tickets say investigate and document, do not change, so these are documents.
docs/METADATA-PASSES.md(#18) — new finding:length(payload)is visible and is not on/metadata. Fourteen envelopes, fourteen distinct lengths, clustering by kind (~6.3K ×2, ~9.4K ×6, ~13.9K ×4, ~16.7K ×2). Sealed sender removes attribution from the row; it does not remove size, timing or order, and a long enough window is made of those. Size-bucketing is proposed for its own ticket with numbers, since the trade has to be measured rather than asserted.Two of the ticket's own bullets are stale, and the doc says so rather than repeating them: retention does sweep (in-process supervised loop, logged every cycle), and the unsealed fallback is measured —
sends_sealed/sends_unsealedin the sweep line, and 14/14 sealed on live traffic. Fourteen envelopes from two accounts is not a sample to generalise from.docs/IDENTIFYING-DATA.md(#19) — the edge-log retention timer runs hourly, exits 0, and has pruned 0 MB on every run since it was installed. Not because it is broken:edge-traefik-1was recreated 13 hours ago for the obfs4 deploy and only.logand.log.1exist, and the script starts at.2. "Exit 0" and "0 MB" together are not evidence, so the glob was checked against a fixture instead of assumed — it removes.2upward including two-digit rotations and keeps the live file and one rotation.Docker's own cap on that container is
max-file 10 × 100m= about 3½ days of client IPs. Loweringmax-fileto2would put the bound at ~18 hours in Docker itself. Proposed, not done — that container fronts every site on the box.Also stale in #19:
usersno longer has dead tuples (autovacuum has run); onlyenvelopesdoes.mentioned in issue #18
mentioned in issue #19
mentioned in issue flamenet-messenger-ios#71
mentioned in commit
bd46682723changed the description