Tenant Notifications
Tenant Notifications
The tenant notification system automatically sends agency-branded emails and SMS messages to tenants at key points in the tenancy lifecycle. It is powered by Inngest and supports opt-out tracking, idempotent delivery, and a full audit trail.
How It Works
Notifications are dispatched by sending an Inngest event from anywhere in the application. The tenantNotificationSend function picks up the event, resolves tenant and agency details, checks opt-out preferences, and delivers via email (Resend) and/or SMS (Twilio).
Every delivery — whether successful, skipped, or opted-out — is recorded in the tenant_notification_log table.
Triggering a Notification
Send the tenant/notification.send Inngest event with the following payload:
await inngest.send({
name: "tenant/notification.send",
data: {
orgId: "org_123",
portalTokenId: "token_456",
notificationType: "welcome", // see Notification Types below
// Optional context fields:
tenancyTermId: "term_789",
taskTitle: "Pay holding deposit",
documentType: "Gas Safety Certificate",
daysRemaining: 30,
endDate: "15 March 2025",
},
});
Required Fields
| Field | Type | Description |
|---|---|---|
orgId | string | The organisation ID |
portalTokenId | string | The tenant portal token used to look up tenant details and opt-out preferences |
notificationType | string | The type of notification to send (see below) |
Optional Context Fields
| Field | Type | Used By |
|---|---|---|
tenancyTermId | string | All types; links the notification to a specific tenancy term |
taskTitle | string | task_assigned — name of the task assigned to the tenant |
documentType | string | compliance_update — e.g. "Gas Safety Certificate", "EPC", "EICR" |
daysRemaining | number | renewal_notice — days until tenancy end |
endDate | string | renewal_notice — human-readable end date, e.g. "15 March 2025" |
Notification Types
welcome
Sent when a tenancy is created. Includes:
- Portal access link
- Tenancy start date
- Monthly rent amount
Delivery is idempotent — a welcome notification is only sent once per portal token.
task_assigned
Sent when a task is assigned to the tenant. Includes:
- Task title (from
taskTitle) - Cost breakdown (holding deposit, tenancy agreement)
compliance_update
Sent when a compliance document is updated. Includes:
- Document type (from
documentType), e.g. Gas Safety Certificate, EPC, EICR
renewal_notice
Sent when a renewal notice is dispatched. Includes:
- Days remaining (from
daysRemaining) - Tenancy end date (from
endDate) - Urgency indicators based on proximity to end date
Delivery is idempotent — renewal notices are deduplicated per portal token.
Agency Branding
All email templates use the agencyName field from your agency branding configuration. Emails are delivered white-labelled under the agency's name rather than a platform identity.
Opt-Out Tracking
Opt-outs are stored in the tenant_notification_log table as records with channel = "opt_out". When an opt-out record exists for a given portal token:
- Both email and SMS delivery are skipped
- No further notifications of any type are sent to that tenant
To opt a tenant out, insert a record into tenant_notification_log with channel = "opt_out" for the relevant portalTokenId.
Delivery Channels
| Channel | Provider | Behaviour when unavailable |
|---|---|---|
Resend (via existing sendEmail wrapper) | Error is retried per Inngest retry policy | |
| SMS | Twilio | Gracefully skipped if the tenant has no phone number on record |
Audit Logging
Every notification dispatched (regardless of channel or outcome) is written to the audit log via logAudit. This provides a full history of tenant communications at the organisation level.
Database Table: tenant_notification_log
The tenant_notification_log table records every notification event.
| Column | Type | Description |
|---|---|---|
id | text (PK) | Auto-generated UUID |
orgId | text | Organisation ID |
portalTokenId | text | Tenant portal token |
notificationType | text | One of welcome, task_assigned, compliance_update, renewal_notice |
channel | text | One of email, sms, opt_out |
status | text | Delivery status |
messageId | text | Provider message ID (Resend or Twilio) |
metadata | jsonb | Additional context |
createdAt | timestamp | Record creation timestamp |
Indexes: orgId, portalTokenId, notificationType, channel, createdAt
Infrastructure Notes
- The Inngest function applies concurrency limits and throttling to prevent burst sending.
- The function uses
NonRetriableErrorfor permanent failures (e.g. missing tenant record) to avoid infinite retries. - Batch tracking is supported for bulk notification scenarios.
- The schema file
src/db/tenant-notification-schema.tsis included indrizzle.config.tsso migrations are generated automatically.