iolite/Nebula

Behaviors

In Nebula, you don't build queues, endpoints, services, repositories, or dependency injection. You describe what composition of entities you care about, and the platform handles routing, execution, and state management. This is what makes a Nebula application fundamentally different from a traditional backend.

Declarative Interest

A system in Nebula declares a query that describes the entities it wants to process. When entities match that query — because they gained a component, lost one, changed a property, or had a relationship updated — Nebula routes them to the system automatically.

Here's a real production system that scores healthcare denial opportunities based on urgency and financial value:

DenialPrioritySystem

and
hasAccount
hasCustomer
hasDenialOpportunity
hasAppealTimelyFiling

The system doesn't poll. It doesn't subscribe to a message bus. It simply declares "I care about entities that have these four components" and Nebula ensures it sees every matching entity when changes occur.

Entity Changes+ Component~ Property⇄ LinkNebulaQuery Enginematches queriesDenialPriorityhas('DenialOpportunity')has('AppealTimelyFiling')TeamAssignmenthas('Account')owned(false)Notificationreceived('DenialPriority')gt(compositeScore, 500)

Query Operators

Nebula's query language goes far beyond simple component existence checks. Operators compose together to express precise interest in entity state:

Real-time operators form the foundation — has, and, or, not, plus field comparisons like eq, gt, lte, contains, and oneOf.

Temporal operators let you reason about time:

Differential operators react to state transitions:

Relationship operators filter across the entity graph:

Ownership operators filter by principal:

These operators compose naturally. A query that starts simple can become as specific as you need:

  1. Start broadhas('Account') — all account entities
  2. Add component requirementsand(has('Account'), has('DenialOpportunity')) — accounts flagged for denial
  3. Add temporal precision — add changed('2024-01-01', '2024-03-01', ...) to find accounts whose claim status changed in Q1

Processing Results

When Nebula routes entities to your system, your code receives them in batches with full context. You can inspect the current and previous state of every component, make changes to the entities you received, and even find and modify related entities through programmatic search.

protected async processBatchCoreAsync(
  batch: readonly EntityContext[]
) {
  for (const ctx of batch) {
    const opp = ctx.get<IDenialOpportunity>('DenialOpportunity');
    const atf = ctx.get<IAppealTimelyFiling>('AppealTimelyFiling');

    const daysRemaining = computeDaysRemaining(atf.deadline);
    const urgencyScore = computeUrgencyScore(daysRemaining, atf.days);

    const children = await this.getEntityChildrenAsync(ctx.id, {
      limit: 500,
    });
    const totalValue = sumClaimValues(children.results);

    ctx.upsert('DenialPriority', {
      daysRemaining,
      totalValue,
      urgencyScore,
      compositeScore: urgencyScore * totalValue,
      calculatedAt: new Date().toISOString(),
    });
  }
  // All changes are committed atomically at the end
}

All changes are committed atomically after your code returns. If something fails, nothing is partially written. This transactional guarantee means your system code doesn't need to handle rollbacks or partial state.

Frontend Queries

The same query language works in the browser. React components use useEntities to declare what data they need, and Nebula keeps the results up to date in real-time via WebSocket subscriptions.

import { and, has } from '@nebula/sdk';
import { useEntities, useSubscription } from '@nebula/react';

function DenialQueue() {
  const query = and(has('Account'), has('DenialPriority'));
  const { entities, total, hasMore } = useEntities(query, 0, 20);

  useSubscription(query, {
    onCreated: (entity) => { /* new match appeared */ },
    onUpdated: (entity) => { /* existing match changed */ },
    onDeleted: (entity) => { /* match no longer qualifies */ },
  });

  return (
    <div>
      <h2>{total} accounts in denial queue</h2>
      {entities.map(entity => (
        <AccountRow key={entity.id} entity={entity} />
      ))}
    </div>
  );
}

There's no separate API layer between your frontend and the data. The query you write in React is the same query your systems use on the backend.


Behaviors define what your application cares about. Orchestration explains how Nebula ensures those behaviors execute reliably at scale. For creative ways to apply these query patterns, see Component Query Refiners and Async Workflows.