@imola-solutions/notifications
v0.1.0
Published
Unified notification service extracted from BOPC-ITMP. Templates, per-user preferences, pluggable email/SMS/inApp drivers, dispatcher worker, inbox popover + dashboard card. Consumers inject apiClient + currentUser + drivers.
Readme
@imola-solutions/notifications
Unified notification service extracted from BOPC-ITMP. Templates, per-user
preferences, pluggable email / SMS / in-app drivers, dispatcher worker, inbox
popover + dashboard card. Consumers inject apiClient + currentUser +
drivers.
Install
npm install @imola-solutions/notifications @imola-solutions/uiPeer deps (already in most React + MUI stacks):
react>= 18,react-dom>= 18@mui/material>= 5,@emotion/react>= 11,@emotion/styled>= 11@phosphor-icons/react>= 2,dayjs>= 1
Minimal usage
import { createNotificationService, NotificationInbox } from "@imola-solutions/notifications";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(URL, ANON_KEY);
const service = createNotificationService({
apiClient: supabase,
currentUser: { id: "...uuid...", name: "Ada", email: "[email protected]" },
});
export function DashboardHeader({ userId, onNavigate }) {
const [anchor, setAnchor] = React.useState(null);
return (
<>
<IconButton onClick={(e) => setAnchor(e.currentTarget)}>
<BellIcon />
</IconButton>
<NotificationInbox
service={service}
userId={userId}
anchorEl={anchor}
open={Boolean(anchor)}
onClose={() => setAnchor(null)}
onNavigate={onNavigate}
/>
</>
);
}Factory API
const svc = createNotificationService({
apiClient, // Supabase-compatible
currentUser, // { id, name, email }
drivers: {
email: sesDriver, // optional — defaults to console driver
sms: snsDriver, // optional — defaults to console driver
inApp: null, // uses built-in DB writer by default
},
onAudit, // optional structured-events callback
onNotify, // optional toast/snackbar sink
});
svc.notify({ userId, templateKey, variables, channels, meta });
svc.notifyMany({ userIds, templateKey, variables, channels, meta });
svc.notifyRole({ role, templateKey, variables, channels, meta }); // fanout via user_roles
svc.listForUser(userId, { limit, unreadOnly });
svc.countUnread(userId);
svc.markRead(notificationId);
svc.markUnread(notificationId);
svc.markAllRead(userId);
svc.getPreferences(userId);
svc.updatePreferences(userId, { email, sms, inApp }, { templateKey });
svc.listTemplates({ activeOnly });
svc.getTemplate(key);
svc.upsertTemplate({ key, subject, body, channels, notice_type, workflow_trigger });
svc.disableTemplate(key);
svc.listDeliveries({ limit });
svc.listPendingDeliveries({ limit });Driver contract
Every driver is a plain object with a single send method:
export function createSomeDriver(config) {
return {
async send({ to, subject, body, meta }) {
// returns { sent: true, providerId } or throws
},
};
}The package ships:
createInAppDriver({ apiClient })— writes to thenotificationstablecreateConsoleDriver({ channel })— safe default that logs a structured line to stdout; used as the fallback foremailandsms.
See src/drivers/README.md for SES / SNS / Twilio implementation examples.
Worker startup
The dispatcher polls pending_notifications and hands each row to the
matching driver:
import { createClient } from "@supabase/supabase-js";
import { runNotificationDispatch, createConsoleDriver } from "@imola-solutions/notifications";
import { createSesDriver } from "./ses-driver.js";
const apiClient = createClient(URL, SERVICE_ROLE_KEY);
const drivers = {
email: createSesDriver({ region: "us-east-1", fromAddress: "[email protected]" }),
sms: createConsoleDriver({ channel: "sms" }),
};
const { stop } = runNotificationDispatch({
apiClient,
drivers,
intervalMs: 30_000,
});
process.on("SIGTERM", stop);For one-shot cron runs use dispatchPending({ apiClient, drivers, limit }).
SQL migrations
Run both files against your database (Supabase SQL editor works fine):
sql/init.sql— createsnotifications,notification_templates,notification_template_revisions,notification_preferences,pending_notifications, and thefn_resolve_user_role_sethelper. All idempotent (CREATE TABLE IF NOT EXISTS,CREATE OR REPLACE FUNCTION).sql/rls.sql— enables RLS withall_authpolicies matching@imola-solutions/workflow-builder. Follow-up will tighten per-row policies once the consumer's auth backend is confirmed.
Wiring into @imola-solutions/workflow-builder's RunDialog
RunDialog exposes three no-op shim slots for notification side effects. Wire them to this service:
import { createNotificationService } from "@imola-solutions/notifications";
import { RunDialog } from "@imola-solutions/workflow-builder";
const notifications = createNotificationService({ apiClient, currentUser });
<RunDialog
workflowService={wf}
currentUser={currentUser}
role={role}
bopcShims={{
async createNotification(_kind, { step, instance, person, kind }) {
if (!person?.value) return;
await notifications.notify({
userId: person.value,
templateKey: kind === "returned" ? "workflow_step_returned" : "workflow_step_assigned",
variables: {
stepLabel: step?.label,
instanceName: instance?.name,
actor: currentUser.name,
},
meta: {
kind: "workflow",
related_type: "workflow_instance",
related_id: instance?.id,
action_url: `/dashboard/workflows/${instance?.id}`,
},
});
},
async notifyStepAssignees(step, instance, kind) {
const assignees = Array.isArray(step?.assignees) ? step.assignees : [];
await notifications.notifyMany({
userIds: assignees.map((a) => a.user_id).filter(Boolean),
templateKey: kind === "returned" ? "workflow_step_returned" : "workflow_step_assigned",
variables: { stepLabel: step?.label, instanceName: instance?.name },
meta: {
kind: "workflow",
related_type: "workflow_instance",
related_id: instance?.id,
},
});
},
async notifyStepAdmins(step, instance) {
await notifications.notifyRole({
role: "administrator",
templateKey: "workflow_step_admin_alert",
variables: { stepLabel: step?.label, instanceName: instance?.name },
meta: {
kind: "workflow",
related_type: "workflow_instance",
related_id: instance?.id,
},
});
},
}}
/>;Exports
createNotificationService— core factorycreateTemplateService,createPreferenceService,createDeliveryService— sub-service factories if you need them standalonecreateInAppDriver,createConsoleDriverNotificationInbox,RecentNotificationsCarduseNotificationActionsrunNotificationDispatch,dispatchPendinginterpolate— mustache-style{{key}}helper used by the template service
