# Run Nitro applications on Watt with @platformatic/nitro

[Nitro](https://nitro.build/) 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](https://docs.platformatic.dev/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](https://lovable.dev/) that use this stack. Nitro’s scheduled tasks are included too. The scheduler integration lets the [Intelligent Command Center (ICC)](https://icc.platformatic.dev/), Platformatic’s control plane for Node.js deployments, coordinate each run across instances so that only one instance runs each task.

* * *

## **Two ways to use Nitro**

Nitro applications commonly arrive in one of two shapes.

The first is a standalone Nitro application. It uses the Nitro CLI directly:

```plaintext
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:

```javascript
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.

* * *

## **Start with an existing application**

Install Watt and the Nitro capability in the application:

```plaintext
npm install wattpm @platformatic/nitro
```

Add a `watt.json` file:

```json
{
 "$schema": "https://schemas.platformatic.dev/@platformatic/nitro/3.63.0.json",
 "application": {
   "basePath": "/frontend"
 }
}
```

Then use the same commands that Watt applications use elsewhere:

```plaintext
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.

```json
{
 "$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.

* * *

## **A Nitro server in the Watt runtime**

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](https://docs.platformatic.dev/docs/reference/gateway/overview) 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.

* * *

## **Scheduled work without duplicate runs**

Nitro has its own scheduler. Tasks live in tasks/, and scheduledTasks maps cron expressions to them:

```typescript
// tasks/db/cleanup.ts
export default defineTask({
 meta: { name: 'db:cleanup', description: 'Remove expired records' },
 run() {
   return { result: 'ok' }
 }
})
```

```typescript
// 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:

```typescript
// 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](https://docs.platformatic.dev/docs/guides/scheduler) 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:

```plaintext
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 handoff from Watt to ICC**

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.

![](https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/72ce316f-4fbe-4a95-8f37-02e33e549e2e.png align="center")

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](https://icc.platformatic.dev/intelligent-command-center/user-interface/watt-detail/scheduled-jobs/): 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.

* * *

## **Conclusion**

`@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](https://github.com/platformatic/platformatic) and the [ICC repository](https://github.com/platformatic/intelligent-command-center).
