-
Notifications
You must be signed in to change notification settings - Fork 1
Push notification provider #199
Copy link
Copy link
Open
Description
A push notification provider has to be included to allow to send push notifications to registered devices. We would like to start with web push notifications using RFC 8030 / VAPID. We also have to review on how the devices are stored in the database and whether the required data could be added in our current table schema. We probably would like to look into packages such as webpush-go.
package push
import (
"encoding/json"
"fmt"
"net/http"
webpush "github.com/SherClockHolmes/webpush-go"
)
// Subscription mirrors the PushSubscription from the browser's Push API.
// This is what you store in your device token table.
type Subscription struct {
Endpoint string `json:"endpoint" db:"endpoint"`
Keys struct {
P256dh string `json:"p256dh" db:"p256dh"`
Auth string `json:"auth" db:"auth"`
} `json:"keys"`
}
// Payload is the notification content sent to the service worker.
type Payload struct {
Title string `json:"title"`
Body string `json:"body"`
URL string `json:"url,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
// VAPIDConfig holds your server identity keys.
// Generate once with: webpush.GenerateVAPIDKeys()
type VAPIDConfig struct {
PublicKey string
PrivateKey string
Subject string // "mailto:you@lunogram.io" — required by VAPID spec
}
// Send delivers a push notification to a single subscription.
// No Firebase, no third-party service — this speaks the Web Push protocol
// directly to the browser vendor's push service (RFC 8030).
func Send(sub Subscription, payload Payload, vapid VAPIDConfig) error {
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal payload: %w", err)
}
resp, err := webpush.SendNotification(body, &webpush.Subscription{
Endpoint: sub.Endpoint,
Keys: webpush.Keys{
P256dh: sub.Keys.P256dh,
Auth: sub.Keys.Auth,
},
}, &webpush.Options{
VAPIDPublicKey: vapid.PublicKey,
VAPIDPrivateKey: vapid.PrivateKey,
Subscriber: vapid.Subject,
TTL: 60, // seconds the push service holds the message
Urgency: "normal", // normal | low | very-low | high
// Topic: "campaign-123", // optional: replaces previous with same topic
})
if err != nil {
return fmt.Errorf("send notification: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusCreated: // 201 — successfully queued
return nil
case http.StatusGone: // 410 — subscription expired, delete from DB
return fmt.Errorf("subscription expired (410): remove from device table")
case http.StatusTooManyRequests: // 429 — rate limited by push service
return fmt.Errorf("rate limited (429): back off and retry")
default:
return fmt.Errorf("unexpected status %d from push service", resp.StatusCode)
}
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels