Yoke.sh
Runtime

Runtime Architecture

YokeRuntime components, service wiring, and end-to-end data flow through Postgres, fan-out, queues, and Deno.

YokeRuntime is the top-level assembly of every kernel subsystem. It implements the Runtime trait and wires Postgres, fan-out, scheduling, storage, and the embedded Deno host into one long-lived process.

Component map

YokeRuntime holds:

FieldResponsibility
servicesStreamWithFanout, instance registry, Storage — shared by the op handler
denoEmbeddedDenoHost — package load, dispatch_process, filter eval
fanoutFanoutEngine — index rebuild, async enqueue
schedulerQueueDrainScheduler — per-instance queue drain loop
storageStorage enum (PgSqlite | S3) — actor SQLite + ctx.state
_worker_registryRegisters in runtime_workers, claims unassigned instances
_stream_catchupBackground tail for cold-start / multi-executor fan-out
_timer_dispatchTimer dispatch — fires due rows from scheduled_events
_lease_heartbeatOptional tenant lease extension (YOKE_EXECUTOR_MODE)

RuntimeServices groups the three handles the JSON-RPC op handler needs: the stream (with fan-out wrapper), the instance registry, and storage.

Data flow diagram

flowchart TB
    subgraph external [External]
        PortalHTTP[Portal HTTP routes]
        UserTS[User TypeScript package]
    end

    subgraph deno [Embedded Deno]
        WorkerRPC[WorkerRpc JSON-RPC]
        ProcessFn["actor.process(ctx, event)"]
        PortalIface[Portal interface methods]
    end

    subgraph rust [YokeRuntime Rust]
        OpHandler[RuntimeOpHandler]
        StreamWF[StreamWithFanout]
        PgStream[PgStream]
        Fanout[FanoutEngine]
        Queue[PgActorQueue]
        Scheduler[QueueDrainScheduler]
        Storage[Storage pg_sqlite or S3]
    end

    subgraph pg [Postgres]
        StreamEvents[(stream_events)]
        ActorQueues[(actor_queues)]
        Instances[(actor_instances)]
        ActorSqlite[(actor_sqlite)]
    end

    PortalHTTP --> OpHandler
    UserTS --> WorkerRPC
    WorkerRPC <-->|unix socket| OpHandler

    OpHandler --> StreamWF
    StreamWF --> PgStream
    PgStream --> StreamEvents
    StreamWF -->|async| Fanout
    Fanout --> Queue
    Queue --> ActorQueues

    Scheduler --> Queue
    Scheduler -->|dispatch_process| WorkerRPC
    WorkerRPC --> ProcessFn
    ProcessFn -->|stream_append op| OpHandler
    ProcessFn -->|portal_call op| PortalIface

    OpHandler --> Storage
    Storage --> ActorSqlite
    Fanout --> Instances
    Scheduler --> Instances

Append path (hot path)

  1. Deno SDK calls stream_append op → RuntimeOpHandler.
  2. StreamWithFanout::append_and_fanout writes via PgStream::append (assigns seq, inserts row).
  3. Append returns AppendResult { seq, id } to the caller immediately.
  4. Fan-out dispatches asynchronously (or deferred during nested actor ops — see Fan-out).
  5. record_dispatched updates the per-tenant fan-out cursor in runtime_metadata.

Pseudocode:

append_and_fanout(input):
  result = stream.append(input)
  record metrics
  if should_defer_fanout():
    spawn fanout.dispatch(event)  // avoid re-entering Deno
  else
    fanout.dispatch(event)
  return result

Process path (actor drain)

  1. QueueDrainScheduler tick loop lists actor instances and active group_id values.
  2. Per (instance_id, group_id) it acquires semaphore permits up to queue_policy.concurrency.
  3. claim_next marks a row processing in actor_queues.
  4. EmbeddedDenoHost::dispatch_process RPC runs process() in Deno.
  5. On success → queue.complete deletes the row. On failure → mark_failed retries or dead-letters.

See Scheduler and Queues.

Op handler bridge

RuntimeOpHandler implements OpHandler and is installed on the Deno worker RPC channel. Every SDK kernel call (ctx.append, ctx.state, ctx.instances.create, conversations, schedules) maps to a method string handled in the runtime.

The handler holds references to stream, instances, fan-out, storage, and conversations so Deno never touches Postgres directly.

Background tasks

TaskTriggerPurpose
Stream catch-upLISTEN + 30s pollFan out events appended while executor was cold
Timer dispatchLISTEN + next-fire sleepAppend due scheduled_events to stream
Worker heartbeat15s intervalUpdate runtime_workers.last_heartbeat
Executor lease15s interval (optional)Extend tenant_runtimes lease TTL

Horizontal scaling

Multiple runtime workers share one Postgres database. Each registers in runtime_workers and claims actor_instances rows where worker_id IS NULL. New instances are assigned to the creating worker's ID. The scheduler only drains queues for instances owned by the local worker.

VariablePurpose
YOKE_WORKER_IDStable worker identity (defaults to {hostname}-{uuid})

Stale workers

Workers with no heartbeat for 60 seconds are excluded from the yoke_runtime_workers gauge. Instance ownership is not automatically migrated yet — plan worker drains before shutdown.

Control plane vs executor

The control-plane HTTP API uses NoOpFanout so appends write to stream_events only. Tenant executors fan-out via stream_catchup (boot and LISTEN yoke_stream_append while hot). Duplicate delivery is prevented by fanout_delivered (see Fan-out).

Subsystem modules

The Rust runtime is organized around these responsibilities:

AreaRole
Runtime assembly + op handlerBoot, JSON-RPC bridge, kernel orchestration
StreamPer-tenant sequenced append to stream_events
Fan-outTwo-tier filters → actor_queues
QueuesPersisted per-instance work queues
SchedulerDrain loops, concurrency, Deno dispatch_process
Timer dispatchscheduled_events → stream
Tenant schedulerRuntime lifecycle for hosted deployments
Storagepg_sqlite vs S3 backend selection
SQLite layerPer-instance file + __yoke_state CAS
Embedded Denolibdeno worker host
Worker RPCJSON-RPC framing
Portal HTTPRoutes, /health, /metrics
MigrationsSQL schema applied at boot

On this page