notifications/docs/service-overview.md
Ali Asadi 354bbc26cf docs(notifications): add service overview, implementation guide, and frontend handoff doc
Three docs for three audiences: service-overview.md for anyone integrating
with or operating the service, implementation.md for engineers maintaining
this codebase, and frontend-notification-click-actions.md as a self-contained
handoff for the client team to start building tap-to-navigate against — it
flags the click_url URL format as an unconfirmed placeholder needing their
input, and catalogs every notification type currently sending one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 11:39:30 +03:30

6.7 KiB

Notifications Service — Overview

What it is

A Django/DRF microservice in the Gooyal/Winsoo ecosystem that delivers push notifications (via a self-hosted Gotify server) and email on behalf of other backend services. It does not originate any notifications itself — every message is submitted by a producer service (chat, promotions, advertising, ...) over an authenticated API call.

chat / promotions / advertising / ...
        │  OAuth2 client-credentials
        ▼
notifications service (this repo)
        │                       │
        ▼                       ▼
   Gotify server           SMTP server
   (push delivery)         (email delivery)

Tech stack

Layer Choice
Framework Django 5.2 + Django REST Framework
Auth django-oauth-toolkit — this service is an OAuth2 resource server, validating caller tokens by introspection against the Gooyal accounts service
Push backend Gotify 2.6.3 (pinned — see local dev setup below), driven through a generated client (gotify_rest_api_client/)
Async Celery + Celery Beat, Redis as broker/result backend
DB PostgreSQL
Email Django's SMTP backend
Bulk import xlrd (legacy .xls only)
Docs drf-spectacular (OpenAPI/Swagger at /swagger/)

Capabilities

1. Push notifications

  • Single push: POST /push/application/<user_uuid>/application/ — a producer service pushes one message to one user.
  • Bulk push: an admin-triggered Excel upload (BulkPushMessage) fans out a push per row to many users at once.
  • Click actions: an optional click_url on either path is delivered to the client as Gotify's own client::notification.click.url extra. See notification-click-actions.md and frontend-notification-click-actions.md for the full mechanism and integration contract.

2. Email

  • Single email: POST /email/application/<user_uuid>/ — sent immediately via Celery.
  • Queued digest: emails can be attached to an EmailQueue; a periodic task (send_email_for_queued_events) batches queued emails per queue and sends a single digest, throttled to once per 10 minutes per queue.

How a producer service integrates

Every producer vendors a generated OpenAPI client (gooyal_notifications_client, built from this service's own swagger spec) and wraps it in a small utils/clients/notifications_client.py:

  1. Obtains an access token via the OAuth2 client-credentials grant against the Gooyal accounts service, using its own CLIENT_ID/CLIENT_SECRET (cached until near expiry).
  2. Calls push_application_application_create (or email_application_application_create) against NOTIFICATIONS_BASE_PUBLIC_URL, scoped to the target user_uuid.
  3. This service resolves the calling application from the token (apps.gooyal_oauth2.utils.get_application) and records it against the created PushMessage/Email row — the producer is never sent as free-form data, it's derived from the authenticated client.

Endpoints are scope-gated per action (notifications.application.push:submit_message, notifications.application.email:submit_email, notifications.push:get_client_token) via IsAuthenticatedOrTokenMatchesOASRequirements.

As of this writing, three services integrate: chat (live, sends on every new message), promotions (wired, currently sends empty extras), advertising (the only service actually using click_url today — see the implementation doc).

Domain model

Model Purpose
PushUser Per-user Gotify identity — owns a Gotify user account, a Gotify client token, and an application token. Created lazily on first push (PushUser.objects.submit(user)).
PushMessage One push notification: title, message, priority, extras (free-form JSON), click_url (optional), delivery state.
BulkPushMessage An uploaded spreadsheet of push messages plus success/failure counters and state.
Email One email: title, message, extras, optional queue, delivery state.
EmailQueue A named digest queue; batches Email rows created since it last sent.

Configuration (environment variables)

Variable Purpose
DEBUG Django debug flag
DB_NAME / DB_USER / DB_PASSWORD / DB_HOST / DB_PORT Postgres connection
REDIS_BASE_URL Celery broker/result backend + cache
GOTIFY_BASE_PUBLIC_URL Base URL of the Gotify server
GOTIFY_ADMIN_CLIENT_TOKEN Admin token used to provision Gotify users/clients/applications
BASE_OAUTH2_PROVIDER_PUBLIC_URL / BASE_OAUTH2_PROVIDER_PRIVATE_URL Gooyal accounts service, for issuing and introspecting tokens
CLIENT_ID / CLIENT_SECRET This service's own OAuth2 introspection credentials
SCOPES OAuth2 scopes this service requests
EMAIL_HOST / EMAIL_PORT / EMAIL_HOST_USER / EMAIL_HOST_PASSWORD / EMAIL_USE_TLS / EMAIL_USE_SSL / EMAIL_TIMEOUT SMTP config

Local development

Three Docker containers back local dev (see .env):

Container Image Host port
notif_postgres postgres:16 5433
notif_redis redis:7-alpine 6380
notif_gotify gotify/server:2.6.3 8888

The Gotify image must stay pinned to 2.6.3 — later versions (v3+) issue longer tokens that don't fit this service's client_token/application_token CharField(max_length=32) columns.

docker start notif_postgres notif_redis notif_gotify
python manage.py migrate
python manage.py runserver

Deployment

  • Dockerfile — Debian-based image, installs requirements.txt.
  • run.sh — waits for Postgres, runs migrations, serves via gunicorn main.wsgi:application.
  • celery.sh — runs a combined worker + beat process (celery -A main worker -B).

Known gaps

  • No automated tests in apps/push_notifications or apps/emails (both tests.py are stubs).
  • Email._send_email() hard-codes the recipient (xdshia49@gmail.com) instead of self.user's real email — looks like a debugging leftover, not wired to actual user emails yet.
  • Celery Beat is registered two different ways: apps.emails.tasks assigns celery_app.conf.beat_schedule directly at import time, separate from the more conventional CELERY_BEAT_SCHEDULE Django setting used elsewhere in the Gooyal codebase (e.g. advertising). Both work, but it's an inconsistency worth normalizing.
  • URL namespace collision: apps/push_notifications/application_urls.py and user_urls.py both set app_name = 'push_notifications', producing a urls.W005 warning on every manage.py check. Harmless (URLs still resolve) but ambiguous for reverse lookups.