Gingee executes your backend logic using JavaScript files that live inside your app's secure box folder. For consistency and ease of use, all executable scripts—whether they are handling a live API request, acting as middleware, or performing a one-time setup task—share the same fundamental structure. This guide explains the three types of scripts and the powerful $g object that connects them.
All Gingee scripts, regardless of their purpose, follow this simple and mandatory pattern:
// A script must export a single asynchronous function.
module.exports = async function () {
// The entire logic is wrapped in a call to the global 'gingee()' function.
await gingee(async function ($g) {
// Your application code goes here.
// You use the '$g' object to interact with the world.
});
};
This unified structure ensures that every piece of executable code runs within the same secure, sandboxed environment and receives a properly configured context object ($g).
Required modules and bare $g: Keep await gingee(async ($g) => { ... }) on entry scripts. Inside that handler, required box helpers may use bare $g (live, request-local — ALS-backed Proxy) without passing $g as an argument. Prefer reading $g at use time; const local_$g = $g is OK for the root binding, but do not stash $g.response / $g.request on module scope, and do not touch $g at module top level.
// box/lib/greeter.js
module.exports = {
sendHello() {
$g.response.send({ message: "Hello, World!", app: $g.app.name });
},
};
While the structure is the same, the purpose of a script and the context it runs in can differ. There are four types of scripts you can create, plus WebSocket handlers (different entry signature) and queue job handlers (same gingee() pattern, $g.queue context).
This is the most common type of script. It runs in direct response to an incoming HTTP request from a browser or client.
routes.json.$g Context: Has access to the full $g object, including:
$g.request: To get headers, query parameters, and the request body.$g.response: To send a response back to the client.$g.log and $g.app.Example (box/api/users/get.js):
module.exports = async function () {
await gingee(async ($g) => {
const userId = $g.request.query.id;
// ... logic to fetch user from database ...
$g.response.send({ id: userId, name: "Alex" });
});
};
These scripts run before every Server Script in your application. They act as middleware.
app.json via the "default_include" array. They run in the order they are listed, before the final Server Script is executed.$g Context: Has access to the full $g object, just like a Server Script. A key feature is that if a Default Include script uses $g.response.send(), the request lifecycle is immediately terminated, and no further scripts (including the main Server Script) will be executed.Example (box/auth_middleware.js):
module.exports = async function () {
await gingee(async ($g) => {
const token = $g.request.headers["x-auth-token"];
if (!isValid(token)) {
// This ends the request immediately.
$g.response.send({ error: "Unauthorized" }, 401);
}
// If we don't send a response, execution continues to the next script.
});
};
With module_override, middleware may rebind require specifiers for the rest of the request (protected bare names, other bare names, relative or box-root paths). The replacement script must live under the app box. Nested require inside that script uses normal gbox jailing; the override map is not re-applied so wrappers can load the real platform module.
// box/middleware/fs_policy.js — listed in app.json default_include
module.exports = async function () {
await gingee(async ($g) => {
const rel = String($g.boxRelativeScript || "");
if (rel.startsWith("sandboxed/")) {
// Only module_override required to install; real fs still needs fs if the wrapper uses it
$g.overrideModule("fs", "library/fswrapper.js");
$g.overrideModule("crypto", "library/crypto_wrap.js");
// Prefer box-relative map keys for relative requires
$g.overrideModule("sandboxed/helper", "library/helper_wrap.js");
$g.overrideModule("shared/bare_util", "library/bare_util_wrap.js");
}
});
};
See Permissions Guide → Module overrides, and web/appsandboxtest/.
These scripts run once when your application is loaded by the server. They are not tied to any HTTP request.
app.json via the "startup_scripts" array. They run in the order they are listed when the Gingee server starts, when an app is newly installed, or after an app is upgraded or rolled back.$g Context: Receives a specialized, non-HTTP version of the $g object.
$g.log, $g.app.$g.request and $g.response are null, as there is no incoming request or outgoing response.Example (box/setup/create_schema.js):
module.exports = async function () {
await gingee(async ($g) => {
const db = require("db");
$g.log.info("Checking for Users table...");
const sql =
'CREATE TABLE IF NOT EXISTS "Users" (id SERIAL PRIMARY KEY, email TEXT)';
await db.execute("main_db", sql);
$g.log.info("Database schema is ready.");
});
};
Long-lived connections use a different entry signature (no gingee() wrapper required). Configure them in app.json → websockets and grant the websockets permission.
ws(s)://host/{appFolder}{path} (default path /ws). The master accepts the upgrade; handlers run in-process on the master (not isolation workers).module.exports = async function (socket, ctx) { … }
socket: send, close, join(room), leave(room), to(room).send(…), on('message'|'close'), optional tenantId / metactx: { app, log, query, path, headers, meta, remoteAddress }require('websockets').toRoom(room, payload) (same permission).require('websockets').tenantRoom(tenantId, name) → t:{tenantId}:{name}.web/ginchat/ — UI at /ginchat/.Example (box/realtime/handler.js):
module.exports = async function (socket, ctx) {
const ws = require("websockets");
const room = ws.tenantRoom(
ctx.query.tenant || "demo",
ctx.query.room || "lobby",
);
socket.join(room);
socket.send({ type: "welcome" });
socket.on("message", (raw) => {
socket.to(room).send({ echo: raw });
});
};
See Server Config → websockets and App Structure.
Deferred jobs use the same module.exports + gingee() pattern as HTTP scripts. Place handlers under box/jobs/{name}.js (or map names in app.json → queue.jobs). Grant the queue permission; enqueue with require('queue').add(name, payload).
$g Context:
$g.queue: { id, name, payload, attempt } for the current job.$g.log, $g.app, and other modules per granted permissions.queue config). Operators use Glade → Queue / DLQ for live jobs and DLQ retry/discard.Example (box/jobs/send-welcome.js):
module.exports = async function () {
await gingee(async ($g) => {
const { payload, attempt, id } = $g.queue;
// … do work …
});
};
See Server Config → queue, App Developer Guide, and Glade Admin.
$g Object: Full API ReferenceThe $g object is the heart of the server script API. It provides a simplified and secure facade for interacting with the HTTP request, building a response, logging, and accessing application configuration.
$g.requestAn object containing all the details of the incoming HTTP request.
$g.request.url
URL object$g.request.protocol
string'http' or 'https'.$g.request.hostname
stringHost header (e.g., 'localhost:7070').$g.request.method
string'GET', 'POST', 'PUT').$g.request.path
string'/users/list').$g.request.headers
object$g.request.headers['user-agent']).$g.request.cookies
object$g.request.query
object$g.request.params
objectroutes.json.path: "/users/:userId/posts/:postId" and a request to /users/123/posts/abc, $g.request.params would be { "userId": "123", "postId": "abc" }.$g.request.body
object | string | nullgingee() middleware automatically parses the body based on the Content-Type header.
application/json: An object.application/x-www-form-urlencoded: An object.multipart/form-data: An object containing text fields and a files object. Each file in files includes its name, type, size, and its content as a Buffer in the data property.null.$g.responseAn object used to build the outgoing HTTP response. You modify its properties and then call $g.response.send() to send it.
$g.response.status
number200send().$g.response.status = 404;$g.response.headers
object{ 'Content-Type': 'text/plain' }$g.response.cookies
objectsend() method will format these into Set-Cookie headers.HttpOnly, maxAge), use the cookie module. This is a shortcut for simple key-value cookies.$g.response.body
anynullsend() method.$g.response.send(data, [status], [contentType])
data: The content to send.
string or Buffer, it's sent as-is.object or Array, it is automatically JSON.stringify()-ed, and the Content-Type is set to application/json.status (optional): A number to set the HTTP status code, overriding $g.response.status.contentType (optional): A string to set the Content-Type header, overriding $g.response.headers['Content-Type'].$g.response.send({ user: 'test' });$g.response.send(imageBuffer, 200, 'image/png');send() after a stream has been started with startStream(). Use endStream() instead.$g.limits, abort signal)When a server script runs under the engine limits module:
$g.limits.remainingMs — milliseconds left on the non-stream request budget (or null if not applicable)$g.limits.deadline — absolute epoch ms deadline$g.limits.signal / $g.request.signal — AbortSignal aborted on request timeout (passed to httpclient automatically)$g.limits.config — effective limits object for this requestNon-stream scripts that exceed request_timeout_ms receive a platform 504 if they have not yet completed. After startStream(), stream idle and hard timeouts apply instead. Concurrency overloads return 503 before the script runs.
Outbound httpclient calls use limits.outbound_timeout_ms by default and are subject to max_concurrent_outbound. They are also checked against server egress policy (gingee.json → egress, default protected); denied URLs return 403 with code: 'EGRESS_DENIED' (private/loopback/metadata blocked unless you configure exceptions or mode: "off").
When a script is invoked by the CRON scheduler (see app.json → schedules), there is no HTTP request. The gingee() middleware still provides $g, with:
$g.request.method: "SCHEDULE"$g.request.body: the job’s optional payload from app.json$g.schedule: { name, cron, timezone, runId, scheduledAt, attempt, targetType, path }$g.response.send(...): records a result for logs (does not write to a client socket)startStream / writeSSE / endStream are not supported in schedule contextfs path resolution: leading / = scope root (box/); no leading slash = directory of the currently executing gbox script (for a job, that is usually the scheduled script under box/jobs/). Prefer leading / when an HTTP endpoint under box/ must read the same file.For long-running or progressive output (for example, require('ai').chatStream(...)), use the streaming helpers on $g.response instead of a single send(). These write to the underlying HTTP response without exposing Node's raw res object to the sandbox.
$g.response.startStream([status], [contentType], [extraHeaders])
Content-Type is text/event-stream; charset=utf-8 (Server-Sent Events).Cache-Control: no-cache, Connection: keep-alive, and X-Accel-Buffering: no for proxy-friendly streaming.extraHeaders is an object of additional headers to set before the body starts.$g.response.write(chunk)
Buffer chunk to the open stream.$g.response.writeSSE(payload)
payload is an object, it is JSON.stringify-ed. Format: data: …\n\n.$g.response.endStream()
send() for request lifecycle).Example (AI streaming via SSE):
module.exports = async function () {
await gingee(async ($g) => {
const ai = require("ai");
const messages = $g.request.body.messages;
$g.response.startStream(200, "text/event-stream; charset=utf-8");
try {
for await (const chunk of ai.chatStream({ messages })) {
if (chunk.done) {
$g.response.writeSSE({
type: "done",
text: chunk.text,
model: chunk.model,
provider: chunk.provider,
usage: chunk.usage || null,
});
} else if (chunk.textDelta) {
$g.response.writeSSE({ type: "delta", textDelta: chunk.textDelta });
}
}
} catch (err) {
$g.response.writeSSE({ type: "error", error: err.message });
} finally {
$g.response.endStream();
}
});
};
Clients typically consume this with fetch() + ReadableStream (POST bodies are not supported by browser EventSource).
$g.logA direct reference to the apps's logger instance, pre-configured with the request's context.
$g.log.info(message, [meta])$g.log.warn(message, [meta])$g.log.error(message, [meta])message is a string, and the optional meta object can contain any additional data you want to log (like a user ID or a full error stack).$g.appAn object containing safe, read-only configuration data for the current application.
$g.app.name: (string) The app's display name from app.json.$g.app.version: (string) The app's version from app.json.$g.app.description: (string) The app's description from app.json.$g.app.env: (object) The custom environment variables defined in the env block of app.json.NOTE: The $g object will not have the $g.request and $g.response objects for a startup script.