DEV Community

Bipin C
Bipin C

Posted on

# Self-Hosted Push Notifications Part-7

Self-Hosted Push Notifications Specification

Part 7: Best Practices, Security & Optimization

Version: 1.0
Last Updated: October 2025
Prerequisites: Part 6: Monitoring, Debugging & Troubleshooting
Author: Bunty9
License: MIT (Free to use and adapt)


Table of Contents

  1. Security Best Practices
  2. VAPID Key Management
  3. Input Validation
  4. SQL Injection Prevention
  5. Authentication & Authorization
  6. Performance Optimization
  7. Code Organization
  8. Testing Strategies
  9. Documentation Standards
  10. Deployment Checklist

Security Best Practices

1. Secrets Management

❌ Bad Practice:

// Hardcoded secrets
const VAPIDPrivateKey = "BNw7fc1ayj3-Az-OJ8DrOj3EDAHCORwO_r3SsrpwkzQ"
Enter fullscreen mode Exit fullscreen mode

✅ Good Practice:

// Load from environment
vapidPrivateKey := os.Getenv("VAPID_PRIVATE_KEY")
if vapidPrivateKey == "" {
    log.Fatal("VAPID_PRIVATE_KEY is required")
}

// Or from secrets manager
func loadVAPIDKeys() (string, string, error) {
    secretName := "vapid-keys"
    region := "us-east-1"

    sess, err := session.NewSession(&aws.Config{
        Region: aws.String(region),
    })
    if err != nil {
        return "", "", err
    }

    svc := secretsmanager.New(sess)
    input := &secretsmanager.GetSecretValueInput{
        SecretId: aws.String(secretName),
    }

    result, err := svc.GetSecretValue(input)
    if err != nil {
        return "", "", err
    }

    var secretData map[string]string
    json.Unmarshal([]byte(*result.SecretString), &secretData)

    return secretData["public_key"], secretData["private_key"], nil
}
Enter fullscreen mode Exit fullscreen mode

2. Rate Limiting

Apply rate limiting at multiple levels:

// Per-user rate limiting
type UserRateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.RWMutex
}

// Global rate limiting
type GlobalRateLimiter struct {
    limiter *rate.Limiter
}

// IP-based rate limiting (for public endpoints)
type IPRateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.RWMutex
}
Enter fullscreen mode Exit fullscreen mode

3. HTTPS Enforcement

// Redirect HTTP to HTTPS
func httpsRedirectMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Header.Get("X-Forwarded-Proto") == "http" {
            httpsURL := "https://" + c.Request.Host + c.Request.RequestURI
            c.Redirect(http.StatusMovedPermanently, httpsURL)
            c.Abort()
            return
        }
        c.Next()
    }
}
Enter fullscreen mode Exit fullscreen mode

4. CORS Configuration

// Strict CORS configuration
corsConfig := cors.Config{
    AllowOrigins: []string{
        "https://yourdomain.com",
        "https://app.yourdomain.com",
    },
    AllowMethods: []string{
        "GET", "POST", "PUT", "DELETE", "OPTIONS",
    },
    AllowHeaders: []string{
        "Origin", "Content-Type", "Authorization",
    },
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}

router.Use(cors.New(corsConfig))
Enter fullscreen mode Exit fullscreen mode

5. Content Security Policy

// Add CSP headers
func securityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Content-Security-Policy",
            "default-src 'self'; "+
            "script-src 'self' 'unsafe-inline'; "+
            "style-src 'self' 'unsafe-inline'; "+
            "img-src 'self' data: https:;")
        c.Header("X-Content-Type-Options", "nosniff")
        c.Header("X-Frame-Options", "DENY")
        c.Header("X-XSS-Protection", "1; mode=block")
        c.Next()
    }
}
Enter fullscreen mode Exit fullscreen mode

VAPID Key Management

Key Rotation Strategy

When to Rotate:

  • Every 6-12 months (scheduled)
  • Suspected key compromise
  • Team member departure
  • Security audit requirement

Rotation Process:

// 1. Generate new keys
newPrivateKey, newPublicKey, _ := webpush.GenerateVAPIDKeys()

// 2. Store new keys (keep old keys for transition period)
os.Setenv("VAPID_PUBLIC_KEY_NEW", newPublicKey)
os.Setenv("VAPID_PRIVATE_KEY_NEW", newPrivateKey)

// 3. Support both keys temporarily
func (ps *PushService) sendToSubscription(subscription models.PushSubscription, payload types.PushPayload) error {
    // Try new key first
    err := ps.sendWithKey(subscription, payload, ps.newVapidPrivateKey, ps.newVapidPublicKey)

    // Fallback to old key
    if err != nil {
        err = ps.sendWithKey(subscription, payload, ps.oldVapidPrivateKey, ps.oldVapidPublicKey)
    }

    return err
}

// 4. After transition period (e.g., 30 days), remove old keys
Enter fullscreen mode Exit fullscreen mode

Key Storage Best Practices

❌ Never do this:

# Committed to git
git add .env
git commit -m "Add VAPID keys"
Enter fullscreen mode Exit fullscreen mode

✅ Always do this:

# .gitignore
.env
.env.local
.env.production

# Kubernetes secrets
kubectl create secret generic vapid-keys \
  --from-literal=public-key='BOzzMgOMwVyuziwHiI8...' \
  --from-literal=private-key='BNw7fc1ayj3-Az-OJ8...'

# AWS Secrets Manager
aws secretsmanager create-secret \
  --name vapid-keys \
  --secret-string '{"public":"BOzz...","private":"BNw7..."}'
Enter fullscreen mode Exit fullscreen mode

Input Validation

1. Subscription Endpoint Validation

func validateSubscriptionEndpoint(endpoint string) error {
    // Must be HTTPS
    if !strings.HasPrefix(endpoint, "https://") {
        return errors.New("endpoint must use HTTPS")
    }

    // Valid URL format
    u, err := url.Parse(endpoint)
    if err != nil {
        return fmt.Errorf("invalid endpoint URL: %w", err)
    }

    // Must be from trusted push services
    allowedHosts := []string{
        "fcm.googleapis.com",
        "updates.push.services.mozilla.com",
        "push.apple.com",
        "wns2-*.notify.windows.com",
    }

    hostAllowed := false
    for _, allowed := range allowedHosts {
        if strings.Contains(u.Host, allowed) || u.Host == allowed {
            hostAllowed = true
            break
        }
    }

    if !hostAllowed {
        return fmt.Errorf("endpoint host not allowed: %s", u.Host)
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

2. Payload Validation

func validatePushPayload(payload types.PushPayload) error {
    // Title required, max 65 characters
    if payload.Title == "" {
        return errors.New("title is required")
    }
    if len(payload.Title) > 65 {
        return errors.New("title must be 65 characters or less")
    }

    // Body required, max 240 characters
    if payload.Body == "" {
        return errors.New("body is required")
    }
    if len(payload.Body) > 240 {
        return errors.New("body must be 240 characters or less")
    }

    // URL validation (if provided)
    if payload.URL != "" {
        if !strings.HasPrefix(payload.URL, "/") && !strings.HasPrefix(payload.URL, "http") {
            return errors.New("URL must be relative or absolute")
        }
    }

    // Validate actions (max 2 on mobile, 4 on desktop)
    if len(payload.Actions) > 4 {
        return errors.New("maximum 4 actions allowed")
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

3. User Input Sanitization

import "html"

func sanitizePushPayload(payload *types.PushPayload) {
    // Escape HTML in title and body
    payload.Title = html.EscapeString(payload.Title)
    payload.Body = html.EscapeString(payload.Body)

    // Trim whitespace
    payload.Title = strings.TrimSpace(payload.Title)
    payload.Body = strings.TrimSpace(payload.Body)
}
Enter fullscreen mode Exit fullscreen mode

SQL Injection Prevention

Use Parameterized Queries

❌ Vulnerable to SQL Injection:

// NEVER DO THIS
query := fmt.Sprintf("SELECT * FROM push_subscriptions WHERE user_id = '%s'", userID)
db.Raw(query).Scan(&subscriptions)
Enter fullscreen mode Exit fullscreen mode

✅ Safe with Parameterized Queries:

// GORM automatically uses parameterized queries
db.Where("user_id = ?", userID).Find(&subscriptions)

// Raw query with parameters
db.Raw("SELECT * FROM push_subscriptions WHERE user_id = ?", userID).Scan(&subscriptions)
Enter fullscreen mode Exit fullscreen mode

Input Validation for UUIDs

func validateUUID(id string) (uuid.UUID, error) {
    parsedUUID, err := uuid.Parse(id)
    if err != nil {
        return uuid.Nil, fmt.Errorf("invalid UUID format: %w", err)
    }
    return parsedUUID, nil
}

// Usage
func (ps *PushService) SendToUser(userIDStr string, payload types.PushPayload) error {
    userID, err := validateUUID(userIDStr)
    if err != nil {
        return err
    }

    // Safe to use
    var subscriptions []models.PushSubscription
    ps.db.Where("user_id = ?", userID).Find(&subscriptions)
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Authentication & Authorization

JWT Best Practices

// 1. Use strong secret keys (256+ bits)
jwtSecret := []byte(os.Getenv("JWT_SECRET"))
if len(jwtSecret) < 32 {
    log.Fatal("JWT_SECRET must be at least 32 characters")
}

// 2. Set appropriate expiration
claims := jwt.MapClaims{
    "sub":      userID,
    "iat":      time.Now().Unix(),
    "exp":      time.Now().Add(24 * time.Hour).Unix(), // 24 hours
    "isAdmin":  false,
}

// 3. Validate all claims
func validateJWT(tokenString string) (*jwt.Token, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Validate signing method
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return jwtSecret, nil
    })

    if err != nil {
        return nil, err
    }

    // Validate expiration
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        if exp, ok := claims["exp"].(float64); ok {
            if time.Unix(int64(exp), 0).Before(time.Now()) {
                return nil, errors.New("token expired")
            }
        }
    }

    return token, nil
}
Enter fullscreen mode Exit fullscreen mode

Role-Based Access Control

// Define roles
const (
    RoleUser       = "user"
    RoleCaretaker  = "caretaker"
    RoleSpaceAdmin = "space_admin"
    RoleSuperAdmin = "super_admin"
)

// Check permissions
func (pc *PushController) SendTestNotification(c *gin.Context) {
    userRole, exists := c.Get("userRole")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
        return
    }

    // Only super admins can send test notifications
    if userRole != RoleSuperAdmin {
        c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
        return
    }

    // Proceed with test notification...
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

1. Database Indexing

-- Essential indexes
CREATE INDEX CONCURRENTLY idx_push_subscriptions_user_active
ON push_subscriptions(user_id, is_active)
WHERE is_active = TRUE;

CREATE INDEX CONCURRENTLY idx_push_subscriptions_admin_active
ON push_subscriptions(admin_id, is_active)
WHERE is_active = TRUE;

CREATE INDEX CONCURRENTLY idx_push_notification_logs_sent_status
ON push_notification_logs(sent_at DESC, status);

-- Composite indexes for complex queries
CREATE INDEX CONCURRENTLY idx_push_subscriptions_composite
ON push_subscriptions(user_id, is_active, last_used_at DESC);
Enter fullscreen mode Exit fullscreen mode

2. Connection Pooling

func setupDatabasePool(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        PrepareStmt: true, // Prepare statements
        Logger:      logger.Default.LogMode(logger.Silent),
    })
    if err != nil {
        return nil, err
    }

    sqlDB, err := db.DB()
    if err != nil {
        return nil, err
    }

    // Optimize connection pool
    sqlDB.SetMaxOpenConns(100)              // Max open connections
    sqlDB.SetMaxIdleConns(10)               // Max idle connections
    sqlDB.SetConnMaxLifetime(time.Hour)     // Max connection lifetime
    sqlDB.SetConnMaxIdleTime(10 * time.Minute) // Max idle time

    return db, nil
}
Enter fullscreen mode Exit fullscreen mode

3. Caching Strategies

import "github.com/patrickmn/go-cache"

type CachedPushService struct {
    *PushService
    cache *cache.Cache
}

func NewCachedPushService(db *gorm.DB) *CachedPushService {
    return &CachedPushService{
        PushService: NewPushService(db),
        cache:       cache.New(5*time.Minute, 10*time.Minute),
    }
}

// Cache subscription lookups
func (cps *CachedPushService) GetUserSubscriptions(userID uuid.UUID) ([]models.PushSubscription, error) {
    cacheKey := fmt.Sprintf("user_subscriptions:%s", userID)

    // Try cache first
    if cached, found := cps.cache.Get(cacheKey); found {
        return cached.([]models.PushSubscription), nil
    }

    // Query database
    var subscriptions []models.PushSubscription
    err := cps.db.Where("user_id = ? AND is_active = ?", userID, true).
        Find(&subscriptions).Error

    if err != nil {
        return nil, err
    }

    // Cache for 5 minutes
    cps.cache.Set(cacheKey, subscriptions, cache.DefaultExpiration)

    return subscriptions, nil
}

// Invalidate cache on subscription changes
func (cps *CachedPushService) Subscribe(userID *uuid.UUID, adminID *uuid.UUID, req types.SubscribePushRequest) (*types.SubscribePushResponse, error) {
    resp, err := cps.PushService.Subscribe(userID, adminID, req)
    if err != nil {
        return nil, err
    }

    // Invalidate cache
    if userID != nil {
        cps.cache.Delete(fmt.Sprintf("user_subscriptions:%s", *userID))
    }
    if adminID != nil {
        cps.cache.Delete(fmt.Sprintf("admin_subscriptions:%s", *adminID))
    }

    return resp, nil
}
Enter fullscreen mode Exit fullscreen mode

4. Batch Operations

// Batch insert notification logs
func (ps *PushService) batchInsertLogs(logs []models.PushNotificationLog) error {
    batchSize := 100

    for i := 0; i < len(logs); i += batchSize {
        end := i + batchSize
        if end > len(logs) {
            end = len(logs)
        }

        batch := logs[i:end]

        if err := ps.db.CreateInBatches(batch, batchSize).Error; err != nil {
            return err
        }
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

5. Goroutine Pooling

type WorkerPool struct {
    tasks   chan func()
    workers int
}

func NewWorkerPool(workers int) *WorkerPool {
    wp := &WorkerPool{
        tasks:   make(chan func(), 1000),
        workers: workers,
    }

    // Start workers
    for i := 0; i < workers; i++ {
        go wp.worker()
    }

    return wp
}

func (wp *WorkerPool) worker() {
    for task := range wp.tasks {
        task()
    }
}

func (wp *WorkerPool) Submit(task func()) {
    wp.tasks <- task
}

// Usage in PushService
func (ps *PushService) SendToMultipleUsers(userIDs []uuid.UUID, payload types.PushPayload) error {
    pool := NewWorkerPool(50) // 50 concurrent workers

    for _, userID := range userIDs {
        uid := userID // Capture for closure
        pool.Submit(func() {
            ps.SendToUser(uid, payload)
        })
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Code Organization

Project Structure

backend/
├── cmd/
│   └── server/
│       └── main.go                 # Entry point
├── config/
│   ├── database.go                 # DB setup
│   └── vapid.go                    # VAPID config
├── models/
│   ├── push_subscription.go        # GORM models
│   └── push_notification_log.go
├── services/
│   ├── push_service.go             # Core logic
│   └── push_service_test.go        # Unit tests
├── controllers/
│   ├── push_controller.go          # HTTP handlers
│   └── push_controller_test.go
├── routes/
│   └── push_routes.go              # Route definitions
├── middleware/
│   ├── auth.go                     # Authentication
│   ├── rate_limiter.go             # Rate limiting
│   └── cors.go                     # CORS
├── types/
│   └── push_types.go               # Request/response types
├── utils/
│   ├── response.go                 # HTTP helpers
│   ├── device_detector.go          # User agent parsing
│   └── validator.go                # Input validation
├── workers/
│   ├── booking_reminder_worker.go  # Background workers
│   └── notification_scheduler.go
├── metrics/
│   └── push_metrics.go             # Prometheus metrics
├── migrations/
│   └── 001_create_push_tables.sql
├── .env.example
├── .gitignore
├── go.mod
└── README.md
Enter fullscreen mode Exit fullscreen mode

Naming Conventions

// Use clear, descriptive names
 func (ps *PushService) SendToUser(userID uuid.UUID, payload types.PushPayload) error
 func (ps *PushService) Send(id string, p interface{}) error

// Constants in UPPER_SNAKE_CASE
const (
    MAX_RETRY_ATTEMPTS     = 3
    DEFAULT_BATCH_SIZE     = 100
    WORKER_INTERVAL_MINUTES = 5
)

// Private methods start with lowercase
func (ps *PushService) sendToSubscription(...) error
func (ps *PushService) logSuccess(...) error

// Public methods start with uppercase
func (ps *PushService) SendToUser(...) error
func (ps *PushService) Subscribe(...) error
Enter fullscreen mode Exit fullscreen mode

Testing Strategies

1. Unit Tests

// services/push_service_test.go

func TestPushService_Subscribe(t *testing.T) {
    // Setup test database
    db := setupTestDB(t)
    defer teardownTestDB(db)

    pushService := NewPushService(db)
    userID := uuid.New()

    req := types.SubscribePushRequest{
        Endpoint: "https://fcm.googleapis.com/test-endpoint",
        Keys: types.SubscriptionKeys{
            P256dh: "test-p256dh-key",
            Auth:   "test-auth-key",
        },
        UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0.0.0",
    }

    // Test subscription
    response, err := pushService.Subscribe(&userID, nil, req)

    assert.NoError(t, err)
    assert.True(t, response.Success)
    assert.NotEqual(t, uuid.Nil, response.SubscriptionID)
    assert.Contains(t, response.DeviceName, "Chrome")

    // Verify database
    var subscription models.PushSubscription
    err = db.Where("endpoint = ?", req.Endpoint).First(&subscription).Error
    assert.NoError(t, err)
    assert.Equal(t, userID, *subscription.UserID)
    assert.True(t, subscription.IsActive)
}
Enter fullscreen mode Exit fullscreen mode

2. Integration Tests

func TestPushNotificationFlow(t *testing.T) {
    // Setup test environment
    db := setupTestDB(t)
    defer teardownTestDB(db)

    pushService := NewPushService(db)
    userID := uuid.New()

    // 1. Subscribe
    subResp, err := pushService.Subscribe(&userID, nil, testSubscriptionRequest())
    assert.NoError(t, err)

    // 2. Send notification
    payload := types.PushPayload{
        Title: "Test Notification",
        Body:  "This is a test",
    }
    err = pushService.SendToUser(userID, payload)
    assert.NoError(t, err)

    // 3. Verify log created
    var log models.PushNotificationLog
    err = db.Where("subscription_id = ?", subResp.SubscriptionID).
        First(&log).Error
    assert.NoError(t, err)
    assert.Equal(t, "Test Notification", log.Title)
}
Enter fullscreen mode Exit fullscreen mode

3. End-to-End Tests

// e2e/push-notifications.spec.ts

import { test, expect } from '@playwright/test';

test('push notification subscription flow', async ({ page, context }) => {
  // Grant notification permission
  await context.grantPermissions(['notifications']);

  // Navigate to app
  await page.goto('http://localhost:3000');

  // Wait for service worker registration
  await page.waitForFunction(() => {
    return navigator.serviceWorker.controller !== null;
  });

  // Click subscribe button
  await page.click('[data-testid="subscribe-notifications"]');

  // Wait for subscription success message
  await expect(page.locator('[data-testid="subscription-success"]')).toBeVisible();

  // Verify subscription in backend
  const devices = await page.evaluate(async () => {
    const response = await fetch('/api/push/devices');
    return response.json();
  });

  expect(devices.count).toBeGreaterThan(0);
});
Enter fullscreen mode Exit fullscreen mode

4. Load Testing

// k6-load-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
  stages: [
    { duration: '1m', target: 50 },   // Ramp up to 50 users
    { duration: '3m', target: 50 },   // Stay at 50 users
    { duration: '1m', target: 100 },  // Ramp up to 100 users
    { duration: '3m', target: 100 },  // Stay at 100 users
    { duration: '1m', target: 0 },    // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],   // Less than 1% failure rate
  },
};

export default function () {
  const payload = JSON.stringify({
    endpoint: `https://fcm.googleapis.com/test-${__VU}-${__ITER}`,
    keys: {
      p256dh: 'test-p256dh-key',
      auth: 'test-auth-key',
    },
    userAgent: 'k6-load-test',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
      'Cookie': 'auth-token=test-token',
    },
  };

  const res = http.post('http://localhost:8081/api/push/subscribe', payload, params);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response has subscriptionId': (r) => JSON.parse(r.body).subscriptionId !== undefined,
  });

  sleep(1);
}
Enter fullscreen mode Exit fullscreen mode

Documentation Standards

Code Comments

// PushService handles all push notification operations.
//
// It manages subscriptions, sends notifications via the Web Push Protocol,
// tracks delivery status, and handles automatic retry logic.
//
// Example usage:
//   pushService := NewPushService(db)
//   payload := types.PushPayload{
//       Title: "Booking Confirmed",
//       Body:  "Your booking is confirmed",
//   }
//   err := pushService.SendToUser(userID, payload)
type PushService struct {
    db              *gorm.DB
    vapidPublicKey  string
    vapidPrivateKey string
    vapidSubject    string
}

// SendToUser sends a notification to all active devices of a user.
//
// Parameters:
//   userID - UUID of the user to notify
//   payload - Notification content (title, body, etc.)
//
// Returns:
//   error - nil on success, error if no active subscriptions or send fails
//
// The notification is sent to all active devices in parallel using goroutines.
// Individual device failures do not cause the entire operation to fail.
func (ps *PushService) SendToUser(userID uuid.UUID, payload types.PushPayload) error {
    // Implementation...
}
Enter fullscreen mode Exit fullscreen mode

API Documentation

# openapi.yml

openapi: 3.0.0
info:
  title: Push Notifications API
  version: 1.0.0

paths:
  /api/push/subscribe:
    post:
      summary: Subscribe to push notifications
      description: Creates or updates a push subscription for the authenticated user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - endpoint
                - keys
              properties:
                endpoint:
                  type: string
                  description: Browser push service URL
                  example: "https://fcm.googleapis.com/..."
                keys:
                  type: object
                  required:
                    - p256dh
                    - auth
                  properties:
                    p256dh:
                      type: string
                      description: Public key for encryption
                    auth:
                      type: string
                      description: Authentication secret
                userAgent:
                  type: string
                  description: Browser user agent
      responses:
        '200':
          description: Subscription successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  subscriptionId:
                    type: string
                    format: uuid
                  deviceName:
                    type: string
                    example: "Chrome on macOS"
        '400':
          description: Invalid request
        '401':
          description: Unauthorized
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

Pre-Deployment

  • [ ] VAPID keys generated and configured
  • [ ] Environment variables set for all environments
  • [ ] Database migrations applied
  • [ ] All tests passing (unit, integration, E2E)
  • [ ] Code reviewed and approved
  • [ ] Security audit completed
  • [ ] Performance benchmarks met
  • [ ] Documentation updated

Deployment

  • [ ] Deploy to staging environment
  • [ ] Run smoke tests in staging
  • [ ] Verify service health checks passing
  • [ ] Check metrics in Grafana
  • [ ] Deploy to production with rolling update
  • [ ] Monitor error rates during deployment
  • [ ] Verify zero downtime

Post-Deployment

  • [ ] Monitor metrics for 1 hour
  • [ ] Check error tracking (Sentry)
  • [ ] Verify push notifications sending successfully
  • [ ] Test subscription flow end-to-end
  • [ ] Review logs for anomalies
  • [ ] Update status page
  • [ ] Notify stakeholders of deployment

Summary

You now have:

✅ Security best practices (secrets, HTTPS, CORS, CSP)
✅ VAPID key management and rotation
✅ Comprehensive input validation
✅ SQL injection prevention
✅ Authentication & authorization patterns
✅ Performance optimization techniques
✅ Code organization standards
✅ Testing strategies (unit, integration, E2E, load)
✅ Documentation standards
✅ Complete deployment checklist


Next Steps

➡️ Part 8: Complete Reference & FAQ

Part 8 (final) will cover:

  1. Complete API reference
  2. Frequently Asked Questions (FAQ)
  3. Troubleshooting guide
  4. Browser compatibility matrix
  5. Migration guides
  6. Glossary of terms
  7. Additional resources
  8. Community support

Top comments (0)