Skip to content
Guide

Publish an App notification

Publish an App-owned result notification through the host contracts and check delivery authority.

Use NotificationContribution and NotificationPublisher when an App has consumed a durable business event and its own transaction has reached a result. Do not import Core notification classes or insert a notification row.

Register the notification contribution

Add the contribution directory to the App's existing package manifest. This is the same contributionLocations() shape used by App manifests:

Code example
PHP
use Nexia\AppRuntime\AbstractPackageAppManifest;

final class WorkshopAppManifest extends AbstractPackageAppManifest
{
    public function contributionLocations(): array
    {
        return [[
            'namespace' => 'Workshop\\Contribution',
            'directory' => __DIR__.'/Contribution',
        ]];
    }
}
Code example
PHP
use Nexia\Events\EventEnvelope;
use Nexia\Notification\Contracts\NotificationContribution;
use Nexia\Notification\Contracts\NotificationIntent;
use Nexia\Notification\Contracts\NotificationPublisher;
use Nexia\Notification\NotificationCategory;
use Nexia\Notification\NotificationPublication;
use Nexia\ResourceReference\ResourceRef;

final class NoteCompletedIntent implements NotificationIntent
{
    public function key(): string { return 'workshop.note.completed'; }
    public function category(): string { return 'workshop.work'; }
    public function policySnapshot(): array
    {
        return [
            'preference_mode' => 'default_on',
            'email_policy' => 'in_app_only',
            'realtime_attention' => 'center_only',
            'realtime_variant' => 'success',
        ];
    }
    public function deepLink(array $params): ?string { return '/apps/workshop/notes/'.$params['note_id']; }
    public function render(array $params, string $locale): array
    {
        return ['title' => $locale === 'ko' ? '노트 완료' : 'Note complete', 'body' => null];
    }
}

final class WorkshopNotifications implements NotificationContribution
{
    public function notificationCategories(): iterable
    {
        return [new NotificationCategory('workshop.work', 'workshop.notifications.work', 60)];
    }

    public function notificationIntents(): iterable
    {
        return [new NoteCompletedIntent]; // key/category start with workshop.
    }
}

// Inside the App's source-event Inbox handler, after its result write succeeds:
/** @var NotificationPublisher $notifications */
/** @var EventEnvelope $event */
$notifications->publish($event, new NotificationPublication(
    intentKey: 'workshop.note.completed',
    owner: new ResourceRef('workshop', 'workshop.note', $note->public_id, $note->displayLabel()),
    legalEntityId: $note->legal_entity_id,
    recipientIds: [$requesterId],
    params: ['note_id' => $note->public_id],
    dedupeKey: 'note:'.$note->public_id.':completed',
));

Check delivery and access

The source is the App's own result-event EventEnvelope; its event name and producer key must start with the contributing App key, and it must carry the same Legal Entity. Invoke the publisher inside the source App Inbox handler and do not catch a publication failure: it rolls back that source handler, marks its existing Inbox work failed, and the existing Inbox retry re-delivers the source event. Core skips inactive Apps, checks that the intent and owner belong to the active App, resolves the owner for each recipient under that recipient's current read authority and Legal Entity, applies existing preferences/email delivery, and records per-recipient dedupe. The intent renderer receives the recipient locale when the notification is displayed. If access is revoked later, Core hides the row and suppresses an unsent email.

Source of truth: docs/developers/content/en/platform-extensions/publish-notification.md