Contents

offskylab/swift-nebula

> [!WARNING]

Why Nebula?

Most microservice teams default to HTTP for everything — including internal service-to-service calls. This works, but it comes with costs:

  • Every internal hop carries the overhead of HTTP headers, serialization, and layer traversal
  • Reverse proxies repeat that cost at each layer
  • For AI-heavy teams running Python inference services, every millisecond matters — and internal HTTP is often the silent bottleneck

Nebula is built around a different premise: internal services deserve a faster, leaner, and more opaque protocol.

How It Works

Nebula organizes services into a cosmic hierarchy, with an Ingress as the infrastructure entry point:

Ingress     root discovery node, the single entry point for clients
  └── Galaxy     service registry and coordinator
        └── Cluster     load balancer, manages a pool of Stellars per namespace
              └── Stellar     the actual service provider

Services register under a namespaced address following forward order (matching the discovery path): {galaxy}.{amas}.{stellar}

A Planet (client) connects to Ingress to discover the Stellar address, then connects directly — no intermediate hops on every call. Cluster is managed by Galaxy automatically and only intervenes during failover.

| Role | Type | Description | |------|------|-------------| | Ingress | StandardIngress | Root discovery node. Galaxies register with Ingress on startup. Planet sends find here; Ingress routes to the appropriate Galaxy. Default port 22400. | | Galaxy | StandardGalaxy | Service registry. Automatically creates and manages a LoadBalanceCluster per namespace when a Stellar registers. | | Cluster | LoadBalanceCluster | Load balancer. Maintains a pool of Stellars, distributes via round-robin. System-managed by Galaxy — not created manually. | | Stellar | ServiceStellar | Service host. Runs one or more named Service objects, each with methods. | | Planet | RoguePlanet | Client actor. Discovers Stellar via Ingress, then connects directly. Falls back through Cluster when a Stellar becomes unreachable. |

Planet Connection Model

Discovery: Planet  Ingress  Galaxy  return Stellar address
Normal:    Planet ──────────────────────────────► Stellar (direct)
Failover:  Planet  notify Cluster (dead Stellar)  get next Stellar  reconnect directly

The Protocol

Nebula uses its own binary wire protocol — NMT (Nebula Matter Transfer) — over TCP with a compact 27-byte fixed header.

Why "Matter"?

In networking, the unit of transmission is conventionally called an envelope — a wrapper with a header describing the contents inside. Nebula uses the same structural concept: a fixed-length header carrying routing metadata, followed by a serialized body.

The name Matter is intentional. In the Nebula metaphor, celestial bodies (Galaxy, Cluster, Stellar, Planet) communicate by transferring matter through the nebula — just as stars exchange energy and particles across space. Matter is the M in NMT (Nebula Matter Transfer): it is the thing being transmitted, not just a technical wrapper.

Same structure as an envelope. Different name — because in this universe, what flows between nodes is matter.

┌─────────────┬─────────┬──────┬───────┬─────────────────────┬────────────────┬──────────────┐
  Magic (4)   Ver (1)  Type  Flags     MessageID (16)     Length (4)      Body (N)    
  "NBLA"  0x01 (1)    (1)         UUID            UInt32 BE       MessagePack 
└─────────────┴─────────┴──────┴───────┴─────────────────────┴────────────────┴──────────────┘

No HTTP. No Protobuf schema files. Binary, fast, and purpose-built.

The body is serialized with MessagePack (via hirotakan/MessagePacker).

Message Types

| Value | Name | Description | |-------|------|-------------| | 0x01 | clone | Fetch remote identity info | | 0x02 | register | Register a namespace (Stellar→Galaxy, Galaxy→Ingress) | | 0x03 | find | Look up a namespace — returns Stellar + Cluster addresses | | 0x04 | call | Invoke a service method | | 0x05 | reply | Response to any of the above | | 0x06 | activate | Reserved | | 0x07 | heartbeat | Reserved | | 0x08 | unregister | Notify Cluster that a Stellar is unreachable; returns next available Stellar |

Design Goals

  • Zero HTTP for internal traffic — TCP + MessagePack, not REST
  • Namespace-based discovery — services are addressable by logical name, not hardcoded IPs
  • Ingress as single entry point — Planet only needs to know the Ingress address
  • Planet connects directly to Stellar — zero intermediate hops on the normal call path
  • Cluster as load balancer and failover — system-managed, not user-facing
  • Swift-native — built on Swift NIO with async/await and Actor, not callback chains
  • Embeddable by default — no external dependencies required to get started

Requirements

  • Swift 6.0+
  • macOS 13+

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/gradyzhuo/swift-nebula.git", from: "0.0.1"),
],
targets: [
    // Core protocol and transport
    .target(name: "MyTarget", dependencies: ["Nebula"]),

    // Optional: NMTServer conformance to ServiceLifecycle.Service
    .target(name: "MyTarget", dependencies: ["Nebula", "NebulaServiceLifecycle"]),
]

Quick Start

1. Start an Ingress (discovery root)

import Nebula
import NIO

let ingressAddress = try SocketAddress(ipAddress: "::1", port: 22400)
let ingress = StandardIngress(name: "ingress")
let ingressServer = try await Nebula.server(with: ingress).bind(on: ingressAddress)

2. Start a Galaxy and register with Ingress

let galaxy = StandardGalaxy(name: "production")
let galaxyServer = try await Nebula.server(with: galaxy)
    .bind(on: SocketAddress(ipAddress: "::1", port: 0))  // dynamic port

// Register Galaxy with Ingress
let ingressClient = try await NMTClient.connect(to: ingressAddress, as: .ingress)
try await ingressClient.registerGalaxy(
    name: "production",
    address: galaxyServer.address,
    identifier: galaxy.identifier
)

3. Define and start a Stellar (service host)

Galaxy automatically creates and manages a LoadBalanceCluster for the namespace.

import MessagePacker

let stellar = ServiceStellar(name: "Embedding", namespace: "production.ml.embedding")

let w2v = Service(name: "w2v")
w2v.add(method: "wordVector") { args in
    let result = ["vector": [0.1, 0.2, 0.3]]
    return try MessagePackEncoder().encode(result)
}
stellar.add(service: w2v)

let stellarServer = try await Nebula.server(with: stellar)
    .bind(on: SocketAddress(ipAddress: "::1", port: 7000))

// Register with Galaxy — Cluster is created automatically
try await galaxy.register(namespace: stellar.namespace, stellarEndpoint: stellarServer.address)

4. Call from a Planet (client)

Use an nmtp:// URI to address a service. Host:port is the Ingress address.

let planet = try await Nebula.planet(
    connecting: "nmtp://[::1]:22400/production.ml.embedding/w2v/wordVector"
)

let result = try await planet.call(
    arguments: ["words": ["慢跑", "反光", "排汗"]]
)

Arguments support strings, integers, doubles, booleans, and arrays — expressed as Swift literals via ArgumentValue.

URI Format

The connection URI locates a namespace via Ingress. Each path segment maps to a level in the cosmic hierarchy:

nmtp://localhost:22400/production/ml/embedding
       └─────────────┘ └────────┘ └┘ └───────┘
       Ingress address  Galaxy   Cluster  Stellar

Path segments are joined with . internally to form the namespace production.ml.embedding.

Namespace follows forward order — broadest first, most specific last. Reading left to right matches the discovery routing path: Ingress → Galaxy → Cluster → Stellar.

Naming Rules

Astral node names (Galaxy, Cluster, Stellar) must not contain . — the dot is reserved as the namespace separator. Names like my.galaxy or ml.v2 will throw an error at init time.

Running the Quickstart

Sample code lives in its own repository: see OffskyLab/swift-nebula-samples (runs Ingress, Galaxy, Stellar, and a client task together with graceful shutdown on Ctrl+C).

Dependencies

Status

Active development. Core protocol and transport layer complete.

Package Metadata

Repository: offskylab/swift-nebula

Default branch: main

README: README.md