Tide

Architecture

Internal design, tech stack, and implementation details

Overview

Tide is a Rust workspace. The core runtime, stores, runner, and serve crates are argon-free; the shipped binary that embeds Argon lives in an excluded workspace (tide-argon) so the default build and CI never fetch the private Argon repo.

tide/
├── cli/              # CLI command logic (run / resume / test / schema / serve)
├── runtime/          # Core runtime (Deno Core + V8)
│   └── src/
│       ├── lib.rs    # Runtime orchestration, run/test entry points
│       ├── ops.rs    # Deno ops (file, step, sleep, console, ai, hitl, state)
│       ├── store.rs  # DurableStore trait + effect-intent reservation
│       ├── state.rs  # StateHost world-state capability surface
│       ├── loader.rs # TypeScript module loader
│       ├── runtime.js# JavaScript globals and sandbox setup
│       └── tide.d.ts # TypeScript type definitions
├── store-fs/         # Filesystem storage backend
├── store-sqlite/     # SQLite storage backend
├── store-postgres/   # PostgreSQL storage backend
├── tide-runner/      # Workflow runner: source + queue + cooperative drive
├── tide-serve/       # HTTP /v1 control surface over the runner (axum)
├── tide-argon/       # Shipped binary: embeds Argon for the state.* backend
├── examples/         # Example workflows
└── docs/             # Documentation site (Fumadocs)

Tech Stack

ComponentTechnologyPurpose
RuntimeDeno Core (V8)JavaScript/TypeScript execution
LanguageRustCore runtime, storage, CLI
TypeScriptdeno_astOn-the-fly transpilation
VFSvfs crate (MemoryFS)In-memory virtual filesystem
SQLiterusqlite (bundled)SQLite storage backend
PostgreSQLsqlxPostgres storage backend
AsyncTokioAsync runtime (the run future is !Send — V8)

Execution Flow

1. CLI parsing (cli/src/lib.rs)

The CLI parses the subcommand (run / resume / test / schema / serve) and its flags, builds the storage backend and the world-state Backend, and dispatches:

  • Parses -i, --store, --input / --input-file, --argon, --json (and, on the embedded binary, --world-dir / --fresh)
  • Validates --input is valid JSON
  • Creates Arc<dyn DurableStore> (fs, sqlite, or postgres)
  • Runs the workflow via run_with_state_host_and_overlay(...), settling the run's world-state fork through the Backend seam

2. State Initialization (runtime/src/lib.rs)

The run() function sets up the runtime:

  1. Load journal from store -- empty on first run
  2. Resolve input -- on replay, use stored input; on first run, persist CLI input
  3. Resolve timestamp -- on replay, load stored timestamp; on first run, capture and persist Date.now()
  4. Create VFS -- starts as empty MemoryFS (state rebuilt from journal replay)
  5. Build DurableState -- contains journal, position counter, store reference, VFS snapshot slot, transaction buffer

3. V8 Runtime Setup

JsRuntime::new(RuntimeOptions {
    module_loader: TsModuleLoader,
    startup_snapshot: TIDE_SNAPSHOT,  // Pre-compiled runtime.js
    extensions: [tide extension],
})

The V8 snapshot (TIDE_SNAPSHOT) is built at compile time from runtime.js. It contains:

  • console.log/error (calls op_console_log)
  • readFile, writeFile, readFileBytes, writeFileBytes, removeFile, listFiles
  • sleep (calls op_set_timeout)
  • step (calls op_step_begin/op_step_end)
  • S schema builder (all methods)
  • __tideValidate (schema validation function)
  • __initFrozenTime (frozen Date overrides)
  • __initSandbox (security sandbox)

4. Initialization Sequence

After the V8 runtime is created:

  1. Inject DurableState and VfsPath into OpState
  2. Call __initFrozenTime() -- overrides Date.now(), new Date(), performance.now()
  3. Call __initSandbox() -- disables eval/Function, removes globals, freezes prototypes
  4. Load and evaluate the user module (top-level code runs, schema is defined)
  5. Verify export default { schema, main } convention
  6. Check if schema requires input
  7. Parse input JSON, validate against schema with __tideValidate
  8. Call main(input) via orchestration script

5. Op Execution (runtime/src/ops.rs)

Each JavaScript API call maps to a Rust op via Deno Core's op system:

JavaScriptRust OpJournal Op
readFile(path)op_read_fileop_read_file
writeFile(path, contents)op_write_fileop_write_file
readFileBytes(path)op_read_file_bytesop_read_file_bytes
writeFileBytes(path, contents)op_write_file_bytesop_write_file_bytes
removeFile(path)op_remove_fileop_remove_file
listFiles(path?)op_list_filesop_list_files
sleep(ms)op_set_timeoutop_set_timeout
console.log/errorop_console_logop_console
step(name, fn) beginop_step_beginop_step_begin
step(name, fn) endop_step_endop_step_complete
Date.now()op_get_frozen_timestamp(not journaled)

Each op follows the same pattern:

  1. Check if replaying -- return stored result from journal
  2. Execute live -- perform the actual operation
  3. Record -- create journal entry with result
  4. Commit -- if not in a step, persist immediately

6. Transaction Buffer

The transaction buffer for steps:

DurableTransaction {
    journal_entries: Vec<JournalEntry>,  // Buffered during step
}
  • Outside a step: Each op records + commits immediately (single entry)
  • Inside a step: Ops record to the buffer; on success, ALL entries commit atomically; on failure, buffer is discarded and VFS is restored from snapshot

7. Module Loader (runtime/src/loader.rs)

The TsModuleLoader handles:

  • Resolution: Only allows file:// scheme (blocks https://, data:, etc.)
  • Loading: Reads file from disk, transpiles TypeScript if needed
  • Supported extensions: .ts, .mts, .cts, .tsx, .jsx, .js, .mjs, .cjs, .json
  • Uses deno_ast for TypeScript transpilation

8. Frozen Timestamp

Date.now() returns the same frozen value for the entire invocation:

  • First run: captures real Date.now() at startup, persists it
  • Replay: loads stored timestamp from storage

If Date.now() returned real time, workflows that branch on time could take different paths on replay, causing determinism violations. Freezing time makes all time-dependent code deterministic.

9. Console Log Journaling

Console output is stored in the journal as op_console entries:

  • Live execution: Prints to stdout/stderr AND records in journal
  • Replay: Suppresses live console calls; journal entries are drained and printed by replay_entry() when the next real op replays
  • Trailing logs: After execution completes, flush_console_logs() prints any remaining console entries

On this page