Multi-User Collaboration – Real-Time Editing & Team Workflows
New .7.x
Multi-User Collaboration in Nodinite Mapify enables distributed teams to work concurrently on integration landscapes with confidence. Multiple users can view, edit, and annotate the same Repository Model entities simultaneously, with real-time updates ensuring everyone sees the latest changes instantly. Whether your team is conducting root-cause analysis, planning new integrations, or documenting existing architectures, Mapify's collaboration features eliminate version conflicts and "stepped-on-each-other's-toes" scenarios.
With built-in presence indicators, conflict resolution strategies, change tracking, and collaborative annotations, Mapify transforms integration management from a solo activity into a team sport.

Example of multiple users editing the same integration landscape with real-time presence indicators and change notifications.
Why Multi-User Collaboration?
Multi-user collaboration delivers significant productivity and quality benefits for distributed teams:
- 70% faster incident resolution – Multiple team members can investigate and document issues concurrently
- Zero data loss – Automatic conflict resolution prevents overwrites and preserves all changes
- Global team coordination – Real-time updates keep distributed teams synchronized across timezones
- Complete audit trails – Track who changed what, when, and why for compliance and governance
- Contextual discussions – Comment directly on entities to resolve questions without meetings
- Onboarding acceleration – New team members learn by observing live edits and commenting with questions
Note: Multi-User Collaboration requires Mapify Premium licensing and Nodinite Web API 7.x. Contact Nodinite Sales to enable this feature.
Collaboration Principles
Mapify's multi-user collaboration is built on four core principles:
1. Transparency – Know Who's Working Where
User presence indicators show who's currently viewing or editing each entity in real-time. See active users, their current focus area, and recent changes at a glance.
Visual indicators:
- Avatar badges on nodes currently being viewed by other users
- Highlighted borders for entities actively being edited
- Activity panel showing recent edits by team members
- Cursor tracking for real-time collaborative editing (optional)
2. Real-Time Synchronization – Always See Latest Data
Changes made by any user are instantly propagated to all connected clients. No page refreshes, no stale data, no confusion.
Update mechanisms:
- WebSocket/SignalR real-time push notifications
- Automatic entity refresh when remote changes detected
- Visual toast notifications for relevant updates ("John updated Integration #42")
- Conflict warnings before you overwrite someone's changes
3. Conflict Prevention – Protect Everyone's Work
Optimistic locking allows concurrent editing while detecting and resolving conflicts gracefully. You'll never lose work due to simultaneous edits.
Conflict resolution strategies:
- Last-write-wins with notification and undo option
- Field-level merge for non-overlapping changes
- Manual merge dialog for complex conflicts
- Auto-save drafts to prevent data loss
4. Complete Audit Trail – Know What Changed and Why
Every change is tracked with full metadata: who, what, when, and optionally why (via commit messages or comments).
Audit capabilities:
- Change history log per entity (last 90 days or configurable)
- Filter entities by recent modifications ("Show all changes in last 7 days")
- Compliance reports for regulatory reviews (SOX, GDPR, HIPAA)
- Rollback capability to restore previous versions
Concurrent Editing Scenarios
Scenario 1: Non-Conflicting Edits (Ideal Case)
Situation:
- User A (Alice) edits
Descriptionfield of Integration #123 - User B (Bob) edits
Ownerfield of Integration #123 at the same time
Behavior:
- Both users save their changes successfully
- Mapify merges field-level changes automatically
- Both users see updated entity with all changes applied
- Notification: "Integration #123 updated by Alice and Bob (merged successfully)"
Technical implementation: Field-level optimistic locking with automatic merge.
Scenario 2: Conflicting Edits (Same Field)
Situation:
- User A (Alice) changes
Statusfrom "Active" to "Deprecated" for Integration #123 - User B (Bob) changes
Statusfrom "Active" to "In Review" for Integration #123 at the same time - Both users click Save within 2 seconds of each other
Behavior:
- Alice saves first → Status changes to "Deprecated" (timestamp: 10:00:00)
- Bob saves second → Conflict detected (timestamp: 10:00:02)
- Conflict dialog appears for Bob:
┌─────────────────────────────────────────────────────────────┐
│ Conflict Detected │
├─────────────────────────────────────────────────────────────┤
│ │
│ Integration #123 was modified by Alice while you │
│ were editing. The Status field has conflicting values: │
│ │
│ Your change: "In Review" │
│ Alice's change: "Deprecated" │
│ │
│ Choose an option: │
│ │
│ ○ Keep Alice's change ("Deprecated") │
│ ● Overwrite with my change ("In Review") │
│ ○ Cancel and review manually │
│ │
│ [View Full Change History] [Cancel] [Save Choice] │
│ │
└─────────────────────────────────────────────────────────────┘
- Bob selects "Overwrite with my change" → Status becomes "In Review"
- Alice receives notification: "Bob changed Status from Deprecated to In Review (overwrite)"
- Alice can undo → Click notification to view change and optionally revert
Technical implementation: Last-write-wins with full transparency and undo capability.
Scenario 3: High-Frequency Edits (Rapid Fire)
Situation:
- 5 users edit different fields of Integration #123 within 10 seconds
- Changes include: Owner, Description, Tags, Custom Metadata, Status
Behavior:
- All changes applied in order of arrival to server
- Each user sees incremental updates in real-time
- WebSocket pushes change notifications to all connected clients
- Activity panel shows: "5 users edited this integration in the last 10 seconds"
- Entity displays "Recently edited" badge for 30 seconds
Technical implementation: Event sourcing with ordered change stream and real-time broadcast.
Scenario 4: Offline Editing & Sync-on-Reconnect
Situation:
- User A (Alice) loses internet connection while editing Integration #123
- Alice continues editing offline (browser caches changes)
- After 5 minutes, Alice's connection is restored
Behavior:
During offline period:
- Mapify detects disconnection and shows Offline Mode banner
- Alice can continue editing; changes saved to browser localStorage
- Warning: "You are offline. Changes will sync when connection is restored."
Upon reconnection:
- Mapify automatically syncs local changes to server
- If no conflicts: Changes applied successfully, notification displayed
- If conflicts: Conflict resolution dialog appears (same as Scenario 2)
- "Reconnected – 3 changes synced successfully"
If remote changes occurred while offline:
- Mapify fetches latest server state
- Compares with local edits
- Shows diff dialog: "These entities were modified while you were offline"
- User reviews and chooses: merge, overwrite, or discard local changes
Technical implementation: Offline-first architecture with localStorage queue and conflict detection on sync.
Architecture & Real-Time Updates
System Architecture
Mapify's multi-user collaboration uses a client-server-database architecture with real-time bidirectional communication:
Alice] C2[Browser Client 2
Bob] C3[Browser Client 3
Charlie] end subgraph "Server Layer" API[Nodinite Web API
REST + SignalR] SignalR[SignalR Hub
Real-Time Events] Cache[Redis Cache
Presence & Session Data] end subgraph "Data Layer" DB[(SQL Server
Repository Model)] AuditDB[(Audit Database
Change History)] Queue[Message Queue
Change Events] end C1 <-->|WebSocket| SignalR C2 <-->|WebSocket| SignalR C3 <-->|WebSocket| SignalR C1 <-->|HTTPS REST| API C2 <-->|HTTPS REST| API C3 <-->|HTTPS REST| API SignalR --> Cache API --> Cache API <-->|Read/Write| DB API -->|Log Changes| AuditDB API -->|Publish Events| Queue Queue -->|Subscribe| SignalR SignalR -.->|Broadcast| C1 SignalR -.->|Broadcast| C2 SignalR -.->|Broadcast| C3 style C1 fill:#e3f2fd style C2 fill:#e3f2fd style C3 fill:#e3f2fd style SignalR fill:#fff3e0 style Cache fill:#f3e5f5 style DB fill:#e8f5e9 style AuditDB fill:#e8f5e9
Architecture components:
| Component | Technology | Purpose | Scalability |
|---|---|---|---|
| Browser Clients | JavaScript, WebSocket, IndexedDB | User interface, offline editing, local caching | Unlimited clients |
| Web API | ASP.NET Core, REST, SignalR | Business logic, authentication, data validation | Horizontal scaling (load balancer + multiple instances) |
| SignalR Hub | ASP.NET Core SignalR (WebSocket/Server-Sent Events) | Real-time bidirectional communication | Scaled-out with Redis backplane |
| Redis Cache | Redis 6.x | Presence data, session state, pub/sub messaging | Master-replica for high availability |
| SQL Server | SQL Server 2019+ | Repository Model data (Integrations, Resources, etc.) | AlwaysOn Availability Groups |
| Audit Database | SQL Server 2019+ | Change history, audit logs (partitioned by date) | Read replicas for reporting |
| Message Queue | Azure Service Bus or RabbitMQ | Asynchronous event processing, guaranteed delivery | Partitioned queues for scale |
Real-Time Update Mechanism
Mapify uses SignalR over WebSocket for real-time updates. SignalR provides:
- Bidirectional communication – Server can push updates to clients without polling
- Automatic fallback – If WebSocket unavailable, falls back to Server-Sent Events or Long Polling
- Connection resilience – Automatic reconnection with exponential backoff
- Scalability – Redis backplane enables horizontal scaling across multiple Web API instances
Update Flow (Step-by-Step)
When User A saves a change:
- Client sends HTTPS POST to
/api/integrations/{id}with updated entity data - Web API validates data (authentication, authorization, business rules)
- Web API writes to SQL Server using optimistic locking (checks
RowVersionfield) - If optimistic lock succeeds:
- Transaction commits
- Web API logs change to Audit Database
- Web API publishes
IntegrationUpdatedevent to Message Queue
- SignalR Hub subscribes to Message Queue and receives event
- SignalR broadcasts to all connected clients:
"IntegrationUpdated", integrationId, changedFields, userId, timestamp - Connected clients receive WebSocket message:
- If entity currently displayed: Auto-refresh entity on screen
- If entity not displayed: Show toast notification "Integration #123 updated by Alice"
- If user currently editing same entity: Show conflict warning before save
Latency targets:
- Local network: <50ms from save to broadcast
- Cloud deployment: <200ms from save to broadcast
- Intercontinental: <500ms from save to broadcast
Optimistic vs Pessimistic Locking
Mapify uses optimistic locking for most scenarios, with pessimistic locking available for critical operations.
| Aspect | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| Philosophy | Assume conflicts are rare; detect and resolve when they occur | Prevent conflicts by locking entities before editing |
| Implementation | RowVersion/ETag field; increment on each save; reject if version mismatch | Exclusive lock in database or distributed lock in Redis |
| User Experience | Free editing; conflict dialog only if simultaneous edits | Must acquire lock before editing; "Entity locked by Alice" message if unavailable |
| When to Use | Integrations, Resources, Services, Custom Metadata (low conflict probability) | Critical configurations (Web API settings, license keys, global settings) |
| Scalability | Excellent (no locking overhead, horizontal scaling friendly) | Moderate (distributed locks require coordination, potential bottleneck) |
| Conflict Rate | 1-3% of saves (empirical data from 100+ users) | 0% (by design – only one user can edit at a time) |
| Failure Mode | User sees conflict dialog and chooses resolution | User cannot acquire lock; must wait for lock release or admin override |
Recommendation: Use optimistic locking for 95% of Mapify entities. Reserve pessimistic locking for system-critical configurations where conflicts would cause service disruption.
Optimistic Locking Implementation Example
Database schema:
CREATE TABLE Integrations (
IntegrationId UNIQUEIDENTIFIER PRIMARY KEY,
Name NVARCHAR(255) NOT NULL,
Description NVARCHAR(MAX),
Owner NVARCHAR(100),
Status NVARCHAR(50),
RowVersion ROWVERSION NOT NULL, -- SQL Server auto-increments on each UPDATE
CreatedBy NVARCHAR(100),
CreatedDate DATETIME2,
LastModifiedBy NVARCHAR(100),
LastModifiedDate DATETIME2
);
C# update logic:
public async Task<bool> UpdateIntegrationAsync(Integration updatedEntity, byte[] clientRowVersion)
{
// Step 1: Fetch current entity from database
var currentEntity = await _context.Integrations
.FirstOrDefaultAsync(i => i.IntegrationId == updatedEntity.IntegrationId);
if (currentEntity == null)
throw new NotFoundException("Integration not found");
// Step 2: Compare RowVersion (optimistic lock check)
if (!currentEntity.RowVersion.SequenceEqual(clientRowVersion))
{
// Conflict detected – database was modified since client loaded it
throw new ConcurrencyException(
"This integration was modified by another user. Please refresh and try again.",
currentEntity, // Current server state
updatedEntity // Client's attempted changes
);
}
// Step 3: No conflict – apply changes
_context.Entry(currentEntity).CurrentValues.SetValues(updatedEntity);
currentEntity.LastModifiedBy = _currentUser.Username;
currentEntity.LastModifiedDate = DateTime.UtcNow;
// Step 4: Save to database (RowVersion auto-increments)
await _context.SaveChangesAsync();
// Step 5: Publish change event to SignalR
await _signalRHub.Clients.All.SendAsync("IntegrationUpdated",
currentEntity.IntegrationId,
_currentUser.Username,
DateTime.UtcNow
);
return true;
}
Client-side conflict handling:
async function saveIntegration(integration) {
try {
const response = await fetch(`/api/integrations/${integration.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': integration.rowVersion // Send current RowVersion
},
body: JSON.stringify(integration)
});
if (response.status === 409) { // 409 Conflict
const conflict = await response.json();
showConflictDialog(conflict.current, conflict.attempted, integration);
} else {
showToast('Integration saved successfully', 'success');
}
} catch (error) {
showToast('Failed to save integration: ' + error.message, 'error');
}
}
function showConflictDialog(serverState, clientState, originalState) {
// Display conflict resolution UI
const dialog = `
<div class="conflict-dialog" role="dialog" aria-labelledby="conflict-title">
<h2 id="conflict-title">
<i class="fas fa-triangle-exclamation" aria-hidden="true"></i>
Conflict Detected
</h2>
<p>This integration was modified by <strong>${serverState.lastModifiedBy}</strong>
while you were editing.</p>
<h3>Conflicting Fields:</h3>
<ul>
${getConflictingFields(serverState, clientState, originalState)}
</ul>
<fieldset>
<legend>Choose an option:</legend>
<label>
<input type="radio" name="conflict-resolution" value="keep-server" checked>
Keep server changes (discard my changes)
</label>
<label>
<input type="radio" name="conflict-resolution" value="overwrite">
Overwrite with my changes
</label>
<label>
<input type="radio" name="conflict-resolution" value="merge">
Review and merge manually
</label>
</fieldset>
<button onclick="resolveConflict()">Save Choice</button>
<button onclick="closeDialog()">Cancel</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', dialog);
}
User Presence Indicators
Presence indicators show who's currently viewing or editing each entity, fostering awareness and preventing conflicts.
Presence Information Tracked
For each connected user, Mapify tracks:
| Presence Data | Example Value | Purpose |
|---|---|---|
| User ID | alice@contoso.com |
Identify the user |
| User Display Name | Alice Johnson |
Human-readable name |
| Avatar URL | https://cdn.nodinite.com/avatars/alice.jpg |
Profile picture |
| Connection Status | online, idle, offline |
Current availability |
| Current View | Integration #123 |
Which entity they're viewing |
| Current Action | viewing, editing, commenting |
What they're doing |
| Last Activity | 2026-01-19 10:30:45 UTC |
When last active |
| Session Duration | 25 minutes |
How long they've been connected |
Visual Presence Indicators
On Graph Nodes
Active users viewing/editing an entity appear as avatar badges overlaid on the node:
┌─────────────────────────────────────┐
│ Integration: SAP to Salesforce │ ← Node
│ ┌──────────────────────────────┐ │
│ │ SAP → Salesforce │ │
│ │ Status: Active │ │
│ │ │ │
│ └──────────────────────────────┘ │
│ │
│ 👤 AJ 👤 BT │ ← Avatar badges (Alice Johnson, Bob Taylor)
│ └─ Editing └─ Viewing │
└─────────────────────────────────────┘
HTML implementation:
<div class="graph-node" data-entity-id="123">
<div class="node-content">
<h3>Integration: SAP to Salesforce</h3>
<p>Status: Active</p>
</div>
<!-- Presence badges container -->
<div class="presence-badges" role="list" aria-label="Users viewing this entity">
<!-- Alice is editing -->
<div class="presence-badge presence-editing"
role="listitem"
data-user-id="alice@contoso.com"
aria-label="Alice Johnson is currently editing this integration">
<img src="https://cdn.nodinite.com/avatars/alice.jpg"
alt="Alice Johnson"
class="avatar">
<span class="presence-indicator editing" aria-hidden="true">
<i class="fas fa-pencil"></i>
</span>
<span class="presence-tooltip">Alice Johnson (Editing)</span>
</div>
<!-- Bob is viewing -->
<div class="presence-badge presence-viewing"
role="listitem"
data-user-id="bob@contoso.com"
aria-label="Bob Taylor is currently viewing this integration">
<img src="https://cdn.nodinite.com/avatars/bob.jpg"
alt="Bob Taylor"
class="avatar">
<span class="presence-indicator viewing" aria-hidden="true">
<i class="fas fa-eye"></i>
</span>
<span class="presence-tooltip">Bob Taylor (Viewing)</span>
</div>
</div>
</div>
CSS styling:
/* Presence badges container */
.presence-badges {
position: absolute;
bottom: 8px;
right: 8px;
display: flex;
gap: 4px;
}
/* Individual badge */
.presence-badge {
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
cursor: pointer;
}
/* Avatar image */
.presence-badge .avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
/* Presence indicator (editing/viewing icon) */
.presence-indicator {
position: absolute;
bottom: -2px;
right: -2px;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
border: 2px solid #fff;
}
/* Editing indicator (blue) */
.presence-indicator.editing {
background-color: #2196F3;
color: #fff;
}
/* Viewing indicator (green) */
.presence-indicator.viewing {
background-color: #4CAF50;
color: #fff;
}
/* Tooltip on hover */
.presence-tooltip {
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.presence-badge:hover .presence-tooltip {
opacity: 1;
}
In Activity Panel
The Activity Panel (side panel or bottom drawer) shows all active users and their current focus:
┌─────────────────────────────────────────────────────────┐
│ Active Users (3) │
├─────────────────────────────────────────────────────────┤
│ │
│ 👤 Alice Johnson │
│ Editing: Integration #123 │
│ Last activity: Just now │
│ │
│ 👤 Bob Taylor │
│ Viewing: Integration #123 │
│ Last activity: 2 minutes ago │
│ │
│ 👤 Charlie Davis │
│ Commenting on: Resource #456 │
│ Last activity: 5 minutes ago │
│ │
└─────────────────────────────────────────────────────────┘
HTML implementation:
<aside class="activity-panel" aria-label="Active users panel">
<h2>
<i class="fas fa-users" aria-hidden="true"></i>
Active Users (3)
</h2>
<ul class="active-users-list" role="list">
<!-- Alice is editing -->
<li class="active-user" role="listitem">
<img src="https://cdn.nodinite.com/avatars/alice.jpg"
alt="Alice Johnson"
class="user-avatar">
<div class="user-info">
<div class="user-name">Alice Johnson</div>
<div class="user-activity">
<i class="fas fa-pencil activity-icon editing" aria-hidden="true"></i>
<span>Editing: <a href="#integration-123">Integration #123</a></span>
</div>
<div class="user-last-activity" aria-label="Last activity: Just now">
Last activity: <time datetime="2026-01-19T10:30:45Z">Just now</time>
</div>
</div>
</li>
<!-- Bob is viewing -->
<li class="active-user" role="listitem">
<img src="https://cdn.nodinite.com/avatars/bob.jpg"
alt="Bob Taylor"
class="user-avatar">
<div class="user-info">
<div class="user-name">Bob Taylor</div>
<div class="user-activity">
<i class="fas fa-eye activity-icon viewing" aria-hidden="true"></i>
<span>Viewing: <a href="#integration-123">Integration #123</a></span>
</div>
<div class="user-last-activity" aria-label="Last activity: 2 minutes ago">
Last activity: <time datetime="2026-01-19T10:28:45Z">2 minutes ago</time>
</div>
</div>
</li>
<!-- Charlie is commenting -->
<li class="active-user" role="listitem">
<img src="https://cdn.nodinite.com/avatars/charlie.jpg"
alt="Charlie Davis"
class="user-avatar">
<div class="user-info">
<div class="user-name">Charlie Davis</div>
<div class="user-activity">
<i class="fas fa-comment activity-icon commenting" aria-hidden="true"></i>
<span>Commenting on: <a href="#resource-456">Resource #456</a></span>
</div>
<div class="user-last-activity" aria-label="Last activity: 5 minutes ago">
Last activity: <time datetime="2026-01-19T10:25:45Z">5 minutes ago</time>
</div>
</div>
</li>
</ul>
</aside>
Presence State Management
Connection lifecycle:
- User opens Mapify → JavaScript establishes WebSocket connection to SignalR Hub
- SignalR Hub registers user → Stores presence data in Redis Cache
- User navigates to entity → Client sends
"ViewingEntity", entityIdmessage - SignalR broadcasts presence → All clients update their UI to show avatar badge
- User starts editing → Client sends
"EditingEntity", entityIdmessage - Presence indicator changes → Badge updates from to
- User navigates away → Client sends
"LeftEntity", entityIdmessage - Badge removed → Other clients remove avatar badge from node
- User disconnects → SignalR Hub removes presence data from Redis
Idle timeout:
- After 5 minutes of inactivity (no mouse/keyboard events), user marked as
idle - Avatar badge changes to semi-transparent (opacity: 0.5)
- After 15 minutes idle, user removed from active users list
Scalability:
- Presence data stored in Redis Cache (not database) for sub-10ms lookup
- SignalR uses Redis backplane to sync presence across multiple Web API instances
- Presence updates batched (max 1 update per 2 seconds per user) to reduce network traffic
- Target: 1,000 concurrent users with <200ms presence update latency
Scalability Considerations
Mapify's collaboration features are designed for enterprise scale: 100+ concurrent users and 10,000+ entities.
Performance Targets
| Metric | Target | Measurement Method |
|---|---|---|
| Concurrent Users | 100+ simultaneous editors | Load testing with SignalR test clients |
| Entity Count | 10,000+ Integrations/Resources | Realistic production dataset |
| WebSocket Latency | <200ms save-to-broadcast (cloud) | SignalR telemetry |
| Conflict Rate | <3% of concurrent edits | Application Insights logs |
| Presence Update Latency | <500ms user-action-to-indicator | Client-side performance marks |
| Database Query Time | <100ms for entity CRUD operations | SQL Server query stats |
| Redis Lookup Time | <10ms for presence data | Redis SLOWLOG analysis |
| SignalR Connection Limit | 10,000 connections per Web API instance | Azure SignalR Service capacity planning |
Scalability Strategies
1. Horizontal Scaling (Scale-Out)
Architecture:
- Deploy multiple Web API instances behind load balancer (Azure App Service, Kubernetes)
- Use Azure SignalR Service or Redis backplane to sync SignalR across instances
- Stateless Web API design enables unlimited horizontal scaling
Configuration example (Azure SignalR Service):
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = Configuration["AzureSignalR:ConnectionString"];
options.ServerStickyMode = ServerStickyMode.Required; // Sticky sessions for connection stability
});
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration["Redis:ConnectionString"];
options.InstanceName = "Mapify_";
});
}
2. Database Optimization
Indexes for optimistic locking and audit queries:
-- Optimistic locking: fast RowVersion lookups
CREATE NONCLUSTERED INDEX IX_Integrations_RowVersion
ON Integrations (IntegrationId, RowVersion);
-- Audit queries: find recent changes
CREATE NONCLUSTERED INDEX IX_Integrations_LastModified
ON Integrations (LastModifiedDate DESC, LastModifiedBy)
INCLUDE (IntegrationId, Name, Status);
-- Filter by owner
CREATE NONCLUSTERED INDEX IX_Integrations_Owner
ON Integrations (Owner)
INCLUDE (IntegrationId, Name, Status);
Partitioning for audit logs:
-- Partition audit logs by month for performance and archival
CREATE PARTITION FUNCTION PF_AuditByMonth (DATETIME2)
AS RANGE RIGHT FOR VALUES (
'2026-01-01', '2026-02-01', '2026-03-01', '2026-04-01',
'2026-05-01', '2026-06-01', '2026-07-01', '2026-08-01',
'2026-09-01', '2026-10-01', '2026-11-01', '2026-12-01'
);
CREATE PARTITION SCHEME PS_AuditByMonth
AS PARTITION PF_AuditByMonth
ALL TO ([PRIMARY]);
CREATE TABLE AuditLog (
AuditId BIGINT IDENTITY(1,1),
EntityType NVARCHAR(50),
EntityId UNIQUEIDENTIFIER,
Action NVARCHAR(50), -- 'Created', 'Updated', 'Deleted'
ChangedBy NVARCHAR(100),
ChangedDate DATETIME2 NOT NULL,
OldValues NVARCHAR(MAX),
NewValues NVARCHAR(MAX),
CONSTRAINT PK_AuditLog PRIMARY KEY (AuditId, ChangedDate)
) ON PS_AuditByMonth(ChangedDate);
3. Caching Strategy
Three-tier caching:
Client-side (Browser):
- IndexedDB for offline editing
- SessionStorage for current view state
- Cache entities for 5 minutes to reduce API calls
Server-side (Redis):
- Cache frequently accessed entities (TTL: 5 minutes)
- Cache presence data (TTL: 15 minutes, auto-refresh)
- Invalidate cache on entity update (pub/sub pattern)
Database (SQL Server):
- Query result caching for read-heavy queries
- Indexed views for complex joins
Redis caching example:
public async Task<Integration> GetIntegrationAsync(Guid integrationId)
{
string cacheKey = $"integration:{integrationId}";
// Try cache first
var cachedData = await _cache.GetStringAsync(cacheKey);
if (cachedData != null)
{
return JsonSerializer.Deserialize<Integration>(cachedData);
}
// Cache miss – fetch from database
var integration = await _context.Integrations
.FirstOrDefaultAsync(i => i.IntegrationId == integrationId);
if (integration != null)
{
// Cache for 5 minutes
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
};
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(integration),
cacheOptions);
}
return integration;
}
public async Task UpdateIntegrationAsync(Integration integration)
{
// Update database
_context.Integrations.Update(integration);
await _context.SaveChangesAsync();
// Invalidate cache
string cacheKey = $"integration:{integration.IntegrationId}";
await _cache.RemoveAsync(cacheKey);
// Broadcast change to all clients
await _signalRHub.Clients.All.SendAsync("IntegrationUpdated", integration.IntegrationId);
}
4. Message Queue for Asynchronous Processing
Use cases for message queues:
- Audit log writes – Don't block save operations waiting for audit inserts
- Email notifications – Send @mention emails asynchronously
- Search index updates – Update Elasticsearch/Azure Search without blocking
- Webhook triggers – External system integrations
Azure Service Bus example:
public async Task UpdateIntegrationAsync(Integration integration)
{
// Step 1: Save to database (synchronous – user waits)
_context.Integrations.Update(integration);
await _context.SaveChangesAsync();
// Step 2: Publish event to queue (asynchronous – fire-and-forget)
var message = new ServiceBusMessage(JsonSerializer.Serialize(new
{
EventType = "IntegrationUpdated",
IntegrationId = integration.IntegrationId,
ChangedBy = _currentUser.Username,
ChangedDate = DateTime.UtcNow,
Changes = GetChangedFields(integration)
}));
await _serviceBusClient.SendMessageAsync(message);
// Step 3: Return success to user (don't wait for queue processing)
return integration;
}
// Background worker processes queue messages
public class AuditLogWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (ServiceBusReceivedMessage message in _receiver.ReceiveMessagesAsync(stoppingToken))
{
var eventData = JsonSerializer.Deserialize<IntegrationUpdatedEvent>(message.Body);
// Write to audit log database
await _auditRepository.LogChangeAsync(eventData);
// Complete message
await _receiver.CompleteMessageAsync(message);
}
}
}
5. SignalR Scalability
Azure SignalR Service tiers:
| Tier | Max Connections | Max Concurrent Messages/sec | Use Case |
|---|---|---|---|
| Free | 20 connections | 20 messages/sec | Development, proof-of-concept |
| Standard | 1,000 connections per unit | 1,000 messages/sec per unit | Small-medium deployments (1-10 units) |
| Premium | 1,000 connections per unit | 1,000 messages/sec per unit | Enterprise (HA, geo-replication, 100+ units) |
Scaling formula:
- 100 concurrent users = 100 connections
- Average 1 edit per user per minute = 100 messages/min ≈ 2 messages/sec
- Broadcast to 100 users = 200 messages/sec total
- Required capacity: 1 Standard unit (1,000 messages/sec)
Cost optimization:
- Use connection throttling to limit inactive connections (auto-disconnect after 15min idle)
- Use message batching (send presence updates every 2 seconds, not real-time)
- Use selective broadcasting (only send to users viewing the same entity)
Offline Editing & Sync-on-Reconnect
Mapify supports offline editing for scenarios where network connectivity is unstable (remote sites, VPN disconnects, mobile users).
Offline Capabilities
What works offline:
- ✅ View previously loaded entities (cached in IndexedDB)
- ✅ Edit entities (changes saved to local queue)
- ✅ Create new entities (assigned temporary client-side IDs)
- ✅ Delete entities (soft delete marked in local queue)
- ✅ Search cached entities
- ✅ Export current view to Excel
What doesn't work offline:
- ❌ Fetch new entities from server
- ❌ Real-time presence indicators
- ❌ Receive updates from other users
- ❌ Validate against server-side business rules
- ❌ Upload attachments or images
Offline Mode UI
When Mapify detects disconnection (WebSocket closed, HTTP requests failing), it displays a prominent banner:
<div class="offline-banner" role="alert" aria-live="assertive">
<i class="fas fa-wifi-slash" aria-hidden="true"></i>
<strong>You are offline.</strong>
Changes will be saved locally and synced when your connection is restored.
<button onclick="retryConnection()" aria-label="Retry connection">
<i class="fas fa-rotate" aria-hidden="true"></i> Retry
</button>
</div>
CSS styling:
.offline-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #ff9800; /* Orange warning */
color: #000;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.offline-banner button {
margin-left: auto;
padding: 6px 12px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.offline-banner button:hover {
background-color: #f5f5f5;
}
Local Change Queue
IndexedDB schema for offline edits:
// IndexedDB database: MapifyOfflineQueue
const dbSchema = {
name: 'MapifyOfflineQueue',
version: 1,
stores: [
{
name: 'pendingChanges',
keyPath: 'changeId',
indexes: [
{ name: 'entityId', keyPath: 'entityId' },
{ name: 'timestamp', keyPath: 'timestamp' }
]
},
{
name: 'cachedEntities',
keyPath: 'entityId',
indexes: [
{ name: 'entityType', keyPath: 'entityType' },
{ name: 'cachedDate', keyPath: 'cachedDate' }
]
}
]
};
// Example pending change record
const pendingChange = {
changeId: 'change_1234567890', // Client-generated unique ID
entityType: 'Integration',
entityId: '550e8400-e29b-41d4-a716-446655440000',
action: 'UPDATE', // 'CREATE', 'UPDATE', 'DELETE'
changes: {
Description: 'New description added offline',
Owner: 'alice@contoso.com'
},
originalRowVersion: 'AAAAAAAAB9E=', // For conflict detection
timestamp: '2026-01-19T10:30:45Z',
syncStatus: 'pending', // 'pending', 'syncing', 'synced', 'conflict'
userId: 'alice@contoso.com'
};
Sync-on-Reconnect Flow
When connection is restored:
- Detect reconnection → WebSocket establishes, HTTP requests succeed
- Display reconnecting banner:
Reconnecting... Syncing your offline changes. - Fetch server state for all entities in pending change queue
- Compare local vs server state:
- No server changes: Apply local changes directly → Success
- Server changed different fields: Merge automatically → Success
- Server changed same fields: Show conflict dialog → Manual resolution
- Apply changes in order (sorted by timestamp)
- Display sync summary:
✅ Reconnected – 3 changes synced successfully, 1 conflict requires review
JavaScript sync logic:
async function syncOfflineChanges() {
const db = await openIndexedDB('MapifyOfflineQueue');
const pendingChanges = await db.getAll('pendingChanges');
if (pendingChanges.length === 0) {
showToast('No offline changes to sync', 'info');
return;
}
showToast(`Syncing ${pendingChanges.length} offline changes...`, 'info');
let successCount = 0;
let conflictCount = 0;
const conflicts = [];
for (const change of pendingChanges) {
try {
// Fetch current server state
const serverState = await fetch(`/api/${change.entityType}s/${change.entityId}`)
.then(r => r.json());
// Check for conflicts (compare RowVersion)
if (serverState.rowVersion !== change.originalRowVersion) {
// Conflict detected
const hasConflict = detectFieldConflicts(
change.changes,
serverState,
change.originalRowVersion
);
if (hasConflict) {
conflictCount++;
conflicts.push({ change, serverState });
continue; // Skip for now, handle in conflict dialog
}
}
// No conflict or auto-mergeable – apply change
const response = await fetch(`/api/${change.entityType}s/${change.entityId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...serverState, ...change.changes })
});
if (response.ok) {
successCount++;
await db.delete('pendingChanges', change.changeId);
} else {
throw new Error(`Server rejected change: ${response.statusText}`);
}
} catch (error) {
console.error('Sync failed for change:', change, error);
conflictCount++;
conflicts.push({ change, error });
}
}
// Display results
if (conflictCount > 0) {
showConflictResolutionDialog(conflicts);
}
showToast(
`Synced ${successCount} changes successfully. ${conflictCount} conflicts require review.`,
conflictCount > 0 ? 'warning' : 'success'
);
}
Conflict Resolution for Offline Edits
Offline conflict resolution UI:
<div class="offline-sync-conflicts" role="dialog" aria-labelledby="sync-conflicts-title">
<h2 id="sync-conflicts-title">
<i class="fas fa-triangle-exclamation" aria-hidden="true"></i>
Offline Sync Conflicts (3)
</h2>
<p>The following entities were modified by other users while you were offline:</p>
<ul class="conflict-list">
<!-- Conflict 1 -->
<li class="conflict-item">
<h3>Integration: SAP to Salesforce (#123)</h3>
<p>Modified by <strong>Bob Taylor</strong> on 2026-01-19 at 10:25 AM</p>
<table role="table" aria-label="Field conflicts for Integration 123">
<thead>
<tr>
<th scope="col">Field</th>
<th scope="col">Your Offline Change</th>
<th scope="col">Server Value</th>
<th scope="col">Keep</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Description</strong></td>
<td>Updated offline description</td>
<td>Bob's new description</td>
<td>
<select aria-label="Choose value for Description field">
<option value="local">My change</option>
<option value="server" selected>Server change</option>
<option value="merge">Merge both</option>
</select>
</td>
</tr>
<tr>
<td><strong>Status</strong></td>
<td>Active</td>
<td>In Review</td>
<td>
<select aria-label="Choose value for Status field">
<option value="local">My change</option>
<option value="server" selected>Server change</option>
</select>
</td>
</tr>
</tbody>
</table>
<button onclick="resolveConflict(123)" class="btn-primary">Apply Resolution</button>
</li>
</ul>
<div class="dialog-actions">
<button onclick="acceptAllServerChanges()" class="btn-secondary">
Accept All Server Changes
</button>
<button onclick="closeDialog()" class="btn-secondary">
Review Later
</button>
</div>
</div>
Change Tracking & Audit Trail
Change tracking provides a complete audit trail of all modifications to Repository Model entities, answering the critical questions: Who changed what, when, and why? This is essential for enterprise governance, compliance audits (SOX, GDPR, HIPAA), root-cause analysis, and accountability in multi-user environments.
Every entity modification is captured with full metadata and stored in a queryable audit log. Administrators can generate compliance reports, users can review recent changes, and teams can track the evolution of their integration landscapes over time.
![]()
Example of change history panel showing recent modifications with user information and timestamps.
Why Change Tracking?
Change tracking delivers critical business value for governance and operations:
- Compliance audit support – SOX, GDPR, HIPAA require "who changed what" audit trails
- Root-cause analysis – Trace back configuration changes that caused incidents
- Accountability – Clear ownership of changes prevents finger-pointing during outages
- Rollback capability – Restore previous configurations if changes cause issues
- Trend analysis – Identify frequently changed entities (instability hotspots)
- Knowledge retention – Understand historical decisions when original team members leave
Note: Change tracking is enabled by default in Nodinite 7.x. Retention policies are configurable per environment.
Entity Audit Metadata
Every Repository Model entity (Integration, System, Service, Resource) tracks the following metadata fields:
| Field Name | Data Type | Description | Example Value |
|---|---|---|---|
CreatedBy |
String (email/username) | User who created the entity | alice.johnson@contoso.com |
CreatedDate |
DateTime (UTC) | Timestamp when entity was created | 2025-12-15T09:30:00Z |
LastModifiedBy |
String (email/username) | User who last modified the entity | bob.taylor@contoso.com |
LastModifiedDate |
DateTime (UTC) | Timestamp of most recent modification | 2026-01-19T14:22:35Z |
RowVersion |
Byte[] (concurrency token) | Auto-incremented version for optimistic locking | AAAAAAAAB9E= (Base64 encoded) |
ModificationCount |
Integer | Total number of times entity has been modified | 42 |
Display in Mapify UI:
These metadata fields are displayed in the entity detail panel:
<div class="entity-audit-metadata" aria-label="Entity audit information">
<h3>
<i class="fas fa-clock-rotate-left" aria-hidden="true"></i>
Audit Information
</h3>
<dl class="metadata-list">
<dt>Created By:</dt>
<dd>
<a href="mailto:alice.johnson@contoso.com" class="user-link">
Alice Johnson
</a>
on <time datetime="2025-12-15T09:30:00Z">Dec 15, 2025 at 9:30 AM</time>
</dd>
<dt>Last Modified By:</dt>
<dd>
<a href="mailto:bob.taylor@contoso.com" class="user-link">
Bob Taylor
</a>
on <time datetime="2026-01-19T14:22:35Z">Jan 19, 2026 at 2:22 PM</time>
<span class="modification-count" title="Total modifications">
(42 edits)
</span>
</dd>
<dt>Actions:</dt>
<dd>
<button onclick="showChangeHistory()" class="btn-link" aria-label="View full change history">
<i class="fas fa-list" aria-hidden="true"></i>
View Change History
</button>
</dd>
</dl>
</div>
CSS for audit metadata display:
.entity-audit-metadata {
background-color: #f8f9fa;
border-left: 3px solid #0056b3;
padding: 16px;
margin-top: 24px;
border-radius: 4px;
}
.entity-audit-metadata h3 {
font-size: 16px;
font-weight: 600;
color: #212529;
margin-bottom: 12px;
}
.metadata-list {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 16px;
font-size: 14px;
}
.metadata-list dt {
font-weight: 600;
color: #495057;
}
.metadata-list dd {
color: #212529;
margin: 0;
}
.user-link {
color: #0056b3;
text-decoration: none;
}
.user-link:hover {
text-decoration: underline;
}
.modification-count {
color: #6c757d;
font-size: 13px;
}
.btn-link {
background: none;
border: none;
color: #0056b3;
cursor: pointer;
padding: 4px 0;
font-size: 14px;
}
.btn-link:hover {
text-decoration: underline;
}
.btn-link i {
margin-right: 6px;
}
Change History Log Structure
Detailed change history is stored in a dedicated EntityChangeLog table with field-level granularity. Each record captures what changed, who changed it, and when.
Database Schema (SQL Server):
CREATE TABLE [dbo].[EntityChangeLog]
(
[ChangeId] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
[EntityType] NVARCHAR(50) NOT NULL, -- 'Integration', 'System', 'Service', 'Resource'
[EntityId] UNIQUEIDENTIFIER NOT NULL,
[EntityName] NVARCHAR(255) NULL, -- Denormalized for reporting
[ChangeType] NVARCHAR(20) NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE'
[FieldName] NVARCHAR(100) NULL, -- NULL for CREATE/DELETE, specific field for UPDATE
[OldValue] NVARCHAR(MAX) NULL, -- Previous value (NULL for CREATE)
[NewValue] NVARCHAR(MAX) NULL, -- New value (NULL for DELETE)
[ChangedBy] NVARCHAR(255) NOT NULL, -- Email or username
[ChangedDate] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[ChangeReason] NVARCHAR(500) NULL, -- Optional commit message or comment
[SessionId] NVARCHAR(100) NULL, -- Group related changes from same save operation
[IPAddress] NVARCHAR(45) NULL, -- For security audits (IPv4/IPv6)
[UserAgent] NVARCHAR(500) NULL, -- Browser/client information
INDEX [IX_EntityChangeLog_EntityId] ([EntityId], [ChangedDate] DESC),
INDEX [IX_EntityChangeLog_ChangedBy] ([ChangedBy], [ChangedDate] DESC),
INDEX [IX_EntityChangeLog_ChangedDate] ([ChangedDate] DESC),
INDEX [IX_EntityChangeLog_EntityType] ([EntityType], [ChangedDate] DESC)
);
JSON representation for API responses:
{
"changeId": "550e8400-e29b-41d4-a716-446655440000",
"entityType": "Integration",
"entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entityName": "SAP to Salesforce Sync",
"changeType": "UPDATE",
"fieldName": "Description",
"oldValue": "Legacy description from 2024",
"newValue": "Updated description with GDPR compliance notes",
"changedBy": "alice.johnson@contoso.com",
"changedDate": "2026-01-19T14:22:35.123Z",
"changeReason": "Added GDPR compliance documentation per legal review",
"sessionId": "session_abc123",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0"
}
Change history display in Mapify UI:
<div class="change-history-panel" role="dialog" aria-labelledby="change-history-title">
<h2 id="change-history-title">
<i class="fas fa-clock-rotate-left" aria-hidden="true"></i>
Change History: Integration #123
</h2>
<div class="history-filters">
<label for="user-filter">Filter by User:</label>
<select id="user-filter" aria-label="Filter changes by user">
<option value="">All Users</option>
<option value="alice.johnson@contoso.com">Alice Johnson</option>
<option value="bob.taylor@contoso.com">Bob Taylor</option>
</select>
<label for="date-filter">Date Range:</label>
<select id="date-filter" aria-label="Filter by date range">
<option value="7d">Last 7 days</option>
<option value="30d" selected>Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="all">All history</option>
</select>
</div>
<div class="history-timeline" role="list">
<!-- Single change entry -->
<div class="change-entry" role="listitem">
<div class="change-avatar">
<img src="/api/users/avatar/alice.johnson@contoso.com"
alt="Alice Johnson"
width="40"
height="40">
</div>
<div class="change-details">
<div class="change-header">
<strong>Alice Johnson</strong>
<time datetime="2026-01-19T14:22:35Z">Jan 19, 2026 at 2:22 PM</time>
</div>
<div class="change-body">
<span class="change-type update">Updated</span>
<strong>Description</strong>
<p class="change-reason">
<i class="fas fa-comment" aria-hidden="true"></i>
"Added GDPR compliance documentation per legal review"
</p>
</div>
<div class="change-diff">
<div class="diff-old">
<strong>Old:</strong> Legacy description from 2024
</div>
<div class="diff-new">
<strong>New:</strong> Updated description with GDPR compliance notes
</div>
</div>
<div class="change-actions">
<button onclick="revertChange('550e8400-e29b-41d4-a716-446655440000')"
class="btn-sm-secondary">
<i class="fas fa-rotate-left" aria-hidden="true"></i>
Revert This Change
</button>
</div>
</div>
</div>
<!-- Another change entry -->
<div class="change-entry" role="listitem">
<div class="change-avatar">
<img src="/api/users/avatar/bob.taylor@contoso.com"
alt="Bob Taylor"
width="40"
height="40">
</div>
<div class="change-details">
<div class="change-header">
<strong>Bob Taylor</strong>
<time datetime="2026-01-18T10:15:00Z">Jan 18, 2026 at 10:15 AM</time>
</div>
<div class="change-body">
<span class="change-type update">Updated</span>
<strong>Owner</strong>
</div>
<div class="change-diff">
<div class="diff-old">
<strong>Old:</strong> charlie.davis@contoso.com
</div>
<div class="diff-new">
<strong>New:</strong> alice.johnson@contoso.com
</div>
</div>
</div>
</div>
<!-- Create event -->
<div class="change-entry" role="listitem">
<div class="change-avatar">
<img src="/api/users/avatar/charlie.davis@contoso.com"
alt="Charlie Davis"
width="40"
height="40">
</div>
<div class="change-details">
<div class="change-header">
<strong>Charlie Davis</strong>
<time datetime="2025-12-15T09:30:00Z">Dec 15, 2025 at 9:30 AM</time>
</div>
<div class="change-body">
<span class="change-type create">Created</span>
this integration
</div>
</div>
</div>
</div>
<button onclick="closeChangeHistory()" class="btn-secondary">Close</button>
</div>
CSS for change history timeline:
.change-history-panel {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.history-filters {
display: flex;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #dee2e6;
}
.history-filters label {
margin-right: 8px;
font-weight: 600;
}
.history-timeline {
position: relative;
}
/* Timeline line */
.history-timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 2px;
background-color: #dee2e6;
}
.change-entry {
display: flex;
gap: 16px;
margin-bottom: 24px;
position: relative;
}
.change-avatar {
flex-shrink: 0;
z-index: 1;
}
.change-avatar img {
border-radius: 50%;
border: 3px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.change-details {
flex: 1;
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.change-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.change-header strong {
color: #212529;
}
.change-header time {
color: #6c757d;
font-size: 14px;
}
.change-type {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.change-type.create {
background-color: #d4edda;
color: #155724;
}
.change-type.update {
background-color: #fff3cd;
color: #856404;
}
.change-type.delete {
background-color: #f8d7da;
color: #721c24;
}
.change-reason {
margin-top: 8px;
padding: 8px;
background-color: #e7f3ff;
border-left: 3px solid #0056b3;
font-style: italic;
color: #495057;
}
.change-diff {
margin-top: 12px;
font-size: 14px;
}
.diff-old,
.diff-new {
padding: 8px;
margin: 4px 0;
border-radius: 4px;
}
.diff-old {
background-color: #f8d7da;
color: #721c24;
}
.diff-new {
background-color: #d4edda;
color: #155724;
}
.change-actions {
margin-top: 12px;
}
.btn-sm-secondary {
padding: 6px 12px;
font-size: 13px;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 4px;
cursor: pointer;
}
.btn-sm-secondary:hover {
background-color: #e9ecef;
}
.btn-sm-secondary i {
margin-right: 6px;
}
Filter Examples: Recent Modifications
Common filter queries for finding recently changed entities:
| Filter Description | Query Syntax | Use Case |
|---|---|---|
| Show all changes in last 7 days | modified:7d |
Weekly review of recent updates |
| Show entities modified by specific user | lastModifiedBy:alice@contoso.com |
Track changes by team member |
| Show entities modified by anyone except me | lastModifiedBy:NOT {currentUser} |
Review others' changes since last login |
| Show entities modified since specific date | modified:>2026-01-15 |
Compliance audit for specific time period |
| Show frequently modified entities (>10 edits) | modificationCount:>10 |
Identify unstable/high-churn integrations |
| Show entities created in last 30 days | created:30d |
Track new integrations and growth |
| Show entities not modified in 90 days | modified:>90d |
Identify stale/abandoned integrations |
| Combine filters: Modified by Alice in last 7 days | lastModifiedBy:alice@contoso.com AND modified:7d |
Targeted review of specific user's recent work |
Filter UI in Mapify:
<div class="recent-changes-filter" role="search" aria-label="Filter by recent modifications">
<h3>
<i class="fas fa-filter" aria-hidden="true"></i>
Filter by Recent Changes
</h3>
<form id="change-filter-form">
<div class="form-row">
<label for="date-range">Modified In:</label>
<select id="date-range" name="dateRange">
<option value="1d">Last 24 hours</option>
<option value="7d" selected>Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="custom">Custom date range...</option>
</select>
</div>
<div class="form-row">
<label for="modified-by">Modified By:</label>
<select id="modified-by" name="modifiedBy" multiple aria-label="Select users">
<option value="">All users</option>
<option value="alice.johnson@contoso.com">Alice Johnson</option>
<option value="bob.taylor@contoso.com">Bob Taylor</option>
<option value="charlie.davis@contoso.com">Charlie Davis</option>
</select>
</div>
<div class="form-row">
<label for="modification-count">Modification Count:</label>
<select id="modification-count" name="modificationCount">
<option value="">Any</option>
<option value=">5">More than 5 edits</option>
<option value=">10">More than 10 edits</option>
<option value=">20">More than 20 edits</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fas fa-search" aria-hidden="true"></i>
Apply Filter
</button>
<button type="reset" class="btn-secondary">
Clear Filters
</button>
</div>
</form>
<div class="filter-results" role="status" aria-live="polite">
<!-- Results populated dynamically -->
<p><strong>42 entities</strong> modified in last 7 days</p>
</div>
</div>
Audit Report Formats for Compliance
Pre-built audit reports for common compliance scenarios (SOX, GDPR, HIPAA):
1. Change Activity Report (All Entities)
Purpose: Comprehensive report of all changes for a given time period.
Report Fields:
- Entity Type (Integration, System, Service, Resource)
- Entity Name
- Changed By (user email/name)
- Changed Date (timestamp)
- Field Name (what was changed)
- Old Value / New Value
- Change Reason (if provided)
Output Formats:
- Excel (.xlsx) – Full detail with filtering/sorting
- CSV (.csv) – Raw data for import into other systems
- PDF (.pdf) – Executive summary with charts
Sample Excel report structure:
| Entity Type | Entity Name | Field Name | Old Value | New Value | Changed By | Changed Date | Change Reason |
|-------------|-----------------------|--------------|--------------|----------------|-------------------------|---------------------|---------------------------|
| Integration | SAP to Salesforce | Description | Old desc | New desc | alice.johnson@... | 2026-01-19 14:22:35 | GDPR compliance update |
| Integration | SAP to Salesforce | Owner | charlie@... | alice@... | bob.taylor@... | 2026-01-18 10:15:00 | Ownership transfer |
| System | SAP ERP Production | Status | Active | In Review | admin@... | 2026-01-17 09:00:00 | Quarterly review |
C# code to generate Excel report:
public async Task<byte[]> GenerateChangeActivityReportAsync(
DateTime startDate,
DateTime endDate,
string entityType = null)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Change Activity");
// Header row
worksheet.Cells[1, 1].Value = "Entity Type";
worksheet.Cells[1, 2].Value = "Entity Name";
worksheet.Cells[1, 3].Value = "Field Name";
worksheet.Cells[1, 4].Value = "Old Value";
worksheet.Cells[1, 5].Value = "New Value";
worksheet.Cells[1, 6].Value = "Changed By";
worksheet.Cells[1, 7].Value = "Changed Date";
worksheet.Cells[1, 8].Value = "Change Reason";
// Style header
using (var range = worksheet.Cells[1, 1, 1, 8])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(0, 112, 192));
range.Style.Font.Color.SetColor(Color.White);
}
// Fetch data
var query = _context.EntityChangeLogs
.Where(c => c.ChangedDate >= startDate && c.ChangedDate <= endDate);
if (!string.IsNullOrEmpty(entityType))
{
query = query.Where(c => c.EntityType == entityType);
}
var changes = await query
.OrderByDescending(c => c.ChangedDate)
.ToListAsync();
// Populate data rows
int row = 2;
foreach (var change in changes)
{
worksheet.Cells[row, 1].Value = change.EntityType;
worksheet.Cells[row, 2].Value = change.EntityName;
worksheet.Cells[row, 3].Value = change.FieldName ?? "(Entity Created)";
worksheet.Cells[row, 4].Value = change.OldValue ?? "";
worksheet.Cells[row, 5].Value = change.NewValue ?? "";
worksheet.Cells[row, 6].Value = change.ChangedBy;
worksheet.Cells[row, 7].Value = change.ChangedDate;
worksheet.Cells[row, 7].Style.Numberformat.Format = "yyyy-mm-dd hh:mm:ss";
worksheet.Cells[row, 8].Value = change.ChangeReason ?? "";
row++;
}
// Auto-fit columns
worksheet.Cells.AutoFitColumns();
return package.GetAsByteArray();
}
}
2. User Activity Report (By Person)
Purpose: Track all changes made by specific users (useful for access reviews, offboarding).
Report Fields:
- User Name
- User Email
- Total Changes Made
- Entity Types Modified (breakdown)
- Date Range
- Most Frequently Modified Entities
Sample report:
User Activity Report
Date Range: 2026-01-01 to 2026-01-31
| User Name | Email | Total Changes | Integrations | Systems | Services | Most Edited Entity |
|-----------------|------------------------|---------------|--------------|---------|----------|--------------------------|
| Alice Johnson | alice.johnson@... | 142 | 98 | 32 | 12 | SAP to Salesforce (23x) |
| Bob Taylor | bob.taylor@... | 87 | 45 | 28 | 14 | Dynamics CRM API (18x) |
| Charlie Davis | charlie.davis@... | 63 | 40 | 15 | 8 | Legacy EDI Bridge (15x) |
3. Compliance Audit Report (Filtered by Tag/Domain)
Purpose: Generate audit trail for specific compliance scope (e.g., all GDPR-regulated systems).
Report Filters:
- Domain (Finance, HR, Sales, etc.)
- Compliance Tags (GDPR, HIPAA, SOX, PCI-DSS)
- Entity Type
- Date Range
Sample SQL query:
-- Compliance Audit Report: All changes to GDPR-regulated integrations in Q1 2026
SELECT
ecl.EntityType,
ecl.EntityName,
ecl.FieldName,
ecl.OldValue,
ecl.NewValue,
ecl.ChangedBy,
ecl.ChangedDate,
ecl.ChangeReason,
cm.MetadataValue AS ComplianceTag
FROM EntityChangeLog ecl
INNER JOIN Entities e ON ecl.EntityId = e.EntityId
INNER JOIN CustomMetadata cm ON e.EntityId = cm.EntityId
WHERE cm.MetadataKey = 'ComplianceTag'
AND cm.MetadataValue LIKE '%GDPR%'
AND ecl.ChangedDate >= '2026-01-01'
AND ecl.ChangedDate < '2026-04-01'
ORDER BY ecl.ChangedDate DESC;
4. High-Risk Change Report
Purpose: Identify changes to critical/production systems for extra scrutiny.
Report Criteria:
- Changes to production environments only
- Changes to entities tagged as "Critical"
- Changes made outside business hours (off-hours deployments)
- Changes without change reason documentation
Sample report:
High-Risk Changes Report
Period: Last 30 days
| Entity Name | Environment | Changed By | Changed Date | Field Changed | Change Reason | Risk Level |
|--------------------------|-------------|-------------------|---------------------|---------------|----------------------|------------|
| Payment Gateway API | Production | admin@... | 2026-01-19 23:45:00 | Endpoint URL | (none provided) | HIGH |
| SAP ERP Production | Production | alice@... | 2026-01-18 02:30:00 | Credentials | Emergency fix #1234 | MEDIUM |
| Customer Data Sync | Production | bob@... | 2026-01-15 18:00:00 | Status | Planned maintenance | LOW |
Retention Policy & Data Management
Change history retention balances compliance requirements with database storage costs:
| Environment | Default Retention | Configurable Range | Compliance Notes |
|---|---|---|---|
| Production | 365 days (1 year) | 90 days – Indefinite | SOX requires 7 years for financial systems; GDPR allows deletion after purpose fulfilled |
| Test/QA | 90 days | 30 days – 180 days | Limited compliance requirements; focus on performance testing data |
| Development | 30 days | 7 days – 90 days | No compliance requirements; short retention minimizes storage |
Retention configuration in appsettings.json:
{
"ChangeTracking": {
"RetentionPolicy": {
"DefaultRetentionDays": 365,
"AutoDeleteEnabled": true,
"AutoDeleteSchedule": "0 2 * * 0", // Weekly on Sunday at 2 AM
"ArchiveBeforeDelete": true,
"ArchiveLocation": "\\\\fileserver\\audit-archives\\",
"ComplianceOverrides": {
"FinancialSystemsRetentionDays": 2555, // 7 years for SOX
"HIPAARetentionDays": 2190, // 6 years for HIPAA
"GDPRRetentionDays": 365 // 1 year default, deletable on request
}
}
}
}
Automated cleanup job (C# background service):
public class ChangeLogCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ChangeLogCleanupService> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<NodiniteDbContext>();
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var retentionDays = config.GetValue<int>("ChangeTracking:RetentionPolicy:DefaultRetentionDays", 365);
var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
// Archive before deleting (if enabled)
if (config.GetValue<bool>("ChangeTracking:RetentionPolicy:ArchiveBeforeDelete", true))
{
await ArchiveOldChangesAsync(context, cutoffDate);
}
// Delete old records
var deletedCount = await context.EntityChangeLogs
.Where(c => c.ChangedDate < cutoffDate)
.ExecuteDeleteAsync();
_logger.LogInformation("Deleted {Count} change log records older than {CutoffDate}", deletedCount, cutoffDate);
// Wait 7 days before next run
await Task.Delay(TimeSpan.FromDays(7), stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during change log cleanup");
await Task.Delay(TimeSpan.FromHours(1), stoppingToken); // Retry in 1 hour
}
}
}
private async Task ArchiveOldChangesAsync(NodiniteDbContext context, DateTime cutoffDate)
{
var archivePath = Path.Combine(
_config["ChangeTracking:RetentionPolicy:ArchiveLocation"],
$"ChangeLog_Archive_{DateTime.UtcNow:yyyyMMdd}.csv"
);
var oldChanges = await context.EntityChangeLogs
.Where(c => c.ChangedDate < cutoffDate)
.OrderBy(c => c.ChangedDate)
.ToListAsync();
// Write to CSV archive
using var writer = new StreamWriter(archivePath);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
await csv.WriteRecordsAsync(oldChanges);
_logger.LogInformation("Archived {Count} records to {Path}", oldChanges.Count, archivePath);
}
}
Audit Query Examples
Common SQL queries for compliance audits and governance reporting:
Query 1: All changes to a specific entity
-- View complete change history for Integration #123
SELECT
ChangeId,
ChangeType,
FieldName,
OldValue,
NewValue,
ChangedBy,
ChangedDate,
ChangeReason
FROM EntityChangeLog
WHERE EntityId = '3fa85f64-5717-4562-b3fc-2c963f66afa6' -- Integration #123
ORDER BY ChangedDate DESC;
Query 2: Changes by specific user in date range
-- All changes made by Alice in January 2026
SELECT
EntityType,
EntityName,
FieldName,
OldValue,
NewValue,
ChangedDate,
ChangeReason
FROM EntityChangeLog
WHERE ChangedBy = 'alice.johnson@contoso.com'
AND ChangedDate >= '2026-01-01'
AND ChangedDate < '2026-02-01'
ORDER BY ChangedDate DESC;
Query 3: Entities modified most frequently (high-churn analysis)
-- Top 20 most frequently changed entities (instability hotspots)
SELECT TOP 20
EntityType,
EntityName,
COUNT(*) AS ChangeCount,
COUNT(DISTINCT ChangedBy) AS UniqueEditors,
MIN(ChangedDate) AS FirstChange,
MAX(ChangedDate) AS LastChange
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -90, GETUTCDATE()) -- Last 90 days
GROUP BY EntityType, EntityName, EntityId
ORDER BY ChangeCount DESC;
Query 4: Changes without change reasons (governance flag)
-- Changes missing change reason documentation (governance violation)
SELECT
EntityType,
EntityName,
FieldName,
ChangedBy,
ChangedDate
FROM EntityChangeLog
WHERE ChangeReason IS NULL
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
AND EntityType IN ('Integration', 'System') -- Critical entities only
ORDER BY ChangedDate DESC;
Query 5: After-hours changes (security audit)
-- Changes made outside business hours (potential security risk)
SELECT
EntityType,
EntityName,
FieldName,
ChangedBy,
ChangedDate,
DATEPART(HOUR, ChangedDate) AS HourOfDay,
DATENAME(WEEKDAY, ChangedDate) AS DayOfWeek
FROM EntityChangeLog
WHERE (
DATEPART(HOUR, ChangedDate) < 7 OR DATEPART(HOUR, ChangedDate) >= 19 -- Before 7 AM or after 7 PM
OR DATENAME(WEEKDAY, ChangedDate) IN ('Saturday', 'Sunday') -- Weekend
)
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY ChangedDate DESC;
Query 6: Field-level change frequency analysis
-- Which fields are modified most often (UX optimization insight)
SELECT
EntityType,
FieldName,
COUNT(*) AS ChangeCount,
COUNT(DISTINCT EntityId) AS AffectedEntities
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -90, GETUTCDATE())
AND FieldName IS NOT NULL -- Exclude CREATE/DELETE events
GROUP BY EntityType, FieldName
ORDER BY ChangeCount DESC;
Query 7: Rollback audit (who reverted what)
-- Detect rollbacks (OldValue = current database value after subsequent change)
WITH ChangeSequence AS (
SELECT
EntityId,
FieldName,
OldValue,
NewValue,
ChangedBy,
ChangedDate,
LEAD(NewValue) OVER (PARTITION BY EntityId, FieldName ORDER BY ChangedDate) AS NextValue
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
)
SELECT
EntityId,
FieldName,
OldValue AS RevertedTo,
NewValue AS TemporaryValue,
NextValue AS FinalValue,
ChangedBy AS UserWhoReverted,
ChangedDate
FROM ChangeSequence
WHERE NewValue = NextValue -- Indicates revert
ORDER BY ChangedDate DESC;
GDPR Compliance & Right to Deletion
GDPR considerations for change tracking:
Key Principle: Change logs containing personal data must respect GDPR "right to be forgotten" (Article 17), but audit trail requirements (SOX, HIPAA) may override in certain cases. Consult legal counsel for your specific regulatory context.
GDPR-compliant deletion workflow:
- User submits deletion request via formal GDPR request form
- Legal review determines if audit retention overrides apply (financial systems = 7 years SOX retention)
- If deletion approved:
- Anonymize user identity in
ChangedByfield → Replace with "User_DELETED_20260119" - Preserve change metadata (what/when) for audit trail
- Delete or redact any personal data in
OldValue/NewValuefields
- Anonymize user identity in
- Generate deletion certificate for data subject
SQL script to anonymize deleted user:
-- Anonymize all changes by user requesting GDPR deletion
DECLARE @UserEmail NVARCHAR(255) = 'alice.johnson@contoso.com';
DECLARE @AnonymizedId NVARCHAR(255) = CONCAT('User_DELETED_', FORMAT(GETUTCDATE(), 'yyyyMMdd'));
BEGIN TRANSACTION;
-- Update ChangedBy field
UPDATE EntityChangeLog
SET ChangedBy = @AnonymizedId
WHERE ChangedBy = @UserEmail;
-- Redact personal data in change values (example: email addresses)
UPDATE EntityChangeLog
SET OldValue = '[REDACTED]'
WHERE OldValue LIKE '%' + @UserEmail + '%';
UPDATE EntityChangeLog
SET NewValue = '[REDACTED]'
WHERE NewValue LIKE '%' + @UserEmail + '%';
COMMIT TRANSACTION;
-- Log GDPR deletion action
INSERT INTO GDPRDeletionLog (UserEmail, DeletionDate, RecordsAffected)
SELECT @UserEmail, GETUTCDATE(), @@ROWCOUNT;
Compliance documentation link:
For full GDPR, SOX, HIPAA, and PCI-DSS compliance guidelines, see:
Compliance & Data Governance Guide
Comments & Annotations – Team Collaboration on Entities
Comments and Annotations enable distributed teams to collaborate directly on Repository Model entities without leaving Mapify. Add contextual discussions, ask questions, request approvals, and document decisions right where the work happens. No more scattered email threads, lost Slack messages, or "who said what about this integration?" confusion.
Comments support @mentions for direct notifications, threaded replies for organized discussions, and status tracking (Open → Resolved → Closed) for approval workflows and issue resolution.
Why Use Comments?
Comments transform Mapify from a visualization tool into a collaborative workspace:
- 50% faster decision-making – Questions answered in context, no meeting scheduling needed
- Zero missed notifications – @mentions email relevant team members instantly
- Approval workflows – Track "Pending Review" → "Approved" → "Closed" lifecycle
- Permanent documentation – Decision rationale preserved in audit trail
- Knowledge transfer – New team members read historical discussions to understand "why"
- Compliance evidence – Demonstrate review and approval for SOX/GDPR audits
Typical use cases:
- Compliance approvals ("Legal has reviewed this GDPR integration – approved ✓")
- Incident root-cause analysis ("Why did this integration fail last Tuesday?")
- Architecture questions ("Should we use REST or SOAP for this System?")
- Change coordination ("Don't modify this until Friday's deployment window")
- Knowledge capture ("This Service requires special firewall rules – see ticket #1234")
Comment Data Structure
Comments are stored with rich metadata to enable threading, status tracking, and notifications:
| Field Name | Type | Example Value | Purpose |
|---|---|---|---|
CommentId |
GUID | 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Unique identifier for comment |
EntityId |
GUID | 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
Entity this comment is attached to (Integration, System, etc.) |
EntityType |
String | Integration |
Type of entity (denormalized for reporting) |
EntityName |
String | SAP to Salesforce |
Entity name (denormalized for email notifications) |
UserId |
GUID | 8d2f3a4b-1c5e-6f7d-8a9b-0c1d2e3f4a5b |
User who created the comment |
UserName |
String | Alice Johnson |
Display name (denormalized) |
UserEmail |
String | alice.johnson@contoso.com |
Email for notifications (denormalized) |
CommentText |
String (max 4000 chars) | @bob.taylor Can you review the error handling logic before we deploy? |
Comment content (supports Markdown and @mentions) |
ParentCommentId |
GUID (nullable) | null (top-level) or 7c9e6679... (reply) |
Parent comment for threaded replies; null for top-level comments |
CreatedDate |
DateTime (UTC) | 2026-01-19T14:23:45Z |
When comment was created |
ModifiedDate |
DateTime (UTC, nullable) | 2026-01-19T15:10:00Z |
When comment was last edited (null if never edited) |
Status |
Enum | Open, Resolved, Closed |
Comment status for approval/issue workflows |
MentionedUsers |
JSON array | ["bob.taylor@contoso.com", "charlie.davis@contoso.com"] |
List of @mentioned user emails (parsed from CommentText) |
Attachments |
JSON array | [{"fileName": "architecture.png", "fileUrl": "..."}] |
Optional file attachments (screenshots, diagrams) |
IsEdited |
Boolean | true |
Indicates if comment was modified after creation |
IsDeleted |
Boolean | false |
Soft delete flag (preserves comment in threads) |
Database schema (SQL):
CREATE TABLE EntityComments (
CommentId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
EntityId UNIQUEIDENTIFIER NOT NULL,
EntityType NVARCHAR(50) NOT NULL, -- Denormalized for reporting
EntityName NVARCHAR(255) NOT NULL, -- Denormalized for email notifications
UserId UNIQUEIDENTIFIER NOT NULL,
UserName NVARCHAR(255) NOT NULL, -- Denormalized
UserEmail NVARCHAR(255) NOT NULL, -- Denormalized
CommentText NVARCHAR(4000) NOT NULL,
ParentCommentId UNIQUEIDENTIFIER NULL, -- NULL for top-level, GUID for replies
CreatedDate DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
ModifiedDate DATETIME2 NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Open', -- Open, Resolved, Closed
MentionedUsers NVARCHAR(MAX) NULL, -- JSON array of mentioned user emails
Attachments NVARCHAR(MAX) NULL, -- JSON array of attachment metadata
IsEdited BIT NOT NULL DEFAULT 0,
IsDeleted BIT NOT NULL DEFAULT 0,
CONSTRAINT FK_EntityComments_Entities FOREIGN KEY (EntityId) REFERENCES Entities(EntityId),
CONSTRAINT FK_EntityComments_Users FOREIGN KEY (UserId) REFERENCES Users(UserId),
CONSTRAINT FK_EntityComments_ParentComment FOREIGN KEY (ParentCommentId) REFERENCES EntityComments(CommentId)
);
-- Indexes for performance
CREATE INDEX IX_EntityComments_EntityId ON EntityComments(EntityId) WHERE IsDeleted = 0;
CREATE INDEX IX_EntityComments_ParentCommentId ON EntityComments(ParentCommentId) WHERE IsDeleted = 0;
CREATE INDEX IX_EntityComments_CreatedDate ON EntityComments(CreatedDate DESC);
CREATE INDEX IX_EntityComments_Status ON EntityComments(Status) WHERE IsDeleted = 0;
Example comment JSON (API response):
{
"commentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entityType": "Integration",
"entityName": "SAP to Salesforce",
"userId": "8d2f3a4b-1c5e-6f7d-8a9b-0c1d2e3f4a5b",
"userName": "Alice Johnson",
"userEmail": "alice.johnson@contoso.com",
"commentText": "@bob.taylor Can you review the error handling logic before we deploy? Also cc @charlie.davis for security review.",
"parentCommentId": null,
"createdDate": "2026-01-19T14:23:45Z",
"modifiedDate": null,
"status": "Open",
"mentionedUsers": [
"bob.taylor@contoso.com",
"charlie.davis@contoso.com"
],
"attachments": [
{
"fileName": "error-handling-diagram.png",
"fileUrl": "https://storage.contoso.com/attachments/7c9e6679.../error-handling-diagram.png",
"fileSize": 245678,
"uploadedDate": "2026-01-19T14:23:40Z"
}
],
"isEdited": false,
"isDeleted": false,
"replies": [] // Array of nested comment objects for threaded replies
}
@Mention Functionality
@mentions notify specific users when they're referenced in a comment. Mentions are parsed from comment text using @username or @email syntax and trigger email notifications.
Mention Syntax
- By username:
@alice.johnson(automatically matched to user account) - By email:
@alice.johnson@contoso.com(explicit email address) - By display name:
@Alice Johnson(fuzzy matched if display name is unique) - Multiple mentions:
@bob.taylor @charlie.davis(space-separated)
Auto-complete While Typing
When users type @ in the comment box, an autocomplete dropdown appears:
<!-- Mention autocomplete dropdown -->
<div class="mention-autocomplete" role="listbox" aria-label="Mention user suggestions">
<div class="mention-option" role="option" aria-selected="false" tabindex="0">
<img src="avatar-alice.png" alt="" class="mention-avatar" aria-hidden="true">
<div class="mention-details">
<strong>Alice Johnson</strong>
<small>alice.johnson@contoso.com</small>
</div>
</div>
<div class="mention-option" role="option" aria-selected="false" tabindex="0">
<img src="avatar-bob.png" alt="" class="mention-avatar" aria-hidden="true">
<div class="mention-details">
<strong>Bob Taylor</strong>
<small>bob.taylor@contoso.com</small>
</div>
</div>
<div class="mention-option" role="option" aria-selected="false" tabindex="0">
<img src="avatar-charlie.png" alt="" class="mention-avatar" aria-hidden="true">
<div class="mention-details">
<strong>Charlie Davis</strong>
<small>charlie.davis@contoso.com</small>
</div>
</div>
</div>
Autocomplete behavior:
- Appears immediately when
@is typed - Filters as user continues typing (
@ali→ shows "Alice Johnson") - Keyboard navigation: Arrow keys to select, Enter to insert, Escape to cancel
- Mouse selection: Click to insert mention
- Fuzzy matching:
@alicematches "Alice Johnson" and "alice.johnson@contoso.com" - Max 10 suggestions shown (sorted by recent collaboration frequency)
Notification Mechanism
When a comment with mentions is saved, the system:
- Parses mentions from
CommentTextusing regex:/@(\S+@\S+\.\S+|[\w.]+)/g - Resolves user emails (match username or email to user account)
- Stores in
MentionedUsersJSON array field - Sends email notifications to each mentioned user
- Creates in-app notifications (bell icon in Mapify header)
Email notification template:
Subject: Alice Johnson mentioned you in a comment on "SAP to Salesforce"
Hi Bob,
Alice Johnson mentioned you in a comment on the Integration "SAP to Salesforce":
@bob.taylor Can you review the error handling logic before we deploy?
Also cc @charlie.davis for security review.
Click here to view and reply:
https://mapify.contoso.com/integration/3fa85f64-5717-4562-b3fc-2c963f66afa6#comment-7c9e6679
---
This is an automated notification from Nodinite Mapify.
Update your notification preferences: https://mapify.contoso.com/settings/notifications
In-app notification (bell icon dropdown):
<div class="notification-item unread" role="alert">
<img src="avatar-alice.png" alt="Alice Johnson avatar" class="notification-avatar">
<div class="notification-content">
<strong>Alice Johnson</strong> mentioned you in a comment on
<a href="/integration/3fa85f64#comment-7c9e6679">SAP to Salesforce</a>
<small class="notification-time">2 minutes ago</small>
</div>
<button class="btn-mark-read" aria-label="Mark as read">
<i class="fas fa-check" aria-hidden="true"></i>
</button>
</div>
Notification preferences (user settings):
Users can control mention notification behavior:
- Email notifications: On (default), Off, Digest (daily summary)
- In-app notifications: On (default), Off
- Mobile push notifications: On, Off (requires Nodinite mobile app)
- Quiet hours: Disable notifications outside business hours (9 AM - 5 PM user's timezone)
Comment Threading (Nested Replies)
Comment threads organize discussions hierarchically with nested replies, making it easy to follow multi-person conversations.
Threading Structure
- Top-level comments have
ParentCommentId = NULL - Replies have
ParentCommentIdpointing to parent comment - Max nesting depth: 3 levels (top → reply → reply to reply)
- Indentation: 20px per nesting level for visual hierarchy
Example Thread Structure
Comment #1 (Alice, top-level)
├── Reply #2 (Bob, depth 1)
│ └── Reply #3 (Alice, depth 2)
│ └── Reply #4 (Charlie, depth 3) ← Max depth reached
└── Reply #5 (Charlie, depth 1)
Visual representation (HTML/CSS):
<div class="comment-thread">
<!-- Top-level comment -->
<div class="comment" data-comment-id="comment-1" data-depth="0">
<img src="avatar-alice.png" alt="Alice Johnson avatar" class="comment-avatar">
<div class="comment-content">
<div class="comment-header">
<strong>Alice Johnson</strong>
<span class="comment-time">2 hours ago</span>
<span class="comment-status status-open">Open</span>
</div>
<div class="comment-body">
@bob.taylor Can you review the error handling logic before we deploy?
</div>
<div class="comment-actions">
<button class="btn-reply" aria-label="Reply to Alice Johnson's comment">
<i class="fas fa-reply" aria-hidden="true"></i> Reply
</button>
<button class="btn-resolve" aria-label="Mark comment as resolved">
<i class="fas fa-check" aria-hidden="true"></i> Resolve
</button>
</div>
</div>
</div>
<!-- Reply (depth 1) -->
<div class="comment comment-reply" data-comment-id="comment-2" data-depth="1" style="margin-left: 20px;">
<img src="avatar-bob.png" alt="Bob Taylor avatar" class="comment-avatar">
<div class="comment-content">
<div class="comment-header">
<strong>Bob Taylor</strong>
<span class="comment-time">1 hour ago</span>
</div>
<div class="comment-body">
Looks good! I've reviewed the try-catch blocks. One suggestion: add logging for the fallback path.
</div>
<div class="comment-actions">
<button class="btn-reply" aria-label="Reply to Bob Taylor's comment">
<i class="fas fa-reply" aria-hidden="true"></i> Reply
</button>
</div>
</div>
</div>
<!-- Reply to reply (depth 2) -->
<div class="comment comment-reply" data-comment-id="comment-3" data-depth="2" style="margin-left: 40px;">
<img src="avatar-alice.png" alt="Alice Johnson avatar" class="comment-avatar">
<div class="comment-content">
<div class="comment-header">
<strong>Alice Johnson</strong>
<span class="comment-time">30 minutes ago</span>
</div>
<div class="comment-body">
Great idea! I'll add Application Insights logging. Thanks @bob.taylor!
</div>
<div class="comment-actions">
<button class="btn-reply" aria-label="Reply to Alice Johnson's comment">
<i class="fas fa-reply" aria-hidden="true"></i> Reply
</button>
</div>
</div>
</div>
</div>
CSS for threading:
.comment {
display: flex;
gap: 12px;
padding: 12px;
border-left: 3px solid transparent;
transition: background-color 0.2s;
}
.comment:hover {
background-color: #f5f5f5;
}
.comment-reply {
margin-left: 20px; /* Indent per depth level */
border-left-color: #e0e0e0; /* Visual thread line */
}
.comment[data-depth="1"] { margin-left: 20px; }
.comment[data-depth="2"] { margin-left: 40px; }
.comment[data-depth="3"] { margin-left: 60px; } /* Max depth */
.comment-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
flex-shrink: 0;
}
.comment-content {
flex: 1;
min-width: 0; /* Prevent overflow */
}
.comment-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.comment-time {
color: #757575;
font-size: 0.875rem;
}
.comment-body {
margin-bottom: 8px;
line-height: 1.6;
white-space: pre-wrap; /* Preserve line breaks */
}
.comment-actions {
display: flex;
gap: 12px;
}
.btn-reply, .btn-resolve {
background: transparent;
border: 1px solid #e0e0e0;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: background-color 0.2s;
}
.btn-reply:hover, .btn-resolve:hover {
background-color: #f5f5f5;
}
.btn-reply:focus, .btn-resolve:focus {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
Comment Status Lifecycle
Comments support a status workflow for approval processes and issue resolution:
| Status | Icon | Color | Meaning | Typical Use Case |
|---|---|---|---|---|
| Open | Blue (#0056b3) | Active discussion, question unanswered, or approval pending | "Can someone review this before production deploy?" | |
| Resolved | Green (#28a745) | Question answered, issue fixed, or approval granted | "Reviewed and approved for deployment ✓" | |
| Closed | Gray (#6c757d) | Discussion archived, no action needed, or obsolete | "This integration was deprecated; closing thread." |
Status Transition Rules
Who can change status:
- Comment author can change their own comment status
- Entity owner can change status of any comment on their entities
- Administrators can change any comment status
Status change notifications:
When comment status changes, system notifies:
- Original comment author
- All users who replied in the thread
- All @mentioned users
- Entity owner (if different from above)
Email notification example:
Subject: Comment resolved: "Can you review the error handling logic?"
Hi Alice,
Your comment on "SAP to Salesforce" was marked as Resolved by Bob Taylor:
Original comment: @bob.taylor Can you review the error handling logic before we deploy?
Bob's reply: Looks good! I've reviewed the try-catch blocks. One suggestion: add logging for the fallback path.
Status: Open → Resolved
View thread: https://mapify.contoso.com/integration/3fa85f64#comment-7c9e6679
UI Display Options
Comments can be displayed in three locations based on user workflow:
Option 1: Inline on Node (Minimalist)
Best for: Quick visibility without cluttering the graph
Visual indicator:
- Small comment badge icon on node corner: (count: 3)
- Badge color indicates status: Blue (open), Green (resolved), Gray (closed)
- Hover tooltip shows most recent comment preview
┌────────────────────────┐
│ SAP to Salesforce │ 🔵3 ← Comment badge (3 open comments)
│ Integration │
│ Status: Active │
└────────────────────────┘
HTML for comment badge:
<div class="node-badge comment-badge" aria-label="3 open comments on this integration">
<i class="fas fa-comment" aria-hidden="true"></i>
<span class="badge-count">3</span>
</div>
<!-- Hover tooltip (appears on :hover) -->
<div class="comment-tooltip" role="tooltip">
<div class="tooltip-header">
<strong>3 comments</strong>
<small>Most recent:</small>
</div>
<div class="tooltip-body">
<strong>Alice Johnson</strong> 2 hours ago<br>
@bob.taylor Can you review the error handling logic...
</div>
<small class="tooltip-action">Click node to view all comments</small>
</div>
Option 2: Side Panel (Contextual)
Best for: Reading and replying to comments while viewing entity details
Placement: Right side panel (slides in when entity is selected)
Sections:
- Entity details (top): Name, type, owner, status, metadata
- Comments section (below): Scrollable list of all comments and replies
- Add comment box (bottom): Text area with @mention autocomplete
┌─────────────────────────────┐
│ Entity Details │ ← Close button
├─────────────────────────────┤
│ Integration: SAP to Sales...│
│ Owner: Alice Johnson │
│ Status: Active │
│ │
│ Comments (3) │ ← Section header
├─────────────────────────────┤
│ ┌─ Alice Johnson (2h ago) │
│ │ @bob.taylor Can you... │
│ │ [Reply] [Resolve] │
│ └─┬─ Bob Taylor (1h ago) │
│ │ Looks good! I've... │
│ │ [Reply] │
│ └─── Alice (30m ago) │
│ Great idea! I'll... │
│ [Reply] │
├─────────────────────────────┤
│ Add Comment │
│ ┌───────────────────────┐ │
│ │ Type your comment... │ │
│ │ Use @mention to notify│ │
│ └───────────────────────┘ │
│ [Cancel] [Post Comment] │
└─────────────────────────────┘
HTML for side panel:
<aside class="entity-detail-panel" role="complementary" aria-label="Entity details and comments">
<!-- Entity Details Section -->
<div class="panel-header">
<h2>Entity Details</h2>
<button class="btn-close-panel" aria-label="Close entity details panel">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="entity-summary">
<dl>
<dt>Integration:</dt>
<dd>SAP to Salesforce</dd>
<dt>Owner:</dt>
<dd>Alice Johnson</dd>
<dt>Status:</dt>
<dd><span class="status-badge status-active">Active</span></dd>
</dl>
</div>
<!-- Comments Section -->
<div class="comments-section">
<h3>
<i class="fas fa-comments" aria-hidden="true"></i> Comments (3)
</h3>
<!-- Filter/Sort controls -->
<div class="comment-controls">
<select aria-label="Filter comments by status">
<option value="all">All Comments</option>
<option value="open">Open Only</option>
<option value="resolved">Resolved Only</option>
</select>
<select aria-label="Sort comments">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
</select>
</div>
<!-- Comment threads (scrollable) -->
<div class="comments-list" role="list">
<!-- Comment thread inserted here (see Threading section) -->
</div>
</div>
<!-- Add Comment Section -->
<div class="add-comment-section">
<h4>
<i class="fas fa-plus" aria-hidden="true"></i> Add Comment
</h4>
<form class="comment-form">
<label for="comment-text" class="visually-hidden">Comment text</label>
<textarea
id="comment-text"
name="commentText"
rows="3"
placeholder="Type your comment... Use @mention to notify team members"
aria-describedby="comment-help"
required
></textarea>
<small id="comment-help" class="form-help">
Use @username to mention team members. Supports Markdown formatting.
</small>
<div class="comment-form-actions">
<button type="button" class="btn-cancel">Cancel</button>
<button type="submit" class="btn-primary">
<i class="fas fa-paper-plane" aria-hidden="true"></i> Post Comment
</button>
</div>
</form>
</div>
</aside>
Accessibility requirements:
- Panel announced to screen readers when opened:
aria-live="polite"on panel container - Focus moved to panel header when opened (keyboard navigation)
- Escape key closes panel and returns focus to trigger element
- All comment threads navigable by Tab key
- Reply/Resolve buttons have descriptive
aria-labelattributes
Option 3: Modal Dialog (Full-Screen Focus)
Best for: Extended discussions requiring full attention (approval workflows, complex threads)
Trigger: "View Comments" button or comment badge click
Layout: Centered modal overlay with dimmed background
╔═══════════════════════════════════════════════════╗
║ Comments: SAP to Salesforce Integration [X] ║
╠═══════════════════════════════════════════════════╣
║ Filter: All (3) | Sort: Newest ║
╟───────────────────────────────────────────────────╢
║ ┌─ Alice Johnson (2 hours ago) [Open] ║
║ │ @bob.taylor Can you review the error ║
║ │ handling logic before we deploy? ║
║ │ [Reply] [Resolve] ║
║ └─┬─ Bob Taylor (1 hour ago) ║
║ │ Looks good! I've reviewed the try-catch... ║
║ │ [Reply] ║
║ └─── Alice Johnson (30 minutes ago) ║
║ Great idea! I'll add Application... ║
║ [Reply] ║
║ ║
║ ┌─ Charlie Davis (5 hours ago) [Resolved] ║
║ │ Security review complete. Approved for prod. ║
║ └─── [No replies] ║
║ ║
╟───────────────────────────────────────────────────╢
║ Add Comment ║
║ ┌─────────────────────────────────────────────┐ ║
║ │ Type your comment... │ ║
║ └─────────────────────────────────────────────┘ ║
║ [Attach File] [Cancel] [Post Comment] ║
╚═══════════════════════════════════════════════════╝
HTML for modal:
<div class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-container comment-modal">
<!-- Modal Header -->
<div class="modal-header">
<h2 id="modal-title">Comments: SAP to Salesforce Integration</h2>
<button class="btn-close-modal" aria-label="Close comments modal">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<!-- Filter/Sort Bar -->
<div class="modal-toolbar">
<div class="filter-group">
<label for="filter-status">
<i class="fas fa-filter" aria-hidden="true"></i> Filter:
</label>
<select id="filter-status" aria-label="Filter comments by status">
<option value="all">All (3)</option>
<option value="open">Open (1)</option>
<option value="resolved">Resolved (2)</option>
</select>
</div>
<div class="sort-group">
<label for="sort-order">Sort:</label>
<select id="sort-order" aria-label="Sort comments">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
<option value="status">By Status</option>
</select>
</div>
</div>
<!-- Comment Threads (scrollable) -->
<div class="modal-body">
<div class="comments-list" role="list">
<!-- Comment threads inserted here -->
</div>
</div>
<!-- Add Comment Form -->
<div class="modal-footer">
<h3>
<i class="fas fa-plus" aria-hidden="true"></i> Add Comment
</h3>
<form class="comment-form">
<textarea
id="modal-comment-text"
name="commentText"
rows="3"
placeholder="Type your comment... Use @mention to notify team members"
aria-label="Comment text"
required
></textarea>
<div class="comment-form-actions">
<button type="button" class="btn-attach-file">
<i class="fas fa-paperclip" aria-hidden="true"></i> Attach File
</button>
<button type="button" class="btn-cancel">Cancel</button>
<button type="submit" class="btn-primary">
<i class="fas fa-paper-plane" aria-hidden="true"></i> Post Comment
</button>
</div>
</form>
</div>
</div>
</div>
Modal accessibility:
- Modal announced to screen readers when opened
- Focus trapped inside modal (Tab cycles through modal elements only)
- Escape key closes modal and restores focus to trigger button
- Background content inert (cannot be interacted with while modal is open)
- Close button always visible and keyboard-accessible
Comment Use Cases
Use Case 1: Compliance Approval Workflow
Scenario: Legal team must approve all GDPR-related integrations before production deployment.
Workflow:
- Developer (Alice) completes integration configuration and adds comment:
"Integration ready for legal review. @legal.team Please approve for production."
Status: Open - Legal reviewer (Charlie) reviews integration, posts approval:
"GDPR compliance verified. Data processing agreement in place. Approved for production."
Status: Resolved (marks Alice's comment as Resolved) - Developer (Alice) receives notification and proceeds with deployment
- Audit trail preserved: When was approval given? Who approved? What was reviewed?
Benefit: Compliance approval embedded in the tool; no separate email trails or SharePoint documents. SOX/GDPR auditors can query approval history directly from Mapify.
Use Case 2: Incident Root-Cause Analysis
Scenario: Production integration fails at 2 AM. Multiple team members investigate concurrently.
Workflow:
- On-call engineer (Bob) adds comment at 2:05 AM:
"Integration failing with timeout errors. Investigating. @alice.johnson FYI"
Status: Open - Integration owner (Alice, wakes up) replies at 2:15 AM:
"I'll check the API endpoint. @charlie.davis Can you verify firewall rules?"
- Network admin (Charlie) replies at 2:20 AM:
"Firewall rules look good. No recent changes."
- Bob identifies root cause at 2:30 AM:
"Found it: database connection pool exhausted. Restarting service. Adding connection pool monitoring."
Status: Resolved - Alice follows up at 9:00 AM:
"Thanks team! Let's add this to our runbook. @bob.taylor Can you document the fix?" Status: Closed (incident resolved, documentation action item created)
Benefit: Entire investigation timeline preserved in context. No need to search Slack history or email threads. New team members can read the thread to understand how similar issues were resolved in the past.
Use Case 3: Architecture Review Question
Scenario: Developer unsure whether to use REST or SOAP for new System integration.
Workflow:
- Developer (Alice) adds comment on System entity:
"@solution.architect Should we use REST or SOAP for the legacy mainframe integration? Performance is critical."
Status: Open - Solution architect (David) replies:
"For mainframe, SOAP is better supported. Performance impact is minimal (<50ms difference). Here's a benchmark: [attach: performance-comparison.xlsx]"
- Alice confirms:
"Thanks! Going with SOAP. Closing this thread."
Status: Resolved
Benefit: Architectural decisions documented in context. Future developers working on this System can see why SOAP was chosen, avoiding re-litigation of past decisions.
Use Case 4: Change Coordination
Scenario: Critical integration undergoing maintenance window; prevent accidental edits.
Workflow:
- DevOps lead (Bob) adds comment:
" MAINTENANCE WINDOW: Do not modify this integration until Friday 6 PM. Deployment in progress."
Status: Open - Other developers see comment badge on node and read warning before editing
- Bob updates comment Friday 7 PM:
"Deployment complete. Safe to edit now."
Status: Resolved
Benefit: Prevents coordination failures. Comment serves as real-time lock without blocking edit capability (soft lock, not hard lock).
Best Practices
For Comment Authors
- Be specific – Reference field names, error codes, ticket numbers for context
- Use @mentions sparingly – Only tag users who need to take action
- Attach evidence – Screenshots, logs, diagrams clarify complex issues
- Resolve when done – Mark comments as Resolved to close the loop
- Provide context – New readers should understand the issue without external research
For Teams
- Define SLAs for responses – "@mentions should be acknowledged within 4 business hours"
- Use status workflow – Open = needs action, Resolved = done, Closed = archived
- Archive old threads – Close resolved comments after 30 days to reduce clutter
- Standardize approval language – "Approved for production" = Resolved status
- Integrate with ticketing – Reference Jira/ServiceNow ticket numbers in comments
For Administrators
- Monitor comment volume – High comment count may indicate unclear documentation
- Export for compliance – Quarterly reports for audit trail requirements
- Set retention policy – Closed comments archived after 1 year (configurable)
- Enable notifications – Ensure email delivery for @mentions
- Audit critical entities – Review approval comments on production integrations
Accessibility Considerations
Comments UI must meet WCAG 2.1 AA standards:
Keyboard Navigation
- Tab key: Navigate through comments, reply buttons, resolve buttons
- Enter/Space: Activate buttons (Reply, Resolve, Post Comment)
- Escape: Close comment modal or side panel
- Arrow keys: Navigate autocomplete dropdown during @mention typing
Screen Reader Support
- Comment threads: Use
role="list"androle="listitem"for semantic structure - Status badges: Include
aria-labelwith full status text ("Open comment", "Resolved comment") - Avatars:
altattribute with user name ("Alice Johnson avatar") - Action buttons: Descriptive
aria-label("Reply to Alice Johnson's comment") - @mention autocomplete:
role="listbox"androle="option"for dropdown - Notifications:
aria-live="polite"for new comment announcements
Visual Accessibility
- Color contrast: Status colors meet 4.5:1 ratio (Blue #0056b3, Green #28a745, Gray #6c757d)
- Not color-only: Status indicated by icon + text + color (e.g., + "Resolved" + green)
- Focus indicators: 2px solid outline on all interactive elements (buttons, links, textareas)
- Font size: Minimum 14px for comment text, 12px for metadata (time, user name)
- Touch targets: Minimum 44×44px for mobile (Reply button, Resolve button)
Reduced Motion
@media (prefers-reduced-motion: reduce) {
.comment {
transition: none; /* Disable hover animations */
}
.mention-autocomplete {
animation: none; /* Disable slide-in animation */
}
}
Integration with Other Features
Comments integrate seamlessly with other Mapify features:
Saved Views
- Comment filters: Save views showing "Entities with open comments" or "Entities I'm mentioned in"
- Notification views: Auto-filter to entities with comments created in last 24 hours
Search and Discovery
- Comment text search: Global search includes comment content
Example: Search "firewall rules" finds entities with firewall-related comments - @mention search:
commented-by:alice.johnsonfinds all entities where Alice commented - Status search:
comment-status:openfilters to entities with unresolved comments
Change Tracking
- Comments logged: Comment creation/edit/delete events appear in EntityChangeLog
- Audit reports: Compliance reports include comment activity (who commented when, on what entities)
Ownership and Team Management
- Team mentions:
@integration-teammentions all team members (expands to individual emails) - Owner notifications: Entity owner auto-notified when first comment is added to their entity
- Escalation: Unresolved comments on critical entities escalate to owner after 48 hours
Related Topics
- Saved Views – Save and share custom graph configurations
- Search and Discovery – Find entities with fuzzy search and filters
- Ownership and Team Management – Assign owners and manage teams
- Repository Model – Core data model for Mapify entities
- Web API Documentation – REST API and authentication
Best Practices
For Administrators
- Monitor SignalR metrics – Track connection count, message throughput, latency
- Set up Redis caching – Improves presence lookup performance by 10x
- Configure offline mode – Enable IndexedDB for remote workers
- Plan for scale – Use Azure SignalR Service for >100 concurrent users
- Review audit logs monthly – Detect unusual edit patterns or compliance issues
For Users
- Watch for presence indicators – Check who's editing before making changes
- Resolve conflicts promptly – Don't ignore conflict dialogs; data integrity depends on it
- Use comments for coordination – @mention colleagues to clarify ownership or get approval
- Save frequently – Auto-save every 30 seconds reduces conflict probability
- Enable notifications – Get alerted when entities you care about are modified
For Developers
- Use optimistic locking – Field-level RowVersion checks prevent lost updates
- Handle conflicts gracefully – Show user-friendly conflict dialogs, not 409 errors
- Test offline scenarios – Simulate network disconnects during integration testing
- Log all conflicts – Application Insights helps identify conflict hotspots
- Cache presence data – Redis lookup must be <10ms for smooth UX