Skip to content

quadkit-notification delivers messages across multiple channels through a common set of protocols — MailerProtocol (email), SMSChannelProtocol (SMS), PushChannelProtocol (push), and InboxStoreProtocol (in-app inbox). Each channel supports multiple providers and named multi-backend DI binding.

ChannelProviderExtra / PackageProduction ReadyBest For
EmailSMTP(none)YesTransactional email, internal systems
EmailSendGrid[sendgrid]YesHigh-volume marketing + transactional
SMSTwilio[twilio]YesOTP, alerts, verification
PushFCM(core)YesAndroid + Web push notifications
PushAPNs[apns]YesiOS push notifications
PushWeb Push(core via pywebpush)YesBrowser push (RFC 8030)
InboxDatabase(core via quadkit-sql)YesIn-app notification bell
InboxMemory(none)NoUnit tests, prototyping

Direct SMTP delivery via SMTPMailer. Supports STARTTLS (port 587) and SSL (port 465) with optional authentication. Configured under the mailer config section.

mailer:
backends:
- name: transactional
driver: smtp
primary: true
from_email: [email protected]
from_name: Quadkit
smtp:
host: smtp.example.com
port: 587
username: "${SMTP_USER}"
password: "${SMTP_PASS}"
use_tls: true
from quadkit.notification.mailer import SMTPMailer
mailer = SMTPMailer(host="smtp.example.com", port=587, username="user", password="pass")
await mailer.send(Mailable(to="[email protected]", subject="Hello", body="..."))

API-based email delivery via SendGridMailer. Supports sandbox mode for testing without sending real emails.

mailer:
backends:
- name: marketing
driver: sendgrid
from_email: [email protected]
sendgrid:
api_key: "${SENDGRID_API_KEY}"
sandbox_mode: false

Either mailer implementation can be wrapped by RetryingMailer for transparent retry logic; MailerProvider does it for the primary binding when retry_max_attempts is greater than zero, recording attempts in a DeliveryStoreProtocol.


SMS delivery via the Twilio API. Requires an account SID, auth token, and a sender phone number in E.164 format. Configured under sms_backends in NotificationConfig.

notification:
sms_backends:
- name: primary
driver: twilio
primary: true
twilio:
account_sid: "${TWILIO_ACCOUNT_SID}"
auth_token: "${TWILIO_AUTH_TOKEN}"
from_number: "+15551234567"
from quadkit.notification.backends.sms import TwilioSMS
sms = TwilioSMS(account_sid="AC...", auth_token="...", from_number="+15551234567")
await sms.send(SMSMessage(to="+15559876543", body="Your code is 123456"))

Firebase Cloud Messaging for Android and cross-platform push. Configured with a server API key. The FCMPush implementation handles device registration tokens and notification payload formatting.

notification:
push_backends:
- name: mobile
driver: fcm
primary: true
fcm:
server_key: "${FCM_SERVER_KEY}"

Apple Push Notification service for iOS. Requires an Apple Developer Team ID, Auth Key ID, the .p8 ECDSA private key, and a bundle identifier. Sandbox mode is available for development builds.

notification:
push_backends:
- name: ios
driver: apns
apns:
team_id: ABC123DEFG
key_id: XYZ789ABCD
apns_auth_key: "${APNS_AUTH_KEY}"
bundle_id: com.example.MyApp
sandbox: false

Browser push notifications (RFC 8030) using VAPID signatures. Requires a VAPID public/private key pair and a subject URI (typically mailto:).

notification:
push_backends:
- name: browser
driver: web_push
web_push:
vapid_private_key: "${VAPID_PRIVATE_KEY}"
vapid_public_key: "${VAPID_PUBLIC_KEY}"
vapid_claims_subject: mailto:[email protected]
from quadkit.notification.backends.push import WebPushChannel
wp = WebPushChannel(vapid_private_key="...", vapid_public_key="...")
await wp.send(PushMessage(token="browser-sub", title="New message", body="You have mail"))

The InboxService provides an in-app inbox with read/unread tracking and pagination. retention_days is configuration only; no automatic pruning job is provided. Two backends:

  • DatabaseInboxStore — SQL-backed store; the default when configured through InboxModule, requiring a bound database provider.
  • InMemoryInboxStore — ephemeral, single-process store; used by InboxModule.stub() and by InboxService() when constructed without a store.
inbox:
store_backend: database
max_page_size: 50
retention_days: 30
mark_read_on_fetch: false
from quadkit.notification import InboxService
# Direct construction uses memory. Only trusted application code sends.
inbox = InboxService()
message = await inbox.send(user_id="user-42", title="Welcome!", body="Thanks for joining")
messages = await inbox.get_inbox(user_id="user-42", limit=20, offset=0)

The built-in SQL store applies recipient filtering and page limits in the query, with ID as a tie-breaker for equal creation times. Offset pagination is not a snapshot: concurrent inserts or deletes can shift later pages. Older custom stores remain compatible through a list-and-slice fallback, which can load the whole inbox. count() for all messages still reads the full inbox. The local service rejects list results with a different owner and oversized pages, and checks both ID and owner on single-message reads. A store must still correctly scope mutations and unread counts; these checks cannot enforce a misbehaving store’s write semantics. Explicitly supplied falsey store objects are retained rather than replaced by an in-memory store.

The service does not authenticate users or authorize sends. For an HTTP or tool adapter, take the subject from verified server-side identity and use it for scoped operations. get_message(id) is unscoped; use get_message_for_user(id, subject) for a recipient-facing lookup, while still authenticating the request independently. The setting above is not automatically applied to direct InboxService() construction.


If you need…Choose…
Transactional email via existing SMTPMailer → SMTP
High-volume email with analyticsMailer → SendGrid
SMS alerts and OTPNotification → Twilio
Android / web pushPush → FCM
iOS pushPush → APNs
Browser push (RFC 8030)Push → Web Push
In-app notification bellInbox → Database
mailer:
backends:
- name: transactional
driver: smtp
primary: true
smtp:
host: smtp.example.com
- name: marketing
driver: sendgrid
sendgrid:
api_key: "${SENDGRID_API_KEY}"
notification:
sms_backends:
- name: alerts
driver: twilio
primary: true
twilio:
account_sid: "${TWILIO_SID}"
auth_token: "${TWILIO_TOKEN}"
from_number: "+15551234567"
push_backends:
- name: mobile
driver: fcm
primary: true
fcm:
server_key: "${FCM_KEY}"
from quadkit.notification import InMemoryInboxStore
from quadkit.notification.inbox import InboxService
store = InMemoryInboxStore()
inbox = InboxService(store=store)
# All operations work identically — no external dependencies
await inbox.send(user_id="test", subject="Hello", body="World")