Skip to content
CommunicationDocs 1.3.x

Service Catalog ​

How does Orders know that Inventory has a CheckStock method, accepts a SKU and returns a stock count? From protobuf contracts, not by scanning the network. The service catalog brings those known contracts into the application.

Where are the contracts built?

Both proto files are inputs to the same generation run. One generated catalog already contains both services.

One application · one generation input set
app/
proto/shop/v1/orders.protoOrdersService
proto/shop/v1/inventory.protoInventoryService
buf generateprotobuf-es + catalog plugin
gen/catalog.gen.ts
OrdersServiceInventoryService
Map + type augmentations
Descriptors from *_pb.ts
Runtime map importimport { serviceCatalog }
Same two contracts in the applicationNo hosts. No ports. No network scan.
Runtime valuecatalog: ServiceCatalog
shop.v1.OrdersService
→ OrdersService
shop.v1.InventoryService
→ InventoryService
Pass the map to createServer( { catalog, ... }).
TypeScript onlyConnectumCallMapLoaded by the generated imports
shop.v1.OrdersService/CheckAvailability
shop.v1.InventoryService/CheckStock
Request/response types for each method. Import the declarations; mergeCatalogs merges values, not types.
Several source directories can also feed one generation run. Moving a file into another folder does not require merging catalogs.
Static illustration · No generator runs in this browser.
Application imports for this layout
typescript
import { serviceCatalog } from './gen/catalog.gen.js';

export const catalog = serviceCatalog;
Look inside a catalog entry

The key is the full proto typeName. Its value is a DescService descriptor, not a handler or deployment address.

shop.v1.OrdersService
Descriptor
OrdersService
Unary method
CheckAvailability
Request
CheckAvailabilityRequestsku: string
Response
CheckAvailabilityResponsesku: string; available: boolean
shop.v1.InventoryService
Descriptor
InventoryService
Unary method
CheckStock
Request
CheckStockRequestsku: string
Response
CheckStockResponseunits: number

These examples use unary RPCs. Streaming methods contribute to ConnectumStreamMap instead.

Read all three build layouts

One folder. Both proto files are inputs to the same generation run. One generated catalog already contains both services.

  • app/ → buf generate → gen/catalog.gen.ts
typescript
import { serviceCatalog } from './gen/catalog.gen.js';

export const catalog = serviceCatalog;

Several source directories can also feed one generation run. Moving a file into another folder does not require merging catalogs.

Separate packages. Each directory is an independently generated contract package. The application imports the two parts and merges their runtime maps.

  • packages/orders-contracts/ → buf generate → gen/catalog.gen.ts
  • packages/inventory-contracts/ → buf generate → gen/catalog.gen.ts
typescript
import { mergeCatalogs } from '@connectum/core';
import { serviceCatalog as ordersCatalog } from '@shop/orders-contracts';
import { serviceCatalog as inventoryCatalog } from '@shop/inventory-contracts';

export const catalog = mergeCatalogs(ordersCatalog, inventoryCatalog);

The split here is a generation/package boundary, not just two folders. The package exports must also expose the generated type augmentations.

Separate repositories. Each repository builds a contract package. The consumer installs versioned dependencies, then uses the same imports and merge as in a monorepo.

  • orders-contracts repo/ → buf generate → gen/catalog.gen.ts → @shop/[email protected]
  • inventory-contracts repo/ → buf generate → gen/catalog.gen.ts → @shop/[email protected]
typescript
import { mergeCatalogs } from '@connectum/core';
import { serviceCatalog as ordersCatalog } from '@shop/orders-contracts';
import { serviceCatalog as inventoryCatalog } from '@shop/inventory-contracts';

export const catalog = mergeCatalogs(ordersCatalog, inventoryCatalog);

Package names and 1.0.0 versions are illustrative. Ship compiled descriptors and declarations; consumers need both. This is package delivery, not service discovery.

1. Describe what Inventory offers ​

This excerpt names one RPC in our shared shop.v1 contract:

proto
service InventoryService {
  rpc CheckStock(CheckStockRequest) returns (CheckStockResponse);
}

The complete proto also defines CheckStockRequest with sku and CheckStockResponse with units.

2. Generate the known-service map ​

The catalog plugin reads the proto inputs you give Buf. Alongside generated messages, it emits catalog.gen.ts. Its runtime map contains descriptors:

typescript
export const serviceCatalog = {
  'shop.v1.OrdersService': OrdersService,
  'shop.v1.InventoryService': InventoryService,
} as const;

The same generated module also tells TypeScript which method keys and request/response types belong together. Import it into the application and pass its value to the server:

typescript
createServer({
  catalog: serviceCatalog,
  services: [orders, inventory],
});

The services input supplies the implementations; the catalog supplies contract knowledge.

3. Call the known contract ​

Inside the Orders handler:

ts
const stock = await ctx.call(
  'shop.v1.InventoryService/CheckStock',
  create(CheckStockRequestSchema, { sku: req.sku }),
);

The method key uses the proto name CheckStock, not the handler name checkStock. TypeScript knows the request shape and that stock.units is a number. The full message created here also works with catalog generators before 1.3.0.

Knowing a contract is not finding a server ​

The catalog has no deployment addresses. services mounts local handlers; a remoteResolver supplies a transport for a non-local target. A catalog entry alone neither starts a service nor makes it reachable.

Full catalog topics and earlier section links

Defining services ​

Register handlers or initialize them lazily.

Configuring the catalog ​

Create, freeze and merge runtime catalogs.

Generating the catalog ​

Buf plugins, runtime descriptors and TypeScript augmentations. Both the generated types and runtime catalog must reach the consumer.

Unary calls — ctx.call ​

Request types, procedure keys and dispatch.

Streaming calls — ctx.stream ​

Server, client and bidirectional streams.

Cascade behaviour (CallOptions) ​

Signals, inherited deadlines and explicit overrides.

Header propagation ​

Explicit header forwarding; no implicit credential propagation.

Outgoing interceptors ​

One application policy chain across routes.

Single-image, multiple roles — enabledServices ​

Select locally mounted services and check version boundaries.

Calling from outside a handler ​

Server clients and standalone catalog clients.

Error model ​

Configuration failures, failed calls and route caching.

Complete catalog configuration and behavior.

Watch one call ​

The routing illustration now lives with Remote Resolvers.

Three pieces. Different jobs. ​

Contracts ​

The catalog describes known contracts.

Implementations ​

services supplies local handlers.

Routes ​

remoteResolver supplies remote transports.

Run the same example ​

All six files and the run commands.

package.json ​

Example dependencies and scripts.

buf.yaml ​

Example Buf module.

buf.gen.yaml ​

Example generation plugins.

tsconfig.json ​

Example TypeScript configuration.

proto/shop/v1/catalog.proto ​

Complete Orders and Inventory contract.

src/server.ts ​

Complete handler and server implementations.

Check the result ​

Make the real HTTP/2 request.

Move Inventory out ​

Run the services in separate processes.

Remove the route ​

Observe a missing route.

Keep the full picture ​

Complete configuration, errors and limitations.