Jumentix
Realtime blueprint

Bidirectional APIs with a built-in fallback

Run Socket.IO or gRPC as the primary interface while a separate REST process provides fallback and AsyncAPI documentation.

Jumentix open-source mascot

What this blueprint gives your team

  • Correlated request/response messages
  • Redis Streams and cluster resilience for Socket.IO
  • AsyncAPI contracts shared with generated clients
const socket = io('http://localhost:3001');

socket.emit('request', {
  id: crypto.randomUUID(),
  subject: 'tasks.create',
  payload: { title: 'Realtime task' },
});

socket.on('response', ({ id, payload }) => {
  console.log(id, payload);
});

From zero to first MVP

A practical launch sequence for this blueprint

Use this path when the first MVP must feel live: progress updates, collaboration, notifications, or command results that return through a bidirectional channel.

Pick the live moment

Choose the one event users must see without refreshing, such as task.created or task.statusChanged.

  • Choose tasks.create as the single command the MVP accepts over the socket.
  • Choose tasks.created as the single event the UI renders without refresh.
  • Freeze the message shape: subject plus payload, nothing anonymous.
  • Write acceptance criteria: unsupported subjects return an explicit error.

MVP output: one AsyncAPI channel and one correlated request/response subject.

type Task = { id: string; title: string; categoryId: string; completed: boolean };

type TaskCommand = {
  subject: 'tasks.create';
  payload: { title: string; categoryId: string };
};

type TaskEvent =
  | { subject: 'tasks.ready'; payload: { total: number } }
  | { subject: 'tasks.created'; payload: Task };

Definition of done for the first MVP

  • Every realtime response carries the subject the client sent — no anonymous side effects.
  • The REST fallback returns the same business result when the socket is unavailable.
  • A reconnect drill is captured as evidence before the pilot.
  • Realtime and fallback processes run under a named PM2 profile with inspectable logs.

Deliberately out of scope

  • Multi-region fan-out and Redis Streams clustering.
  • Presence, typing indicators, and history replay.
  • gRPC streaming beyond the first request/response subject.
  • Horizontal scaling of the socket layer.

MVP scope

Keep the first release small enough to prove

One live event

Start with one event that visibly changes the UI or confirms a command.

Realtime slice

One fallback path

Keep REST available so the first MVP has a supportable recovery path.

Reliability slice

One process profile

Run realtime and fallback processes explicitly with local or PM2 profiles.

Operations slice
Proof areaQuestion to answerJumentix mechanismMVP evidence
CorrelationCan the client match every response to its request?Message id, subject contract, response handler tests.No anonymous realtime side effects.
FallbackDoes REST return the same result when realtime is unavailable?Fallback route and shared use-case contract.Degraded mode remains usable.
OperationsCan the process be started, inspected and restarted?PM2/runtime profile and logs.The MVP is demoable outside a dev terminal.

Practical implementation

Complete code for the first working slice

These examples keep Category and Task as the product vocabulary and show the controller, contract, client, worker, or state layer needed to reach a runnable MVP.

type Task = { id: string; title: string; categoryId: string; completed: boolean };
type Client = { send: (message: string) => void };

const tasks = new Map<string, Task>();
const clients = new Set<Client>();

function broadcast(subject: string, payload: unknown) {
  const message = JSON.stringify({ subject, payload });
  for (const client of clients) client.send(message);
}

export function connectTaskSocket(client: Client) {
  clients.add(client);
  client.send(JSON.stringify({ subject: 'tasks.ready', payload: { total: tasks.size } }));
  return () => clients.delete(client);
}

export async function handleTaskMessage(message: { subject: string; payload: { title: string; categoryId: string } }) {
  if (message.subject !== 'tasks.create') return { ok: false, error: 'unsupported subject' };
  const task: Task = {
    id: crypto.randomUUID(),
    title: message.payload.title,
    categoryId: message.payload.categoryId,
    completed: false
  };
  tasks.set(task.id, task);
  broadcast('tasks.created', task);
  return { ok: true, result: task };
}

Build the product. Keep the architecture.

Explore the source, run the factory locally, and turn your next Node.js service into a repeatable platform capability.