Port Nimara CRM — Real-Time Events & Background Jobs
Compiled: 2026-03-11
Real-Time: Socket.io (WebSocket alongside Next.js)
Job Queue: BullMQ + Redis
Transport: Redis adapter for Socket.io (multi-instance ready)
1. Socket.io Architecture
1.1 Connection
1.2 Room Structure
| Room |
Members |
Purpose |
port:{portId} |
All users at that port |
Port-scoped data updates |
user:{userId} |
Single user (all their tabs/devices) |
Personal notifications, reminder alerts, calendar sync updates |
global |
Super admin(s) in cross-port mode |
System alerts, cross-port updates |
berth:{berthId} |
Users viewing a specific berth detail |
Granular berth updates |
client:{clientId} |
Users viewing a specific client detail |
Granular client updates |
interest:{interestId} |
Users viewing a specific interest detail |
Granular interest updates |
2. Real-Time Event Catalog
2.1 Berth Events
| Event |
Room |
Payload |
Trigger |
berth:statusChanged |
port:{portId} |
{ berthId, oldStatus, newStatus, triggeredBy } |
Berth status updated (manual or auto) |
berth:updated |
port:{portId}, berth:{berthId} |
{ berthId, changedFields } |
Any berth field updated |
berth:waitingListChanged |
berth:{berthId} |
{ berthId, action, entry } |
Waiting list entry added/removed/reordered |
berth:maintenanceAdded |
berth:{berthId} |
{ berthId, logEntry } |
New maintenance log entry |
2.2 Client Events
| Event |
Room |
Payload |
Trigger |
client:created |
port:{portId} |
{ clientId, clientName, source } |
New client created |
client:updated |
port:{portId}, client:{clientId} |
{ clientId, changedFields } |
Client fields updated |
client:archived |
port:{portId} |
{ clientId } |
Client archived |
client:restored |
port:{portId} |
{ clientId } |
Client restored from archive |
client:merged |
port:{portId} |
{ survivingId, mergedId } |
Clients merged |
client:noteAdded |
client:{clientId} |
{ clientId, noteId, authorName, preview } |
New note added |
client:duplicateDetected |
port:{portId} |
{ clientAId, clientBId, score, reason } |
Duplicate alert created |
2.3 Interest Events
| Event |
Room |
Payload |
Trigger |
interest:created |
port:{portId} |
{ interestId, clientId, berthId, source } |
New interest created |
interest:updated |
port:{portId}, interest:{interestId} |
{ interestId, changedFields } |
Interest fields updated |
interest:stageChanged |
port:{portId} |
{ interestId, oldStage, newStage, clientName, berthNumber } |
Pipeline stage changed |
interest:berthLinked |
port:{portId} |
{ interestId, berthId } |
Berth linked to interest |
interest:berthUnlinked |
port:{portId} |
{ interestId, berthId } |
Berth unlinked from interest |
interest:archived |
port:{portId} |
{ interestId } |
Interest archived |
interest:noteAdded |
interest:{interestId} |
{ interestId, noteId, authorName, preview } |
Note added to interest |
2.4 Document Events
| Event |
Room |
Payload |
Trigger |
document:created |
port:{portId} |
{ documentId, type, interestId } |
Document record created |
document:sent |
port:{portId} |
{ documentId, type, signerCount } |
Document sent for signing |
document:signed |
port:{portId}, interest:{interestId} |
{ documentId, signerName, signerRole, remainingSigners } |
Individual signer completed |
document:completed |
port:{portId}, interest:{interestId} |
{ documentId, type, interestId, clientName } |
All signers completed |
document:expired |
port:{portId} |
{ documentId } |
Document expired |
document:reminderSent |
interest:{interestId} |
{ documentId, recipientEmail } |
Signing reminder sent |
2.5 Financial Events
| Event |
Room |
Payload |
Trigger |
expense:created |
port:{portId} |
{ expenseId, amount, currency, category } |
Expense created |
expense:updated |
port:{portId} |
{ expenseId, changedFields } |
Expense updated |
invoice:created |
port:{portId} |
{ invoiceId, invoiceNumber, total, clientName } |
Invoice created |
invoice:sent |
port:{portId} |
{ invoiceId, invoiceNumber, recipientEmail } |
Invoice emailed |
invoice:paid |
port:{portId} |
{ invoiceId, invoiceNumber, amount } |
Payment recorded |
invoice:overdue |
port:{portId} |
{ invoiceId, invoiceNumber, daysPastDue } |
Invoice became overdue |
2.6 Reminder & Calendar Events
| Event |
Room |
Payload |
Trigger |
reminder:created |
user:{assigneeId}, port:{portId} |
{ reminderId, title, assignedTo, dueAt } |
Reminder created |
reminder:updated |
user:{assigneeId} |
{ reminderId, changedFields } |
Reminder updated |
reminder:completed |
user:{assigneeId}, port:{portId} |
{ reminderId, title, completedBy } |
Reminder completed |
reminder:overdue |
user:{assigneeId} |
{ reminderId, title, dueAt } |
Reminder became overdue |
reminder:snoozed |
user:{assigneeId} |
{ reminderId, snoozedUntil } |
Reminder snoozed |
calendar:synced |
user:{userId} |
{ eventCount, lastSyncAt } |
Google Calendar sync completed |
calendar:disconnected |
user:{userId} |
{ reason } |
Google Calendar token revoked/expired |
2.7 Notification Events
| Event |
Room |
Payload |
Trigger |
notification:new |
user:{userId} |
{ notificationId, type, title, description, link } |
New notification created |
notification:unreadCount |
user:{userId} |
{ count } |
Unread count changed |
2.8 System Events
| Event |
Room |
Payload |
Trigger |
system:alert |
global |
{ alertType, message, severity } |
System alert triggered |
system:jobFailed |
global |
{ queueName, jobId, error } |
Background job failure |
registration:new |
port:{portId} |
{ clientId, interestId, clientName, berthNumber } |
New website registration |
2.9 Recommendation & Misc Events
| Event |
Room |
Payload |
Trigger |
interest:recommendationsGenerated |
interest:{interestId}, port:{portId} |
{ interestId, count, topBerthId } |
AI berth recommendations generated |
interest:recommendationAdded |
interest:{interestId} |
{ interestId, berthId, source, matchScore } |
Manual recommendation added |
interest:leadCategoryChanged |
port:{portId}, interest:{interestId} |
{ interestId, oldCategory, newCategory, auto } |
Lead category changed (including auto-promotion) |
file:uploaded |
client:{clientId}, port:{portId} |
{ fileId, filename, clientId, category } |
File uploaded |
file:deleted |
client:{clientId} |
{ fileId, filename } |
File deleted |
3. Background Jobs (BullMQ)
3.1 Queue Structure
| Queue |
Concurrency |
Description |
email |
5 |
All email sending (transactional + user) |
documents |
3 |
Documenso API interactions (create, poll, download) |
notifications |
10 |
In-app notification creation and email notification sending |
import |
1 |
Data import jobs (CSV, Excel, berth specs) |
export |
2 |
Data export jobs (CSV, PDF, parent company report) |
reports |
1 |
Scheduled report generation |
webhooks |
5 |
Outbound webhook delivery |
maintenance |
1 |
System maintenance (backups, cleanup, rate refresh) |
ai |
2 |
AI-assisted tasks (receipt scanning, berth spec parsing, file migration) |
bulk |
2 |
Bulk operations (status change, tag, delete) |
3.2 Recurring Jobs
| Job |
Queue |
Schedule |
Description |
signature-poll |
documents |
Every 6 hours |
Rare fallback poll of Documenso for interests with pending signatures. Primary mechanism is Documenso webhooks (instant). This job is a safety net only — catches any edge case where a webhook was lost. |
reminder-check |
notifications |
Every hour |
Check interests with reminder_enabled for inactivity → create follow-up reminders |
reminder-overdue-check |
notifications |
Every 15 minutes |
Check for reminders past due_at → create overdue notifications |
calendar-sync |
maintenance |
Every 30 minutes |
Background poll for all connected Google Calendar users: fetch upcoming events (14 days), upsert into google_calendar_cache, detect moved/deleted CRM-pushed events. Note: additional event-driven syncs fire on user login and on navigation to calendar-displaying pages (if last sync > 5 min ago) |
invoice-overdue-check |
notifications |
Daily at 08:00 |
Check for overdue invoices → update status, create notifications |
tenure-expiry-check |
notifications |
Daily at 08:00 |
Check for berth tenure approaching expiry → create notifications |
currency-refresh |
maintenance |
Every 6 hours |
Refresh exchange rates from Frankfurter API |
database-backup |
maintenance |
Daily at 02:00 |
pg_dump → store in MinIO |
backup-cleanup |
maintenance |
Weekly (Sunday 03:00) |
Delete backups older than retention period (default: 30 days, configurable in system_settings) |
session-cleanup |
maintenance |
Daily at 04:00 |
Remove expired Better Auth sessions |
report-scheduler |
reports |
Every minute |
Check scheduled_reports for reports due to run → enqueue generation |
notification-digest |
email |
Configurable per user |
Send batched email digest of unread notifications |
temp-file-cleanup |
maintenance |
Daily at 05:00 |
Clean up temporary files (report PDFs > 7 days, orphaned uploads) |
form-expiry-check |
maintenance |
Hourly |
Mark expired form submissions as expired (BR-130) |
3.3 Job Retry & Error Handling
Default retry configuration:
- Attempts: 3
- Backoff: Exponential (1s, 10s, 100s)
- Dead letter: After all retries exhausted, move to dead letter queue
- Alert: Dead letter creates system alert notification for super admin
Per-queue overrides:
| Queue |
Max Attempts |
Notes |
email |
5 |
Email delivery can have transient failures |
webhooks |
3 |
Standard exponential backoff |
documents |
5 |
Documenso API can be temporarily unavailable |
import |
1 |
Imports are idempotent — user retries manually |
maintenance |
3 |
Backup failures alert immediately |
3.4 Job Priority
BullMQ supports job priority (lower number = higher priority):
| Priority |
Jobs |
| 1 (highest) |
Password reset emails, system alerts |
| 2 |
Signature webhooks, EOI reminders, reminder overdue checks |
| 3 (default) |
Standard emails, notifications, webhook deliveries, calendar sync |
| 4 |
Export jobs, report generation |
| 5 (lowest) |
Data imports, AI tasks, backup cleanup |
4. Event Flow Examples
4.1 Website Interest Registration
4.2 EOI Signing Flow
4.3 Berth Link with Auto-Status
5. Redis Usage Summary
| Purpose |
Redis Feature |
Details |
| BullMQ job queues |
Lists + Sorted Sets |
10 queues, recurring + one-off jobs |
| Socket.io adapter |
Pub/Sub |
Cross-instance message relay |
| Session cache |
Key-Value |
Better Auth session acceleration |
| Rate limiting |
Sorted Sets |
Per-IP and per-user request counting |
| Application cache |
Key-Value with TTL |
Exchange rates (6h), dashboard aggregates (5m), search results (1m) |
| Deduplication locks |
Key-Value with TTL |
Webhook dedup, notification cooldowns |