# Notifications Service — Overview ## What it is A Django/DRF microservice in the Gooyal/Winsoo ecosystem that delivers **push notifications** (via a self-hosted [Gotify](https://gotify.net) 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//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](notification-click-actions.md) and [frontend-notification-click-actions.md](frontend-notification-click-actions.md) for the full mechanism and integration contract. ### 2. Email - **Single email**: `POST /email/application//` — 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. ```bash 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.