Hosted Modules
Hosted modules expose native Rust functionality to JavaScript through the runtime.
Use hosted modules for Otter-owned APIs such as:
otter:kv;otter:sql;otter:ffi;- standard-facing or runtime-specific modules.
Hosted modules must enforce capabilities at the Rust boundary. Do not trust JavaScript wrappers or TypeScript declarations as the only permission check.
The hosted module crate is crates/otter-modules.
Resolution
Hosted modules are registered on the runtime builder:
let mut runtime = otter_runtime::Runtime::builder()
.hosted_modules(otter_modules::hosted_modules().iter().copied())
.build()?;
The module loader resolves registered otter:* specifiers directly to
their specifier string. The module graph creates a hosted module namespace
instead of reading a source file, so JavaScript can import the surface with
normal ESM syntax:
import { openKv } from "otter:kv";
const store = openKv(":memory:");
Unregistered otter:* specifiers fail resolution.
File Layout
Use the repository naming convention:
| File | Purpose |
|---|---|
module_ext.rs | Rust implementation of native functions |
module.js | JavaScript shim, wrapper, or polyfill |
Rust code owns permissions, resource opening, async dispatch, GC rooting, and external-memory accounting. JavaScript shims may normalize arguments or provide ergonomic exports, but they must not be the only enforcement layer.
Native Boundary
Hosted module native functions should:
- validate arguments on the isolate thread;
- check capabilities before opening host resources;
- allocate through
NativeCtx; - store persistent JS references as branded roots, not raw
Gc<T>; - account host buffers with
ExternalMemory; - for async work, copy owned host data into the future and post an owned completion back to the isolate.
Do not move VM values, handles, contexts, frames, or handle scopes into Rust futures.
Bootstrap And Builders
The production builder/spec flow handles namespace installation. Hosted
modules should use runtime-owned specs such as RuntimeNamespaceSpec or the
HostedModuleCtx / RuntimeObjectBuilder API when their surface needs
capability-aware installation. Keep module registration centralized and easy
to audit. If capability enforcement or bootstrap order is delicate, prefer
explicit manual code over hiding control flow behind a macro.
Module namespaces that need runtime capabilities install through
HostedModuleCtx::method with HostedNativeCall::dynamic(...) closures that
capture owned, Send + Sync host data such as a cloned CapabilitySet.
Plain namespace exports should use HostedModuleCtx::builtin_method,
HostedModuleCtx::property, and HostedModuleCtx::readonly_property.
Receiver-backed resource objects should be created with
RuntimeObjectBuilder::from_host_data(ctx, data) and accessed through
runtime_with_host_data / runtime_with_host_data_mut. This is still a static
registration path: the module specifier list is fixed, resolution is
centralized, and no per-call metadata parser or hot-path dynamic registry is
introduced.
Macros are appropriate when they generate the same static specs and builder calls a manual implementation would write. If a module surface needs new macro ergonomics, add the macro over the builder API rather than bypassing it.
Active Modules
The current active slices are:
otter:kv:openKv/kv, with in-memory and file-backed JSON stores.otter:sql:openSql/sql, backed by SQLite.otter:ffi:dlopen, permission-checked library loading metadata.
otter:kv and otter:sql have module-graph tests that import the module
specifier and execute the exported native functions.