Overview
@lastshotlabs/slingshot-push is Slingshot’s multi-provider push delivery package. It manages
device subscriptions, topic fan-out, delivery records, and provider dispatch for Web Push (VAPID),
iOS (APNS), and Android (FCM).
The push entities themselves follow the shared package-first/entity authoring model;
createPushPackage() is the runtime shell that composes routing, providers, and entity wiring.
What It Owns
Section titled “What It Owns”- Push subscription, topic, topic-membership, and delivery entities
- Per-platform provider wiring (web-push, APNS, FCM)
- Subscription management routes (
/topics/:name/subscribe,/topics/:name/unsubscribe,/ack/:deliveryId) - VAPID public-key endpoint for web clients (
/vapid-public-key) - A delivery adapter that auto-registers with
slingshot-notificationswhen that package is present PushRuntimeCapandPushHealthCapcapabilities for cross-package consumers
Dependencies
Section titled “Dependencies”slingshot-auth— required; all subscription routes require an authenticated userslingshot-notifications— optional; when present, the delivery adapter is registered automatically
Minimum Setup
Section titled “Minimum Setup”createPushPackage({ enabledPlatforms: ['web'], web: { vapid: { publicKey: '...', privateKey: '...', subject: 'mailto:push@example.com' } },});Health
Section titled “Health”Consumers resolve the aggregated health snapshot via the PushHealthCap capability:
import type { HookServices } from '@lastshotlabs/slingshot-core';import { PushHealthCap } from '@lastshotlabs/slingshot-push';
function readPushHealth(ctx: HookServices) { return ctx.capabilities.require(PushHealthCap)();}For iOS, add ios: { auth: { kind: 'p8-token', keyPem: '...', keyId: 'ABC123', teamId: 'TEAM123456' } }.
For Android, add android: { serviceAccount: { project_id: '...', client_email: '...', private_key: '...' } }.
The serviceAccount value can also be a JSON string or a file:// path read at boot time.
Operational Notes
Section titled “Operational Notes”mountPathmust start with/; trailing slashes are trimmed before routes are mounted.- All push routes are declared CSRF-exempt because service workers cannot attach CSRF tokens.
- The VAPID public-key endpoint is a public path — no auth guard applies.
- Topics are auto-created when a device subscribes to a named topic for the first time.
- When
slingshot-notificationsis present, the delivery adapter registers itself automatically duringsetupPost. No manual wiring is needed. - The Android
serviceAccountfield can be an inline object, a JSON string, or afile://URI resolved at startup. Prefer environment variables for key material rather than inlining in config. - Retry policy applies to delivery attempts, not to subscription management operations.
- When a provider send succeeds, the delivery record transitions through
sent→deliveredautomatically. The/ack/:deliveryIdendpoint is for client-side acknowledgement (e.g., a service worker confirming the notification was shown); it moves the record todeliveredfrom the client side. Both paths are now wired.
All-providers-fail contract
Section titled “All-providers-fail contract”router.sendToUser, sendToUsers, and publishTopic never throw when every provider
returns a failure result — total failure is reported in the return value, not via an exception.
Each call resolves to { delivered, attempted, allFailed }: delivered is the number of
successful sends, attempted is the number of subscriptions whose platform had a configured
provider, and allFailed is true iff attempted > 0 && delivered === 0. Callers that need
to branch on total failure should check result.allFailed directly rather than inspecting
push:delivery.failed bus events. A user with no subscriptions, or one whose subs all map to
unconfigured platforms, returns { delivered: 0, attempted: 0, allFailed: false } — that is
not a failure, just nothing to deliver. Per-subscription failures continue to surface via
push:delivery.failed bus events for observability.
Gotchas
Section titled “Gotchas”enabledPlatformscontrols which providers are wired and which entities are active. Listing a platform without supplying its config will fail validation.- VAPID keys should be generated once and stored durably. Rotating them invalidates all existing web-push subscriptions.
- Push routes use
requireUserAuthmiddleware that readsactor.idfrom the resolved auth context. The push package does not expose a service-to-service push route — delivery is handled through the router, not the HTTP surface. slingshot-pushdoes not send push notifications directly on topic subscribe/unsubscribe. Those routes only manage membership records. Actual delivery is triggered through the notifications dispatcher or direct router calls.- Topic fan-out is capped at 10,000 members per publish call. Topics exceeding this threshold will log a warning and deliver only to the first 10,000. Use a cursor-paginated delivery loop outside the package for topics that require full-membership delivery.
- Each
provider.send()call is bounded byproviderTimeoutMsoncreatePushRouter(default 30000). A timed-out send is treated as a transient failure and retried under the configured retry policy, so a slow APNS / FCM endpoint cannot stall the queue worker indefinitely.
Key Files
Section titled “Key Files”src/plugin.tssrc/router.tssrc/types/config.tssrc/providers/web.ts,src/providers/apns.ts,src/providers/fcm.tssrc/formatter.tssrc/deliveryAdapter.ts