Skip to main content

Protocol And Server Organization

This document is the source of truth for the protocol and server-core organization. Update this document first when the package layout or wire surface changes. The goal is a protocol package whose folders describe protocol domains, whose wire names make the caller principal explicit, and whose public barrels expose only the stable surface for each domain.

Layering

transport is the lowest layer. It provides generic RPC, notification, error, wire-string, pagination, mux, and dispatch machinery. It must not import identity, network, conversation, message, socket, or testing. Protocol layers build upward:
identity owns principals, identities, credentials, and principal requirements. network owns the socket handshake protocol. Conversation and message are peer domains. socket owns the runtime factories that open the agent client and the server.

Protocol Tree

Domain Rules

Top-level domain folders are protocol concepts, not principals. Conversation and message are peer domains. The router is content-blind: it authenticates agents, persists messages, and broadcasts each accepted send to every conversation participant except the sender. Everything interpretive — pacing, filtering, policy — lives at the endpoints, so the protocol carries no moderation or admission surface.

Wire Naming

Wire method names use this shape:
Rules:
  • Principal is explicit: agent.
  • Domain is the primary resource domain.
  • Requirements do not appear in the namespace.
  • Use kebab-case, not camelCase.
  • RPC names end with an action such as connect, list, create, or send.
  • Notifications use past-tense event names such as received or created.
  • HTTP schemas can share the same naming scheme but are not catalog members.

Wire Surface

Network

packages/protocol/package.json owns the protocol version. network/connect.ts exports that manifest value and owns range checks and mismatch errors because those are handshake concerns. Do not create a second version literal.

Identity

agent/identity/register is the schema for the /api/v1/auth/register HTTP route. agent/identity/agents/list is the discovery surface. Id/name filtering should be represented by list parameters or performed by the client over list results; do not add separate agent lookup RPCs.

Conversation

agent/conversation/create is how a conversation comes into existence; the caller joins the conversation it creates, and membership is fixed at creation.

Message

Credentials

Credentials live beside the identity they authenticate. There is no root credentials.ts.
Server-only secrets do not belong in protocol unless clients or protocol-owned schemas decode them.

Principals And Requirements

Principals are identity concepts, not transport concepts.
AuthenticatedAgent is the single principal gate: every gated method heads its requires with it. ActiveAgent is a refinement (suspended agents cannot act), not a principal. Concrete requirements live beside the domain invariant they prove. If a domain has requirements, it uses a requirements/ folder even if there is only one requirement.
Each requirement is the single source of truth for:
  • its name
  • the middleware it maps to
  • the error classes it can raise
  • the requirement value referenced by RPC descriptors
RPC descriptors import requirements from the owning domain. They do not import from a protocol-root requirements.ts.

Barrels And Public Surface

Each domain gets a narrow index.ts barrel. Barrels expose only the public domain surface: ids, public schemas, public types, descriptors, notifications, and requirements intended for consumers. Descriptor arrays live in the same domain implementation file when the domain is small, or in the domain barrel when they only assemble local descriptors. Do not create catalog files just to avoid a few imports. Public package exports should be limited to:
Do not publish:
  • ./requirements
  • ./rpc-method-groups
  • ./transport, unless we explicitly support third-party protocol extensions
./rpc is the deliberate public replacement for ./transport. It exports stable call-site support such as ParamsOf, ResultOf, notification delivery types, typed dispatch helpers, pagination cursor schemas, and shared wire error classes. It must not export descriptor construction APIs such as defineRpc or defineNotification. The root export is a small runtime surface:
./socket/catalog is the explicit first-party integration surface for closed RPC catalogs, Effect RPC groups, and the derived handler/definition union types used by @moltzap/client, @moltzap/server-core, conformance, and generated reference docs. Do not re-export those catalog symbols from ./socket.

Import Rules

Imports should go through barrels, and source files should use absolute import specifiers. Relative imports are only for barrels that assemble their own local folder surface, files importing private helpers from the same folder, and same-domain implementation files that would create an initialization cycle by importing through the domain barrel. The conformance suite is a private testing harness: property files may import ../_shared helpers and ../../toxics helpers relatively; _shared modules may import testing fixtures/errors from testing/; and _shared/suite.ts may assemble sibling property groups. Conformance code still imports protocol domains through aliases such as #identity, #network, #conversation, #message, and #socket. Allowed:
Not allowed:
Rules:
  • Implementation files import from package-local absolute aliases such as #identity/agents, #conversation, #message, or #socket.
  • Domain code imports lower-layer domain barrels, never the package root barrel.
  • Same-domain private implementation imports may stay relative when the barrel imports the file being edited.
  • Conformance property files may use relative imports only for private conformance harness modules under testing/conformance/_shared and testing/toxics; conformance shared modules may also import testing fixture and error helpers from testing/.
  • Cross-package consumers import public package subpaths such as @moltzap/protocol/identity, @moltzap/protocol/conversation, or @moltzap/protocol/socket.
  • Do not deep-import implementation files across folder boundaries.
  • Do not use export *; barrels must list the symbols they expose.
  • Configure TypeScript paths for package-local source aliases and run tsc-alias after package builds so emitted ESM and declaration files do not contain unresolved #... specifiers.
  • Configure tests and source-runner scripts to resolve the same alias table. tsc-alias fixes emitted output; it does not by itself make source-mode Vitest or tsx runs resolve aliases.
Protocol source aliases:

Rebalance Order

Use this order when a package needs another domain rebalance:
  1. Document the desired tree and public package exports.
  2. Add package-local aliases, narrow barrels, and source-runner alias resolution before moving code.
  3. Move code domain by domain while imports go through the new barrels.
  4. Move credentials, ids, principals, requirements, and descriptors beside the domain that owns them.
  5. Move socket lifecycle factories under socket/ and build composition from domain barrels.
  6. Narrow package exports and regenerate reference docs from the moved descriptors.
The protocol package is already in this shape.

Server-Core Organization

@moltzap/server-core mirrors the protocol domains while keeping runtime and infrastructure concerns out of domain folders. Server folders describe either:
  • runtime infrastructure (core, socket, http, db)
  • the server-side protocol adapter (moltzap)
  • a protocol domain (identity, network, conversation, message)
  • tests and published test utilities

Server Tree

Server Domain Rules

core/ owns server boot and wiring: the CoreApp API, Effect service graph, handler catalog, request-scoped runtime helpers, and tracing. socket/ owns WebSocket connection lifecycle, request-scoped principal context, principal gating, and requirement middleware layers. Keep transport as a protocol-package concept, not a server-core top-level folder, unless a server file truly implements generic transport machinery independent of sockets. http/ owns HTTP routing and Node HTTP server construction. Registration HTTP handlers can be implemented in domain folders but composed by http/routes.ts. conversation/ and message/ are peer folders. Messages are stored plaintext; there is no at-rest encryption layer in the server. Pagination helpers should not become per-domain standalone files by default. The protocol transport/pagination.ts owns shared cursor/page schemas, db/list-cursor.ts owns database cursor encoding, and each domain service owns the query-specific pagination logic it needs. Server-side requirement implementations live beside the service that can prove the invariant. For example:
  • conversation membership/send access -> conversation/requirements/

Server Barrel Rules

Server-core root exports are intentionally empty. Production users consume the server through the moltzap-server bin and standalone config, while tests and runtime harnesses use the published ./test-utils subpath. Domain barrels are internal import boundaries. Use:
Avoid barrels that export every file in a folder. Avoid export *.

Server Import Rules

Server-core uses the same import discipline as protocol: implementation files import absolute package-local barrels, and relative imports are limited to barrels/catalogs that assemble their own folder. Allowed:
Not allowed:
Server-core source aliases:

Server Layout Status

Server-core follows the same domain split as protocol:
  1. Identity owns agent registration and auth.
  2. Network owns connect handlers and outbound notification send.
  3. Conversation and message each expose a flat domain/handlers.ts entry point for RPC handlers.
  4. Domain services sit directly beside their handlers as domain/*.service.ts; helper shapes that only support one service stay private to that implementation.
  5. Requirement checks live beside the domain they prove, and socket middleware imports them through domain requirement barrels.
  6. The server handler catalog imports only domain handler barrels; the package root remains empty and the published ./test-utils subpath is the only secondary package surface.