Skip to main content

Notifications Service

The Notifications service delivers push notifications (APNs for iOS, FCM for Android), transactional emails (via Resend), and processes events from Redis Streams. Notifications are never sent synchronously — all delivery is triggered via the async job queue.

Quick Reference

PropertyValue
Port8500
LanguageGo
FrameworkChi v5
Module pathgithub.com/gospelib/main/services/notifications
Entry pointcmd/server/main.go
Push providersAPNs (Apple), FCM (Google)
Email providerResend
QueueRedis Streams (gl:events:notifications)

Responsibilities

  • Push notifications — Deliver to iOS (APNs) and Android (FCM) devices
  • Transactional email — Account events, subscription changes, study reminders via Resend
  • Redis Streams consumer — Process notification events from the gl:events:notifications stream
  • Device registration — Track user devices and push tokens (via gl_user_devices table)
  • Preference management — Respect user notification preferences

Running Locally

cd services/notifications
go run ./cmd/server

The service starts on port 8500 and requires Redis to be running.

Environment Variables

VariableDefaultDescription
GOSPELIB_NOTIFICATIONS_PORT8500HTTP listen port
GOSPELIB_NOTIFICATIONS_REDIS_URLredis://localhost:6380Redis for Streams consumer + state
GOSPELIB_NOTIFICATIONS_APNS_KEYAPNs authentication key
GOSPELIB_NOTIFICATIONS_APNS_KEY_IDAPNs key ID
GOSPELIB_NOTIFICATIONS_APNS_TEAM_IDApple Developer Team ID
GOSPELIB_NOTIFICATIONS_FCM_CREDENTIALSFCM service account JSON
GOSPELIB_NOTIFICATIONS_RESEND_API_KEYResend API key for transactional email
GOSPELIB_NOTIFICATIONS_DATABASE_URLpostgres://localhost:5432/gospelibPostgreSQL for device records

Health Check

curl http://localhost:8500/health
# {"status": "ok"}

Job Queue Pattern

The service runs a Redis Streams consumer group that processes notification events asynchronously:

// services/notifications/internal/queue/consumer.go
func (c *Consumer) Run(ctx context.Context) error {
for {
msgs, err := c.redis.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "notifications-workers",
Consumer: c.workerID,
Streams: []string{"gl:events:notifications", ">"},
Count: 10,
Block: 5 * time.Second,
})
if err != nil && err != redis.Nil {
c.log.Error("queue read error", "err", err)
time.Sleep(1 * time.Second)
continue
}
for _, msg := range msgs[0].Messages {
go c.process(ctx, msg)
}
}
}

Consumer Group

  • Group name: notifications-workers
  • Stream: gl:events:notifications
  • Batch size: 10 messages per read
  • Block timeout: 5 seconds
  • Processing: One goroutine per message for concurrent delivery

Event Types

EventSourceAction
notification.pushAny serviceSend push notification to user's devices
notification.emailAny serviceSend transactional email via Resend
user.createdAuth serviceSend welcome email
subscription.changedBilling serviceSend plan change confirmation

Entry Point Pattern

Standard Go service entry point at cmd/server/main.go:

  1. Configure Zerolog (JSON structured logging)
  2. Load config from environment variables
  3. Initialize Redis Streams consumer
  4. Initialize push providers (APNs, FCM) and email provider (Resend)
  5. Create Chi router with middleware
  6. Register routes — health, notification preferences
  7. Start HTTP server + consumer in goroutines
  8. Wait for SIGINT/SIGTERM
  9. Graceful shutdown — drain consumer, stop HTTP server

Docker

FROM golang:1.25-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /app /app
EXPOSE 8500
ENTRYPOINT ["/app"]