Powerful, flexible, and tablet-ready MES for small manufacturers
๐ Try the live demo โ demo-2.getopenmes.com Free demo account โ active for 30 days, no credit card required
OpenMES is a self-hosted, open-source MES (Manufacturing Execution System) built for small and mid-sized manufacturers โ woodworking, metal fabrication, plastics and assembly shops โ who need real production tracking without enterprise complexity or licensing costs.
- ๐ฏ Purpose-built for small manufacturers โ woodworking, metal fabrication, plastics and assembly shops. No enterprise bloat, just what a real shop floor needs
- ๐ Truly free and open-source โ AGPL-3.0, no per-user fees, no modules to unlock, no vendor lock-in
- ๐ฑ Tablet-first design โ touch-optimized screens for shop-floor operators
- ๐ Real-time production visibility โ work orders, batches, quality checks, downtime reasons and reports for every line
- ๐ Connects to your machines and ERP โ native MQTT machine data collection, REST API, CSV/XLS import (SAP, Odoo, Comarch), PrestaShop connector
- ๐ Deploy in minutes โ single-command Docker deployment; an old office PC is enough
- ๐ Security-first โ OWASP Top 10 compliant from day one
- ๐ ISA-95 aligned โ Level 3 MES with ISA-95 / IEC 62264 hierarchies and MOM coverage (details)
Drag-and-drop production scheduling with Gantt-style views across multiple production lines.
- Weekly / Daily / Hourly / Monthly views โ switch between planning horizons
- Drag & drop scheduling โ assign and move work orders across lines and shifts
- Hourly Gantt view โ minute-level precision with resize and cross-line moves
- Real-time polling โ live updates when changes happen on the shop floor
- Backlog panel โ unassigned orders with priority filtering and search
- Overdue alerts โ visual flagging of overdue orders on the timeline
- Multi-line production - Manage multiple production lines simultaneously
- Work order tracking - Complete work order lifecycle management
- Batch production - Support partial completion with multiple batches
- Process templates - Reusable, step-by-step process definitions
- Data import - Bulk-load product types, materials, work orders and recipes from CSV/Excel with column mapping, saved profiles and live progress
- Real-time status - Live production status updates
- Step-by-step guidance - Clear instructions for every operation
- Sequential workflow - Enforce process order to prevent mistakes
- One-tap actions - Start, complete, report issues with single tap
- PWA support - Install on tablets, works offline
- Offline mode - Queue actions when network is unavailable
- Tablet-optimized - Large touch targets (48px+), minimal text input
- Problem reporting - Operators report issues instantly from any step
- Automatic blocking - Critical issues halt production automatically
- Issue escalation - Route problems to supervisors with notifications
- Resolution tracking - Complete issue lifecycle (Open โ Acknowledged โ Resolved โ Closed)
- Predefined categories - Material shortage, quality issues, tool failures, etc.
- Supervisor Dashboard - Real-time KPIs and production metrics
- Interactive Charts - Throughput, cycle time, issue trends, step performance
- Production Reports - Summary, batch completion, downtime reports
- CSV Export - Export all reports for further analysis
- Traceability - Complete audit trail for every action
- Immutable audit logs - PostgreSQL-enforced, cannot be altered
- Complete traceability - Track every action, user, and timestamp
- Role-based access - Admin, Supervisor, Operator roles
- Line-based filtering - Operators only see assigned lines
- Compliance-ready - ISO 9001, AS9100 compatible audit trail
OpenMES is built to be extended! Use our comprehensive hook system to add custom functionality without modifying core code.
A module drops into backend/modules/, is toggled in Admin โ Modules, and
plugs into three kinds of extension point โ with zero runtime cost when
disabled:
- Domain events โ react to production activity:
- Work orders โ
WorkOrderCreated,WorkOrderUpdated,WorkOrderCompleted - Batches & steps โ
BatchCreated,StepStarted,StepCompleted - Machine โ
WorkstationStateChanged,MachineMessageReceived - Users โ
UserAssignedToLine ResourceChangedโ a generic CRUD hook for any curated resource (work orders, customers, materials, lines, โฆ) on create/update/deleteWorkOrderScheduledโ a work order is placed/moved on the planner
- Work orders โ
- Menu hooks (
MenuRegistry) โ add links and dropdowns to the sidebar - Dashboard widget hooks (
WidgetRegistry) โ add cards to the admin dashboard
// In your module's ServiceProvider::boot()
use App\Events\WorkOrder\WorkOrderCompleted;
use App\Services\MenuRegistry;
use Illuminate\Support\Facades\Event;
// React to an order completing (observe โ never mutate core state here)
Event::listen(WorkOrderCompleted::class, function ($event) {
ExternalErp::notifyCompletion($event->workOrder);
});
// Add a link to the sidebar
app(MenuRegistry::class)->addItem('production', 'My Page', url('/modules/mine'));- ERP Integration - Sync with SAP, Odoo, or custom systems
- Custom Notifications - Email, SMS, Slack, Teams
- Quality Control - Automated inspections and checks
- IoT Integration - Connect machines and sensors
- Custom Reports - Generate PDFs, Excel, or API exports
- Inventory Management - Auto-update stock levels
- Barcode/RFID - Track materials and products
๐ ๏ธ Build a module (step-by-step tutorial): backend/modules/README.md โ from an empty folder to a running module, plus a code map of how it all works
๐ Hook reference: HOOKS.md โ every event, menu and widget hook, with payloads and examples
๐ Reference Modules: backend/modules/ExampleShowcase (all hooks) ยท backend/modules/ExampleHooks (minimal)
OpenMES ships with optional modules that can be enabled from Admin โ Modules.
Dedicated station for scanning finished products with a barcode reader (EAN/QR) before shipping or warehouse handoff.
How it works:
- Operator opens
/packaging/stationon a dedicated workstation or tablet - Scans an EAN barcode with a USB/Bluetooth reader (or types it manually)
- The system looks up which work order the EAN belongs to and increments its
packed_qtycounter - Live stats update every 3 seconds: packed today, plan, backlog, realisation %
Features:
- EAN management โ assign one or multiple EAN codes to any work order (
Admin โ Packaging โ EAN Codes) - Scan history โ every scan is logged with timestamp, user, and result (success / unknown EAN / error)
- Shift-based counters โ
packed_qtyresets each shift; shift boundaries are configurable - Manual reset โ
php artisan packaging:reset-shiftresets all counters immediately - Admin dashboard โ read-only overview of all lines with the same live stats as the operator view
Routes:
| URL | Access | Description |
|---|---|---|
/packaging/station |
Operator, Supervisor, Admin | Scanning station |
/packaging/ |
Supervisor, Admin | Admin overview |
/packaging/eans |
Supervisor, Admin | EAN code management |
Required migrations (run automatically on first deploy):
create_work_order_eans_table
create_packaging_scan_logs_table
add_packed_qty_to_work_orders_table
OpenMES uses a dead-simple Laravel monolith architecture:
โโโโโโโโโโโโโโโโโโโ
โ Laravel App โ :80 (serves everything)
โ (Blade + API) โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โโโโโโผโโโโโโ
โ PostgreSQLโ
โโโโโโโโโโโโ
Stack:
- Backend: Laravel 12 with Blade templates
- Frontend: Tailwind CSS 4 + Alpine.js for interactivity
- Real-time: Livewire 4 for dynamic components
- Charts: Chart.js for analytics
- Database: PostgreSQL 17+ with immutable audit logs
- Deployment: Docker Compose (2 containers only!)
- Ultra Simple: Just 2 containers (Laravel + PostgreSQL)
- One-Command Install: clone, run installer, done
- No Reverse Proxy: Laravel serves directly on port 80
- Easy Maintenance: Single codebase, traditional Laravel patterns
- LAN Optimized: Server-rendered pages, perfect for local networks
- Mobile Ready: Responsive Blade templates work on tablets
- Fast: Built-in assets compilation with Vite
OpenMES architecture maps onto the ISA-95 / IEC 62264 standard for Manufacturing Operations Management at Level 3. See docs/isa95.md for the full coverage matrix across Production, Maintenance, Quality, and Inventory operations.
- Docker & Docker Compose (20.10+)
- Git
One command โ clone and run the installer:
git clone https://github.com/Mes-Open/OpenMes.git
cd OpenMes
./install.shinstall.sh generates secure credentials into .env, auto-selects a free host port (80 if it's available, otherwise the next free one โ e.g. 8080), builds the app from the cloned source, and starts it in production. When it finishes it prints your URL and admin login. Use ./install.sh --yes to accept all defaults non-interactively.
Windows? In PowerShell (Docker Desktop) run the equivalent installer โ same behaviour as install.sh:
.\install.ps1(Under WSL2 just use ./install.sh.)
Prefer plain Compose? docker compose up -d --build also works โ it builds from source and serves on port 80 (override with HTTP_PORT/HTTPS_PORT in .env if 80 is taken).
With Docker, the database is migrated and the admin account is created
automatically on first boot (from the credentials in .env) โ no manual wizard
step is needed. Open the URL the installer printed and log in.
Running OpenMES outside Docker (bare PHP)? A web-based setup wizard guides you through database and admin configuration the first time you open the site.
- Login with your admin credentials
- Create production lines in the admin panel
- Add users (operators, supervisors) and assign them to lines
- Import work orders via CSV or create manually
- Install PWA on tablets for offline support
Containers not starting?
# Check container logs
docker-compose logs backend
docker-compose logs postgres
# Restart containers
docker-compose restart
# Rebuild containers (if needed)
docker-compose down
docker-compose build --no-cache
docker-compose up -dDatabase connection errors?
# Make sure postgres is healthy
docker-compose ps
# Check database credentials
grep DB_PASSWORD .env backend/.env
# Restart backend
docker-compose restart backendApplication not loading?
# Check if services are running
docker-compose ps
# View backend logs
docker-compose logs -f backend
# Rebuild backend (includes asset build)
docker-compose build --no-cache backend
docker-compose up -dPort 80 already in use?
# Check what's using port 80
sudo lsof -i :80
# Edit docker-compose.yml to use different port:
# Change: - "80:8000" to "8080:8000"
# Then access at: http://localhost:8080- Open Safari and navigate to OpenMES
- Tap the Share button
- Select "Add to Home Screen"
- Name it "OpenMES" and tap Add
- Launch from home screen
- Open Chrome and navigate to OpenMES
- Tap the menu (โฎ)
- Select "Install app" or "Add to Home Screen"
- Confirm installation
- Launch from home screen
Benefits:
- Full-screen mode (no browser chrome)
- Works offline with automatic sync
- Native app-like experience
- Touch-optimized for manufacturing floor
- User Guides - Operator, Supervisor, and Admin guides
- Customization Layers - Standard / configuration / customer vs vendor custom development
- API Documentation - REST API reference
- PWA Testing Guide - Offline functionality testing
- Technical Documentation - For developers
- MQTT Connectivity Testing - Machine connection testing guide
- Usage reporting - What OpenMES reports about itself, and how to switch it off
OpenMES reports on itself once a day โ versions, which features are switched on, rough size bands, and where errors occur (class, file and line). It never sends anything you entered into OpenMES: no material or product codes, no lot numbers, no order data, no recipes, no personal data, and no error message text. That boundary is enforced by tests, not by good intentions.
It is on by default. Switch it off in Settings โ System โ Usage reporting,
or set OPENMES_TELEMETRY=false before first start. An installation with no
route to the internet behaves exactly like a connected one โ nothing breaks,
nothing slows down, and the log does not fill up.
See what would be sent, at any time, with php artisan telemetry:send --dry-run.
Full details in docs/telemetry.md.
We welcome contributions! Whether it's bug reports, feature requests, documentation, or code - we'd love your help.
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests
- Submit a pull request
See CONTRIBUTING.md for details.
OpenMES is incrementally adopting React via Inertia.js alongside the existing Blade + Livewire UI. Both render trees coexist โ new pages can opt into React without touching anything else.
The point of this setup: pull the repo, drop it on a server (FTP/SSH/whatever), edit .jsx files in place, refresh the browser. No npm install on your laptop, no build step in your hands. A container handles it.
Start the stack with the dev overlay:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -dThat overlay (docker-compose.dev.yml):
- Spins up a
frontendcontainer runningvite build --watch. It watchesbackend/resources/and rebuildsbackend/public/build/in ~100ms whenever you save a file. - Bind-mounts the application source (
app/,routes/,resources/,public/, โฆ) into the backend container so PHP/Blade edits are live too.
Workflow:
- Edit
backend/resources/js/Pages/Foo.jsx(or any source file). - The watcher rebuilds automatically โ check
docker compose logs -f frontendif you want to see it. - Refresh the browser.
-
Create the page component at
backend/resources/js/Pages/Foo.jsx:import { Head } from '@inertiajs/react'; export default function Foo({ greeting }) { return ( <> <Head title="Foo" /> <h1>{greeting}</h1> </> ); }
-
Add a route in
backend/routes/web.php:Route::get('/foo', fn () => Inertia::render('Foo', [ 'greeting' => 'Hello from Laravel', ]));
-
Visit
/foo. Props from the controller arrive as React props.
Production deployments do not use docker-compose.dev.yml. The image's Dockerfile already runs npm ci && npm run build at image build time, so the production container ships with pre-built assets โ no Node process at runtime.
OpenMES live-syncs read data from Laravel to React/mobile clients over a single WebSocket. Clients subscribe to a named collection (a server-defined query) rather than querying tables directly: they load one snapshot over HTTP, then receive deltas as they happen. Writes still go through Laravel controllers as before.
1. snapshot: client โโบ GET /api/collections/{name} (CollectionController, authed + tenant-scoped)
โโ { rows: [...], at: <unix ts> }
2. deltas: Laravel โโบ CollectionChanged โโบ Reverb โโบ private channel col.{tenant}.{collection}
โโ insert / update / delete, applied by the client's collection adapter
writes: client โโบ Laravel controllers (validation, auth, Eloquent events) โโบ Postgres
- Laravel owns the collection registry (
app/Sync/ShapeRegistry.php+app/Sync/Shapes/): each entry pins a table, a column allowlist and an optional server-builtwhere. Clients request collections by name, never by table.CollectionControllerserves the snapshot;App\Events\CollectionChangedbroadcasts each row change. - Reverb runs as a sidecar container speaking the Pusher protocol. Channels
are private and namespaced per tenant (
col.{tenantKey}.{collection}), authorized inroutes/channels.phpvia the session cookie at/broadcasting/auth. - Caddy proxies
/app/*to Reverb. One multiplexed WebSocket carries every collection channel, so the browser's ~6-connection HTTP/1.1 cap doesn't apply and no PHP worker is ever held open. - Clients use
lib/echo.js(one app-widelaravel-echoconnection) andlib/realtimeCollection.js, which feeds a TanStack DB collection.
Two files:
-
Define the shape.
backend/app/Sync/Shapes/MyShape.php:class MyShape extends Shape { public function table(): string { return 'my_table'; } public function columns(): array { return ['id', 'name', 'status']; } public function where(User $user): ?string { return "tenant_id = {$user->tenant_id}"; } }
Simple lookup tables can skip the class and use an inline
['table', 'columns', 'where'?]config instead. -
Register it. Add to the
$shapesmap inbackend/app/Sync/ShapeRegistry.php:'my_collection_name' => MyShape::class,
Then subscribe from React โ the hooks in lib/useSyncedShape.js handle the
snapshot and the channel for you:
const { data, isLoading } = useSyncedShape('my_collection_name');useLiveShape is the same function under a different name (call-site intent
only). Hot app-wide collections are shared via LiveShapesProvider โ don't
subscribe to them again per page.
- Clients cannot pick the table โ they pick a collection name. Adding a new collection is a deliberate code change.
- Clients cannot pick which columns to read โ the shape's
columns()method is the whitelist. Sensitive columns (password hashes, tokens, PII) simply aren't listed. - Clients cannot escape the server WHERE โ both the snapshot and the delta projection are built server-side from the registry entry.
- Both halves are authorized: the snapshot endpoint requires auth and applies the
tenant scope, and the delta channel is private, so a user only receives their
own tenant's rows (plus global
"g"collections).
- App-server runtime. The backend serves via Laravel Octane on RoadRunner
(
octane:startis the DockerfileCMD) โ a concurrent, in-memory runtime. The oldphp artisan servewas single-threaded and serialized every request; it remains only as a documented dev fallback. Octane keeps the framework booted between requests, so watch for state that assumes a fresh boot per request (singletons, static props) โ validate the full app under Octane before a production rollout. - Broadcasting is synchronous.
CollectionChangedimplementsShouldBroadcastNowbecauseQUEUE_CONNECTION=sync; a write's broadcast happens inline with the request. Moving to a real queue driver would make delivery async โ check the latency assumptions in the UI before doing so. - Widget registry regression. The Blade dashboard supported a
WidgetRegistryextension API where modules registered Blade views into named zones (admin_dashboard.kpi,admin_dashboard.main,admin_dashboard.sidebar). The React dashboard does not render these. No bundled module currently uses the registry, so nothing actively breaks โ but a future React-based widget extension API needs to be designed before third-party modules can extend the new dashboard.
OpenMES uses a layered / dual-licensing model:
- Core โ GNU Affero General Public License v3.0 (AGPL-3.0) โ see LICENSE.
- Modules (
modules/) โ Academic Free License 3.0 (AFL-3.0) (permissive; modules may be closed/proprietary) โ see LICENSE-AFL-3.0.txt. - Commercial licenses are also available for partners who cannot accept AGPL obligations, OEM/white-label, and hosted/SaaS offerings.
Under AGPL-3.0 you can use, modify, distribute and use OpenMES privately, provided you disclose source for network/distributed modified versions, keep derivative works under AGPL-3.0, and state your changes.
Contributions are accepted under the Contributor License Agreement (CLA.md) โ you keep your copyright and grant the rights that make the dual-licensing model possible.
See LICENSING.md for the full model and CONTRIBUTING for how to contribute.
- ๐ Read the documentation
- ๐ Search existing issues
- ๐ฌ Ask in discussions
Need help with deployment, customization, or training? Contact us at jakub.przepiora@nice-code.com
Built with โค๏ธ for the manufacturing community
Made by manufacturers, for manufacturers
โญ If you find OpenMES useful, please give it a star!




