Every application built on Gingee follows a simple and consistent structure. This guide breaks down that structure, explains the critical role of the box folder, and provides a comprehensive reference for all the settings available in the app.json and routes.json configuration file.
Every first-level directory inside your server's web root is considered a distinct App. For an application named my_app, the structure looks like this:
web/
βββ my_app/
βββ box/
βββ css/
βββ images/
βββ scripts/
βββ index.html
my_app/: The root folder for the application. The name of this folder becomes the app's unique ID and the first segment of its URL (e.g., http://localhost/my_app/...).
css/, images/, scripts/, etc.: These are public directories. Any file placed here can be accessed directly by its URL. Gingee's static file server will serve these assets. For example, a file at web/my_app/css/style.css is available at /my_app/css/style.css.
index.html: If a user navigates to the app's root URL (/my_app), this file will be served by default (if the app is not an SPA).
box FolderThe box folder is the private, secure core of your application. It contains all your backend logic, configuration, and private data.
Security: The box folder is always protected. No file inside the box can ever be accessed directly from a URL. A request to /my_app/box/app.json, for example, will be blocked with a 403 Access Denied error. This is a fundamental security guarantee of the Gingee platform.
Server Scripts: All your backend API endpoints are JavaScript files that live inside the box. A request to /my_app/api/users is mapped to the file at web/my_app/box/api/users.js.
Configuration: All app-specific configuration, including the crucial app.json file, resides in the box.
Private Data: If your application uses a file-based database like SQLite, its database file should be stored in a subdirectory within the box (e.g., box/data/app.db) to ensure it is protected from direct web access.
app.json File:The app.json file, located at web/my_app/box/app.json, is the central configuration file for your application. It tells the Gingee server how to handle the app, what resources it needs, and how it should behave.
Here is a comprehensive breakdown of all available properties.
{
"name": "My Awesome App",
"description": "This is a demonstration of all app.json settings.",
"version": "1.2.0",
"type": "MPA",
"mode": "production",
"spa": {
"enabled": false,
"dev_server_proxy": "http://localhost:5173",
"build_path": "./dist",
"fallback_path": "index.html"
},
"db": [],
"email": {
"type": "console",
"from": "noreply@example.com",
"from_name": "My App"
},
"ai": {
"type": "mock",
"default_model": "mock-model"
},
"schedules": [],
"startup_scripts": [],
"default_include": [],
"env": {},
"jwt_secret": "a-very-strong-and-unique-secret-key",
"cache": {
"client": {
"enabled": true,
"no_cache_regex": ["/api/realtime"]
},
"server": {
"enabled": true,
"no_cache_regex": ["/api/dynamic-script.js"]
}
}
}
name (string, required)description (string, optional)version (string, optional)type (string, optional)
"MPA" (Multi-Page Application): The default. Serves classic multi-page sites and file-based or manifest-based server scripts under box/."SPA" (Single Page Application): Enables first-class SPA hosting for frameworks such as React, Vue, and Angular. Combined with spa.enabled, Gingee:
development mode, proxies non-API requests to your frontend hot-reload server (spa.dev_server_proxy).production mode, serves compiled assets from spa.build_path and falls back to spa.fallback_path (typically index.html) for client-side routes.box/ (file-based or routes.json) for API endpoints.mode (string, optional)
"production" (Default): The standard mode for live servers. For SPAs, this serves the compiled static assets from the build_path and applies SPA fallback routing."development" : Activates development-only features. For SPAs, this enables the seamless dev server proxy.spa object)This object is used when the app is of "type": "SPA". SPA behavior is active when both "type": "SPA" and "spa.enabled": true are set.
spa.enabled (boolean, required for SPA mode): Must be true to activate SPA features (dev proxy and production fallback).spa.dev_server_proxy (string, optional): (Development only) The full URL of your frontend's hot-reloading development server (e.g., Vite, Angular CLI). Gingee will proxy all non-API requests to this URL when the app's mode is "development". Required in development; missing configuration yields a 500 with a clear misconfiguration message.spa.build_path (string, optional): (Production) The path to the directory containing your compiled frontend assets, relative to the app's root folder. Defaults to ./dist if omitted.spa.fallback_path (string, optional): (Production) The path to the SPA's entrypoint file within the build_path. Defaults to index.html. Gingee serves this file for any request that doesn't match an API route or a static asset, enabling client-side routing.db (array, optional)
type, name, host, user, password, database, etc.ai object, optional)Single generative AI configuration for the app. App config overrides optional server defaults in gingee.json β ai. Requires the ai permission.
type (string): Provider β mock (local/dev), gemini (Google), xai (Grok β P1).api_key (string): Provider API key (not required for mock).default_model / default_vision_model (string, optional)max_output_tokens, timeout_ms, temperature (optional)safety (object, optional): { "enabled": false, "fail_closed": true, "moderate_input": false }API (sandbox): require('ai') β chat, chatStream (async generator), complete, parseDocument, moderate. Pass { config: { β¦ } } as the second argument to override server/app config for one call.
Example:
"ai": {
"type": "gemini",
"api_key": "AIzaβ¦",
"default_model": "gemini-2.5-pro"
}
app.jsonAny string value may be a secret reference resolved by the engine at app load (not by sandbox process.env):
"jwt_secret": "env:GINGEE_MYAPP_JWT_SECRET",
"db": [{
"type": "postgres",
"name": "main",
"host": "db.internal",
"user": "myapp",
"password": "env:GINGEE_MYAPP_DB_PASSWORD",
"database": "myapp"
}],
"email": {
"type": "sendgrid",
"api_key": "env:GINGEE_MYAPP_SENDGRID_KEY",
"from": "noreply@example.com"
},
"ai": {
"type": "gemini",
"api_key": "file:./settings/secrets/myapp_gemini_key"
}
env:NAME β read from the host process environment (set by Docker/K8s/systemd or optional .env when secrets.load_dotenv is true).file:path β read a secret file under server secrets.file_roots only (e.g. Docker/K8s mounted secrets).App scripts never need host process access; resolved values appear on $g.app / module config as normal strings. Server settings: Server Config β secrets.
limits object, optional)Optional tightening of server gingee.json β limits for this app only (cannot raise ceilings).
"limits": {
"request_timeout_ms": 15000,
"max_concurrent_requests": 10,
"outbound_timeout_ms": 8000
}
See Server Config for full field list and defaults. Use this to protect a noisy app from monopolizing the process (lower concurrency) or to fail faster than the server default.
queue object, optional)Optional job name β script map for the background queue. Handlers default to box/jobs/{name}.js if unmapped. Requires the queue permission and server queue.enabled (default true).
"queue": {
"jobs": {
"send-welcome": { "script": "jobs/send_welcome.js" }
}
}
Enqueue from scripts: require('queue').add('send-welcome', payload). Exhausted retries β DLQ; operators use Glade β Queue / DLQ (live jobs + retry/discard). See Server Config β queue.
websockets object, optional)Opt-in WebSocket endpoint for this app. Requires server websockets.enabled (default true), the websockets permission, and a handler under box/. Connections use the public HTTP(S) port: ws://host/{appFolder}{path}.
"websockets": {
"enabled": true,
"path": "/ws",
"handler": "realtime/handler.js",
"auth": "realtime/auth.js",
"allowed_origins": ["https://example.com"]
}
Handler (box/realtime/handler.js):
module.exports = async function (socket, ctx) {
// ctx: { app, log, query, path, headers, meta, remoteAddress }
socket.join('lobby');
socket.send({ type: 'hello' });
socket.on('message', (raw) => {
socket.to('lobby').send({ echo: raw });
});
};
From HTTP scripts: require('websockets').toRoom('lobby', payload). Multi-tenant: use tenantRoom(tenantId, 'lobby'). Multi-node masters: operator sets websockets.fanout.driver: "redis" + sibling websockets.redis. Sample: web/ginchat/. Full server keys: Server Config β websockets.
isolation string, optional)Opt-in process isolation for this appβs server scripts (not static files). Only takes effect when the server has gingee.json β isolation.mode: "process". Privileged apps (e.g. Glade) always stay in-process regardless of this flag.
| Value | Meaning |
|---|---|
"process" |
Run box scripts in a solo child worker (app:<folderName>) unless the app is also in a server group |
"inprocess" |
Force in-process (default when server mode is process but app is unmarked) |
"isolation": "process"
Server-side alternatives (no need to set this flag if you use them):
isolation.apps: ["my-app"] β solo worker by folder nameisolation.groups: { "tenant-a": ["app-one", "app-two"] } β one shared worker for members (do not also list those names in apps unless you want redundancy; group already isolates them)Buffered responses and SSE (startStream / writeSSE / endStream) are supported over IPC. Full server keys: Server Config β isolation.
schedules array, optional)Declarative CRON jobs for this app. Registered only when gingee.json β scheduler.enabled is true on this node (default false; optional multi-node Redis coordination). The app must be granted the scheduler permission. URL targets also require httpclient. Queue targets also require queue. Operators can Run now from Glade Schedules.
Each entry:
| Field | Required | Description |
|---|---|---|
name |
yes | Unique job id within the app (a-zA-Z0-9._-) |
cron |
yes | CRON expression (standard 5-field; seconds supported by engine dialect) |
timezone |
no | IANA timezone (defaults to server scheduler.timezone, usually UTC) |
enabled |
no | Default true. Set false to keep the definition without registering |
timeout_ms |
no | Default 300000 (script) / 60000 (url) / 30000 (queue enqueue) |
overlap |
no | Only "skip" in v1 (skip if previous run still active) |
payload |
no | Passed as $g.request.body for script targets; default payload for queue targets |
target |
yes | See below |
target for scripts (path is relative to the appβs box/ folder only):
"target": { "type": "script", "path": "jobs/nightly_cleanup.js" }
Scheduled scripts run in the same sandbox as HTTP/startup scripts. Use the usual gingee(async ($g) => { β¦ }) form. There is no HTTP connection: $g.request.method is "SCHEDULE", $g.schedule holds { name, cron, timezone, runId, scheduledAt, β¦ }, and $g.response.send(...) records a result in logs (it does not open a network response). Streaming is not supported in schedule context.
fs paths in scheduled scripts: Same rules as all Gingee scripts. A path with a leading / is relative to the scope root (box/ or web/). A path without a leading slash is relative to the executing scriptβs directory. Example: from box/jobs/cleanup.js, fs.writeFile(fs.BOX, 'data/out.json', β¦) writes box/jobs/data/out.json, while fs.writeFile(fs.BOX, '/data/out.json', β¦) writes box/data/out.json. Prefer leading-/ paths when another HTTP script (with a different working directory) must read the same file.
target for external URLs:
"target": {
"type": "url",
"url": "https://partner.example.com/hooks/tick",
"method": "POST",
"headers": { "Authorization": "Bearer β¦" },
"body": { "source": "gingee" }
}
url must be absolute http: or https:. The engine performs the outbound call (app needs httpclient). URLs are checked against server egress policy at registration and again when the job fires (default protected mode blocks private/loopback/metadata). See Server Config β egress.
target for queue (enqueue a background job β multi-node safe when queue.driver is redis):
"target": { "type": "queue", "job": "nightly_cleanup", "payload": { "mode": "full" } }
The schedule only enqueues; the queue worker runs box/jobs/nightly_cleanup.js (or a mapped script). App needs queue + scheduler.
Example:
"schedules": [
{
"name": "nightly_cleanup",
"cron": "0 2 * * *",
"timezone": "UTC",
"payload": { "mode": "full" },
"target": { "type": "script", "path": "jobs/cleanup.js" }
},
{
"name": "nightly_via_queue",
"cron": "0 3 * * *",
"target": { "type": "queue", "job": "nightly_cleanup" }
},
{
"name": "partner_ping",
"cron": "*/15 * * * *",
"target": {
"type": "url",
"url": "https://partner.example.com/hooks/gingee",
"method": "POST"
}
}
]
email object, optional)Single outbound email configuration for the app (no named profiles). App config overrides optional server defaults in gingee.json β email. Requires the email permission.
type (string, required when using email): Provider id β sendgrid or console (dev: logs only, no network).api_key (string): SendGrid API key when type is sendgrid.from (string): Default From address.from_name (string, optional): Default From display name.Runtime override: from a server script you can call email.sendWithConfig(config, message) so a one-off send uses config that overrides both gingee.json and app.json for that transaction only (does not change the app default).
Example app.json:
"email": {
"type": "sendgrid",
"api_key": "SG.xxxxx",
"from": "noreply@example.com",
"from_name": "My App"
}
startup_scripts (array, optional)
box folder."startup_scripts": ["setup/01_schema.js", "setup/02_seed_data.js"]default_include (array, optional)
"lib/auth.js"), it is resolved as a path relative to the app's box folder."auth"), it is resolved from the global modules folder."default_include": ["auth_middleware.js", "lib/request_logger.js"]env (object, optional)
$g.app.env.jwt_secret (string, optional)
auth module for creating and verifying JSON Web Tokens (JWTs).cache (object, optional)
cache.client: Controls browser caching (Cache-Control header).cache.server: Controls server-side caching of static files and transpiled scripts in Memory or Redis.pmft.json File (Permissions Manifest)If you plan to distribute your application as a .gin package, you must declare the permissions it requires in a pmft.json file. This manifest is read by the gingee-cli during the installation process to request consent from the server administrator.
web/<your-app-name>/box/pmft.jsonmandatory) and optional (optional) permissions.For a complete guide on the permissions system and the structure of this file, please see the Gingee Permissions Guide MD HTML.
routes.json File (Manifest-Based Routing)For applications that require more powerful and flexible routing, such as RESTful APIs with dynamic path parameters, you can create a routes.json file. When this file is present in an app's box folder, it activates manifest-based routing, which takes precedence over the default file-based routing.
web/my-app/box/routes.jsonroutes.jsonThe file must contain a single root object with a routes key, which holds an array of route definition objects.
{
"routes": [
{
"path": "/users",
"method": "GET",
"script": "users/list.js"
},
{
"path": "/users/:userId",
"method": "GET",
"script": "users/get.js"
},
{
"path": "/users/:userId",
"method": "PUT",
"script": "users/update.js"
},
{
"path": "/:category/:slug/images/:imageId?",
"method": "GET",
"script": "content/view.js"
}
]
}
Each object in the routes array defines a single endpoint and has the following properties:
path (string, required)
:), followed by its name (e.g., :userId). The name should use standard variable naming conventions.?) to its name (e.g., :imageId?).*) as a wildcard to match the rest of a path.method (string, optional)
GET.GET, POST, PUT, DELETE, PATCH. You can also use ALL to match any method for a given path.script (string, required)
box folder. The .js extension is optional."script": "api/users/get-profile" will execute the file at web/my-app/box/api/users/get-profile.js.When a route with dynamic parameters is matched, Gingee automatically parses the values from the URL and makes them available in your server script via the $g.request.params object.
Example:
routes.json:
{ "path": "/products/:productId/reviews/:reviewId", "script": "reviews/get.js" }
/my-app/products/abc-123/reviews/42box/reviews/get.js):
module.exports = async function() {
await gingee(async ($g) => {
const productId = $g.request.params.productId; // "abc-123"
const reviewId = $g.request.params.reviewId; // "42"
$g.response.send({
message: `Fetching review ${reviewId} for product ${productId}.`
});
});
};