@growth-labs/push
v0.4.0
Published
Web Push API integration for browser notifications. VAPID auth, D1 subscription storage, Turnstile-protected endpoints, Queue-based batch delivery. Sends directly to FCM/Mozilla/Apple push endpoints — no third-party push service.
Readme
@growth-labs/push
Web Push API integration for browser notifications. VAPID auth, D1 subscription storage, Turnstile-protected endpoints, Queue-based batch delivery. Sends directly to FCM/Mozilla/Apple push endpoints — no third-party push service.
No consent package dependency. Browser's native permission prompt ("Allow"/"Block") constitutes GDPR consent for push.
Web Crypto API only. Uses @block65/webcrypto-web-push for VAPID signing (ECDSA P-256) and RFC 8291 encryption — no Node.js crypto dependency.
Config
import push from '@growth-labs/push'
push({
vapidPublicKey: 'BDd8...', // URL-safe base64
vapidPrivateKey: '...', // JWK JSON string (via Cloudflare Secrets)
vapidSubject: 'mailto:[email protected]',
d1Binding: 'SITE_DB',
queueBinding: 'PUSH_QUEUE',
turnstileSiteKey: '...',
turnstileSecretKey: '...',
defaultIcon: '/icons/notification.png',
defaultTtl: 86400, // 24 hours
defaultUrgency: 'normal',
batchSize: 100, // Subscriptions per Queue message
cleanupAfterDays: 90, // Remove stale subscriptions
})What It Injects
Middleware: Injects VAPID public key into context.locals.pushVapidKey.
Routes:
POST /api/push/subscribe— Turnstile-protected subscription endpointPOST /api/push/unsubscribe— remove subscription
Component: <PushOptIn /> — opt-in/opt-out button. Vanilla JS. Handles full lifecycle: check support → check permission → request → subscribe → POST to server.
Sending Notifications (Consumer Calls)
import { sendPushNotification, enqueuePushBatchAll } from '@growth-labs/push/utils'
// Single send
await sendPushNotification(subscription, { title: 'Breaking', body: '...' }, sendContext)
// Batch send to all subscribers (via Queue)
await enqueuePushBatchAll(env.SITE_DB, payload, {
queue: env.PUSH_QUEUE,
siteId: 'fronts', // required since 0.4.0
batchSize: 100,
})Queue Consumer
Package provides handler, consumer wires it up:
import { handlePushQueue } from '@growth-labs/push/consumer'
export default {
async queue(batch, env) {
await handlePushQueue(batch, {
db: env.SITE_DB,
vapid: { subject: '...', publicKey: '...', privateKey: env.VAPID_PRIVATE_KEY },
defaults: { icon: '/icons/notification.png', ttl: 86400, urgency: 'normal' },
})
}
}VAPID credentials passed via QueueConsumerContext at runtime — never in Queue messages.
D1 Tables (prefixed gl_)
gl_push_subscriptions— endpoint, p256dh key, auth key, topics, failure_count, last_delivery_at
VAPID Key Setup (One-Time)
npx @growth-labs/push generate-vapid
# Store private key:
npx wrangler secret put VAPID_PRIVATE_KEY
npx wrangler secret put VAPID_PUBLIC_KEYWrangler Bindings
[[d1_databases]]
binding = "SITE_DB"
database_id = "..."
[[queues.producers]]
binding = "PUSH_QUEUE"
queue = "push-sends"
[[queues.consumers]]
queue = "push-sends"
max_batch_size = 5Key Patterns
- Virtual module:
virtual:growth-labs/push/config - Expired subscriptions (404/410 from push service) auto-cleaned
- 10+ consecutive failures → subscription removed
.astrocomponent files ship as source, not compiled
