Use the core test fake to check what your application submits without making network requests. Choose Web Push to send encrypted notifications to browser subscriptions, including supported Safari web apps. Neither backend tracks device delivery or stores your application’s registrations.
Compatibility Matrix
These capabilities describe Arkoren’s exposed behavior. The fake records local acceptance only; its browser-shaped input does not emulate a browser or provider.
| Driver | Browser Subscriptions | Native Tokens | External Submission | No-Store TTL | Delivery Tracking | Sender Events |
|---|---|---|---|---|---|---|
| Test Fake | No | No | ||||
| Web Push | Yes | No | Yes | No |
Events belong to the sender, not the driver. Direct driver calls emit none.
push.Token does not select a provider automatically, and Web Push is not a
native mobile push integration.
Test Fake
pushtest.Fake is bundled with arkoren.dev/core. It needs no credentials,
construction options, external process, or lifecycle hooks. In a test, import
arkoren.dev/core/service/push and its pushtest subpackage:
fake := pushtest.New()
sender := push.New(fake)
Pass sender to the application code under test. The fake records immutable
messages and resolved TTL values; submission tests
show precise assertions and failure injection. Sent() returns a detached slice
and message metadata accessors return copies.
Portable validation happens before recording or consuming an injected failure.
The fake does not enforce Web Push origin or payload limits, encrypt messages,
persist data, or simulate delivery. No cleanup is required; Reset clears
recordings and failure settings when a test deliberately reuses the instance.
See the pushtest API reference.
Web Push
webpushpush submits standards-based encrypted Web Push. You need a browser-issued
subscription, a persistent VAPID identity, and an operator contact
URI. Browser Web Push does not require a native APNs certificate or Apple
Developer Program membership; browser integration
still supplies registration and display code.
Installing Web Push
From your application’s module directory, install the driver:
go get arkoren.dev/driver/webpush
With identity as a loaded key.Signing value, construct the driver using
arkoren.dev/driver/webpush/webpushpush and wrap it with
arkoren.dev/core/service/push:
driver, err := webpushpush.New(identity, "mailto:push@example.com")
Handle a construction error before wiring the driver. New validates the key,
contact, and settings without contacting a provider or registering a browser.
The contact is an operator’s mailto: address or HTTPS URI, not the customer’s
notification address.
sender := push.New(driver)
Reuse the constructor’s pointer for concurrent sends; do not copy its
synchronized authorization cache. The driver owns its signing-scalar copy and
hardened HTTP client, not your original identity. It has no Start, Stop,
Ping, native-client accessor, or credential-zeroization operation.
Bounding Requests
A complete send has a ten-second timeout by default; a shorter caller context
deadline wins. If your request budget is three seconds, import time and use
this alternative construction:
driver, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
connection.Timeout(3 * time.Second)
})
Zero restores ten seconds; a negative timeout fails construction. This bound controls request work, not the notification’s offline retention.
Endpoint Policy
Subscription endpoints are untrusted capability URLs. By default the driver
permits https://fcm.googleapis.com,
https://updates.push.services.mozilla.com, and DNS hosts below
.push.apple.com at a label boundary. The bare push.apple.com host and
lookalike suffixes do not match. It requires canonical lowercase DNS names and
HTTPS on port 443; explicit :443 is accepted. IP literals, URL credentials,
fragments, and noncanonical authorities are rejected.
Keep this curated policy unless you need a reviewed alternative. To restrict submission to Google’s origin, replace the defaults at construction:
driver, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
connection.AllowedOrigins("https://fcm.googleapis.com")
})
AllowedOrigins replaces rather than extends the defaults. Entries are exact
HTTPS origins without paths, queries, trailing slashes, or wildcards; an empty
list denies all origins. This is trusted deployment configuration: never build
the allowlist from a browser’s registration payload.
Approving an arbitrary subscription origin can expose your server to SSRF. Keep a narrow reviewed allowlist. Even allowed origins must resolve entirely to public addresses; the driver validates DNS at dialing and connects to a validated address while TLS verifies the original hostname.
The owned transport does not follow redirects or use environment proxies, and there is no custom HTTP-client bypass. Request duration and response reads are bounded. Private-network emulators and localhost providers are not supported targets; use the fake for offline application tests.
Sending Notifications
Send the typed order notice through the configured
sender using a stored push.Subscription value. The browser receives flat JSON
with title, optional body, and optional string-valued data, not a native
provider envelope. Recipient endpoints and encryption keys are absent from that
payload; the service worker
must read this shape.
The encoded JSON must fit within 3,993 plaintext bytes, including field names,
punctuation, and escaping. Encryption overhead and the final-record delimiter
bring the supported envelope to 4,096 bytes. Oversized input fails with
push.ErrInvalidMessage before transport; measuring body characters alone is
not sufficient.
TTL is rounded down to whole seconds. The representable limit is 2,147,483,647
whole seconds; a value whose whole-second count exceeds it fails rather than
being clamped. Providers can impose smaller bounds. Raw SendConfig.TTL == 0
requests no-store, unlike zero in the sender’s per-call option.
Interpreting Responses
HTTP 201 establishes acceptance and returns an empty Receipt.ID, even if
optional headers are missing or response cleanup fails. It does not establish
browser display. Other response classes are:
| Response | Classification |
|---|---|
| 404 or 410 | push.ErrNotFound |
| 429 | push.ErrRateLimited |
| 400 or 413 | push.ErrInvalidMessage |
| 500–599 | push.ErrUnavailable |
| Other 2xx | push.ErrOutcomeUnknown |
Authentication failures, redirects, and other unclassified responses remain
typed provider errors, not expired-registration or unsupported-operation signals.
Use errors.As with *webpushpush.ProviderError to inspect numeric StatusCode
and nonnegative RetryAfter; these fields do not schedule retries. Provider
bodies and capability URLs are not retained in that error.
Transport failures after dispatch conservatively preserve unknown acceptance and native/context causes. Do not replay them automatically or delete a registration after an authentication failure. On confirmed not-found responses, invalidate only the submitted registration version; see outcome handling.
Credential Ownership
Concurrent sends share an origin-scoped VAPID authorization until five minutes before its twelve-hour expiry. This cache belongs to one driver instance, not other replicas. It holds at most 256 unexpired origins; adding another beyond that limit fails closed. Keep the identity stable and the origin policy narrow.
Formatting the constructor pointer with %v, %+v, or %#v writes
[REDACTED Web Push driver]. This does not make arbitrary serialization,
reflection, driver copies, or native objects safe to log. Explicitly unwrapped
transport causes can contain sensitive URLs even when top-level error text is
bounded.
See the Web Push API reference.
Sender Settings
Sender settings apply to either backend. With an existing driver, import
time and arkoren.dev/core/service/push to request a one-hour default:
sender := push.New(driver, func(delivery *push.Builder) {
delivery.TTL(time.Hour)
})
A zero builder TTL restores 24 hours; a negative value disables offline storage. The construction draft cannot reconfigure a published sender. The sender shares the driver and does not take over its lifecycle.
If you need submission observations, supply a shared *event.Bus named bus
and a shared *telemetry.Telemetry named instrumentation:
sender := push.New(driver, func(delivery *push.Builder) {
delivery.Events(bus)
delivery.Telemetry(instrumentation)
})
Nil events disable notifications; nil telemetry follows the process default on each call. The submission observation contract explains privacy and outcome limits. These settings add neither delivery tracking nor background work.
See the sender API reference.