Cluster Overview
The Unix socket that ties your processes together
Info
This page documents the underlying app.serve() / JustScale().build() API. Most apps don't call it directly. The canonical entrypoint is defineApp(import.meta, env => JustScale().add(env)...) exported from src/app.ts; the just CLI handles env selection, build, compile, and serve. See Quick Start for the typical project shape (no main.ts, no manual app.serve()). The lower-level API below is for embedding, tests, and tooling that needs to drive the runtime by hand.
What is the Cluster?
When the app starts, JustScale opens a Unix domain socket alongside any HTTP / WebSocket / gRPC transports you wired up. That socket is the cluster — a private, same-user-only RPC channel that lets external processes drive the running app: the just CLI, sibling worker processes, signal delivery between durable processes, scheduled tasks, and HMR reload coordination.
The same controllers serve all of it. A route defined with Cli('migrate') is callable from the terminal via just migrate; a route defined with Get('/users') is callable over HTTP. Define once, expose across whichever transports your app loaded.
Creating a Cluster (low-level)
Under the hood, defineApp calls JustScale().add(...).build() and then app.serve(...). Tests and embeddings can do this by hand:
import JustScale from '@justscale/core';
import { UserController } from './controllers/user';
import { DbController } from './controllers/db';
const app = JustScale()
.add(UserController)
.add(DbController)
.build();
// Start serving — HTTP + cluster socket for CLI
await app.serve({ http: 3000 });This single call:
- Starts an HTTP server on port 3000 (when
@justscale/httpis loaded) - Creates a Unix socket for CLI communication
- Registers all transport handlers automatically
Multi-Transport Controllers
Controllers can define routes for multiple transports. Each transport has its own route factory:
import { createController } from '@justscale/core';
import { Get, Post } from '@justscale/http';
import { Cli } from '@justscale/core/cli';
import { body } from '@justscale/http/builder';
import { z } from 'zod';
import { UserService } from '../services/user';
const CreateUserArgs = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const UserController = createController('user', {
inject: { users: UserService },
routes: (services) => ({
// HTTP route - accessible via REST API
create: Post('/')
.body(CreateUserArgs)
.handle(async ({ body, res }) => {
const user = await services.users.create(body);
res.json({ user });
}),
// CLI route - accessible via CLI commands
createCli: Cli('create')
.input(CreateUserArgs)
.handle(async ({ args, io }) => {
const user = await services.users.create(args);
io.log(`Created user: ${user.email}`);
io.result({ email: user.email });
}),
// HTTP route for listing
list: Get('/')
.handle(async ({ res }) => {
const allUsers = await services.users.findAll();
res.json({ users: allUsers });
}),
// CLI route for listing
listCli: Cli('list')
.handle(async ({ io }) => {
const allUsers = await services.users.findAll();
io.table(allUsers, ['email', 'name']);
}),
}),
});This controller exposes both HTTP and CLI interfaces:
# HTTP usage:
curl -X POST http://localhost:3000/user \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123"}'
curl http://localhost:3000/user
# CLI usage:
just user create --email test@example.com --password secret123
just user listApp Methods
app.serve()
Start serving all transports. This is the primary way to start your application:
import JustScale from '@justscale/core';
const app = JustScale().build();
// HTTP + cluster socket
await app.serve({ http: 3000 });
// Custom socket path
await app.serve({
http: 3000,
socketPath: '/tmp/my-app.sock',
});
// Socket only (no HTTP)
await app.serve({ noSocket: false });
// HTTP only (no socket — CLI won't work)
await app.serve({
http: 3000,
noSocket: true,
});app.stop()
Stop the app and clean up all resources:
await app.stop();Direct invocation (tests)
A built app exposes its container and routes for in-process testing or direct invocation:
import { invoke } from '@justscale/core/cli';
// Direct CLI command invocation — no socket, no HTTP
const result = await invoke(app, 'user create', {
email: 'test@example.com',
password: 'secret123',
});
// In-process HTTP testing (no listen() required)
import { createTestClient } from '@justscale/testing';
import { httpTransport } from '@justscale/http/testing';
const client = await createTestClient(app, {
transports: { http: httpTransport },
});
const typed = client.http.useControllers({ users: UserController });Features with JustScale
Features plug into the same .add(...) chain and can contribute routes to multiple transports:
import JustScale from '@justscale/core';
import { AuthFeature } from '@justscale/auth';
import { UserController } from './controllers/user';
const app = JustScale()
.add(AuthFeature())
.add(UserController)
.build();
await app.serve({ http: 3000 });The auth feature now provides both HTTP and CLI routes:
# HTTP routes:
curl -X POST http://localhost:3000/auth/register
curl http://localhost:3000/auth/me?token=xxx
# CLI routes:
just auth create-user --email admin@example.com
just auth list-users
just auth delete-session --session abc123Transport Plugins
Transports are registered as plugins. When you import a transport package, it automatically registers itself with the cluster system:
// These imports auto-register their transports:
import '@justscale/http'; // registers HTTP transport
import '@justscale/core/cli'; // registers CLI transport
import JustScale from '@justscale/core';
import { UserController } from './controllers/user';
// The app knows about all registered transports
const app = JustScale()
.add(UserController)
.build();
// app.serve() starts all registered transports
await app.serve({ http: 3000 });Info
Future transports (WebSockets, gRPC, etc.) will follow the same pattern - just import the package and define routes using the transport's route factory.
Cluster Socket
When app.serve() is called, a Unix domain socket is created for inter-process communication. This socket:
- Enables the
justscaleCLI to communicate with your running app - Supports bidirectional streaming for progress indicators and prompts
- Uses local credentials for security (only accessible by the same user)
- Auto-cleans up on process exit
await app.serve({ http: 3000 });
console.log(`Socket path: ${app.socketPath}`);
// Socket path: /tmp/justscale-1000.sock
console.log(`Is serving: ${app.isServing}`);
// Is serving: trueConnecting to a Running Cluster
You can connect to a running cluster from another process:
import { connectToCluster } from '@justscale/core/cluster';
// Connect to the running cluster via Unix socket
const client = await connectToCluster();
// Invoke a CLI command
const result = await client.invoke('user create', {
email: 'test@example.com',
password: 'secret123',
});
// Stream output
client.on('stdout', (data) => console.log(data));
client.on('stderr', (data) => console.error(data));
await client.close();Testing a built app
For testing, drive the built app directly — no listen() needed:
import { describe, test } from 'node:test';
import assert from 'node:assert';
import JustScale from '@justscale/core';
import { invoke } from '@justscale/core/cli';
import { createTestClient } from '@justscale/testing';
import { httpTransport } from '@justscale/http/testing';
import { UserController } from './controllers/user';
describe('UserController', () => {
const app = JustScale()
.add(UserController)
.build();
test('creates user via CLI', async () => {
const result = await invoke(app, 'user create', {
email: 'test@example.com',
password: 'secret123',
});
assert.ok(typeof (result as any).id === 'string');
});
test('creates user via HTTP', async () => {
const client = await createTestClient(app, {
transports: { http: httpTransport },
});
const { api } = client.http.useControllers({ users: UserController });
const response = await api.users.create({
email: 'test@example.com',
password: 'secret123',
});
assert.strictEqual(response.status, 200);
});
});