Repository navigation
feat: push notifications (server side): devices, a test send, receipts - #38
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated on ThorSPB (dotnet ef migrations add, dotnet build). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or codes, races, limits) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13
What
push_devices: id,user_id(FK → users, cascade),token(unique),platform(android|ios),created_at,last_registered_at.push_receipts_pending:ticket_id,device_id(FK → push_devices, cascade),created_at.AddPushDevices, generated on ThorSPB.POST /me/devices{ token, platform }:last_registered_at);platformis missing, a number, or not one of the two names;DELETE /me/devices{ token }:{ token }.POST /me/notifications/test:{ sent };Retry-Afterwithin 60 s, per student, in memory;IPushSenderwithExpoPushSender: a typed HttpClient onhttps://exp.host, 15 s timeout;Push:Expo:AccessToken, sent as Bearer.errors[].code(nevermessageordetails, which list tokens).DeviceNotRegistereddeletes the device at once, unless it registered again after the send started;push_receipts_pendingrow stamped with the send time;PushReceiptChecker(a BackgroundService, always registered):JsonStringEnumConverter(CamelCase, allowIntegerValues: false), plusRespectRequiredConstructorParameters;UseStatusCodePages;ThrowOnBadRequest = falsein every environment._(PUSH_TOO_MANY_EXPERIENCE_IDSpasses, anything with a token can't); anything else becomesUnknown.^exp\.host:443$added todeploy/pi/egress-allow.deploy/pi/README.mdsays installing it is a manual step, and why the proxy is used.POST /meand on token change; delete on sign-out.Why
Server side of push (#13): devices can be registered and reached, dead tokens clean themselves up without taking a reinstalled phone with them, and nothing about a student or a token reaches the logs.
Decisions beyond the issue
DELETE /me/devicestakes the token in the body, not the path. Paths are logged: by our request line, in every line'sRequestPathscope, and in nginx's access log. A token in the URL would leak on every sign-out.DELETE /me/devicesbody is a 400 (declared in the contract), not a silent 204. Only the token's value is kept unprobeable."android" | "ios"), only by name.platformwas the first enum in the contract.\z, not$(.NET's$also accepts a trailing newline), and capped at 200 characters to match the column.Microsoft.EntityFrameworkCore.Relationalis pinned to 10.0.13 in Infrastructure. Without it, the Api's direct relational calls hit MSB3277 (two Relational versions), which fails the build under warnings-as-errors.SELECT … FOR UPDATE), so the cap holds and an overlappingDELETE /megives a 404;NoNetworkHandler), so no test can reach the network.How I tested it
PushTests(Postgres, Expo stubbed):last_registered_at; a token moves between students;"windows", no token or no JSON are 400 ProblemDetails, nothing stored;{}are 400 ProblemDetails;Retry-After: 30, then fine again after 61 s; no devices doesn't use up the minute; no account is 404;DeviceNotRegisteredticket deletes the device; a device registered again during the send survives one;DELETE /meremoves devices and receipts.PushReceiptTests:DeviceNotRegisteredand not-ready receipts are each handled; rows expire after 24 h; nothing pending means no requests;DeviceNotRegisteredreceipt;dataas an array, or a non-JSON answer → a failed batch, rows and devices kept;DbUpdateExceptionat Error, so turning onInclude Error Detailwould fail it), aPUSH_TOO_MANY_EXPERIENCE_IDSrefusal whose details list tokens, moves, refusals, a 400 body, DeviceNotRegistered tickets and receipts. No token, oid, user id or email appears in any message, exception or scope value.ExpoPushSenderTests:Unknown;errors) logs the codes and never the tokens in its details; a refused receipt read names the codes and not the details;TestNotificationLimitTests: a slot is held a minute, a released slot is free at once, and expired entries are dropped on every call.PushSetupTests: the handler keeps the default proxy, with a 15 s timeout, the base address, and a Bearer only when configured.PerUserDataTests: both walks reachpush_devicesandpush_receipts_pending, all cascade;PushDeviceTests(Domain): token shapes, the length cap,Register.API contract
POST /me/devices(RegisterPushDevice): 201/200DeviceResponse, 400, 404, 401;DELETE /me/devices(UnregisterPushDevice): bodyUnregisterDeviceRequest, 204, 400, 401;POST /me/notifications/test(SendTestNotification): 200TestNotificationResponse, 404, 429, 502, 401.RegisterDeviceRequest,UnregisterDeviceRequest,DeviceResponse,TestNotificationResponse,PushPlatform("android" | "ios").Checklist
🤖 Generated with Claude Code