Installation
Install OpenMDM, set up your database schema, and create your first MDMInstance.
Installation
This page gets you from zero to a working MDMInstance you can call methods on. The next page (Quick Start) wires it into a running HTTP server.
Prerequisites
- Node.js 20+ (or Bun 1.1+, or Deno 1.40+).
- A database supported by Drizzle ORM — Postgres, MySQL, or SQLite. All examples here use Postgres.
- A push provider. For public fleets with Google Play Services, use FCM (a Firebase project and a service-account JSON). For private networks or GMS-less devices, use MQTT with your own broker.
OpenMDM does not need a Firebase or FCM setup during development if you use MQTT or skip push entirely — but a push provider is mandatory for any real fleet, because commands are delivered via push wake-up.
Install the packages
The minimum set:
pnpm add @openmdm/core @openmdm/drizzle-adapter @openmdm/hono @openmdm/push-fcm
pnpm add drizzle-orm postgres honodrizzle-orm and postgres are peer-like deps that live in your app, not ours. hono is the HTTP framework — if you're already on Hono, you already have it.
If you're using MQTT instead of FCM:
pnpm add @openmdm/push-mqttIf you want APK hosting via presigned URLs:
pnpm add @openmdm/storage-s3Generate the database schema
OpenMDM ships its schema as a Drizzle-compatible definition you can generate into your project. The CLI writes it to a file you manage with drizzle-kit:
npx openmdm generate --adapter drizzle --provider pg --output ./src/db/schema.tsThen run Drizzle's normal migration workflow:
npx drizzle-kit generate
npx drizzle-kit migrateThis creates the mdm_devices, mdm_policies, mdm_commands, mdm_events, mdm_groups, mdm_applications, mdm_push_tokens, and mdm_app_deployments tables in your existing database, alongside your app's own tables.
Why no separate database? That's the whole point. OpenMDM writes into your Postgres so device state, user state, and business data live in one transactional world.
Create the MDM instance
Create one file — convention is src/mdm.ts — that exports a single configured MDMInstance:
import { createMDM } from '@openmdm/core';
import { drizzleAdapter } from '@openmdm/drizzle-adapter';
import { mdmSchema } from '@openmdm/drizzle-adapter/postgres';
import { fcmPushAdapter } from '@openmdm/push-fcm';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL!);
const db = drizzle(client, { schema: mdmSchema });
export const mdm = createMDM({
database: drizzleAdapter(db, {
tables: {
devices: mdmSchema.mdmDevices,
policies: mdmSchema.mdmPolicies,
applications: mdmSchema.mdmApplications,
commands: mdmSchema.mdmCommands,
events: mdmSchema.mdmEvents,
groups: mdmSchema.mdmGroups,
deviceGroups: mdmSchema.mdmDeviceGroups,
pushTokens: mdmSchema.mdmPushTokens,
appVersions: mdmSchema.mdmAppVersions,
rollbacks: mdmSchema.mdmRollbacks,
// Required for plugins that need cross-instance persistence
// (the kiosk plugin's lockout counters are the main example).
pluginStorage: mdmSchema.mdmPluginStorage,
},
}),
push: fcmPushAdapter({
credentialPath: './firebase-service-account.json',
// Data-only messages keep the agent in control of how the
// notification is surfaced — important for silent commands.
dataOnly: true,
}),
enrollment: {
// HMAC secret used to sign enrollment payloads. Must match the
// secret compiled into the Android agent build. Rotate by
// rebuilding the agent and the server in lockstep.
deviceSecret: process.env.DEVICE_SECRET!,
autoEnroll: true,
},
auth: {
// Secret used to sign device-facing JWTs after enrollment.
deviceTokenSecret: process.env.JWT_SECRET!,
},
// Persist plugin state through the database adapter. Required for
// plugins that track stateful per-device data (kiosk lockout
// counters, geofence zone dwell, etc.). Without this, plugins fall
// back to in-memory state that is lost on restart and silently
// breaks horizontal scaling.
pluginStorage: { adapter: 'database' },
serverUrl: process.env.SERVER_URL, // e.g. "https://mdm.example.com"
});That's the whole SDK boot. mdm is an object with mdm.devices, mdm.policies, mdm.commands, mdm.apps, mdm.groups, mdm.push, mdm.on(...), and more. You import it anywhere in your app that needs to talk to devices.
Environment variables
# Database
DATABASE_URL=postgres://user:password@localhost:5432/yourapp
# Enrollment HMAC secret — must match the Android agent's compiled-in secret
DEVICE_SECRET=<64-char hex; generate with `openssl rand -hex 32`>
# Device token signing secret (separate from DEVICE_SECRET by design)
JWT_SECRET=<64-char hex; generate with `openssl rand -hex 32`>
# Public base URL of your backend
SERVER_URL=https://mdm.example.com
# Firebase (if using FCM)
# OR provide credentialPath to a service-account JSONTwo secrets, not one.
DEVICE_SECRETsigns enrollment payloads (proves a device is allowed to enroll).JWT_SECRETsigns the device tokens returned after successful enrollment (proves a subsequent request is from an enrolled device). RotatingJWT_SECRETinvalidates all device sessions but does not force re-enrollment; rotatingDEVICE_SECRETrequires rebuilding and redeploying the Android agent.
Verify the install
Create a throwaway script to confirm the instance is healthy:
import { mdm } from '../src/mdm';
const { devices, total } = await mdm.devices.list({ limit: 1 });
console.log(`OpenMDM is alive. ${total} devices enrolled.`);
process.exit(0);pnpm tsx scripts/verify-mdm.tsIf it prints a device count (even zero), you're good.
Next
- Quick Start — mount the HTTP routes, enroll a test device, send your first command.
- Architecture — how enrollment, heartbeats, and commands actually flow between your server and the device.