Notifications
Notifications let one class deliver the same event across multiple channels:
- database records
- logs
- custom transports you register yourself
In the starter kit, notifications sit on top of the same database, mail, and queue systems you already configure elsewhere.
Quick Example
// app/Notifications/WelcomeNotification.js
import { Notification } from "@admicaa/netpress";
import WelcomeMail from "../Mail/WelcomeMail.js";
export default class WelcomeNotification extends Notification {
via() {
return ["database", "mail"];
}
toDatabase(notifiable) {
return {
type: "welcome",
message: `Welcome aboard, ${notifiable.name}!`,
};
}
toMail(notifiable) {
return new WelcomeMail(notifiable);
}
}
const user = await User.findOrFail(userId);
await user.notify(new WelcomeNotification());
One notification class decides the channels in via(), then builds the per-channel payload with methods like toDatabase(), toMail(), or toLog().
Starter Setup
The starter kit does not fully wire notifications for you by default. To use them in an app, do the setup once:
1. Generate the notification and table migration
npm run artisan -- make:notification WelcomeNotification --migration
npm run artisan -- migrate
That writes:
app/Notifications/WelcomeNotification.jsdatabase/migrations/<timestamp>_create_notifications_table.js
If the notifications table already exists, you can skip --migration.
2. Make the model notifiable
Usually this is your User model:
// app/Models/User.js
import bcrypt from "bcryptjs";
import { BaseModel, Notifiable, config } from "@admicaa/netpress";
await config.load("auth");
export default class User extends Notifiable(BaseModel) {
static hidden = ["password", "refreshToken", "passwordResetToken", "passwordResetExpires"];
static fillable = [
"name",
"email",
"password",
"role",
"refreshToken",
"passwordResetToken",
"passwordResetExpires",
"oauthProvider",
"oauthId",
];
}
Notifiable(BaseModel) adds:
notify(notification)notifyNow(notification)notifications()unreadNotifications()readNotifications()- read-state helpers like
markNotificationAsRead(...)
3. Add optional config only if you need overrides
Notifications work with framework defaults, so a config file is optional.
Create config/notifications.js only when you want to change the default channels, table name, or queued job name:
export default {
default: ["database"],
table: "notifications",
connection: null,
morph: {
typeColumn: "notifiable_type",
idColumn: "notifiable_id",
},
queue: {
job: "SendNotificationJob",
},
channels: {
database: {},
mail: {},
log: {},
},
};
Then load it from a provider, usually app/Providers/AppServiceProvider.js:
import notificationsConfig from "../../config/notifications.js";
import { ServiceProvider, config, setNotificationsConfig } from "@admicaa/netpress";
export default class AppServiceProvider extends ServiceProvider {
async register() {
await config.loadMany(["cache", "storage", "view", "cookies"]);
setNotificationsConfig(notificationsConfig);
}
}
Creating Notifications
Generate a new class with:
npm run artisan -- make:notification InvoicePaid
Example:
import { Notification } from "@admicaa/netpress";
export default class InvoicePaid extends Notification {
constructor(invoice) {
super();
this.invoice = invoice;
}
via() {
return ["database", "mail", "log"];
}
toDatabase() {
return {
type: "invoice_paid",
invoiceId: this.invoice.id,
amountCents: this.invoice.amountCents,
};
}
toMail(notifiable) {
return {
to: notifiable.email,
subject: `Receipt for invoice #${this.invoice.id}`,
html: `<p>Thanks for your payment.</p>`,
};
}
toLog(notifiable) {
return {
level: "info",
message: `Invoice ${this.invoice.id} paid by ${notifiable.email}`,
};
}
}
Sending Notifications
For a single model:
const user = await User.findOrFail(userId);
await user.notify(new InvoicePaid(invoice));
For multiple recipients:
import { sendNotification } from "@admicaa/netpress";
await sendNotification(admins, new InvoicePaid(invoice));
Channel routing
By default the mail channel uses notifiable.email. Override that when needed:
class User extends Notifiable(BaseModel) {
routeNotificationForMail() {
return this.billingEmail || this.email;
}
}
Delivery Channels
Database channel
The default channel persists notifications through DatabaseNotification.
The bundled migration creates fields like:
idtypenotifiable_typenotifiable_iddataread_atcreated_atupdated_at
Once a model is notifiable, you get inbox-style helpers:
const all = await user.notifications().latest("created_at").get();
const unread = await user.unreadNotifications().get();
const read = await user.readNotifications().get();
await user.markNotificationAsRead(notificationId);
await user.markNotificationAsUnread(notificationId);
await user.markAllNotificationsAsRead();
That is enough to build an inbox page or notifications dropdown in the starter.
Mail channel
toMail() can return:
- a plain envelope object
- a rendered component payload
- a
BaseMailsubclass
That means notifications reuse the same mail system described in Mail instead of introducing a second email path.
Log channel
toLog() writes a structured log entry through the shared logger:
toLog(notifiable) {
return {
level: "info",
message: `Welcomed ${notifiable.name}`,
data: { userId: notifiable.id },
};
}
Queued Notifications
If a notification should be deferred, enable queueing on the class:
export default class HeavyReportNotification extends Notification {
static shouldQueue = true;
via() {
return ["database", "mail"];
}
}
When queueing is enabled, Netpress builds the payload first, then pushes a job instead of sending synchronously.
Starter requirements
- Your queue runtime should already be configured through
config/queue.js. - Redis-backed queue workers should be available when you want async delivery.
- Re-export the worker job so discovery can find it:
// app/Jobs/SendNotificationJob.js
export { SendNotificationJob as default } from "@admicaa/netpress";
- Start a worker:
npm run artisan -- queue:work --queue=notifications
At the moment, queued notifications also need a queue instance available to the notification manager, either through the queue.notifications container binding or a custom NotificationManager({ queue }) setup. If no notification queue is bound, the manager logs the problem and falls back to synchronous delivery so notifications are not silently dropped.
notifyNow(...) always bypasses queueing:
await user.notifyNow(new HeavyReportNotification(report));
Custom Channels
Any object with resolvePayload(...) and send(...) can act as a channel.
import { getNotificationManager } from "@admicaa/netpress";
class SmsChannel {
builderMethod() {
return "toSms";
}
async resolvePayload(notifiable, notification) {
return notification.toSms?.(notifiable) ?? null;
}
async send(_notifiable, _notification, payload) {
await twilioClient.messages.create({
from: "+15555555555",
to: payload.phone,
body: payload.body,
});
return { delivered: true, channel: "sms" };
}
}
getNotificationManager().extend("sms", new SmsChannel());
Then use it from the notification class:
class TwoFactorCode extends Notification {
via() {
return ["sms"];
}
toSms(notifiable) {
return {
phone: notifiable.phone,
body: `Your code is ${this.code}`,
};
}
}
Realtime Delivery
The framework ships a socket channel that pushes notifications through the realtime sockets module. Add it to via() and implement toSocket(notifiable) alongside the other builders:
import { Notification } from "@admicaa/netpress";
export default class InvoicePaidNotification extends Notification {
constructor(invoice) {
super();
this.invoice = invoice;
}
via() {
return ["database", "mail", "socket"];
}
toDatabase() {
return { type: "invoice_paid", invoiceId: this.invoice.id };
}
toMail(notifiable) {
return { to: notifiable.email, subject: "Receipt", html: "<p>Thanks!</p>" };
}
toSocket(notifiable) {
return {
event: "notification:new",
room: `users.${notifiable.id}`,
data: {
title: "Invoice paid",
amount: this.invoice.amountCents,
type: "invoice_paid",
},
};
}
}
When Socket.attach(server) has been called, the channel resolves SocketManager from the container and broadcasts to the room you returned from toSocket(). If event or room is omitted the channel falls back to the configured user room (users.{id}) and the notifications.newEvent event name (default notification:new).
socket is a soft channel: when the socket server is not attached the channel logs not-attached and skips delivery without raising — your database and mail channels still complete normally. This makes it safe to add socket to via() even in environments where the realtime stack is disabled (CLI commands, queue workers, tests).
Auto-Sync on Read State Changes
The Notifiable mixin emits realtime updates when the read state changes:
| Method | Event | Payload |
|---|---|---|
markNotificationAsRead(id) | notification:read | { id, read_at } |
markNotificationAsUnread(id) | notification:read | { id, read_at: null } |
markAllNotificationsAsRead() | notification:read-all | { ids: [...], read_at } |
Each event is broadcast to the recipient's user room so a logged-in client sees the badge update without re-polling. Event names are configurable via sockets.notifications in config/sockets.js.
Frontend Example
import { io } from "socket.io-client";
const socket = io("/", { auth: { token } });
socket.on("notification:new", (n) => store.commit("notifications/add", n));
socket.on("notification:read", ({ id }) => store.commit("notifications/markRead", id));
socket.on("notification:read-all", () => store.commit("notifications/clear"));
See the Realtime Sockets page for the full socket API.
Safety Notes
Notifications often end up in long-lived places like database rows and logs. Netpress sanitizes sensitive payload keys and token-like values before delivery.
Still, the normal rule is:
- do not put raw passwords, tokens, or secrets in notification payloads
- prefer ids, labels, and safe summary data
Commands
npm run artisan -- make:notification WelcomeNotification
npm run artisan -- make:notification WelcomeNotification --migration
npm run artisan -- migrate
npm run artisan -- queue:work --queue=notifications