You’re reading the v0.1 development preview. APIs may change.
~ 4 min readUpdated 2026-10-29

In-app notifications and queued email

Use the notification service for a private inbox and optional background mail. SQL inbox insertion and queue dispatch use the same database transaction. Email delivery runs in a separately supervised worker. This is separate from the existing synchronous account-verification/password-recovery transport.

Prepare and register

php elang platform:setup

Register the provider in the bootstrap used by both HTTP requests and workers:

$app->provider(new Elang\Notification\Provider());

The provider registers the fixed notification.mail handler. JSON payloads cannot choose arbitrary PHP classes or code. Mail payloads are encrypted under APP_KEY before entering the queue. Keep that key stable and backed up. Inbox text is private through authorization but is not automatically encrypted at rest; apply database/disk encryption appropriate to your data.

Send and read

$notifications = $app->container->get(Elang\Notification\Notifications::class);
$id = $notifications->send(
    $user,
    'Export ready',
    'Your requested report is ready to review.',
    '/reports/42',
    mail: true,
);
$inbox = $notifications->inbox($user);
$notifications->markRead($user, $id);

Subjects are limited to 160 characters and bodies to 16,000 bytes. Optional links must be local paths of at most 500 bytes; protocol-relative links, backslashes, controls, and spaces are rejected. Render text with template escaping. Inbox queries and read updates always constrain user_id; another user cannot mark your message as read. The service returns the latest 100 inbox records.

A notification link is navigation, not authorization. The destination must still check record ownership/team membership. For tenant data, put only the minimum useful information in inbox/email text: removing a member cannot erase an email already delivered to them.

Configure real email

MAILER_DSN=smtp://username:encoded-password@smtp.example.com:587
MAIL_FROM=notifications@example.com

Store real values outside Git and URL-encode credentials inside DSNs. Prefer a secret manager or a protected environment file. Use the transport provider's documented TLS settings. Configure SPF, DKIM, and DMARC for the sending domain, confirm the authorized sender, and test bounce/complaint handling. A configured DSN alone does not prove delivery or domain reputation.

php elang queue:work
php elang queue:failed

null://null is intentionally rejected by queued notification delivery so a worker does not silently discard mail. A development SMTP capture server such as Mailpit accepts messages without sending to real recipients. Account verification/recovery already offers an encrypted local mailbox in APP_ENV=local when the transport is null; that mailbox is a development convenience, not a production mail service.

Retry and delivery semantics

The worker reserves a job with a fenced lease, retries with bounded backoff, and records the exception class without message bodies or credentials. Inspect failed job metadata, fix the cause, then retry a reviewed job:

php elang queue:retry JOB_ID_FROM_FAILED_LIST

Delivery is at least once. If an SMTP server accepts a message but the worker loses its acknowledgment or dies before completing the job, a retry may send it again. For providers that support idempotency, add a stable delivery key and provider-specific adapter. Never promise exactly-once mail.

Keep jobs shorter than the configured lease or implement a reviewed renewal policy. Do not put raw verification links, tokens, passwords, or personal data into unencrypted queue payloads. Monitor queue lag, failures, worker heartbeats, and outbox backlog. See operations, mail, and queues.

Need a hand?

Check the troubleshooting guide, look up a term in the glossary, or work through the complete task board project.

Search Elang documentation

Start typing to find your next step.