From platformatic-runtime
ALWAYS consult this skill when the user works with Platformatic Runtime, watt.json, the create() function pattern, inter-service communication via plt.local, or any project using @platformatic/* packages. This skill contains critical patterns for multi-service composition, runtime configuration, service entry points, and testing strategy that Claude cannot produce correctly without consulting. Without this skill, Claude will incorrectly call app.ready() inside create(), hardcode service URLs instead of using plt.local, and misconfigure watt.json environment variable mapping. Covers: watt.json configuration (dev and production), create() function pattern, inter-service communication via plt.local, pnpm workspace monorepo structure, service configuration with env-schema, unit vs integration testing strategy, common pitfalls. Trigger on ANY mention of: platformatic, watt, watt.json, plt, plt.local, @platformatic/*, create() function, platformatic runtime, platformatic service, platformatic node, entrypoint, watt build, watt resolve, watt.production.json, service composition, multi-service runtime.
How this skill is triggered — by the user, by Claude, or both
Slash command
/platformatic-runtime:platformatic-runtimeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides patterns for managing projects based on **Platformatic Runtime** — a framework that orchestrates multiple Node.js/Fastify services in a single process, enabling efficient inter-service communication and centralized management.
This skill provides patterns for managing projects based on Platformatic Runtime — a framework that orchestrates multiple Node.js/Fastify services in a single process, enabling efficient inter-service communication and centralized management.
@platformatic/node or @platformatic/service)project-root/
├── watt.json # Runtime configuration (development)
├── watt.production.json # Runtime configuration (production)
├── package.json # Monorepo root
├── pnpm-workspace.yaml # pnpm workspace definition
├── test/ # Centralized tests for all services
│ ├── helpers/
│ │ └── localtest.ts # Test environment setup
│ ├── scripts/
│ │ ├── executors/ # Testcontainers setup/teardown
│ │ └── runners/ # Container runners (mysql, redis, localstack)
│ ├── unit/ # Unit tests per service
│ │ ├── service-a/
│ │ └── service-b/
│ └── integration/ # Integration tests per service
│ ├── service-a/
│ └── service-b/
├── web/ # Platformatic services
│ ├── service-a/
│ │ ├── watt.json # Service configuration
│ │ ├── index.ts # Entry point (export create())
│ │ ├── config.ts # Environment configuration
│ │ ├── plugins/ # Fastify plugins
│ │ ├── routes/ # HTTP/REST routes
│ │ ├── graphql/ # GraphQL resolvers and schema
│ │ ├── helpers/ # Business logic
│ │ └── types/ # TypeScript types
│ └── service-b/
├── db/ # Prisma schema per service
│ └── service-a/
│ ├── schema.prisma
│ └── prisma.config.ts
├── clients/ # Generated API clients
├── plugins/ # Common plugins shared across services
├── cdk/ # AWS CDK infrastructure
└── external/ # External services (git resolved)
pnpm-workspace.yaml:
packages:
- "web/*"
- "plugins"
- "clients"
Root package.json:
{
"name": "my-platform",
"private": true,
"scripts": {
"dev": "watt",
"build": "watt build",
"test": "tap --timeout=90",
"test:coverage": "tap --coverage-report=lcovonly --coverage-report=text"
},
"engines": {
"node": ">=22.0.0"
}
}
For tap
before/afterhooks,localtest.tshelper, and Testcontainers executor/runner setup, see the tap-testcontainers skill.
watt.json{
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/3.19.0.json",
"watch": true,
"applications": [
{
"id": "service-a",
"path": "./web/service-a",
"env": {
"DATABASE_NAME": "{DATABASE_NAME_SERVICE_A}"
}
},
{
"id": "external-service",
"path": "{PLT_EXTERNAL_PATH}",
"url": "[email protected]:org/external-service.git",
"gitBranch": "main",
"watch": false
},
{
"id": "gateway",
"path": "./web/gateway",
"config": "./watt.dev.json"
}
],
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
},
"managementApi": "{PLT_MANAGEMENT_API}",
"entrypoint": "gateway"
}
watt.production.json{
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/3.19.0.json",
"watch": false,
"applications": [
{
"id": "service-a",
"path": "./web/service-a"
},
{
"id": "gateway",
"path": "./web/gateway"
}
],
"logger": {
"level": "info"
},
"server": {
"hostname": "0.0.0.0",
"port": "{PORT}"
},
"managementApi": false,
"entrypoint": "gateway"
}
Variables in the format {VAR_NAME} are replaced with values from:
.env file in the project rootenv configuration in the applications blockEach service has its own watt.json:
{
"$schema": "https://schemas.platformatic.dev/@platformatic/node/3.19.0.json"
}
config.tsEach service defines its own typed configuration using env-schema and typebox:
// web/service-a/config.ts
import { type Static, Type } from "typebox";
import { envSchema } from "env-schema";
const ConfigSchema = Type.Object({
APP_ENV: Type.String({ default: "dev" }),
DATABASE_NAME: Type.String(),
REDIS_URL: Type.String({ default: "redis://localhost:6379" }),
});
const config = envSchema<Static<typeof ConfigSchema>>({
schema: ConfigSchema,
dotenv: false, // env vars come from watt.json and .env at root level
});
const configExport = {
env: config.APP_ENV,
database: {
name: config.DATABASE_NAME,
},
redis: {
url: config.REDIS_URL,
},
};
export type ConfigType = typeof configExport;
export default configExport;
create() functionEach Platformatic service must export a create() function that returns a Fastify instance:
// web/service-a/index.ts
import path from "node:path";
import autoload from "@fastify/autoload";
import fastifyRedis from "@fastify/redis";
import fastify, { type FastifyInstance } from "fastify";
import config, { type ConfigType } from "./config.js";
declare module "fastify" {
export interface FastifyInstance {
config: ConfigType;
}
}
export async function create(): Promise<FastifyInstance> {
const app = fastify({ logger: true });
app.decorate("config", config);
await app.register(fastifyRedis, config.redis);
await app.register(autoload, {
dir: path.resolve(import.meta.dirname, "./plugins"),
options: { ...config },
});
await app.register(autoload, {
dir: path.resolve(import.meta.dirname, "./routes"),
});
return app;
}
Note: Do NOT call app.ready() inside create() — the Platformatic Runtime handles the lifecycle.
// web/service-a/routes/v1/index.ts
import type { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import type { FastifyInstance, FastifyServerOptions } from "fastify";
import { Type } from "typebox";
const ResponseSchema = Type.Object({
success: Type.Boolean(),
data: Type.Optional(Type.Any()),
});
export default function (
_fastify: FastifyInstance,
_options: FastifyServerOptions,
done: (err?: Error) => void,
) {
const fastify = _fastify.withTypeProvider<TypeBoxTypeProvider>();
fastify.get(
"/items",
{
schema: {
tags: ["items"],
response: {
200: ResponseSchema,
},
},
},
async (request, reply) => {
// Business logic via helpers
reply.send({ success: true, data: [] });
},
);
done();
}
Plugins in the root plugins/ directory are shared across multiple services. Register them via autoload or explicit import:
// plugins/prisma/index.ts
import type { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import { PrismaClient } from "@prisma/client";
declare module "fastify" {
export interface FastifyInstance {
prisma: PrismaClient;
}
}
const prismaPlugin = (
fastify: FastifyInstance,
options: { databaseUrl: string },
next: (err?: Error) => void,
) => {
const prisma = new PrismaClient({
datasources: {
db: { url: options.databaseUrl },
},
});
fastify.decorate("prisma", prisma);
fastify.addHook("onClose", async () => {
await prisma.$disconnect();
});
next();
};
export default fp(prismaPlugin, {
name: "prisma",
});
Services within the same runtime can communicate via fetch using the service name as hostname. The runtime intercepts these calls and routes them internally (no network overhead):
// Inside service-a, calling service-b
const response = await fetch("http://service-b.plt.local/api/data", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
The pattern http://<service-id>.plt.local routes the request directly to the target service within the runtime process.
For Testcontainers setup (executors, runners,
localtest.ts, reaper management), see the tap-testcontainers skill. This section covers Platformatic-specific testing patterns only.
IMPORTANT: Do not always use the create() function for tests.
create() only to verify the correct startup of the entire application and the interaction between componentsThis approach ensures:
Test a single route by registering only what it needs:
// test/unit/service-a/routes/v1/items.test.ts
import "../../../helpers/localtest";
import t from "tap";
import fastify from "fastify";
import type { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import itemsRoute from "../../../../../web/service-a/routes/v1/index.js";
t.test("GET /items returns success", async (t) => {
const app = fastify({ logger: false })
.withTypeProvider<TypeBoxTypeProvider>();
// Register only the route under test (and its direct dependencies)
await app.register(itemsRoute);
await app.ready();
t.teardown(() => app.close());
const response = await app.inject({
method: "GET",
url: "/items",
});
t.equal(response.statusCode, 200);
const body = response.json();
t.equal(body.success, true);
});
Test the full service startup using create():
// test/integration/service-a/startup.test.ts
import "../../helpers/localtest";
import t from "tap";
import { create } from "../../../web/service-a/index.js";
t.test("service-a starts correctly", async (t) => {
const app = await create();
await app.ready();
t.teardown(() => app.close());
t.ok(app.config, "config is decorated");
t.ok(app.redis, "redis plugin is registered");
const response = await app.inject({
method: "GET",
url: "/up",
});
t.equal(response.statusCode, 200);
});
The folder structure in test/unit/service-name/ should mirror web/service-name/:
web/service-a/ test/unit/service-a/
├── routes/ ├── routes/
│ └── v1/ │ └── v1/
│ └── index.ts │ └── items.test.ts
├── helpers/ ├── helpers/
│ └── calculator.ts │ └── calculator.test.ts
└── plugins/ └── plugins/
└── auth.ts └── auth.test.ts
# Development
pnpm dev # Start runtime in watch mode
# Build
pnpm build # Build all services for production
# Testing
pnpm test # Run all tests (CI mode, containers via executors)
# Watt CLI
npx watt # Start runtime
npx watt build # Build runtime for deployment
npx watt resolve # Resolve git-based external services
test/ folder, organized by type (unit/integration) and then by servicecreate() only for integration: Use the create() function only for integration teststest/unit/service-name/ should mirror the structure of web/service-name/watt.json for shared dependenciesapp.ready() in create(): The runtime manages the service lifecycle — calling ready() inside create() can cause issueshttp://<service-id>.plt.local for internal calls — never hardcode ports or hostnamesapp.ready() in create(): The runtime calls ready() after all services are composed. Calling it inside create() bypasses inter-service wiring and may cause startup failureshttp://localhost:PORT for inter-service calls. Always use http://<service-id>.plt.local — the runtime intercepts these internallyenv mapping in watt.json: If a service needs an env var, it must be mapped in the applications[].env block. The service won't inherit root .env vars automatically unless they match the same namecreate() in unit tests: create() boots the full service with all plugins and dependencies. For unit tests, build a minimal Fastify instance with only what the test needsdotenv: true in service config: Services inside a runtime should use dotenv: false — env vars are provided by the runtime via watt.json, not by a .env file in the service directorynpx claudepluginhub fgiova/claude-marketplace --plugin platformatic-runtimeApplies agentic design patterns like multi-agent, tool use, guardrails, and exception handling to manage multi-tenant MERN platforms with Keycloak, MongoDB, Docker, and Node.js services.
Designs scalable backend APIs and microservices architectures focusing on service boundaries, data contracts, resilience, observability, and distributed systems. Use for new services or integration planning.
Implements backend services, APIs, and business logic. Builds features, fixes bugs, refactors code from specifications.