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.

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.
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:
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.
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:
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.
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.
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.
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():
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.
Finally, some hard rules for embeddings that run foreign code:
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.
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:
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!