Go library for sending Web Push notifications with VAPID.
Portuguese documentation: README.pt-BR.md.
- Go 1.25+
- A valid browser subscription (
endpoint,keys.p256dh,keys.auth) - VAPID credentials (subject, private key, public key)
go get github.com/ESSantana/web-push-gopackage main
import (
"context"
"encoding/json"
"log"
"github.com/ESSantana/web-push-go/webpush"
)
func main() {
vapid, err := webpush.LoadVapid(
"mailto:you@example.com",
"<VAPID_PRIVATE_KEY>",
"<VAPID_PUBLIC_KEY>",
)
if err != nil {
log.Fatal(err)
}
subscription := webpush.Subscription{
Endpoint: "https://push.service/...",
Keys: webpush.Keys{
P256DH: "<P256DH>",
Auth: "<AUTH>",
},
}
message := webpush.Message{
Title: "Hello",
Options: webpush.MessageOptions{
Body: "Your notification was sent successfully.",
Tag: "demo",
Data: map[string]any{
"url": "http://localhost:8080/",
},
},
}
payload, err := json.Marshal(message)
if err != nil {
log.Fatal(err)
}
client := webpush.NewWebPushClient(vapid)
err = client.PrepareAndSendMessage(
context.Background(),
subscription,
string(payload),
webpush.NotificationOptions{Urgency: webpush.UrgencyNormal},
)
if err != nil {
log.Fatal(err)
}
}VAPID and subscription keys are accepted in raw or padded base64, URL-safe or standard, and are normalized internally to raw URL-safe.
This project uses webpush.Subscription:
{
"endpoint": "https://...",
"keys": {
"p256dh": "...",
"auth": "..."
}
}The library sends payloads using webpush.Message:
{
"title": "Title",
"options": {
"body": "Text",
"icon": "https://...",
"tag": "example-tag",
"data": {
"url": "http://localhost:8080/"
}
}
}In the Service Worker, use event.data.json() and call showNotification(title, options).
The example/ folder includes:
- index.html: creates subscription and copies JSON
- service-worker.js: minimal listeners (
install,activate,push,notificationclick,notificationclose) - main.go: send example using the library
Suggested flow:
- Open
index.htmlin a local environment compatible with Service Workers. - Subscribe for push and copy the subscription JSON.
- Paste it into
subscriptionStringinexample/main.go. - Run:
go run ./exampleWithHttpClient(...)WithConcurrentSending(true|false)WithMaxConcurrency(n)WithPackSize(n)WithVapidExpiration(d)— lifetime of the VAPID JWT (default 3h, max 24h per RFC 8292)
You can also queue messages with PrepareAndPackMessage and send them with SendPackedMessages.
All prepare methods take a context.Context that bounds the HTTP round trip
(deadline/cancellation).
PrepareAndSendMessage(ctx, subscription, payload, options)- validates the subscription
- encrypts the payload
- builds a request with Web Push + VAPID headers
- sends the request
PrepareMessage(ctx, subscription, payload, options)returns a ready*http.Request.SendMessage(req)sends it later (useful for custom logging/inspection/retry).
PrepareAndPackMessage(ctx, ...)appends requests to an internal queue.SendPackedMessages()sends all queued requests and clears the queue. Every message is attempted even when earlier ones fail; failures are returned as a single joined error (unwrap witherrors.As/errors.Is):- sequential mode (default): sends one-by-one in order
- concurrent mode (
WithConcurrentSending(true)): sends in parallel up toWithMaxConcurrency(...)
CollectPackedMessages()returns and clears the queue without sending.
The client is safe for concurrent use.
TTL: if not set inNotificationOptions, defaults to 1 day.Urgency: if set, sent as theUrgencyheader.Topic: if set, sent as theTopicheader.- default HTTP client timeout: 10s (when
WithHttpClientis not provided).
SendMessage treats these HTTP status codes as success:
200 OK201 Created202 Accepted
Any other status returns a *webpush.ResponseError carrying the endpoint,
status code, and response body, so callers can classify the outcome without
parsing error strings:
var respErr *webpush.ResponseError
if errors.As(err, &respErr) {
switch {
case respErr.SubscriptionGone(): // 404/410 — delete the stored subscription
case respErr.Unauthorized(): // 401/403 — VAPID keys don't match the subscription
case respErr.PayloadTooLarge(): // 413 — shrink the payload
case respErr.Transient(): // 429/5xx — safe to retry later
}
}Common cause: missing endpoint, keys.p256dh, or keys.auth in JSON.
Check webpush/subscription.go.
Common cause: invalid base64url keys or mismatched private/public key pair.
Validate keys and regenerate with NewVapid if needed.
Quick checklist:
- notification permission granted
- Service Worker active
- subscription is current (not expired/revoked)
- payload matches SW format (
{ title, options })
Send options.data.url in payload.
Example:
{
"title": "Title",
"options": {
"body": "Message",
"data": {
"url": "http://localhost:8080/"
}
}
}401/403: invalid VAPID, incorrect subject, invalid signature/token.410 Gone: subscription expired/revoked (create a new one in browser).
Use errors.As with *webpush.ResponseError and its helpers
(SubscriptionGone, Unauthorized, Transient, ...) to handle these
programmatically.
No license file is currently defined in this repository.