Repository navigation
feat: add opt-in Remotes web push notifications #1138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
xHeaven
merged 18 commits into
dev-null-sh:production
from
xHeaven:feat/remotes-web-push
Oct 26, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
cedb04f
feat: deliver remotes notifications with web push
xHeaven 60b2dc6
fix: recover stalled browser push enrollment
xHeaven ce8a953
feat: make off-site remotes notifications opt-in
xHeaven 56061b7
fix: preserve push enrollment during passive sync failures
xHeaven eb04836
docs: describe opt-in push for installed apps
xHeaven 42e988f
perf: skip remote recipients without push subscriptions
xHeaven d79bab5
docs: remove unsolicited web push setup guide
xHeaven 12f6229
fix: limit push enrollments to twenty per account
xHeaven ea8a067
chore: merge production into the web push feature
xHeaven 44d8abb
chore: include the latest apps page update
xHeaven ab5f20b
fix: retry only failed remote push subscriptions
xHeaven ac0eb0d
fix: prevent push enrollment from surviving revocation
xHeaven 523e6e9
fix: hold push session locks for the full request
xHeaven 71a9413
fix: bind push consent to the initiating account
xHeaven 690d202
fix: preserve shared push subscriptions across accounts
xHeaven 489816d
test: generate Wayfinder routes before unit tests
xHeaven bb6e480
fix: bound remote push delivery per recipient
xHeaven db19efd
fix: serialize browser push consent across tabs
xHeaven File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions; | ||
|
|
||
| use App\Models\PushSubscription; | ||
| use App\Models\User; | ||
|
|
||
| final readonly class CreatePushSubscription | ||
| { | ||
| public function handle(User $user, string $endpoint): PushSubscription | ||
| { | ||
| // One browser subscription belongs to only the account currently using it. | ||
| return PushSubscription::query()->updateOrCreate( | ||
| ['endpoint_hash' => hash('sha256', $endpoint)], | ||
| ['user_id' => $user->id, 'endpoint' => $endpoint], | ||
| ); | ||
| } | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions; | ||
|
|
||
| use App\Models\PushSubscription; | ||
| use App\Models\User; | ||
|
|
||
| final readonly class DeletePushSubscription | ||
| { | ||
| public function handle(User $user, string $endpointHash): void | ||
| { | ||
| PushSubscription::query()->where('user_id', $user->id)->where('endpoint_hash', $endpointHash)->delete(); | ||
| } | ||
| } |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Controllers; | ||
|
|
||
| use App\Actions\CreatePushSubscription; | ||
| use App\Actions\DeletePushSubscription; | ||
| use App\Http\Requests\CreatePushSubscriptionRequest; | ||
| use App\Http\Requests\DeletePushSubscriptionRequest; | ||
| use App\Models\User; | ||
| use Illuminate\Container\Attributes\CurrentUser; | ||
| use Illuminate\Http\JsonResponse; | ||
| use Illuminate\Http\Response; | ||
|
|
||
| final readonly class PushSubscriptionController | ||
| { | ||
| public function index(): JsonResponse | ||
| { | ||
| return response()->json([ | ||
| 'public_key' => config('services.web_push.private_key') ? config('services.web_push.public_key') : null, | ||
| ])->header('Cache-Control', 'private, no-store'); | ||
| } | ||
|
|
||
| public function store(CreatePushSubscriptionRequest $request, #[CurrentUser] User $user, CreatePushSubscription $action): Response | ||
| { | ||
| $subscription = $action->handle($user, $request->string('endpoint')->toString()); | ||
| $request->session()->put('push_endpoint_hash', $subscription->endpoint_hash); | ||
|
|
||
| return response()->noContent(); | ||
| } | ||
|
|
||
| public function destroy(DeletePushSubscriptionRequest $request, #[CurrentUser] User $user, DeletePushSubscription $action): Response | ||
| { | ||
| $action->handle($user, hash('sha256', $request->string('endpoint')->toString())); | ||
| $request->session()->forget('push_endpoint_hash'); | ||
|
|
||
| return response()->noContent(); | ||
| } | ||
| } |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Middleware; | ||
|
|
||
| use Closure; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Config; | ||
| use Illuminate\Support\Facades\DB; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| final readonly class SerializePushSessions | ||
| { | ||
| /** | ||
| * @param Closure(Request): (Response) $next | ||
| */ | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| $sessionId = $request->cookie(Config::string('session.cookie')); | ||
|
|
||
| if (! is_string($sessionId)) { | ||
| return $next($request); | ||
| } | ||
|
|
||
| // Run before StartSession and hold through its save; a cache lease can expire mid-enrollment. | ||
| return DB::transaction(function () use ($sessionId, $request, $next): Response { | ||
| DB::select('select pg_advisory_xact_lock(hashtextextended(?, 0))', [hash('sha256', 'remote-push-session:'.$sessionId)]); | ||
|
|
||
| return $next($request); | ||
| }); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Http\Middleware; | ||
|
|
||
| use App\Models\User; | ||
| use Closure; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\DB; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| final readonly class SerializePushSubscriptions | ||
| { | ||
| /** | ||
| * @param Closure(Request): (Response) $next | ||
| */ | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| $user = $request->user(); | ||
| assert($user instanceof User); | ||
|
|
||
| // Hold the account lock across validation and creation, including concurrent browser enrollments. | ||
| return DB::transaction(function () use ($user, $request, $next): Response { | ||
| User::query()->whereKey($user->id)->lockForUpdate()->firstOrFail(); | ||
| $user->refresh(); | ||
|
|
||
| return $next($request); | ||
| }); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.