When building internal enterprise tools, one of the first requirements that eventually appears is deceptively simple:
Who has which device?
A company may have hundreds or thousands of laptops, phones, tablets, and other endpoints assigned to employees across departments and locations.
At first, this sounds like a straightforward CRUD problem.
You create a Device collection, associate devices with employees, and build a dashboard.
Then the organization introduces a Mobile Device Management (MDM) platform such as Fleet, Microsoft Intune, or Jamf.
And suddenly, the architecture becomes much more interesting.
You now have two systems that know about devices—but they know about them for very different reasons.
Your CRM cares about:
- Who is assigned a device
- When the device was assigned
- Which department the employee belongs to
- Whether the device is available or assigned
- The history of assignments
- Administrative actions performed on the device
The MDM cares about:
- Operating system information
- Hardware identifiers
- Device health
- Last check-in time
- Security configuration
- Remote device operations
That creates an important architectural question:
Where should the source of truth live?
In this post, I'll walk through the architecture we used to build an MDM-agnostic Device Management module using NestJS, MongoDB/Mongoose, and TanStack Start, with Fleet as the initial MDM provider.
The goal was simple:
Use the capabilities of an MDM without allowing the MDM vendor to become part of our CRM's core architecture.
The Problem With Coupling Your CRM Directly to Your MDM
The easiest implementation is often the most dangerous one.
Imagine your CRM frontend needs to display an employee's device.
Instead of querying your own database, the frontend calls Fleet:
CRM → Fleet API → Device
Or perhaps you synchronize Fleet's database structure directly into your CRM:
Fleet data model → CRM data model
Both approaches work.
Until they don't.
The problem is that Fleet is an implementation detail.
Your CRM shouldn't fundamentally care whether the underlying MDM is Fleet, Intune, Jamf, or another provider.
Your business requirement is:
"Show me the device assigned to this employee."
The MDM requirement is:
"Give me the technical state of this endpoint."
Those are related concerns, but they are not the same concern.
If the CRM becomes tightly coupled to Fleet, switching providers becomes an application rewrite instead of a provider migration.
That is the architectural problem we wanted to avoid.
The Architecture: A Tale of Two Contexts
We solved this by drawing a hard boundary between business state and technical state.
graph TD
A[CRM Frontend] -->|REST API| B(NestJS Backend)
B -->|Mongoose| C[(MongoDB)]
B -->|Provider Interface| D[Fleet API]
D --> E[Employee Devices]
The important part of this architecture isn't simply that NestJS sits between the frontend and Fleet.
The important part is what NestJS owns.
Our CRM owns the business representation of a device.
The MDM owns the technical representation.
The backend provides the translation layer between the two.
The CRM owns three important concepts
1. Device
A generic CRM representation of a device.
It contains a decoupled providerId rather than exposing Fleet-specific implementation details throughout the application.
2. DeviceAssignment
A historical ledger describing who owned which device and when.
This is business information.
For example:
MacBook Pro
↓
Assigned to Emmanuel
↓
Engineering
↓
Assigned: January 2026
↓
Returned: June 2026
That history belongs to the CRM.
3. DeviceAction
An immutable record of administrative actions performed against a device.
Examples include:
- Lock
- Wipe
- Other administrative operations
The MDM provides the mechanism for executing the operation.
The CRM owns the record of the business action.
This separation is the foundation of the architecture.
1. The Provider Abstraction
Once we decided that Fleet should be replaceable, the next question was:
How do we prevent Fleet-specific code from leaking into the rest of the application?
The answer was an abstraction.
In NestJS, we created a DeviceProvider interface.
export interface DeviceProvider {
getDevice(providerId: string): Promise<RemoteDevice | null>;
listDevices(): Promise<RemoteDevice[]>;
lockDevice(providerId: string): Promise<DeviceActionResponse>;
wipeDevice(providerId: string): Promise<DeviceActionResponse>;
}
This interface defines what our application needs from an MDM provider.
Notice what isn't there.
There is no:
FleetDevice
There is no Fleet-specific API request.
There is no Fleet authentication logic.
There is no Fleet-specific HTTP implementation.
The rest of the application only knows that a device provider can:
- Retrieve a device
- List devices
- Lock a device
- Wipe a device
That's it.
Fleet becomes an implementation detail
We then implemented a FleetProvider.
Its responsibility is to deal with Fleet's API, authentication headers, request formats, response formats, and other provider-specific concerns.
Conceptually:
DeviceService
│
▼
DeviceProvider
│
▼
FleetProvider
│
▼
Fleet API
If the organization eventually decides to move from Fleet to Microsoft Intune, the architecture doesn't need to change fundamentally.
We implement:
IntuneProvider
which satisfies the same interface.
The application continues talking to:
DeviceProvider
rather than directly to:
FleetProvider
This is a small abstraction with a significant architectural benefit.
The provider becomes replaceable.
2. Background Synchronization
There is another problem with calling the MDM directly from the frontend.
External APIs introduce latency and availability concerns.
If every time an administrator opens the device dashboard we do this:
Frontend
↓
NestJS
↓
Fleet
↓
NestJS
↓
Frontend
then the user experience depends on the MDM's response time.
It also means our CRM becomes unnecessarily dependent on the availability of the external provider.
Instead, we introduced background synchronization.
Using NestJS's scheduling capabilities, a DeviceSyncWorker periodically retrieves device information from the provider.
Every five minutes, the worker asks the FleetProvider for the current device list.
The relevant technical information is then synchronized into MongoDB.
For example:
lastSeenAtosVersion- Hardware information
- Provider identifiers
The hardware serial number is used as the unique key for the synchronization process.
Conceptually:
Every 5 minutes
│
▼
DeviceSyncWorker
│
▼
FleetProvider
│
▼
Fleet API
│
▼
Device data
│
▼
MongoDB
Now the frontend doesn't need to wait for Fleet.
Instead:
TanStack Start
↓
NestJS API
↓
MongoDB
The dashboard is reading from our own database.
This gives us a much more predictable frontend experience while still keeping the CRM's technical device information reasonably synchronized with the MDM.
3. Auditable Administrative Actions
Device management isn't just another CRUD feature.
Some operations are destructive.
Consider:
Wipe Device
If an administrator accidentally wipes the wrong laptop, the consequences can be significant.
If an administrator's account is compromised, the consequences can be even worse.
That means device actions need to be treated differently from ordinary database updates.
We introduced explicit action records and an audit trail.
Before communicating with the MDM provider, the DeviceService creates a DeviceAction record.
For example:
// 1. Create audit log
const audit = new this.actionModel({
deviceId: device._id,
actionType: 'WIPE',
initiatedBy: userId,
reason: requestReason, // Explicitly required from the frontend!
status: ActionStatus.PENDING,
});
await audit.save();
// 2. Perform action via provider
const result = await this.deviceProvider.wipeDevice(device.fleetHostId);
// 3. Update audit log success/failure
This creates an important sequence:
Admin requests WIPE
↓
Create audit record
↓
Status = PENDING
↓
Call MDM provider
↓
Operation succeeds/fails
↓
Update action record
The system therefore has a durable record of what happened.
Why create the record first?
Because the action itself is important business information.
We don't want the only evidence of a wipe attempt to exist inside the MDM provider.
The CRM should know:
- Which device was targeted
- Who initiated the action
- Why the action was requested
- What action was requested
- Whether it is pending
- Whether it succeeded
- Whether it failed
The MDM executes the operation.
The CRM maintains the business-level audit history.
Global Audit Logging
We also configured a NestJS AuditTrailInterceptor to globally record operations across the API.
That includes:
GET
POST
PATCH
DELETE
This provides a broader application-level audit trail while DeviceAction provides a domain-specific record for sensitive device operations.
The distinction is useful.
The interceptor answers:
"What happened in the application?"
The DeviceAction answers:
"What administrative action was requested against this device?"
Together, they provide much stronger visibility into device management activity.
4. The Frontend Experience
Once the backend has abstracted the MDM provider, the frontend becomes significantly simpler.
We used TanStack Start to build the Device Management experience.
The administrator doesn't need to leave the CRM and open Fleet just to perform an action.
Instead, the CRM provides a native device dashboard.
From the frontend's perspective, the operation is simple:
// Inside our DeviceActionDialog component
const handleConfirm = async () => {
if (actionType === "LOCK") {
await apiActions.devices.lock(id, { reason });
toast.success("Lock command sent successfully.");
}
};
Notice something important here.
The frontend doesn't know that Fleet exists.
It doesn't need to know:
Fleet API
Fleet authentication
Fleet request format
Fleet response format
It simply asks the backend:
Lock this device.
The backend handles everything else.
That is exactly what a good abstraction should achieve.
What Happens If We Replace Fleet?
This is where the architecture pays off.
Suppose the organization decides:
"We're moving from Fleet to Microsoft Intune."
With a tightly coupled architecture, that could mean changing:
- Frontend API calls
- Device models
- Controllers
- Services
- Authentication
- Provider-specific response handling
- Database structures
- Business logic
With our abstraction, the migration becomes substantially more contained.
We can introduce:
class IntuneProvider implements DeviceProvider {
// Intune-specific implementation
}
The rest of the application continues using:
DeviceProvider
The CRM doesn't need to know whether the underlying provider is Fleet or Intune.
That is the real value of the abstraction.
The Architectural Principle
The deeper lesson here isn't specifically about Fleet.
It's about dependency boundaries.
An MDM is an external infrastructure dependency.
Your CRM is a business system.
Those two systems should communicate, but they shouldn't become the same system.
A useful mental model is:
Business Domain
│
▼
CRM Device Model
│
▼
Provider Abstraction
│
├──────── Fleet
│
├──────── Intune
│
└──────── Jamf
The business domain remains stable.
The infrastructure dependency can change.
That's a much healthier boundary.
Final Architecture
The resulting architecture gives us four important properties:
1. Vendor independence
Fleet is the initial provider, but the CRM isn't architecturally dependent on Fleet.
2. Faster application reads
The frontend reads synchronized device information from our MongoDB rather than waiting for the MDM provider on every dashboard request.
3. Auditable operations
Administrative actions such as lock and wipe are explicitly recorded before the provider operation is performed.
4. Cleaner frontend development
The frontend works with business-level device APIs instead of provider-specific MDM APIs.
Conclusion
When integrating an MDM into an internal CRM, it can be tempting to treat the MDM as the application's device database.
That approach works until your business requirements and your infrastructure requirements begin to diverge.
The better approach is to recognize that they represent two different contexts.
The CRM owns the business state:
Who owns the device?
When was it assigned?
What actions have been performed?
The MDM owns the technical state:
What operating system is installed?
When did the device last check in?
What remote operations can be executed?
NestJS sits between those worlds.
MongoDB provides the CRM's local representation.
A provider abstraction isolates the external MDM.
Background synchronization keeps technical information available locally.
And explicit action records provide an auditable trail for sensitive operations.
The result is a Device Management module that can use Fleet today without making Fleet a permanent architectural dependency.
The goal isn't simply to integrate an MDM.
The goal is to integrate it without allowing it to define your application's architecture.
Top comments (0)