poka.sh

Docs

Architecture

explanation · reviewed 2026-10-07

Poka Architecture

Last updated: 2026-10-26 · Last reviewed: 2026-10-07 (stated in the text below) · Counts and sizes in this file are not maintained by hand; see "Coverage".

Status labels used below: Implemented (in the repo today), Planned (designed, not built, or not found in the code when this file was last reviewed on 2026-10-07).

Overview

A language-agnostic plugin system for file previews:

  • Host: Rust. A Tauri desktop app (desktop/) and a web app (web/) run the same extension pipeline (see the parity contract).
  • Plugins: WebAssembly (.wasm) modules, one file for every platform. The desktop host runs them in-process under Wasmtime; the web app runs them in a Web Worker with a JS WASI shim.
  • Protocol: Protocol Buffers over stdin/stdout (WASI). The schema is core/proto/preview.proto.
  • Registry: the route table web/src/api/formats.json is the catalogue for both hosts; the desktop installs extensions on demand from it (registry_install.rs).

Preview activation:

  • Nautilus (Linux/GNOME): Press Space — a Python extension detects the keypress, reads the selected file URI, and calls Poka via D-Bus.
  • CLI (all platforms): poka <filepath> — sends to a running instance or starts one.
  • Windows Explorer: Implemented — global Ctrl+Space hook (windows_explorer_hook.rs) previews the file selected in Explorer / on the Desktop. (Bare Space is reserved for typing, mirrored from PowerToys' Peek.) macOS Finder remains Planned pending Apple hardware.
  • Web: drop a file or folder, open one, or paste a link on poka.sh.

Coverage: for the current canonical format count, run ../poka-parsers/scripts/format-stats.py. It is the single source of truth, and the numbers in this file are not kept up to date by hand. For scale, on 2026-10-07 it reported 1404 manifests (a plugin.json, or a route in parsers/aliases.toml) and 1374 unique file_extension values. The old "550+ / 745 / 768 / 1600+" figures across docs are superseded. A route is not a measure of depth: poka-parsers/README.md carries the generated figure (real parsers plus shared-binary routes, hex stubs excluded), and the viewer table below shows how many manifests still use the hex-dump fallback.


Architecture

┌──────────────────────────────────────────────────────────────────────┐
│ Desktop host (Rust / Tauri)                                          │
│  poka-core: extension_registry  route a name or content              │
│  poka-core: extension_host      Wasmtime run, limits, deadline       │
│  registry_install            on-demand install, hashes verified      │
│  preview_cache / thumbnails  disk caches (~/.cache/poka/...)         │
└──────────────────────────────┬───────────────────────────────────────┘
                               │ same routing, same protocol
┌──────────────────────────────┴───────────────────────────────────────┐
│ Web app (web/): engine-core.js + wasi.js in a Web Worker             │
└──────────────────────────────┬───────────────────────────────────────┘
                               ▼
              web/src/api/formats.json (route table)
                               │
                  ┌────────────┴────────────┐
                  ▼                         ▼
            parser .wasm              viewer .wasm
        (poka-parsers repo)        (poka-viewers repo)

The host does not contain parser or viewer source. Poka is three code repos joined by the preview.proto contract (this one, poka-parsers, poka-viewers), plus poka-samples, the test corpus.


Plugin System (WebAssembly)

Plugins compile to a single .wasm file that runs on every host Poka builds, via the embedded Wasmtime runtime (desktop) or the browser's WebAssembly engine (web); no Python, Node or any external runtime is required.

Why Wasm rather than native DLLs

Feature Native DLLs WebAssembly
Builds per plugin 6 1
User download size ~3.5 MB ~1.5 MB
Sandbox Process isolation Wasm linear memory
Browser compatible ❌ ✅

(Figures from the original design comparison; not re-measured.)

Extension Structure

Each extension owns one primary file extension (file_extension) and may list aliases: closely related formats the same module genuinely handles. Two different modules claiming the same extension is the anti-pattern; routing resolves any conflict by a fixed rule (see the parity contract).

poka-dxf/
├── plugin.json          # Manifest
└── poka-dxf.wasm       # Universal module

Aliases that need no module of their own can also be routes in poka-parsers/parsers/aliases.toml.

Extension Manifest (plugin.json)

{
  "id": "poka-dxf",
  "name": "DXF Preview",
  "version": "1.0.0",
  "kind": "parser",
  "file_extension": ".dxf",
  "viewer": "poka-vector-viewer",
  "wasm": {
    "path": "poka-dxf.wasm",
    "sha256": "abc123...",
    "size": 1572864,
    "memory_mb": 32,
    "max_time_ms": 10000
  }
}

The example is illustrative (the values are not a real manifest). kind/role is parser or viewer, viewer names the shared viewer a parser composes with, aliases and abi are optional; AGENTS.md ("Extensions") has the full field list.

Extensions communicate via Protocol Buffers over stdin/stdout (WASI). The host sends a PreviewRequest on stdin; the extension writes a PreviewResponse (or, when composed with a viewer, a *Ast message) to stdout.

CLI Usage

poka <filepath>              # Preview a file (sends to running instance, or starts one)
poka                         # Start in background (system tray)

Building Extensions (Rust → Wasm)

rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1

Extensions can be written in any language that compiles to wasm32-wasip1. See ../poka-parsers/parsers/poka-svg/ for a working example.


Module Composition

A pattern that reuses the existing Wasmtime runtime to compose multiple .wasm modules per preview, instead of treating every extension as a self-contained monolith. Two concrete applications motivate it: centralising UI rendering across extensions that share an output shape (text / table / tree / hex / metadata / image / vector / 3D) — implemented — and enabling source languages that don't compile to clean WASI (Mojo, Swift, .NET) — still planned.

Primitive: wasm-to-wasm linking with shared memory — Planned (sidecars)

wasmtime::Linker already supports instantiating multiple modules in the same Store, with one module's exports satisfying another module's imports. If both modules declare (import "env" "memory" ...) bound to the same wasmtime::Memory, they share a linear address space — so a function exported by module A can read/write a buffer that module B allocated.

┌──────────────────────────────────────────────────────────┐
│ wasmtime Store                                           │
│                                                          │
│ ┌───────────┐    calls (import resolution)   ┌────────┐  │
│ │ Module A  │ ─────────────────────────────▶ │Module B│  │
│ │           │                                │        │  │
│ │           │ ◀──────── shared Memory ─────▶ │        │  │
│ └─────┬─────┘                                └────┬───┘  │
│       └───── WASI linker (fd_read, fd_write, …) ──┘      │
└──────────────────────────────────────────────────────────┘

Shared-memory linking is the proposed mechanism for sidecars. The shared viewers that exist today do not use it: the host runs the parser, then passes its *Ast output to the viewer as a second, separate run (parser→viewer composition in extension_host.rs).

Use case A — language sidecar (<lang>-rt.wasm) — Planned

Some languages compile to wasm that depends on per-language runtime imports rather than standard WASI. Mojo's emitted wasm needs ~12 env::* symbols: an aligned allocator, KGEN_CompilerRT_AsyncRT_*, write, math intrinsics, __multi3. Bare wasmtime rejects the module on instantiation.

A small sidecar module can resolve those imports while itself importing only WASI:

poka-fmt-XXX.wasm  (Mojo)  ─── env::write ──────▶  mojo-rt.wasm   ─── wasi::fd_write
                            ─── env::aligned_* ──▶  (shim, ~5 KB) ─── wasi::fd_read
                            ─── env::KGEN_RT_*  ─▶                ─── …

The shim is written once, in a wasm-friendly language (C or Rust), and shipped with the host. New Mojo extensions contain only format-specific logic. The same pattern serves any future language with non-WASI runtime requirements (Swift, .NET, Kotlin/Wasm).

Use case B — shared viewer modules — Implemented

Most text-shaped extensions duplicate HTML/CSS rendering if left as self-contained monoliths. Splitting that into parser modules (format-specific) and a small set of viewer modules (format-agnostic, shared) collapses the duplication. This is live today: of the 896 plugin.json manifests in poka-parsers/parsers/, 862 declare a shared viewer rather than rendering their own HTML/CSS (counted on 2026-10-07; the alias-only routes in aliases.toml have no manifest of their own).

Viewer Handles Manifests using it (2026-10-07)
poka-metadata-card-viewer Key/value metadata cards 294
poka-tree-viewer Nested key/value (JSON, YAML, TOML, XML, INI…) 285
poka-table-viewer Row/column data (CSV, TSV, Parquet, DBF, SQLite…) 94
poka-text-viewer Plain text + syntax-highlighted code 50
poka-image-with-meta-viewer Raster images + metadata 47
poka-3d-viewer Interactive 3D (orbit/zoom real geometry; a host special case, see AGENTS.md) 32
poka-hex-viewer Fallback hex-dump for formats with no real parser yet 22
poka-vector-viewer Vector graphics 21
poka-workbook-viewer Spreadsheet workbooks 7
poka-chart-viewer, poka-slide-viewer Charts, slide decks 3 / 3
poka-ebook-viewer E-books 2
poka-code-viewer, poka-diff-viewer Code, diff 1 / 1

The remaining 34 manifests declare no viewer at all: format-marker extensions that hand real geometry straight to the frontend's Three.js loader, and parsers that render their own result.

Each parser declares which viewer it pairs with in plugin.json. A UI improvement to a viewer immediately propagates to every parser that uses it.

ABI

Protocol Buffers — the same toolchain Poka already uses for the extension↔host hop, used for the parser→viewer hop too: parsers encode an *Ast message (TableAst, TreeAst, TextAst, WorkbookAst, SlideDeckAst, Drawing2DAst, …, all in preview.proto) and viewers decode it. Versioning rides on protobuf's field-number compatibility rules.

Trade-offs

Benefit Cost
New format extension drops to ~30–50 LOC (parsing only) ABI must be versioned and maintained across parsers + viewers
Shared rendering = consistent UI across many extensions per viewer Per-format UX polish gets harder if a format wants bespoke chrome
Sidecars would unlock new source languages without per-extension host changes Cold-start cost roughly doubles (two wasm instantiations per preview)
A first language-runtime shim would make adding the next language ~1 day Debugging a failed preview now spans 2–3 modules

Rollout

Bottom-up adoption: no existing extension is forced to migrate.

  1. Shared viewers (parser + table-viewer) — done; the table viewer alone serves 94 manifests, which settles the premise of the first pilot.
  2. mojo-rt.wasm + one hello-world Mojo extension as a proof of concept for language sidecars — still Planned; it would unblock Mojo experimentation without waiting on Modular to ship a native wasm target (modular/modular#4729). I did not find it in the repos when reviewing this file.

Communication Protocol

Protocol Buffers over stdin/stdout.

Why Protobuf over JSON-RPC:

  • Smaller wire format (binary, no Base64)
  • Faster encoding/decoding
  • Native binary support (images transferred directly)
  • All plugin languages supported

(These are the original design arguments; not re-benchmarked.)

Proto Schema

The schema is core/proto/preview.proto (package poka.preview), the single canonical copy: read it there rather than in a copy here. Parsers and viewers don't compile it themselves: poka-parsers and poka-viewers each have one crate, poka-preview-proto, whose build script generates the prost bindings once, and each parser or viewer depends on it. In outline:

  • Request: PreviewRequest (file path, size hints, quality, a params map).
  • Response: PreviewResponse with a ResponseType (TYPE_IMAGE, TYPE_TEXT, TYPE_SVG, TYPE_HTML, TYPE_MODEL, TYPE_DWG, TYPE_SCENE3D, TYPE_PDF, TYPE_MULTIVIEW, TYPE_METADATA, TYPE_ERROR, …) and one matching payload (ImagePreview, TextPreview, SvgPreview, HtmlPreview, PdfPreview, ModelPreview, DwgDrawing, MultiView, ErrorResponse, …).
  • Facts and integrity: FileFacts/FactEntry for the information card, and IntegrityCheck with IntegrityKind/IntegritySeverity for what a parser found wrong with a file.
  • ASTs for viewers: TableAst, WorkbookAst, SlideDeckAst, TextAst, TreeAst, Drawing2DAst, and more.

ProgressUpdate and DataChunk are in the schema but the hosts do not act on them: the web engine's response mapping lets the Progress and Chunk arms fall through to NoExtension (web/src/engine-core.js), and I found no use of them in the desktop host. Streamed progress and chunking are therefore Planned.

Size limits

What the hosts enforce today, rather than a chunking scheme:

Where Limit
Web engine a file over 256 MiB is refused with a "too large" message (MAX_BYTES, web/src/engine.js)
Desktop, SVG / PDF read whole up to 256 MiB, refused beyond (WHOLE_FILE_MAX, commands.rs)
Desktop, text / source the first 8 MiB, with a note that the rest is cut off (TEXT_PREVIEW_MAX)
Desktop, raster images dimension and pixel limits (image_limits.rs)
Extension output stdout capped at 64 MiB per run (extension_host.rs)
Extension memory and time per manifest: wasm.memory_mb, wasm.max_time_ms

Host Implementation (Rust)

The desktop host's engine is the poka-core crate (core/poka-core), which the local web server (web/) depends on too; its runner is core/poka-core/src/extension_host.rs. For each preview it:

  1. routes the file name (then, where the rules allow, its content) to an extension through extension_registry.rs;
  2. loads the module, verifying its sha256 against the manifest, with an in-memory module cache on top of Wasmtime's disk cache;
  3. builds a WASI context: stdin carries the PreviewRequest, stdout is a capped in-memory pipe, and the file's directory is preopened read-only at /input;
  4. applies the manifest's memory limit (StoreLimits, at most 5 instances) and a deadline from max_time_ms, enforced with Wasmtime epoch interruption driven by one shared ticker;
  5. runs the module and decodes the PreviewResponse from stdout, composing with the viewer module when the manifest names one.

A full-preview response cache (preview_cache.rs, ~/.cache/poka/previews/, keyed by the file's path and mtime plus the extension's id, version and module files) lets a repeat preview skip the wasm run. It is desktop-only.

The web host does the same steps in web/src/engine-core.js and engine-worker.js, with the WASI shim in wasi.js. Rules that keep the two in step are in the parity contract.


Security (Sandboxing)

Wasm sandbox — Implemented

  • Wasmtime configuration with epoch interruption and a per-run memory limit (StoreLimitsBuilder: memory_size from memory_mb, instances(5)).
  • The only filesystem access is the previewed file's directory, read-only, at /input.
  • Module integrity: the sha256 and size in the route table and manifest are checked before a module runs or is installed (scripts/check-route-hashes.py, registry_install.rs).
  • In the browser, previews that produce HTML or SVG are shown in a sandboxed frame under a strict CSP (web/security-headers.mjs; specs web-csp.spec.js, svg-isolation.spec.js).

Planned

  • Platform sandboxing for non-Wasm plugins (none exist today): Linux systemd-run / seccomp / firejail, macOS sandbox-exec, Windows Job Objects.
  • Download verification beyond hashes: certificate pinning, GPG signature validation, licence prompts for proprietary SDKs.
  • Signed registry manifests and static analysis before publishing. ADR 0002 (docs/adr/) records the free-tier registry format.

Thumbnail Generation

Partly implemented. thumbnails.rs writes a downscaled JPEG thumbnail (about 256 px on the long side) into ~/.cache/poka/thumbnails/ after a desktop preview, and the get_thumbnail command reads it back for a thumbnail strip. The key is SHA256(path + mtime), so editing a file invalidates its entry.

The batch CLI (poka thumbnails --size 256 --recursive …, --incremental, --cleanup) and size sets (64/128/256) are Planned: I found no such subcommand in main.rs.


Installation Paths

Extensions live in ~/.poka/extensions/ (Linux/macOS) or %APPDATA%\Poka\extensions\ (Windows), one directory per extension holding plugin.json and the .wasm. Caches are under ~/.cache/poka/ (previews/, thumbnails/). The fuller layout earlier drafts showed (a binaries/ tree of optional proprietary binaries, licenses/, registry/, config/) is Planned; this review did not confirm those directories exist.


Performance

There are no per-format targets or "actual" timings in this file: earlier tables had no source or date. The enforced numbers are in scripts/perf-budgets.json: a ceiling per wasm module (20 MiB) and a preview-latency ceiling (5 s by default, with reasoned per-file allowances), checked by scripts/check-wasm-size.py and node scripts/test-client-engine.mjs --strict. The latter also reports p50/p95/p99/max. They catch regressions; they are not targets.


Bundles — Planned

Install-on-demand bundles (Essentials, Office, Design, Engineering, Scientific, All) are a packaging idea, not built, and the sizes once quoted for them were estimates. Today the web app serves the whole catalogue and the desktop installs per extension.


Technology Stack

Component Technology
Host Rust + Tauri + Wasmtime (desktop); Rust dev server and a static Cloudflare Worker (web)
Protocol Protocol Buffers
Plugins WebAssembly (.wasm)
Build tools wasm32-wasip1 target, wasm-opt
Sandbox Wasm linear memory limits + WASI + epoch deadlines
Distribution Sibling-repo releases pinned in web/extension-releases.json; on-demand install on the desktop

Known Issues

  • Cold start latency: a first preview pays module load and compile time. Mitigated by the in-memory module cache, Wasmtime's disk cache and the desktop preview cache; no pre-warmed pool of instances exists.
  • Progress feedback: large files show no progress. ProgressUpdate is in the schema but unused (see above).
  • Preview cache outside the desktop: only the desktop caches full previews.
  • Signatures: hashes are verified; signatures on modules and registry manifests are not yet.
  • Registry integrity: signed manifests and static analysis before publishing remain Planned.
  • Sandbox hardening: epoch deadlines, memory limits and a read-only preopen exist; syscall-level restriction beyond what WASI exposes does not.

Resolved

  • Base64 overhead → eliminated with Protocol Buffers.
  • Native DLL per-platform builds → replaced by single .wasm per plugin.
  • Dependency conflicts → isolated Wasm environment removes runtime clashes.

Documentation

File Purpose
docs/explanation/architecture.md This file: core architecture and known issues
AGENTS.md Repo working rules, test commands and layout (read this first)
docs/README.md Index of the documentation, with each page's type and audience
docs/tutorials/, docs/how-to/ Using Poka, building from source, adding a format end to end
docs/reference/ The poka-web HTTP API, environment variables and file locations
docs/explanation/pipeline.md The parser and viewer pipeline in a page
docs/adr/ Recorded design decisions
poka-parsers/FILE_FORMATS.csv The format registry: one row per parser, with support status and libraries
poka-parsers/docs/format-coverage-and-architecture.md The 10,000-format target architecture (the only copy of that plan)

Web/Desktop engine parity contract

The desktop and web hosts run the same extension pipeline and must give the same PreviewResult. The contract, the rules that keep the two in step and the known, deliberate divergences are in Desktop and web parity contract.

Revision history

Date Commit Summary
2026-10-26 (this change) refactor: the engine crate is renamed from poka-engine to poka-core (core/poka-core)
2026-10-26 524527d7 refactor: the Rust engine is the core/poka-engine crate (now core/poka-core) both hosts depend on
2026-10-12 (this change) docs: add a status header and a revision history
2026-10-09 f069bc2d docs: describe the XML root and root-namespace routing
2026-10-09 9a5ad733 fix: sniff a .xml by content when poka-xml is not installed
2026-10-08 ef22a4ea feature: route an .xml with a dsbXML root to poka-dsbxml by content
2026-10-08 0eab8e82 docs: viewers use a shared poka-preview-proto crate too