On this pageWorkspace boundariesStartup and ownershipInternal RPCOwnership on the wireGateway: public gRPCFollow a requestReading the Effect codeWhat is implemented, and what is still missingQuality checks

Architecture and source map

This is an executable model of Temporal's service responsibilities. The architecture overview is the orientation; the pinned Go sources below are the implementation references. Roles call each other only through internal RPC, whether they share a process or not. Requests and responses are upstream's own historyservice and matchingservice messages, generated from Temporal's protos and sent as readable proto3 JSON. It is a teaching model of Temporal's topology, not a server that interoperates with upstream services.

Workspace boundaries

The directory names follow upstream Temporal. Upstream has one Go module with many Go packages; here pnpm workspaces are the TypeScript package boundaries, grouped at the service and adapter level. Each production/example/tooling package puts TypeScript under src/, with package.json and tsconfig.json at its root. The test/ workspace keeps its test files directly under that directory. They are private source packages, with explicit exports and TypeScript project references, sharing one lockfile. tsx runs their source; pnpm typecheck builds declarations under ignored dist/ directories to check every package boundary.

pnpm-workspace.yaml uses the isolated linker, disables hoisting, rejects workspace cycles, and aligns Effect and Temporal versions through a catalog. Node can still search parent directories, so installation layout alone is not enforcement. dependency-cruiser checks the source graph against our boundary policy: permitted package edges, declared dependencies, exported entrypoints, no relative cross-package imports, no unresolved imports, and no cycles outside recursive generated protobuf messages. These checks include type-only imports and run locally inside pnpm lint (also included in pnpm check); they replace the old handwritten architecture test.

Persistence contracts are explicit package exports, with no combined stores entrypoint. The boundary policy permits History to import execution-store and visibility, Matching to import task-store, and Frontend to import only visibility. The Worker scanner can import execution, task, and visibility stores. The namespace registry and visibility manager use their respective stores. Adapters implement the store contracts; composition, examples, and tests may wire or inspect them. New persistence entrypoints and consumers require an explicit policy update. These are static import restrictions, including type-only imports; Effect checks layer requirements separately. Upstream Go relies on exported interfaces and dependency injection rather than these per-consumer import restrictions.

pnpm deps:graph generates plain side-by-side comparison pages at dist/architecture/index.html: architectural boundaries, modules, and intra-module dependencies. Both repositories use the same grouping and focus rules, with original imports available in the evidence JSON. pnpm deps:matching starts with Matching expanded. See architecture diagrams for prerequisites, scopes, exports, and narrower focuses.

The compact tiny-temporal service/client/persistence graph below remains available through pnpm deps:mermaid. These source views cannot establish RPC destinations, resolve Effect layer injection, or prove runtime isolation. See the upstream asset review for those separate checks and the runtime diagram. pnpm wire prints observed internal RPC frames.

The current subsystem imports (regenerate with pnpm --silent deps:mermaid):

flowchart LR

subgraph client["client"]
  subgraph client_src["src"]
    client_src_frontend_ts["frontend.ts"]
    client_src_history_ts["history.ts"]
    client_src_matching_ts["matching.ts"]
  end
end
subgraph common["common"]
  subgraph common_persistence["persistence"]
    common_persistence_memory["memory"]
    common_persistence_postgres["postgres"]
  end
  subgraph common_src["src"]
    subgraph common_src_persistence["persistence"]
      common_src_persistence_execution_store_ts["execution-store.ts"]
      common_src_persistence_namespace_store_ts["namespace-store.ts"]
      common_src_persistence_snapshot_ts["snapshot.ts"]
      common_src_persistence_task_store_ts["task-store.ts"]
      common_src_persistence_visibility_store_ts["visibility-store.ts"]
      common_src_persistence_visibility_ts["visibility.ts"]
    end
  end
end
subgraph service["service"]
  service_frontend["frontend"]
  service_history["history"]
  service_matching["matching"]
  service_worker["worker"]
end
common_persistence_memory-->common_src_persistence_execution_store_ts
common_persistence_memory-->common_src_persistence_namespace_store_ts
common_persistence_memory-->common_src_persistence_snapshot_ts
common_persistence_memory-->common_src_persistence_task_store_ts
common_persistence_memory-->common_src_persistence_visibility_store_ts
common_persistence_postgres-->common_src_persistence_execution_store_ts
common_persistence_postgres-->common_src_persistence_namespace_store_ts
common_persistence_postgres-->common_src_persistence_snapshot_ts
common_persistence_postgres-->common_src_persistence_task_store_ts
common_persistence_postgres-->common_src_persistence_visibility_store_ts
common_src_persistence_visibility_ts-->common_src_persistence_visibility_store_ts
service_frontend-->client_src_history_ts
service_frontend-->client_src_matching_ts
service_frontend-->common_src_persistence_visibility_ts
service_history-->common_src_persistence_execution_store_ts
service_history-->client_src_matching_ts
service_history-->common_src_persistence_visibility_ts
service_matching-->common_src_persistence_task_store_ts
service_matching-->client_src_history_ts
service_matching-->client_src_matching_ts
service_worker-->common_src_persistence_execution_store_ts
service_worker-->common_src_persistence_task_store_ts
service_worker-->common_src_persistence_visibility_store_ts

Startup and ownership

cmd/server/src/main.ts             CLI: --service roles, ports, NodeRuntime
  temporal/src/server.ts          coordinate host lifetimes, failure, and scoped shutdown
    temporal/src/fx.ts            one fresh layer graph per host; process-wide shared resources
      service/frontend/       public gRPC listener and WorkflowService handler
      service/history/        shard controller, execution engine, queue processors, RPC handler
      service/matching/       partition managers, backlog, polls, forwarding, RPC handler
      service/worker/         internal background scanner

client/                       routed History and Matching clients (shard / partition owner)
api/                          generated contracts, service errors, and RPC metadata
proto/                        generator and its own protobuf dependencies
common/                       membership, RPC transport, state, persistence interfaces
common/persistence/{memory,postgres}/  separately installed store implementations
sdk/                          embedded teaching client/worker; speaks the public API in-process
examples/official-sdk/        ordinary stock TypeScript SDK consumer over gRPC
Toy boundary What it owns Upstream reference
cmd/server/src/main.ts Flags (--service), ports, static membership; enter the process lifetime cmd/server/main.go
temporal/src/fx.ts A fresh layer graph per host, shared persistence/namespace/membership per process temporal/fx.go
temporal/src/server.ts Coordinate host lifetimes and propagate background failure temporal/server_impl.go
api/ Wire types and RPC groups generated from proto/internal and api_upstream proto/internal, generated api/*service
client/ Route a History request to its shard owner, a Matching request to its partition owner client/history, client/matching
service/frontend Validate requests and forward them; serve the public gRPC API workflow_handler.go
service/history Shard leases, execution transitions with task intents, transfer/timer/visibility queues service/history/handler.go, shard/controller_impl.go
service/matching Partition managers, persisted backlog, polls, forwarding; record starts in History service/matching/handler.go, matching_engine.go
service/worker Internal maintenance jobs, represented by one scanner service/worker/service.go
common/persistence Store capabilities and memory/PostgreSQL implementations common/persistence/data_interfaces.go

A host is one instance of a role, such as history-2. Each host has its own layer graph (Layer.fresh): its identity, its outbound connections, its clients, its engine, and its RPC listener. This matches upstream, where each service is its own fx.App with its own resource graph. Hosts in one process share only persistence, the namespace registry, membership, and visibility, which stand in for the database and the membership ring they would share across processes.

temporal/src/fx.ts defines one layer per role (historyRole, matchingRole, frontendRole, workerRole). A role layer exposes its own services for inspection and keeps its host identity, connections, and clients private. Two compositions use them:

pnpm start runs every role in one process. pnpm start --service history runs only History; a separate process can run Matching, another Frontend and Worker. Membership is static, like upstream's static membership module: one host per role at fixed local ports (HISTORY_PORT 7234, MATCHING_PORT 7235). Separate processes must share PostgreSQL.

Internal RPC

History / Matching / Frontend host
  → client/src/history.ts or client/src/matching.ts      pick the owner host (shard or partition)
  → RcMap of RpcClient per target host           Effect RPC client over a Node socket
  → TCP, newline-delimited JSON frames           proto3 JSON payloads, a timeout header
  → RpcServer.layer on the target host           Effect RPC server over a socket server
  → service/*/handler.ts                         generated group's handlers → engine

Effect RPC has its own framing, so internal frames are not gRPC and cannot reach a real Temporal service. Upstream uses gRPC with protobuf binary between services. The message schemas, routing keys, and error kinds are upstream's.

Ownership on the wire

Gateway: public gRPC

stock Temporal client / worker
  → grpc-js + @temporalio/proto binary codecs
  → frontend/grpc/json.ts: protobufjs object ↔ proto3 JSON (well-known types)
  → generated WorkflowService schemas → Frontend (Effect service)
  → HistoryClient / MatchingClient

grpc/server.ts is an Effect RpcServer protocol over grpc-js. grpc-js decodes the binary message with the published codec; each unary call becomes one protocol client whose request, cancellation (Interrupt), and exit cross the protocol. RpcServer decodes the request with the generated schema and runs the WorkflowService handler under the same deadline middleware as internal calls: the gRPC deadline becomes the timeout header. Requests are fibers owned by the listener scope. Frontend gives Matching a slightly earlier deadline, so an empty long-poll answer arrives before the SDK's deadline. grpc/errors.ts maps service errors to gRPC status codes and hides defect details.

Frontend holds no execution state. It validates namespaces, forwards each request to History or Matching, and translates Matching's poll response into the public one. Its handlers are WorkflowService.toLayer(...), like History's and Matching's. The embedded teaching SDK calls them in process through an Effect RPC client without serialization (RpcTest.makeClient), with the same public messages.

Follow a request

sequenceDiagram
  participant A as Application SDK
  participant F as frontend-1
  participant H as history-N (shard owner)
  participant M as matching-N (partition owner)
  participant P as Persistence
  A->>F: StartWorkflowExecution
  F->>H: historyservice.StartWorkflowExecution
  H->>P: fenced commit: execution, events, transfer intent
  H-->>F: run ID
  F-->>A: run ID
  H->>M: matchingservice.AddWorkflowTask (transfer processor)
  M->>P: spool delivery if no poller is ready
  A->>F: PollWorkflowTaskQueue
  F->>M: matchingservice.PollWorkflowTaskQueue
  M->>H: historyservice.RecordWorkflowTaskStarted
  H->>P: fenced commit: WorkflowTaskStarted
  H-->>M: workflow type, history, event IDs
  M-->>F: task token + history
  F-->>A: PollWorkflowTaskQueueResponse
  A->>F: RespondWorkflowTaskCompleted (commands)
  F->>H: historyservice.RespondWorkflowTaskCompleted (routed by task token)
  H->>P: fenced commit: events, state, new task intents

Every arrow between server roles is an RPC. Matching returns tasks through the pending poll; it never opens a connection to the application worker. Matching builds the poll response and task token from History's RecordWorkflowTaskStarted response, as upstream's createPollWorkflowTaskQueueResponse does.

Visibility is a storage projection, not another server role. History's visibility-processor.ts writes the visibility manager; Frontend reads it.

Reading the Effect code

Follow the Effect service example and layer composition example:

The cluster runtime in effect/cluster was the model for separating placement from transport. Its layer variants (TestRunner, SocketRunner) inspired building the same hosts over different transports. Its entities are not used: Temporal's own shard leases, partition fencing, and forwarding are what this toy teaches.

What is implemented, and what is still missing

Concern Current implementation Upstream behavior still absent
Service ownership Any number of hosts per role, each with its own graph; roles in separate processes with --service Dynamic membership (ringpop), host health, graceful handoff delays
Transport Effect RPC over TCP with generated upstream message schemas; deadlines, interrupts, typed service errors gRPC/protobuf binary, retry policies with backoff, connection pooling, interceptors, TLS
History shards Membership placement, range-ID leases fenced in persistence, ShardOwnershipLost redirects Per-shard queue processors and caches, shard handoff on host loss, replication
Matching placement Partition routing by membership, range-ID fencing, unload on membership change, RPC forwarding Configurable partition counts per queue, forwarder rate limits, distributed backlog readers
Internal Worker Independently scoped scanner Temporal's internal workflows, archival, scheduling, and other maintenance jobs
Visibility Durable asynchronous projection, queried through its manager Full query language and production indexing/storage options

Quality checks

Run pnpm check on Node 24 for typechecking, boundary checks, unit tests, and the stock SDK process integration. Run pnpm test:postgres with a dedicated TEST_DATABASE_URL for SQL, shard-lease fencing between History hosts, roles in separate processes, and process-crash recovery.

test/cluster.test.ts runs two History and two Matching hosts in one process. It checks shard routing, a replaced lease redirecting the client to the new owner, the proto3 JSON frames between named hosts, poll forwarding from a child partition to the root on another host, a partition moving after a membership change, and complete workflows across all four hosts.

The concurrency regressions cover two transfers racing for one waiting poll, partition loads publishing one manager, and concurrent versioned updates having one winner. A Matching host serializes short dispatch operations with one semaphore. It never holds it while waiting for a long poll or calling another partition, which may be served by the same host.