Netpress Laravel-inspired backend framework for Node.js
Frameworkv0.5.3 Starterv0.2.2 Docsv1.1.0
Overview Installation Architecture CLI
Features

Notifications

Notifications let one class deliver the same event across multiple channels:

  • database records
  • mail
  • 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.js
  • database/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:

  • id
  • type
  • notifiable_type
  • notifiable_id
  • data
  • read_at
  • created_at
  • updated_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 BaseMail subclass

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

  1. Your queue runtime should already be configured through config/queue.js.
  2. Redis-backed queue workers should be available when you want async delivery.
  3. Re-export the worker job so discovery can find it:
// app/Jobs/SendNotificationJob.js
export { SendNotificationJob as default } from "@admicaa/netpress";
  1. 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:

MethodEventPayload
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