Adding Email and SMS Notifications¶
This guide is for developers who need to add a new email or SMS (text) notification to Hardhat Flow. For what customers see today, see Notifications.
Architecture (read this first)¶
| Channel | Provider | Code | Preferences |
|---|---|---|---|
| Mailgun HTTP API | apps/notifications/email.py, dispatch.py |
Per-tenant via CompanySettings.email_notification_preferences |
|
| SMS | Twilio REST | apps/notifications/client.py, tasks.py, messages.py |
Fixed recipient rules (Settings UI placeholder only) |
Hard rules:
- Always enqueue with Celery (
.delay(...)). Never call Mailgun or Twilio on the request thread. - Call the task from the view after the state change succeeds (same pattern as existing bid/job/invoice hooks). Do not use Django signals.
- Wrap enqueue in
try/except+ log so a broker blip does not fail the HTTP response. - Keep tenant isolation: filter users by
company_id; usefetch_users_by_ids()for User lookups from tenant-DB models (noselect_relatedon User FKs). - Missing phone/email or missing provider credentials → skip with a warning, do not raise to the user.
HTTP action → view updates DB → task.delay(ids…) → Celery worker
→ resolve recipients → Mailgun or Twilio
Environment¶
| Variable | Channel | Notes |
|---|---|---|
MAILGUN_API_KEY |
Blank in local/CI → sends no-op with warning | |
MAILGUN_DOMAIN |
Verified sending domain (e.g. mg.hardhatflow.com) |
|
DEFAULT_FROM_EMAIL |
Must match the Mailgun domain | |
FRONTEND_URL |
Used for deep links in templates | |
PLATFORM_ADMIN_NOTIFY_EMAILS |
Optional comma-separated extras for access-request alerts | |
TWILIO_ACCOUNT_SID |
SMS | Platform Twilio account |
TWILIO_AUTH_TOKEN |
SMS | Platform Twilio auth |
| Redis / Celery broker | Both | Required for workers + email daily cap counter |
Per-tenant (database, not env):
CompanySettings.twilio_phone_number— outbound SMS From numberCompanySettings.email_notification_preferences— enabled flag + roles per email event
Celery worker must be running (celery -A config worker -l info or Docker Compose worker).
Add a tenant workflow email¶
Use this path for events Owners can toggle under Settings → Email Notifications.
1. Register the event¶
In apps/notifications/preferences.py:
- Add an entry to
TENANT_NOTIFICATION_EVENTS(label,description,template,subject). - Add default roles in
_DEFAULT_EVENT_ROLES.
merge_email_notification_preferences() fills missing events from defaults for existing tenants — no migration required for preference JSON shape.
2. Add templates¶
Create a pair under apps/notifications/templates/notifications/email/:
<template_name>.html<template_name>.txt
template in preferences is the base name (no extension). Context keys must match what the Celery task builds.
3. Add a Celery task in dispatch.py¶
Follow existing tasks (notify_tenant_bid_submitted, notify_tenant_invoice_created, …):
@shared_task(bind=True, max_retries=3, default_retry_delay=60)- Accept
db_alias: str | None = Noneand wrap body intenant_db_for_celery_task(db_alias)(fromapps.companies.middleware). Without the alias, production workers query the platform DB and fail (Table 'defaultdb.bids_bid' doesn't exist). - Load the entity by id (use
all_objectsif soft-deleted rows can still notify). - Build a plain
contextdict (includecompany_nameand a*_urlvia_frontend_url(...)). - Call
_dispatch_tenant_event(company_id, event_key, context, actor_user_id=...).
_dispatch_tenant_event resolves recipients from preferences, excludes the actor, and calls send_transactional_email.delay(...).
4. Hook the view¶
After the successful state change:
from apps.companies.router import get_current_tenant_db
try:
notify_tenant_my_event.delay(
str(entity.id),
str(request.user.id),
db_alias=get_current_tenant_db(),
)
except Exception:
logger.exception("notify_tenant_my_event failed for %s", entity.id)
5. Frontend types¶
Extend NotificationEventKey in frontend/types/api.ts so TypeScript matches the API. The settings UI loads event labels from API meta; keeping the union in sync avoids type drift.
6. Tests and docs¶
- Unit/integration:
apps/notifications/tests/test_dispatch.py, hook coverage in the app’s view tests (assert.delaycalled). - Update Notifications (and Spanish sibling) with the new event.
- Keep this page accurate if the pattern changes.
Add a one-off / platform email¶
For invites, access-request alerts, or other non-preference emails:
- Call
send_transactional_email.delay(to=..., subject=..., template_name=..., context=...)directly, or - Wrap in a
@shared_taskindispatch.py(seesend_tenant_user_welcome_email,notify_platform_admins_access_request).
Still Celery-only. Add HTML + TXT templates under the same templates directory.
Add an SMS notification¶
SMS recipient rules are hardcoded in tasks today (no Owner preference storage yet). The Settings page shows a grayed-out placeholder only.
1. Message builder¶
In apps/notifications/messages.py, add a function that returns a short plain string:
[Hardhat Flow] <Entity>: <identifier> — <what happened>.
Do not hit the database beyond already-loaded relations.
2. Celery task¶
In apps/notifications/tasks.py:
- Load the bid/job (or other entity).
- Load
CompanySettings; bail with a warning iftwilio_phone_numberis blank. - Resolve recipients (Owners via
User.objects.filter(company_id=...), assigned PM/Supervisor viafetch_users_by_ids). - Call
_send_to_users(recipients, from_number, body)(skips users withoutphone).
3. Hook the view¶
notify_my_sms_event.delay(str(entity.id))
(or with an extra arg like notify_priority_set.delay(id, "bid")).
4. Optional UI list¶
If you want the coming-soon Settings list to show the event, update SmsNotificationPlaceholder / related frontend copy. Real SMS preference storage is not implemented yet.
5. Tests and docs¶
apps/notifications/tests/test_tasks.py,test_messages.py, plus view patches.- Update Notifications.
Checklist¶
- [ ] Event registered (email:
preferences.py; SMS: message + task) - [ ] Templates or SMS body added
- [ ] Celery task with retries; soft-skip on missing config/phone/email
- [ ] View calls
.delay(...)after successful mutation; enqueue failures logged - [ ] No cross-tenant User joins from tenant DB (
fetch_users_by_idswhere needed) - [ ] Frontend
NotificationEventKeyupdated for new email events - [ ] Tests cover happy path + skip paths
- [ ] Customer docs updated (
features/notifications.md+.es.md)
Related files¶
| Path | Role |
|---|---|
apps/notifications/preferences.py |
Email event catalog + defaults |
apps/notifications/dispatch.py |
Email Celery tasks + recipient resolution |
apps/notifications/email.py |
Mailgun send + daily cap |
apps/notifications/tasks.py |
SMS Celery tasks |
apps/notifications/messages.py |
SMS body builders |
apps/notifications/client.py |
Twilio wrapper |
apps/companies/models.py |
twilio_phone_number, email_notification_preferences |
apps/companies/views.py |
NotificationSettingsView |
frontend/components/settings/EmailNotificationSettings.tsx |
Email prefs UI |
frontend/components/settings/SmsNotificationPlaceholder.tsx |
SMS prefs placeholder |