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>
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 |
| 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_urlon either path is delivered to the client as Gotify's ownclient::notification.click.urlextra. 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:
- 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). - Calls
push_application_application_create(oremail_application_application_create) againstNOTIFICATIONS_BASE_PUBLIC_URL, scoped to the targetuser_uuid. - This service resolves the calling application from the token (
apps.gooyal_oauth2.utils.get_application) and records it against the createdPushMessage/Emailrow — 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, installsrequirements.txt.run.sh— waits for Postgres, runs migrations, serves viagunicorn main.wsgi:application.celery.sh— runs a combined worker + beat process (celery -A main worker -B).
Known gaps
- No automated tests in
apps/push_notificationsorapps/emails(bothtests.pyare stubs). Email._send_email()hard-codes the recipient (xdshia49@gmail.com) instead ofself.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.tasksassignscelery_app.conf.beat_scheduledirectly at import time, separate from the more conventionalCELERY_BEAT_SCHEDULEDjango 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.pyanduser_urls.pyboth setapp_name = 'push_notifications', producing aurls.W005warning on everymanage.py check. Harmless (URLs still resolve) but ambiguous for reverse lookups.