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>
111 lines
6.7 KiB
Markdown
111 lines
6.7 KiB
Markdown
# 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/<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](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/<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.
|
|
|
|
```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.
|