Jun 7, 20268 min read/2026/06/07/jint-functions-hosted-server-side-functions-that-can-be-gated/

Jint Functions: Hosted Server-Side Functions That Can Be Gated

One of the most interesting things about building a platform is that eventually you stop asking:

How do I add one more feature?

and start asking:

How do I let the system safely grow features without changing the system itself?

That is where Jint functions become very interesting.

A hosted function does not have to be a controller method, a compiled plugin, or a deployment unit. It can be a small JavaScript document stored in the application database and executed server-side by a .NET runtime using Jint.

The important part is not only that the function runs. The important part is that it runs inside a gate.

The Shape of the Idea

A useful pattern is to pair Jint functions with a generic JSON document store. Apps can store data in named collections like:

pos.invoices
pos.customers
notes.items
crm.deals

The store can be backed by PostgreSQL json_store, a table with a collection, an owner_id, and a JSONB data column. This gives the host application one data layer that many app modules can share.

But data without behavior is only half the story.

The same idea can be extended with a _functions collection. Each function is just another document:

{
  "name": "pos.calculate-tax",
  "project": "pos",
  "trigger": "before_create",
  "collection": "pos.invoices",
  "code": "doc.data.iva = +(doc.data.subtotal * 0.13).toFixed(2); return doc;"
}

That document says:

  • when an invoice is created,
  • before it is saved,
  • run this JavaScript,
  • let it modify the document.

That is a hosted server-side function.

No new API controller. No new database migration. No new deployment just to calculate tax.

Why Jint?

Jint is a JavaScript interpreter written in .NET. That detail matters because the function does not need Node.js, a separate worker process, or a container per script.

The host can create a Jint engine inside the .NET API, inject only the objects the function is allowed to use, execute the code, and return a result.

A runner can inject objects like:

ctx     // userId, email, locale, trigger, now, requestId
doc     // the current document, for triggers
args    // request body, for callable functions
store   // query, get, create, update, delete
http    // controlled HTTP calls
secrets // server-side secret access
assert  // test helpers
events  // publish host-level events
console // captured logs

The function author gets a small host API. The application keeps the real power.

That separation is the whole design.

Functions Are Not Just Callables

When people hear "server-side function," they often imagine a simple endpoint:

POST /api/fn/pos.calculate-tax/call

That is useful, but callables are only one trigger type.

The current model includes:

Trigger When it runs What it can do
callable Explicit function call Return a result
test Test runner or function call Assert behavior
before_create Before a document insert Modify or reject the document
after_create After a document insert React without blocking the write
before_update Before a document update Modify or reject the document
after_update After a document update React without blocking the write
scheduled Scheduler or manual trigger Run background work

This makes functions feel less like isolated scripts and more like application behavior.

A before_create function can calculate totals. An after_create function can publish an event. A scheduled function can sync data. A test function can verify that the app still behaves correctly.

The same runner handles all of them.

The Gate Is the Product

The dangerous version of server-side scripting is:

Here is JavaScript. Good luck.

That is not architecture. That is a future incident report.

The useful version is:

Here is a small JavaScript runtime with explicit capabilities, identity, resource limits, and host-controlled authorization.

That is what makes the feature practical.

In this style of implementation, the gate exists at several layers.

1. The HTTP Gate

Function endpoints are not open by default. A function call must come from an authenticated user or from an internal service using the configured internal key.

So this:

POST /api/fn/{name}/call

is not just "run some JavaScript." It is "run this function as this caller, with this request context."

The runner receives ctx.userId, ctx.email, ctx.trigger, and a request correlation id. That means function execution can be tied back to the user and the original request.

2. The Collection Gate

Functions live in _functions, which is a meta-collection. Meta-collections are admin-only unless the call comes through the internal service path.

That means regular users can use functions, but they cannot casually rewrite the function registry.

This matters because hosted functions are not just content. They are executable behavior.

3. The Trigger Gate

The host checks the trigger before running a function as a callable.

If a function is registered as before_create, it should not be callable like a public API. It belongs to the document lifecycle. The function controller rejects that mismatch.

That one rule prevents a whole class of accidental exposure.

4. The Resource Gate

Each function runs inside a constrained Jint engine:

timeout:       5 seconds by default
memory:        16 MB
statements:    50,000
imports:       none
filesystem:    none
network:       only through injected http

There is also support for a per-function timeout override, clamped between a minimum and maximum range. That is useful for legitimate orchestrators, but still keeps runaway code from owning the worker forever.

5. The Capability Gate

The function does not get the world. It gets objects.

If the host injects store, the function can use the store. If the host injects http, the function can make controlled HTTP calls. If the host injects secrets, secrets stay server-side and are accessed through a narrow API.

This is capability-based design in a very practical form:

const customer = store.get("pos.customers", args.customerId);
const invoice = store.create("pos.invoices", {
  customerId: customer.id,
  subtotal: args.subtotal
});

return { invoiceId: invoice.id };

The JavaScript is simple. The boundary around it is doing the serious work.

Before Triggers Are Especially Powerful

The most important trigger type is probably before_create or before_update.

These functions run before the document is persisted. They can modify the incoming document, or they can throw an error and reject the write.

That gives you server-side enforcement without writing a custom controller:

if (!doc.data.customerId) {
  throw new Error("customerId is required");
}

doc.data.subtotal = Number(doc.data.subtotal || 0);
doc.data.iva = +(doc.data.subtotal * 0.13).toFixed(2);
doc.data.total = +(doc.data.subtotal + doc.data.iva).toFixed(2);

return doc;

This is very different from doing the calculation in the browser.

The browser can help the user. The server decides what is true.

Dry Runs Change the Developer Experience

One underrated feature is POST /api/fn/dry-run.

The dry-run endpoint executes arbitrary function code with mock arguments. Reads can hit the real database, but writes are recorded instead of persisted.

That means a developer can test:

store.create("pos.invoices", {
  subtotal: 100
});

and get back a list of planned writes without changing production data.

This is the kind of feature that makes hosted functions feel safe enough to use. It gives the developer a fast loop while keeping the host boundary intact.

Tests as Functions

Because Jint functions already run inside the host application, tests can be functions too.

A test function can use assert:

assert.ok(true, "function runner is alive");
assert.equal(2 + 2, 4, "math still works");

return { pass: true };

That may look small, but it unlocks something important: app behavior can be tested in the same environment where app behavior runs.

For declarative apps, this is a big deal. If screens, collections, and functions are all data, then the test suite can also be data.

The Agent Angle

This architecture also changes how an AI agent interacts with the system.

Instead of hardcoding one tool per app, the agent can use two general primitives:

store.query(collection, filters)
fn.call(name, args)

That means a new app can appear by registering:

  • its collections,
  • its screens,
  • its functions,
  • and its tests.

The agent does not need a new compiled integration for every app. It discovers and calls the platform surface.

This is where the "hosted function" idea becomes more than convenience. It becomes a stable contract between apps, agents, and the host runtime.

Why Not Just Use Webhooks?

Webhooks are useful, but they move execution somewhere else.

That means you now have to manage:

  • deployment,
  • secrets,
  • network access,
  • retries,
  • ownership,
  • observability,
  • and versioning outside the host application.

Hosted Jint functions keep small logic close to the data and the authorization model.

They are not a replacement for every backend service. I would not put heavy DTE signing, long-running video generation, or large integrations inside a tiny Jint function. Those belong in real services.

But validation, enrichment, lightweight orchestration, app-specific rules, and glue logic are perfect candidates.

The Mental Model

The way I think about it is this:

Jint functions are stored procedures for a JSON-native application runtime.

But unlike classic stored procedures, they are:

  • written in JavaScript,
  • hosted by .NET,
  • versioned as app metadata,
  • callable over HTTP,
  • triggered by document lifecycle events,
  • testable with the same runner,
  • and gated by the host.

That last part is the difference between a toy and a system.

Where This Goes

Once you have hosted server-side functions, the next natural step is finer-grained gating:

  • allow only certain roles to call a function,
  • require a network membership before invoking it,
  • mark expensive functions as approval-gated,
  • expose some functions to agents but not humans,
  • expose some functions to humans but not agents,
  • require dry-run before production execution,
  • log every invocation with arguments, elapsed time, and result shape.

The foundation is already there: functions are documents, calls flow through a controller, execution happens inside a sandbox, and the host owns the context.

That is the key architectural move.

Do not make JavaScript powerful by default. Make it powerful by permission.

That is how Jint functions become hosted server-side functions that can be gated.