crumble-web-push
crumble-web-push
Bridge shard that wires Crumble applications to generic Web Push primitives.
Installation
-
Add the dependency to your
shard.yml:dependencies: crumble-web-push: github: your-github-user/crumble-web-push -
Run
shards install -
Require the shard:
require "crumble-web-push"
Setup Checklist
- Generate one VAPID key pair and keep it stable for active subscriptions.
- Set
CRUMBLE_WEB_PUSH_VAPID_PUBLIC_KEYfor the browser-facing controller. - Configure a
Crumble::Web::Push::Server::SubscriptionAdapterfor persistence.Crumble::Web::Push::Server::InMemorySubscriptionAdapteris available as a simple process-local option, but it is non-durable and loses subscriptions on process restart. - Set
CRUMBLE_WEB_PUSH_VAPID_PRIVATE_KEYandCRUMBLE_WEB_PUSH_VAPID_SUBJECTfor server-side delivery. - Trigger sends through
Crumble::Web::Push::Server::Integration.sender. - Remove stored subscriptions when a send outcome reports
cleanup?.
Responsibilities
crumble-web-push owns the Crumble-facing integration points:
- service worker source generation through
Crumble::Web::Push::Client::Integration - the Stimulus subscription controller attached to
ToHtml::Layout - the default subscription sync endpoint at
Crumble::Web::Push::Server::Integration::SubscriptionEndpointResource - ENV-backed
WebPush::Clientconstruction for the default sender facade - adapting stored Crumble subscriptions into
WebPush::Client#sendviaCrumble::Web::Push::Server::Integration::Sender
web-push owns the delivery primitives:
WebPush::Subscriptionparsing and validationWebPush::VapidConfigand auth headersWebPush::Client#send- provider response semantics such as invalid-subscription cleanup and retryability
Minimal Example
A runnable local example lives in examples/minimal_app/:
examples/minimal_app/app.crdefines the page, the test-push resource, and configures the built-in in-memory adapter.examples/minimal_app/run.crboots the example with env-based VAPID config.
Start it locally with:
export CRUMBLE_WEB_PUSH_VAPID_PUBLIC_KEY=...
export CRUMBLE_WEB_PUSH_VAPID_PRIVATE_KEY=...
export CRUMBLE_WEB_PUSH_VAPID_SUBJECT=mailto:admin@example.com
crystal run examples/minimal_app/run.cr -- --port 3000
Then open http://localhost:3000/push_example.
The example shows the complete local flow:
- Requiring
crumble-web-pushregisters the default root-scope push worker throughcrumble'sservice_workerintegration. - The body-level Stimulus controller posts subscribe/unsubscribe changes to
SubscriptionEndpointResource. TestPushesResourceloads the current session's stored subscription and sends a test push through the sender facade.
API Overview
Push worker registration
The shard automatically registers the default root-scope push worker when it is required in a Crumble app. No extra worker setup is needed in application code.
Subscription controller
The shard defines CrumbleWebPush::SubscriptionController and automatically attaches it to the body tag of ToHtml::Layout.
Default body-level values:
endpoint_urlpoints atCrumble::Web::Push::Server::Integration::SubscriptionEndpointResourcevapid_public_keyreadsENV["CRUMBLE_WEB_PUSH_VAPID_PUBLIC_KEY"]and defaults to""
Buttons or links can trigger the controller directly:
button CrumbleWebPush::SubscriptionController.subscribe_action("click"), type: "button" do
"Subscribe"
end
Storage adapter
Use Crumble::Web::Push::Server::SubscriptionAdapter to plug in any persistence backend:
abstract class Crumble::Web::Push::Server::SubscriptionAdapter
abstract def save(subscription : Subscription) : Nil
abstract def delete(session_id : String) : Bool
abstract def get(session_id : String) : Subscription?
end
The shard also ships a built-in process-local adapter:
adapter = Crumble::Web::Push::Server::InMemorySubscriptionAdapter.new
Crumble::Web::Push::Server::Integration.subscription_adapter = adapter
This adapter is safe to use when process-local, non-durable storage is acceptable, including simple production deployments that do not need subscriptions to survive restarts. The shard still does not ship a database-backed implementation.
Sender facade
Use Crumble::Web::Push::Server::Integration.sender to bridge stored subscriptions into WebPush::Client#send:
adapter = Crumble::Web::Push::Server::InMemorySubscriptionAdapter.new
Crumble::Web::Push::Server::Integration.subscription_adapter = adapter
sender = Crumble::Web::Push::Server::Integration.sender
outcomes = sender.send_to_session("session-id", %({"title":"Hello"}), ttl: 60)
outcomes.each do |outcome|
adapter.delete(outcome.subscription.session_id) if outcome.cleanup?
end
If you need to inspect the stored value directly, use adapter.get("session-id"), which returns either one Subscription or nil.
The default sender reads:
CRUMBLE_WEB_PUSH_VAPID_PUBLIC_KEYCRUMBLE_WEB_PUSH_VAPID_PRIVATE_KEYCRUMBLE_WEB_PUSH_VAPID_SUBJECT
Each outcome exposes the upstream WebPush::Client::SendResult helpers through:
sent?cleanup?retryable?status_codeerror_message
Subscription endpoint
Point the shared integration adapter at your persistence backend:
adapter = Crumble::Web::Push::Server::InMemorySubscriptionAdapter.new
Crumble::Web::Push::Server::Integration.subscription_adapter = adapter
The browser posts {action, subscription} to Crumble::Web::Push::Server::Integration::SubscriptionEndpointResource, and the resource always stores the current ctx.session.id.to_s as the subscription owner.
Development
- Install dependencies:
shards install - Run specs:
crystal spec - Format code:
crystal tool format
crumble-web-push
- 2
- 0
- 0
- 0
- 4
- 18 days ago
- February 13, 2026
MIT License
Tue, 05 May 2026 20:42:36 GMT