Run Nitro applications on Watt with @platformatic/nitro

Node.js TSC Member, Principal Engineer at Platformatic, Polyglot Developer. RPG and LARP addicted and nerd on lot more. Surrounded by lovely chubby cats.
Search for a command to run...

Node.js TSC Member, Principal Engineer at Platformatic, Polyglot Developer. RPG and LARP addicted and nerd on lot more. Surrounded by lovely chubby cats.
No comments yet. Be the first to comment.
All durable execution engines give the same versioning advice: pin your runs. Temporal pins runs to worker builds. Vercel's Workflow SDK pins each run to the deployment that started it. Azure Durable

Eve organizes AI agents in a simple way: use Markdown for instructions and skills, TypeScript for tools, and separate files for channels, schedules, and subagents. Behind this setup, Eve relies on the

Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs

How Platformatic ICC Outperforms AWS ECS Target Tracking and Step Scaling

Nitro is a server toolkit used by many JavaScript applications. You can use it on its own for APIs or full-stack servers, or combine it with Vite to get a familiar frontend workflow with a Nitro server in the background. Nitro also powers frameworks like Nuxt.
This flexibility is helpful, but running the application in production brings new challenges. A Nitro build gives you a portable Node.js server, but you still need a way to run it with other apps, expose it through a gateway, collect metrics, and make the most of your deployment’s CPU.
@platformatic/nitro adds Nitro as a first-class Watt capability. It lets Watt run existing Nitro applications without changing their application model. You keep Nitro’s routing, handlers, configuration, and build output. Watt takes care of the application lifecycle and the surrounding runtime.
This feature supports both standalone Nitro apps and Vite apps that use Nitro through nitro/vite. It also works with apps made by Lovable that use this stack. Nitro’s scheduled tasks are included too. The scheduler integration lets the Intelligent Command Center (ICC), Platformatic’s control plane for Node.js deployments, coordinate each run across instances so that only one instance runs each task.
Nitro applications commonly arrive in one of two shapes.
The first is a standalone Nitro application. It uses the Nitro CLI directly:
nitro dev
nitro build
node .output/server/index.mjs
The second is a Vite application that adds Nitro as a plugin. In that case, Vite runs the development server and build, then Nitro provides the production server output:
import { nitro } from 'nitro/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [nitro()]
})
@platformatic/nitro figures out which setup your app uses. If your app has a Vite config file, Watt lets Vite handle development and builds. If not, it uses the standalone Nitro commands. Either way, production uses the Nitro server that gets built.
That distinction matters because it keeps development familiar. Vite applications retain Vite’s development server and HMR. Standalone applications retain nitro dev. The production target is consistent: the server Nitro creates in .output.
If you use Nuxt, keep using @platformatic/nuxt. Nuxt already has its own Watt feature, and Watt will detect it before using the general Nitro integration.
Install Watt and the Nitro capability in the application:
npm install wattpm @platformatic/nitro
Add a watt.json file:
{
"$schema": "https://schemas.platformatic.dev/@platformatic/nitro/3.63.0.json",
"application": {
"basePath": "/frontend"
}
}
Then use the same commands that Watt applications use elsewhere:
wattpm dev
wattpm build
wattpm start
For a standalone application, basePath is passed to Nitro as NITRO_APP_BASE_URL during development and builds. This makes it possible to serve the application at a gateway path without hard-coding that deployment detail into the application.
By default, the settings match Nitro’s standard Node.js output: .output is the build directory and server/index.mjs is the production entry point. If your app uses different settings, update the capability to match.
{
"$schema": "https://schemas.platformatic.dev/@platformatic/nitro/3.63.0.json",
"application": {
"outputDirectory": "dist"
},
"nitro": {
"outputDirectory": "dist",
"entrypoint": "server/main.mjs"
}
}
When you deploy, include the complete output directory. Nitro puts the server, public assets, and copied runtime dependencies there; server/index.mjs alone is not enough.
This feature does not replace Nitro with a different server framework. Nitro still handles all requests. Watt simply starts the server and connects it to the rest of the runtime.
This setup gives your Nitro app the same operational features as other Watt deployments:
It can run beside APIs and other frontend applications in one Watt runtime.
A Watt gateway can expose it under a base path or route traffic to it from another application.
Watt collects HTTP metrics and applies its server configuration, including production HTTPS settings. It monitors worker health and Event Loop Utilization (ELU), and can automatically recycle unhealthy workers.
Production deployments can use direct injection, connection backlog configuration, and multi-worker reusePort support where configured.
If your team already uses Nitro, you don’t need a separate process manager or a custom deployment path. A Vite-plus-Nitro frontend can run in the same runtime as its backend services. A standalone Nitro server can join an existing Watt deployment without losing its CLI or output format.
Handling requests is just one part of what a Nitro app does. Nitro also runs scheduled tasks, but this can become a problem when you have more than one instance. The runtime that starts the server is best placed to solve this issue.
Nitro has its own scheduler. Tasks live in tasks/, and scheduledTasks maps cron expressions to them:
// tasks/db/cleanup.ts
export default defineTask({
meta: { name: 'db:cleanup', description: 'Remove expired records' },
run() {
return { result: 'ok' }
}
})
// nitro.config.ts
import { defineConfig } from 'nitro'
export default defineConfig({
experimental: { tasks: true },
scheduledTasks: {
'0 3 * * *': ['db:cleanup']
}
})
On a laptop, this is exactly right: one process owns the cron expression, and db:cleanup runs once at 3 am. In production, the same code behaves differently. Nitro starts that timer inside every process it runs, so an application with three instances gets three cleanups, three reports, or three data imports. That is a property of the application model, not a bug: nothing in the process knows how many siblings it has.
Taking the schedule out of the app isn’t a good solution, since the task needs the app’s code, credentials, and internal network access. What’s needed is a single owner for the clock and a way to send each run to just one ready instance, while the work stays in the app.
@platformatic/nitro provides that as a Nitro module. Add it to nitro.config and leave the tasks and cron expressions where they are:
// nitro.config.ts
export default defineConfig({
experimental: { tasks: true },
modules: ['@platformatic/nitro/scheduler'], // the only change
scheduledTasks: {
'0 3 * * *': ['db:cleanup']
}
})
The module disables Nitro’s in-process cron runner, reports the configured task groups to Watt, and writes a scheduler manifest into the production output so the built server carries the same information. Watt registers those groups as application scheduler jobs and invokes the tasks through its internal communication channel. experimental.tasks has to be enabled explicitly, because Nitro scans task files before it installs modules.
Nothing else changes. If ICC isn’t set up, Watt schedules and runs tasks locally at the times you specify. If ICC is enabled, it coordinates when work should run, but the selected Watt instance still does the work.
The scheduler guide covers the local configuration, and the runtime exposes the same operations over its management API (GET /api/v1/scheduler, plus pause, resume, and run for a named job). When a runtime is running, use these commands to inspect and control its jobs:
wattpm scheduler [runtime]
wattpm scheduler:pause [runtime] <name>
wattpm scheduler:resume [runtime] <name>
wattpm scheduler:run [runtime] <name>
The optional runtime argument can be a process ID or runtime name. You can leave it out if there’s only one Watt runtime. The list command shows each job’s cron expression, source, paused state, and next run. The other commands let you pause, resume, or run a job right away.
The runtime provides a scheduler inventory with each job’s name, cron expression, source, paused state, and last and next execution times. It includes two kinds of work:
Jobs configured in Watt, such as an HTTP callback to run a nightly cleanup.
Application-level jobs reported by a capability, such as Nitro scheduled tasks.
An external coordinator can check the inventory, pause a local trigger without deleting it, and tell Watt to run a specific job. Even when a job is paused, it can still be run, so ICC doesn’t need to know the app’s internal callback details to dispatch it.
Each instance reports its scheduler inventory to ICC, pauses local triggers when ICC owns scheduling, and reapplies the selected mode after a connection recovery. ICC uses the inventory to register the schedule, selects one ready instance when the cron expression fires, and sends that instance a run-scheduled-job command.
The selected instance calls Watt’s scheduler API, which runs the named job in the same runtime context it would have used locally. Configured jobs keep their callback, method, headers, and retry policy. Application jobs stay inside the capability that reported them. A failed execution is returned to ICC, which can record the failure and apply its scheduling policy; a successful run does not cause the other replicas to repeat the work.
This separation is useful operationally. ICC owns cluster-wide scheduling: one clock per job for the whole deployment rather than one clock per instance, with run history and pause-and-run-now controls exposed in the ICC user interface. It also picks a healthy target for each run. Watt and Nitro own the code that runs, application credentials, internal mesh access, and local retry behaviour. The scheduler does not need public callback URLs to reach an application in the Watt mesh, and moving a job between instances does not require copying job logic into the control plane. Execution is at least once, so scheduled handlers should be idempotent.
The contract is capability-neutral, so the same integration is available to Nuxt applications through @platformatic/nuxt/scheduler, added to modules in nuxt.config with the tasks defined in server/tasks. Both capabilities get the same task discovery, local and external mode switching, and on-demand execution. Runtime scheduler jobs, the HTTP callbacks configured in Watt itself, travel the same path, and there is a compatibility fallback for older runtime and ICC versions.
We plan to add scheduler coordination to all Watt features so they can use the same local and ICC-coordinated execution model.
@platformatic/nitro lets teams keep their existing Nitro apps and move them into the Watt runtime without changing the server, build, or development workflow. Both standalone services and Vite-plus-Nitro apps get the same gateway integration, metrics, HTTPS, and multi-worker production deployments.
If your team runs multiple instances, ICC adds a control plane to the runtime. It manages deployments and makes sure scheduled work runs only once per deployment. Your app code stays with Nitro, while Watt and ICC handle the runtime and operations.
The source is available in the Platformatic monorepo and the ICC repository.