igodwin a35b3e6283 Fix nil pointer panic when notifier Send returns nil result
Guard against nil result before accessing result.Error in processNotification,
and add NtfyNotifier.Validate override so DefaultTopic is considered before
rejecting notifications with zero recipients.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 01:06:27 -07:00
2025-10-26 00:28:53 -07:00
2025-10-16 21:22:51 -07:00
2025-10-16 21:56:03 -07:00
2025-10-26 02:25:24 -07:00
2025-10-30 23:35:28 -07:00
2025-10-30 23:35:28 -07:00
2024-11-01 19:28:32 -07:00
2025-10-18 23:12:25 -07:00

Notifier Microservice

A production-ready notification delivery microservice that supports multiple notification channels (Email, Slack, Ntfy, Stdout) with both REST and gRPC APIs running simultaneously.

Go Version License

Overview

Notifier is a cloud-native microservice designed to handle all your application's notification needs through a single, unified API. Send emails, Slack messages, push notifications, and more with a consistent interface and robust delivery guarantees.

Perfect for:

  • Microservices architectures needing centralized notifications
  • Applications requiring multiple notification channels
  • Systems needing reliable async notification delivery
  • Teams wanting to standardize notification handling

Features

Core Capabilities

  • 🔔 Multi-Channel Support: Email (SMTP), Slack, Ntfy.sh, and Stdout
  • 🚀 Dual API: REST and gRPC running simultaneously in one process
  • 📦 Queue-Based: Async processing with configurable worker pools
  • 🔄 Retry Logic: Exponential backoff with configurable attempts
  • Priority Levels: Low, Normal, High, and Critical
  • 📊 Batch Operations: Send multiple notifications efficiently
  • 🎯 Status Tracking: Monitor notification lifecycle and delivery

Production Features

  • ⚙️ Configuration: Viper-based with environment variable support
  • 🐳 Containerized: Multi-stage Docker builds with non-root user
  • ☸️ Kubernetes Ready: Complete manifests with HPA, health checks, and RBAC
  • 🔒 Secure: Token-based auth for ntfy, TLS support, secret management
  • 📈 Observable: Health endpoints, metrics support, structured logging
  • 🔌 Extensible: Clean interfaces for adding new notifiers

Quick Start

1. Install and Run

# Clone and install dependencies
git clone https://github.com/igodwin/notifier
cd notifier
go mod tidy

# Build and run
make build
./bin/server

Server starts with:

  • REST API on http://localhost:8080
  • gRPC API on localhost:50051
  • Stdout notifier enabled by default

Run in different modes:

./bin/server                    # Both REST and gRPC (default)
SERVER_MODE=rest ./bin/server   # REST only
SERVER_MODE=grpc ./bin/server   # gRPC only

2. Send Your First Notification

curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "stdout",
    "subject": "Hello from Notifier!",
    "body": "Your first notification",
    "recipients": ["console"]
  }'

The notification appears immediately in the server output.

3. Check Status

# Health check
curl http://localhost:8080/health

# Statistics
curl http://localhost:8080/api/v1/stats

Configuration

Basic Setup

Create config.yaml in the project root:

server:
  mode: "both"        # Options: both, rest, grpc
  rest_port: 8080
  grpc_port: 50051
  host: "0.0.0.0"

queue:
  type: "local"       # Local in-memory queue
  worker_count: 10    # Concurrent workers
  retry_attempts: 3
  retry_backoff: "exponential"

notifiers:
  stdout: true        # Always enabled for testing

Email Notifications (SMTP)

Supports multiple email accounts with named instances:

notifiers:
  smtp:
    # Personal account (default)
    personal:
      host: "smtp.gmail.com"
      port: 587
      username: "your-email@gmail.com"
      password: "your-app-password"
      from: "personal@gmail.com"
      use_tls: true
      default: true

    # Work account
    work:
      host: "smtp.company.com"
      port: 587
      username: "you@company.com"
      password: "your-work-password"
      from: "notifications@company.com"
      use_tls: true

Usage:

# Uses default account (personal)
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "email",
    "subject": "Welcome!",
    "body": "Thanks for signing up",
    "recipients": ["user@example.com"]
  }'

# Specify account explicitly
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "email",
    "account": "work",
    "subject": "Welcome!",
    "body": "Thanks for signing up",
    "recipients": ["user@example.com"]
  }'

Slack Notifications

Supports multiple workspaces with named instances:

notifiers:
  slack:
    # Main workspace (default)
    main:
      webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
      username: "Notifier Bot"
      icon_emoji: ":bell:"
      default: true

    # Team workspace
    team-a:
      webhook_url: "https://hooks.slack.com/services/TEAM-A/WEBHOOK/URL"
      username: "Team A Bot"
      icon_emoji: ":rocket:"
      # Channel-specific webhooks
      webhooks:
        "#alerts": "https://hooks.slack.com/services/ALERTS/WEBHOOK"
        "#monitoring": "https://hooks.slack.com/services/MONITORING/WEBHOOK"

Usage:

# Uses default workspace (main)
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "slack",
    "subject": "Deployment Complete",
    "body": "v2.0 deployed to production",
    "recipients": ["#alerts"]
  }'

# Specify workspace explicitly
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "slack",
    "account": "team-a",
    "subject": "Deployment Complete",
    "body": "v2.0 deployed to production",
    "recipients": ["#alerts"]
  }'

Ntfy Push Notifications

Supports multiple ntfy servers with named instances:

notifiers:
  ntfy:
    # Public ntfy.sh server (default)
    public:
      server_url: "https://ntfy.sh"
      token: "tk_your_access_token"    # Optional, for private topics
      default_topic: "myapp-alerts"
      default: true

    # Private self-hosted server
    private:
      server_url: "https://ntfy.mycompany.com"
      username: "your-username"
      password: "your-password"
      default_topic: "company-alerts"
      insecure_skip_verify: false  # Set true for self-signed certs

Usage:

# Uses default server (public)
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ntfy",
    "priority": 3,
    "subject": "Critical Alert",
    "body": "Server CPU at 95%",
    "recipients": ["alerts"],
    "metadata": {
      "tags": ["warning", "rotating_light"],
      "click": "https://dashboard.example.com"
    }
  }'

# Specify server explicitly
curl -X POST http://localhost:8080/api/v1/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ntfy",
    "account": "private",
    "subject": "Critical Alert",
    "body": "Server CPU at 95%",
    "recipients": ["alerts"]
  }'

See docs/NTFY_GUIDE.md for advanced ntfy features (action buttons, attachments, delays, etc.).

Environment Variables

Override any config with environment variables:

export NOTIFIER_SERVER_MODE=both
export NOTIFIER_SERVER_REST_PORT=8080
export NOTIFIER_QUEUE_WORKER_COUNT=20
export NOTIFIER_NOTIFIERS_SMTP_HOST=smtp.gmail.com
export NOTIFIER_NOTIFIERS_SMTP_PASSWORD=secret
export NOTIFIER_NOTIFIERS_NTFY_TOKEN=tk_your_token

Format: NOTIFIER_<SECTION>_<KEY> (use _ for nested keys)

API Reference

REST Endpoints

Method Endpoint Description
GET /health Health check
POST /api/v1/notifications Send single notification
POST /api/v1/notifications/batch Send multiple notifications
GET /api/v1/notifications List notifications (with filters)
GET /api/v1/notifications/{id} Get notification by ID
DELETE /api/v1/notifications/{id} Cancel pending notification
POST /api/v1/notifications/{id}/retry Retry failed notification
GET /api/v1/stats Get service statistics

Request Format

{
  "type": "email|slack|ntfy|stdout",
  "priority": 0-3,
  "subject": "Notification title",
  "body": "Notification body",
  "recipients": ["email@example.com", "#channel", "topic"],
  "metadata": {
    "key": "value"
  },
  "max_retries": 3
}

Response Format

{
  "result": {
    "notification_id": "uuid",
    "success": true,
    "message": "notification queued successfully",
    "sent_at": "2025-10-16T21:05:27Z"
  }
}

Priority Levels

  • 0 - Low (background notifications)
  • 1 - Normal (default)
  • 2 - High (important updates)
  • 3 - Critical (urgent alerts)

Batch Operations

curl -X POST http://localhost:8080/api/v1/notifications/batch \
  -H "Content-Type: application/json" \
  -d '{
    "notifications": [
      {"type": "email", "subject": "Welcome", ...},
      {"type": "slack", "subject": "Alert", ...}
    ]
  }'

Filtering Notifications

# Get all sent email notifications
curl "http://localhost:8080/api/v1/notifications?type=email&status=sent&limit=10"

# Get recent failures
curl "http://localhost:8080/api/v1/notifications?status=failed&limit=20"

gRPC API

The gRPC service mirrors the REST API with full feature parity. See api/grpc/notifier.proto for definitions.

Generate Go code:

make proto-gen
# or
protoc --go_out=. --go-grpc_out=. api/grpc/notifier.proto

Note: gRPC server is running but handler implementation is pending. Protobuf definitions are complete.

Deployment

Docker

Build:

docker build -t notifier:latest .

Run:

docker run -d \
  --name notifier \
  -p 8080:8080 \
  -p 50051:50051 \
  -v $(pwd)/config.yaml:/app/config.yaml:ro \
  notifier:latest

Run in different modes:

# Both REST and gRPC (default)
docker run -d -p 8080:8080 -p 50051:50051 notifier:latest

# REST only
docker run -d -p 8080:8080 -e SERVER_MODE=rest notifier:latest

# gRPC only
docker run -d -p 50051:50051 -e SERVER_MODE=grpc notifier:latest

Docker Compose:

docker-compose up -d

Includes optional services: Kafka, Prometheus, Grafana (commented out by default)

Kubernetes

Deploy:

kubectl apply -f k8s/

Included resources:

  • Deployment (3 replicas with rolling updates)
  • Services (separate for REST and gRPC)
  • ConfigMap (configuration management)
  • HPA (auto-scaling 3-10 pods based on CPU/memory)
  • Ingress (external access with TLS)
  • Secret template (for credentials)

Access locally:

kubectl port-forward svc/notifier-rest 8080:8080
kubectl port-forward svc/notifier-grpc 50051:50051

Using Kustomize:

kubectl apply -k k8s/

Architecture

High-Level Design

┌──────────────┐     ┌──────────────┐
│ REST Client  │────▶│  REST API    │
└──────────────┘     │  (port 8080) │
                     └──────┬───────┘
┌──────────────┐            │
│ gRPC Client  │────▶│  gRPC API    │
└──────────────┘     │ (port 50051) │
                     └──────┬───────┘
                            ▼
                  ┌──────────────────┐
                  │ Notification     │
                  │ Service          │
                  └────────┬─────────┘
                           ▼
                  ┌──────────────────┐
                  │ Queue (Local)    │◀──┐
                  └────────┬─────────┘   │
                           ▼              │
                  ┌──────────────────┐   │
                  │ Worker Pool      │───┘
                  │ (10 workers)     │
                  └────────┬─────────┘
                           ▼
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
   ┌────────┐       ┌──────────┐     ┌──────────┐
   │ SMTP   │       │  Slack   │     │   Ntfy   │
   │Notifier│       │ Notifier │     │ Notifier │
   └────────┘       └──────────┘     └──────────┘

Key Components

  • API Layer: REST (Gorilla mux) and gRPC (Protocol Buffers)
  • Service Layer: Business logic, validation, orchestration
  • Queue: In-memory with optional disk persistence (Kafka planned)
  • Workers: Concurrent processors with retry logic
  • Notifiers: Pluggable providers implementing domain.Notifier
  • Config: Viper-based with file + env var support

See ARCHITECTURE.md for detailed design documentation.

Project Structure

notifier/
├── api/
│   ├── grpc/
│   │   └── notifier.proto          # gRPC service definition
│   └── rest/
│       ├── handlers.go              # HTTP handlers
│       ├── router.go                # Route configuration
│       └── types.go                 # Request/response types
├── cmd/
│   └── server/main.go              # Unified server (configurable mode)
├── internal/
│   ├── config/
│   │   └── config.go               # Configuration management
│   ├── domain/
│   │   ├── notification.go         # Core types
│   │   ├── notifier.go            # Notifier interface
│   │   └── queue.go               # Queue interface
│   ├── notifier/
│   │   ├── notifier.go            # Factory & base
│   │   ├── smtp.go                # Email notifier
│   │   ├── slack.go               # Slack notifier
│   │   ├── ntfy.go                # Ntfy notifier
│   │   └── stdout.go              # Stdout notifier
│   ├── queue/
│   │   └── local.go               # In-memory queue
│   └── service/
│       └── service.go             # Business logic
├── k8s/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   └── kustomization.yaml
├── docs/
│   └── NTFY_GUIDE.md              # Ntfy integration guide
├── config.yaml                 # Default configuration
├── docker-compose.yaml
├── Dockerfile
├── Makefile
├── QUICKSTART.md                   # API examples
├── ARCHITECTURE.md                 # Design documentation
└── README.md                       # This file

Development

Build Commands

make build          # Build server binary
make run            # Run server (both REST and gRPC)
make run-rest       # Run server in REST-only mode
make run-grpc       # Run server in gRPC-only mode
make test           # Run tests with race detector
make test-coverage  # Generate HTML coverage report
make fmt            # Format code with gofmt
make vet            # Run go vet
make lint           # Run golangci-lint
make check          # Run fmt-check + vet + mod verify
make qa             # Run all quality checks
make proto-gen      # Generate protobuf code
make docker-build   # Build Docker image
make clean          # Clean build artifacts
make help           # Show all available targets

Adding a New Notifier

  1. Create internal/notifier/mynotifier.go
  2. Implement domain.Notifier interface
  3. Add config struct to internal/config/config.go
  4. Register in cmd/server/main.go
  5. Update config.yaml with example config
  6. Add tests

Example:

type MyNotifier struct {
    BaseNotifier
    config *MyNotifierConfig
}

func (m *MyNotifier) Send(ctx context.Context, n *domain.Notification) (*domain.NotificationResult, error) {
    // Implementation
}

See ARCHITECTURE.md for detailed guide.

Documentation

Testing

# Run all tests
go test ./...

# Run with coverage
go test -cover ./...

# Run specific package
go test ./internal/notifier/...

Current Status: Core implementation complete, tests pending.

Monitoring & Operations

Health Checks

curl http://localhost:8080/health

Returns:

{
  "status": "healthy",
  "service": "notifier",
  "time": "2025-10-16T21:05:27Z"
}

Statistics

curl http://localhost:8080/api/v1/stats

Returns:

{
  "total_sent": 1234,
  "total_failed": 5,
  "total_pending": 0,
  "total_queued": 2,
  "by_type": {
    "email": 800,
    "slack": 400,
    "ntfy": 34
  },
  "by_status": {
    "sent": 1234,
    "failed": 5
  }
}

Graceful Shutdown

The server handles SIGINT and SIGTERM gracefully:

  • Stops accepting new requests
  • Completes in-flight notifications
  • Drains the queue
  • Closes all connections
  • 30-second timeout

Roadmap

Completed

  • Core notification system
  • REST API (fully functional)
  • gRPC API (protobuf defined)
  • Local queue with workers
  • Multiple notifiers (SMTP, Slack, Ntfy, Stdout)
  • Priority and retry logic
  • Batch operations
  • Docker support
  • Kubernetes manifests with HPA
  • Health checks and stats
  • Configuration management

Planned 🚧

  • gRPC handler implementation
  • Kafka queue adapter
  • Database persistence (PostgreSQL)
  • Notification templates
  • Webhook callbacks
  • Authentication/Authorization (API keys, OAuth)
  • Rate limiting (per client, per notifier)
  • Prometheus metrics
  • OpenTelemetry tracing
  • Comprehensive test suite
  • Circuit breakers for notifiers
  • Dead letter queue
  • Admin dashboard

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure tests pass (go test ./...)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

Please follow Go best practices and maintain test coverage.

Security

Reporting Vulnerabilities

Please report security vulnerabilities privately via GitHub Security Advisories.

Best Practices

  • Store credentials in environment variables or secrets managers
  • Use TLS for production deployments
  • Enable authentication for ntfy private topics
  • Rotate tokens regularly
  • Follow principle of least privilege for K8s RBAC
  • Keep dependencies updated

License

This project is licensed under the MIT License - see LICENSE for details.

Support & Community

Acknowledgments

Built with:


Made with ❤️ for reliable notifications

S
Description
A modular, extensible notifier project supporting multiple notification modes
Readme MIT 22 MiB
Languages
Go 97.3%
Makefile 2.2%
Dockerfile 0.5%