Skip to content

feat(notifications): retire an Android push installation without a session - #2834

Merged
Quick104 merged 1 commit into
fix/apple-push-retire-installationfrom
fix/android-push-retire-installation
Oct 21, 2026
Merged

Quick104 merged 1 commit into
fix/apple-push-retire-installationfrom
fix/android-push-retire-installation

Conversation

@Quick104

@Quick104 Quick104 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Related issue: N/A
Validation tasks: changes #1234 C2

An Android phone that removes a Silo server, or signs out of it, sends one logged-in unregisterPushDevice and then clears its login. If that request doesn't land, the app can't resend it, and the server keeps sending FCM pushes to the phone. This is the Android side of the gap #2826 closes for Apple.

This adds a way for the app to stop those pushes without a login.

Approach

New endpoint: POST /api/v2/notifications/push/devices/{device_id}/retire (retirePushDevice). It takes X-Push-Installation-Key and X-Push-Generation, the same headers as ordered Android registration, and no body. The server already stores the key's hash, so the key authorizes this one call.

  • A newer generation deletes the installation's Android push_devices rows, and their queued attempts go with them. It records the stored owner's removal at that generation. The key hash and owner stay, so a later registration at a higher generation goes through the normal ordered path.
  • Replaying the same retirement returns 200. A wrong key returns 403. An older generation, or a registration's own generation, returns 409, so a late retry can't undo a server the user added back.
  • An installation with no ordered record returns removed: true and nothing is written.
  • The route is public. It has its own per-IP budget, android_push_retire, at 20/min with a burst of 10. The 256-bit key is the real guard.

This is stacked on #2826, which adds the Apple endpoint and the shared generation parser. Retarget to main after #2826 merges. The Android client change that calls this is Silo-Server/silo-android#657.

Validation

  • Repository test against a migrated Postgres: pass. It covers retire and replay, wrong key, stale and equal generations, re-registering after retirement, and a late retirement after re-registering.
  • HTTP test: pass. The call needs no session, and the error mapping is checked.
  • verify-apiv2-openapi, verify-apiv2-web-types, verify-route-inventory: pass. verify-apiv2-contract: 2 changes, none breaking.
  • Deployed to a dev server and exercised through the API: register 200; wrong key 403; stale generation 409; retire and replay 200 with the device rows gone; registration at the retired generation 409 and at a higher one 200; a late, older retirement 409 with the new registration untouched.
  • Not run locally: the full make test-go and the web gate. CI covers them.

Evidence

Evidence: https://evidence.siloserver.org/r/silo-server/pr-2834/

Risks

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern.
  • The Evidence section shows every change a user can see, or says there is none.

AI Disclosure

  • Harness: Claude Code (in T3 Code)
  • Tool(s): Claude Code
  • Model(s): claude-opus-5-5
  • Involvement: Fully AI-generated, human verified
  • Adversarial review: n/a

🤖 Generated with Claude Code

Note

Add session-less Android push device retirement to notifications API

  • Adds POST /api/v2/notifications/push/devices/{device_id}/retire, which takes an installation-key header and generation, and returns a receipt with the generation and a removed flag. The route is registered in notification_android_push.go and wired through the router when a push-device service is configured.
  • PushDeviceRepository.RetireAndroidPush in android_push_ordering.go performs the retirement transactionally under the installation lock. It requires a matching installation key, rejects stale generations, replays an existing removal at the same generation, deletes Android push rows and queued delivery attempts, and writes a removal tombstone. Unknown and bridge-only installations return a removed receipt without changes.
  • Uses a dedicated android_push_retire per-IP rate limit of 20 requests per minute with a burst of 10 in ratelimit.go.
  • Documented in notifications-push-api.md; OpenAPI contract and generated web TypeScript schema updated.
  • Risk: same-generation registration conflicts with retirement and late retirements can no longer remove a newer registration — consumers relying on unordered removal should check RetireAndroidPush generation ordering.

Macroscope summarized e74e963.

…ssion

An Android phone that removes a server or signs out sends one
session-authenticated unregisterPushDevice and then clears its login. When
that request does not land, the app cannot resend it and the server keeps
pushing FCM to the phone.

POST /api/v2/notifications/push/devices/{device_id}/retire
(retirePushDevice) takes the installation key and generation the app still
holds and needs no login session. Under the installation lock, a newer
generation applies an ordered removal on behalf of the stored owner: it
deletes the installation's Android push rows (queued attempts cascade) and
records the owner's removal intent at that generation. A replay at a stored
removal's generation answers removed without writing, an older generation or
the generation of a registration gets 409, a wrong key gets 403, and an
installation with no ordered record answers removed without writing. A later
registration with a higher generation and the same key resumes through the
existing ordered path.

The route is public and spends a new android_push_retire per-IP rate-limit
budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1e7b01b0-5100-41db-9d02-3c196d522345

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@silo-triage silo-triage Bot added the size/XL 500–999 added + deleted lines (all files); change volume, not priority. label Oct 21, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 21, 2026

Copy link
Copy Markdown

visible, evidence incomplete

  • Android push delivery changes when this endpoint retires an installation; the new retirement and generation-ordering behavior in internal/notifications/android_push_ordering.go and internal/apiv2/notification_android_push.go lacks evidence. Add before-and-after API response excerpts for the same installation, showing successful retirement and that a late retirement does not remove a newer registration, and identify the surface and build or commit. The validation notes describe testing but do not include those excerpts.

Suggested fix: add the matching API request/response excerpts under ## Evidence.

Automated check: Macroscope check run agent (gpt-6-luna). Evidence was not reviewed for correctness.

Posted via Macroscope — Visible change evidence

@silo-triage silo-triage Bot added api-v2 API v2 / Huma migration program (epic #135); long-running, stacked by phase bug Something isn't working go Pull requests that update go code impact: security Auth, privacy, or permission boundary priority: P2 Limited scope, workaround exists, or polish stacked Base is another open PR; merge in stack order named in the body labels Oct 21, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 21, 2026

Copy link
Copy Markdown

Validation tasks: should include the dependent Android case. This endpoint is used by the Android sign-out/unregister flow in Silo-Server/silo-android#346 C2 (the linked client PR #657 calls it). Suggested line: Validation tasks: changes #1234 C2; changes Silo-Server/silo-android#346 C2. Both relevant C2 result cells are currently Not run, so no recorded manual result is being invalidated or unblocked.

Automated check: Macroscope check run agent (gpt-6-luna). No validation was performed.

Posted via Macroscope — v1 validation impact

@Quick104
Quick104 merged commit 07cd1e9 into fix/apple-push-retire-installation Oct 21, 2026
20 checks passed
@Quick104
Quick104 deleted the fix/android-push-retire-installation branch October 21, 2026 17:53
Quick104 added a commit that referenced this pull request Oct 21, 2026
…2826)

* feat(notifications): retire an Apple push installation without a session

An iPhone that removes a server makes one session-authenticated attempt to
turn push off and then deletes its session. When that attempt fails, nothing
can stop the server from pushing to the phone, which shows the pushes as
generic banners.

POST /api/v2/devices/push/apple/retire (retireApplePushInstallation) takes
the installation key and generation the app still holds and needs no login
session. Under the installation lock, a newer generation deletes the
installation's Apple push rows (queued attempts cascade) and records a
retirement intent that keeps the key hash and owner. An exact replay answers
the same receipt without writing, an older or conflicting generation gets 409,
a wrong key gets 403, and an installation with no ordered record answers
removed without writing. A later registration with a higher generation and the
same key resumes through the existing ordered path.

The route is public and spends a new apple_push_retire per-IP rate-limit
budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(notifications): retire an Android push installation without a session (#2834)

An Android phone that removes a server or signs out sends one
session-authenticated unregisterPushDevice and then clears its login. When
that request does not land, the app cannot resend it and the server keeps
pushing FCM to the phone.

POST /api/v2/notifications/push/devices/{device_id}/retire
(retirePushDevice) takes the installation key and generation the app still
holds and needs no login session. Under the installation lock, a newer
generation applies an ordered removal on behalf of the stored owner: it
deletes the installation's Android push rows (queued attempts cascade) and
records the owner's removal intent at that generation. A replay at a stored
removal's generation answers removed without writing, an older generation or
the generation of a registration gets 409, a wrong key gets 403, and an
installation with no ordered record answers removed without writing. A later
registration with a higher generation and the same key resumes through the
existing ordered path.

The route is public and spends a new android_push_retire per-IP rate-limit
budget.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-v2 API v2 / Huma migration program (epic #135); long-running, stacked by phase bug Something isn't working go Pull requests that update go code impact: security Auth, privacy, or permission boundary priority: P2 Limited scope, workaround exists, or polish size/XL 500–999 added + deleted lines (all files); change volume, not priority. stacked Base is another open PR; merge in stack order named in the body

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant