Learn Hermes JS Engine - Application Architecture & Isolation
Episode 14 of 23

Learn Hermes JS Engine - Application Architecture & Isolation

This episode covers how to keep the boundary between business logic and native modules, modularize bundles and use ephemeral contexts for isolation, and safely run untrusted scripts inside a Hermes embedding.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

Episode 13 closed with confidence: AOT without JIT gives Hermes an attractive security profile, as long as it's paired with signed bundles and hardening. Episode 14 widens the focus from the runtime level to the architecture level. The question is no longer how the engine works, but how we structure the app so its trust boundaries are clear.

This episode's roadmap: keeping the boundary between business logic and native modules, modularizing bundles and ephemeral contexts, then handling untrusted scripts safely in an embedding.

The Boundary Between Business Logic and Native Modules

In a Hermes app there are two worlds: JavaScript running on top of the engine, and native code communicating with it via JSI. The boundary between them is the trust line. The rule is simple — native modules are the trusted zone, JavaScript is the mutable zone. The fewer privileges JavaScript has, the smaller the damage if its code is compromised.

That means sensitive logic like payments, cryptography, and keychain access shouldn't be implemented in JavaScript. Move it to a native module wrapped in a minimal API:

JSfacade-native-module.js
class PaymentService {
  constructor({ gateway }) {
    this.gateway = gateway;
  }
 
  async charge(userId, amount) {
    return this.gateway.charge(userId, amount);
  }
}

Business logic can still live in JavaScript — that's the engine's strength — but every operation with important side effects must go through a facade supplied by native code. JavaScript never holds the keys, it only requests services.

Separating Layers with Facades and Adapters

A useful pattern is putting native modules behind their own interface, so UI components don't touch native directly. If the native implementation is later swapped, for example moving from an old module to a new one, only the adapter changes:

JSadapter-penyimpanan.js
const storage = {
  async save(key, value) {
    return NativeModules.KeyValueStore.set(key, value);
  },
};
 
async function persistSession(session) {
  await storage.save("session", JSON.stringify(session));
}

With this pattern, the architectural boundary becomes real in code, not just a promise in docs. Every layer knows exactly what it's allowed to call, and every native access can be audited through a single point.

Bundle Modularization and Lazy Loading

A healthy app doesn't load all its code at once. Metro supports bundle splitting, and Hermes executes modules with lazy require. The basic pattern: call require only when the module is truly needed.

JSlazy-require.js
function openSettings() {
  require("./screens/SettingsScreen").mount();
}

Modularization gives two advantages: faster startup time because less bytecode is loaded, and a smaller execution surface. In an embedding, this splitting also means you can load a third-party bundle as a standalone unit — then detach it again without affecting the core app.

Info

Modularization isn't just about performance. Separate bundles are easier to audit and can each be signed and verified independently.

Ephemeral Contexts for Isolation

For untrusted code — plugins, user scripts, or extensions — full isolation matters more than speed. The strategy is to run the script in a specially created new runtime, give it a limited API, then throw it away when done. Creating a new runtime is as easy as calling HermesRuntime::make():

Linuxephemeral-context.cpp
auto isolated = HermesRuntime::make();
 
isolated->global().setProperty(
    *isolated,
    "safeApi",
    safeApi.getObject(*isolated));
 
auto result = isolated->evaluateJavaScript(
    facebook::jsi::String::createFromUtf8(
        *isolated, untrustedCode),
    "plugin.js");
 
isolated.reset();

A reset runtime frees all of its state: objects, heap, and global scope. No state leaks into the next execution. That's why an ephemeral context is safe — no matter what the script does inside, its impact is contained within that runtime's lifetime.

Handling Untrusted Scripts in an Embedding

Finally, some hard rules for embeddings that run foreign code:

  • Don't give full native access: foreign scripts may only call explicit host functions, not entire modules.
  • Turn off eval and dynamic functions: this is the most common path for smuggling in code, as discussed in episode 12.
  • Limit resources: use GC config and execution budgets, for example heap limits and step limits, so a malicious script can't exhaust memory or CPU.
  • Enforce timeouts: a script that never finishes must be stoppable from the host side.
JSpolicy-host.js
const ALLOWED_HOSTS = ["log", "httpRequest"];
 
function evaluateUntrusted(code) {
  const ctx = new HermesContext({
    expose: (name) => ALLOWED_HOSTS.includes(name),
    timeoutMs: 1000,
  });
  return ctx.evaluate(code);
}

The principle is the same at every level: foreign scripts run in a small arena, with clear rules, and are evacuated once done. If you can't constrain a script, then you shouldn't be running it.

Conclusion

A good Hermes architecture is one that's honest about boundaries: trusted native modules behind facades, JavaScript holding business logic without excess privileges, modularized bundles, and foreign code confined to ephemeral contexts that are discarded after use.

The essentials to take home:

  • Native modules are the trusted zone; sensitive logic doesn't live in JavaScript.
  • Facades and adapters make architectural boundaries visible and easy to audit.
  • Bundle modularization shrinks startup and the execution surface.
  • Ephemeral contexts confine foreign scripts; resetting the runtime discards all of its state.
  • Untrusted scripts only get explicit host functions — no eval, no full native access.

In episode 15 we turn back to performance: Performance Optimization Strategies — the JavaScript code patterns most friendly to Hermes, how to avoid allocations in hot paths and the cost of runtime polymorphism, plus profiling hints specific to Hermes. See you there!

Learn Hermes JS Engine - Application Architecture & Isolation | Learn Hermes JS Engine