Skip to content

Contracts, Types & API Boundaries

JobCtrl separates domain meaning, wire validation, transport behavior, and runtime implementation. That separation lets the web app and TypeScript API share precise types without turning HTTP DTOs, SQLite rows, or Python objects into competing domain models.

Read this if you are adding a field, event, route, JSON-RPC method, or typed client call and need to know which layer owns the contract.

Dependency Direction

The important direction is domain vocabulary → wire contract → transport. The API implements the wire contract, while the Python runtime mirrors only the cross-language boundaries it consumes.

Contract Owners

LayerSource ownerContainsMust not contain
Python domain modelworkers/automation/src/jobctrl/domain/Aggregate behavior, invariants, value objects, use cases, and Python domain eventsHTTP status codes, fetch behavior, or database/provider mechanics
Shared TypeScript domain vocabularypackages/domain-types/Branded identities, shared stage/state values, the TypeScript domain-event union, and projection shapes consumed across packagesClaiming authority over Python aggregate behavior, HTTP status codes, database access, or browser globals
REST and JSON-RPC wire contractpackages/contracts/Zod request/response schemas, DTOs, wire enums, API query shapes, JSON-RPC envelopes, params, and result schemasRoute registration, persistence, fetch calls, or ownership of the domain-event union
Typed HTTP transportpackages/api-client/URL/query encoding, HTTP methods, request timeout, typed return values, and transport errorsBusiness rules, server validation, or canonical state
HTTP implementationapps/api/Fastify routing, security gates, status codes, schema application, DTO mapping, projection reads, simple commands, JSON-RPC dispatch, and SSE framingProvider/browser workflow execution or a second copy of shared DTOs
Frontend boundaryapps/web/src/shared/ports/ApiClientPort.ts plus adaptersThe capability the web app consumes and the selected local/demo implementationDirect transport calls from feature components
Python JSON-RPC mirrorworkers/automation/src/jobctrl/domain/rpc/messages.pyPython validation/types for the methods crossing the TypeScript-to-Python boundaryBrowser-facing REST contracts
Physical persistenceOwning SQLite repository/write module and schema initializationTables, columns, indexes, migration compatibility, and row serializationDomain meaning merely because a column exists

packages/contracts depends on packages/domain-types and re-exports selected projection types. That is a dependency, not duplicate ownership: shared TypeScript vocabulary stays in domain-types, while Python aggregates remain authoritative for their behavior and invariants. The contract package exposes only the wire-safe shapes API consumers need.

Three API Boundaries

Browser REST API

The web app uses product routes exposed by the loopback Fastify server. Job/operations reads return DTOs built from projection rows; profile, settings, credentials, compensation-policy, and resume-template reads use their canonical SQLite, config, or secure-store owners directly. Simple commands can complete synchronously; commands accepted for durable execution return workflow identity after the Python runtime has started Temporal work.

The route documentation is intentionally layered:

  1. Local TypeScript API explains route families and core semantics.
  2. The focused Profile & Settings, Jobs & Materials, and Operations & Events pages explain one product boundary at a time.
  3. Complete API Contract owns the exhaustive fields, status codes, precedence rules, and route variants.

Do not reproduce that exhaustive contract here. This page owns layer and source responsibility, not a mutable route catalog.

TypeScript-To-Python JSON-RPC

packages/contracts/src/rpc.ts owns the TypeScript JSON-RPC 2.0 envelope and method schemas. workers/automation/src/jobctrl/domain/rpc/messages.py mirrors the application contract; apps/api/src/json-rpc-adapter.ts supplies the local transport; and workers/automation/src/jobctrl/infrastructure/rpc/server.py dispatches it.

In the current runtime:

  • the TypeScript API starts and reuses one jobctrl rpc subprocess;
  • synchronous methods return their validated result through JSON-RPC;
  • workflow methods return run/workflow identity after the Python side starts a Temporal workflow; and
  • the TypeScript API does not enqueue Temporal work directly.

The browser does not call arbitrary JSON-RPC methods. Product routes validate intent, enforce local security, and translate to the narrower worker command. See Runtime & Processes and Operations & Events API for dispatch and health behavior.

Domain Events Over SSE

Server-Sent Events (SSE) reuse the domain-event vocabulary, but the event union does not live in packages/contracts. Its TypeScript authority is packages/domain-types/src/events/, mirrored by workers/automation/src/jobctrl/domain/events/.

apps/api/src/event-stream.ts frames durable job_events rows as SSE. The web parser checks that the event type belongs to the known domain-event registry, then the Operations invalidation router maps that event to query keys. An SSE payload is an invalidation/change notification; it is not the full job or projection response contract.

The exact framing, replay, tenant, and reconnect rules live in Local TypeScript API and Frontend Realtime.

Logical Types Are Not Rows Or DTOs

A single fact can have three representations without having three owners:

RepresentationPurposeExample responsibility
Domain value/eventExpress meaning and valid statesA branded identity, stage-state union, or past-tense event
Persistence rowStore the fact efficiently and compatiblySnake-case columns, indexes, nullable legacy fields
API DTOPresent a stable, privacy-safe client shapeCamel-case response, summarized evidence, pagination metadata

Translation belongs at the boundary. Repositories translate rows to domain values; API mapping translates projections/domain values to DTOs. A column addition does not automatically become a public field, and a presentation-only DTO field does not automatically become canonical storage.

Cross-Language Parity

TypeScript and Python share concepts by explicit mirrors, not generated code. The domain parity check compares the covered event names/payload fields and pipeline stage/state vocabulary; focused tests and shared fixtures cover additional projection and JSON-RPC shapes. The source anchors are:

  • packages/domain-types/src/events/ and workers/automation/src/jobctrl/domain/events/;
  • packages/domain-types/src/pipeline.ts and workers/automation/src/jobctrl/domain/pipeline_types.py;
  • packages/contracts/src/rpc.ts and workers/automation/src/jobctrl/domain/rpc/messages.py; and
  • scripts/check-domain-type-parity.py plus the focused API/Python parity tests.

Do not assume one parity script proves every contract in both languages. When a boundary grows, add a fixture or assertion at that boundary.

Change Checklist

ChangeRequired ownership pass
New or changed domain value/stateUpdate packages/domain-types, the matching Python domain type when cross-runtime, and parity coverage before consumers
New domain eventUpdate both event registries/factories, the producer, projection handling, frontend invalidation, and parity tests
REST request/response fieldUpdate packages/contracts, API validation/mapping, API-client use, affected frontend port/consumer, and the owning API reference
JSON-RPC method/fieldUpdate packages/contracts/src/rpc.ts, the Python message mirror and handler registration, adapter tests, and dispatch documentation
Projection fieldStart from its canonical data owner, update every responsible projection builder, then the DTO/API/client/UI and shared parity fixture
SQLite-only migration fieldUpdate the owning schema/repository and Storage; expose it through a contract only when a product consumer needs it

Future Architecture (Not Implemented)

The domain-model reference names hosted HTTP/service transports, hosted data adapters, and authenticated tenant injection as evolution seams. The current product uses a loopback Fastify API, a long-lived local JSON-RPC subprocess, local Temporal, and SQLite. See the explicitly future sections in Cross-Context Integration and Cloud Evolution; do not describe those adapters as available current contracts.