Architecture
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.jsonis 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.
- Shared viewers (parser +
table-viewer) — done; the table viewer alone serves 94 manifests, which settles the premise of the first pilot. 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, aparamsmap). - Response:
PreviewResponsewith aResponseType(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/FactEntryfor the information card, andIntegrityCheckwithIntegrityKind/IntegritySeverityfor 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:
- routes the file name (then, where the rules allow, its content) to an extension through
extension_registry.rs; - loads the module, verifying its sha256 against the manifest, with an in-memory module cache on top of Wasmtime's disk cache;
- 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; - applies the manifest's memory limit (
StoreLimits, at most 5 instances) and a deadline frommax_time_ms, enforced with Wasmtime epoch interruption driven by one shared ticker; - runs the module and decodes the
PreviewResponsefrom 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_sizefrommemory_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; specsweb-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.
ProgressUpdateis 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
.wasmper 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 |