Skip to content

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:

embed.tsTypeScript
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/http is 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:

controllers/user.tsTypeScript
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:

Bash
# 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 list

App Methods

app.serve()

Start serving all transports. This is the primary way to start your application:

serve-options.tsTypeScript
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:

stop-app.tsTypeScript
await app.stop();

Direct invocation (tests)

A built app exposes its container and routes for in-process testing or direct invocation:

testing-direct.tsTypeScript
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:

app-with-features.tsTypeScript
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:

Bash
# 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 abc123

Transport Plugins

Transports are registered as plugins. When you import a transport package, it automatically registers itself with the cluster system:

transport-registration.tsTypeScript
// 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 justscale CLI 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
socket-info.tsTypeScript
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: true

Connecting to a Running Cluster

You can connect to a running cluster from another process:

connect-to-cluster.tsTypeScript
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:

user.test.tsTypeScript
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);
  });
});