Cluster Overview

Multi-transport application architecture

What is a Cluster?

A cluster is the recommended way to create JustScale applications. It wraps your app with multi-transport capabilities, allowing the same controllers to serve HTTP requests, CLI commands, and future transports like WebSockets or gRPC.

Instead of creating separate apps for HTTP and CLI, you define routes once and expose them over multiple transports simultaneously.

Creating a Cluster

Use createCluster() to create an application with full cluster capabilities:

index.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { UserController } from './controllers/user';
import { DbController } from './controllers/db';

const cluster = createCluster({
  controllers: [UserController, DbController],
});

// Start serving - HTTP + cluster socket for CLI
await cluster.serve({ http: 3000 });

This single call:

  • Creates the underlying JustScale app
  • Starts an HTTP server on port 3000 (if @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/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('/')
      .apply(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({ id: user.id });
      }),

    // 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, ['id', 'email', 'createdAt']);
      }),
  }),
});

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:
justscale user create --email test@example.com --password secret123
justscale user list

Cluster Methods

cluster.serve()

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

serve-options.tsTypeScript
import { createCluster } from '@justscale/cluster';

const cluster = createCluster({ controllers: [] });

// HTTP + cluster socket
await cluster.serve({ http: 3000 });

// Custom socket path
await cluster.serve({
  http: 3000,
  socketPath: '/tmp/my-app.sock',
});

// Socket only (no HTTP)
await cluster.serve({ noSocket: false });

// HTTP only (no socket - CLI won't work)
await cluster.serve({
  http: 3000,
  noSocket: true,
});

cluster.stop()

Stop the cluster and clean up all resources:

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

cluster.app

Access the underlying JustScale app for testing or direct execution:

cluster-app-usage.tsTypeScript
import { invoke } from '@justscale/cli';

// Direct command invocation
const result = await invoke(cluster.app, 'user create', {
  email: 'test@example.com',
  password: 'secret123',
});

// HTTP request simulation (in tests)
const response = await request(cluster.app)
  .post('/user')
  .send({ email: 'test@example.com', password: 'secret123' });

Features with Clusters

Features work seamlessly with clusters and can contribute routes to multiple transports:

cluster-with-features.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { AuthFeature } from '@justscale/auth';
import { UserController } from './controllers/user';

const cluster = createCluster({
  features: [
    AuthFeature(),
  ],
  controllers: [UserController],
});

await cluster.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:
justscale auth create-user --email admin@example.com
justscale auth list-users
justscale auth delete-session --id 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/cli';   // Registers CLI transport
import { createCluster } from '@justscale/cluster';
import { UserController } from './controllers/user';

// The cluster knows about all registered transports
const cluster = createCluster({
  controllers: [UserController],
});

// cluster.serve() starts all registered transports
await cluster.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 cluster.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 cluster.serve({ http: 3000 });

console.log(`Socket path: ${cluster.socketPath}`);
// Socket path: /tmp/justscale-1000.sock

console.log(`Is serving: ${cluster.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/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 with Clusters

For testing, you can access the underlying app without starting the server:

user.test.tsTypeScript
import { createCluster } from '@justscale/cluster';
import { invoke } from '@justscale/cli';
import { UserController } from './controllers/user';

describe('UserController', () => {
  const cluster = createCluster({
    controllers: [UserController],
  });

  test('creates user via CLI', async () => {
    const result = await invoke(cluster.app, 'user create', {
      email: 'test@example.com',
      password: 'secret123',
    });

    expect(result).toMatchObject({
      id: expect.any(String),
    });
  });

  test('creates user via HTTP', async () => {
    const response = await request(cluster.app)
      .post('/user')
      .send({
        email: 'test@example.com',
        password: 'secret123',
      });

    expect(response.status).toBe(200);
    expect(response.body.user).toMatchObject({
      email: 'test@example.com',
    });
  });
});

Comparison: ClusterBuilder vs Cluster

Understanding when to use each:

builder-vs-cluster.tsTypeScript
// createClusterBuilder - composable, type-safe DI
import { createClusterBuilder } from '@justscale/core';
import { UserController } from './controllers/user';
import { UserService } from './services/user';

const app = createClusterBuilder()
  .add(UserService)
  .add(UserController)
  .build()
  .compile();

// createCluster - multi-transport serving
import { createCluster } from '@justscale/cluster';

const cluster = createCluster({
  controllers: [UserController],
});

await cluster.serve({ http: 3000 });  // HTTP + CLI + future transports
💡

Tip

Use createClusterBuilder() for assembling your application with type-safe dependency injection. Use createCluster() for multi-transport serving.