Gina
Documentation: gina.io/docs · Issues: GitHub · Changelog: CHANGELOG.md
Node.js MVC framework with built-in HTTP/2, multi-bundle architecture, and scope-based data isolation — no Express dependency.
- HTTP/2 first. Built-in
isaacserver with TLS, h2c, ALPN, HTTP/1.1 fallback, and full CVE hardening (Rapid Reset, CONTINUATION flood, RST flood, HPACK bomb) — all on by default. - Multi-bundle. One project hosts multiple independent bundles (API, web, admin, …). Each bundle has its own routing, controllers, models, and config. Share code via the project layer.
- Scope isolation. Run
local,beta, andproductionfrom the same codebase. Scopes propagate through routing, config interpolation, and data (every DB record is stamped with_scope).
Features
| Feature | Detail |
|---|---|
| HTTP/2 server | Built-in isaac engine — TLS, h2c, ALPN, HTTP/1.1 fallback, 103 Early Hints, CVE-hardened |
| Multi-bundle | One project, N independent bundles with shared config and project layer |
| Scope isolation | local / beta / production — per-request and per-record |
| MVC routing | routing.json — declare routes in config, not code; O(m) radix trie lookup |
| Async/await | Controller actions can be async; rejections routed to throwError automatically |
| ORM / entities | EventEmitter-based entity system; SQL files auto-wired to entity methods |
| Connectors | Couchbase, MongoDB, ScyllaDB / Cassandra, MySQL, PostgreSQL, Redis, SQLite, AI (LLM) — loaded from project node_modules |
| AI connector | Any LLM provider via named protocol (anthropic://, openai://, ollama://, …) |
| Template engine | @rhinostone/swig 2.7.2 — maintained fork with CVE-2023-25345 patched; streaming SSE/chunked via renderStream(). Nunjucks supported as opt-in via render.engine = "nunjucks" or per-section "ext": ".njk" |
| Internationalisation | Per-bundle JSON catalogs, t() helper, swig + nunjucks t filter, CLDR plurals, ICU MessageFormat opt-in via t.icu() |
| Observability | Built-in /_gina/metrics Prometheus endpoint (opt-in, IP-allowlisted) — Node.js process metrics + HTTP counter / duration histogram with cardinality-safe route labels |
| Hot reload | WatcherService evicts require.cache only on file change — zero per-request overhead in dev |
| K8s ready | gina-container, gina-init, SIGTERM drain, JSON stdout logging |
| Dependency injection | Mockable connectors and config for unit testing |
| Runtime | Node.js 22–26, or Bun (bun add -g gina) — install + boot validated end-to-end by a CI Bun smoke |
Quick start
npm install -g gina@latest --prefix=~/.npm-global # or, on the Bun runtime: bun add -g gina
gina project:add @myproject --path=$(pwd)/myproject
gina bundle:add api @myproject
gina bundle:start api @myproject
open https://localhost:3100
npm 12+ blocks install scripts by default, and gina's post-install bootstraps
~/.ginaand the framework dependencies. Install withnpm install -g gina@latest --allow-scripts=gina, or allow it once for all global installs withnpm config set allow-scripts=gina --location=user. (Not needed on npm ≤ 11.)
What's in 0.5.19
- Added — route authorization. A
routing.jsonrule can now gate access declaratively, enforced before the controller action runs.param.requireAuth: truerequires an authenticated session (req.session.userset — populating it at login stays the app's job): an unauthenticated XHR gets a 401, a browser navigation gets a non-cacheable redirect tosettings.json > auth.loginRoutewith the original request snapshotted forself.resumeRequest()to replay.param.roles: ["admin", "editor"]requires any one of a set of roles (impliesrequireAuth);param.policy: "ownsInvoice"delegates the decision to a per-bundlepolicies/ownsInvoice.jsfunction for the ownership/record checks roles can't express (AND-composed after roles, allowed only on a literaltrue, a throwing policy denies). Denials stay generic — the required roles and policy name never reach the wire, and the client-served routing maps no longer ship the authorization keys. Author mistakes refuse to boot rather than leaving a route silently open (a non-boolean flag, an undeclaredloginRoute, an invalidrolesshape, a missing / broken /asyncpolicy). Also adds the imperativeself.hasRole(role)controller helper. Routes that declare nothing behave exactly as before. - Added — an audit trail. A user-attributed, append-only record of "who did what to which record when", with its own store, never riding the logger sinks. Opt in with
settings.json > audit.enabled: trueand callself.audit("invoice.delete", { resource: id })from an action; the default backend is an append-only JSONL file at<project>/logs/audit-<bundle>-<env>.jsonl(override withaudit.file, or pointaudit.storeat a connectors.json entry). The actor is a snapshot ofsession.user[audit.actorKey]plus a copy ofuser.roles— never the whole user object — andX-Forwarded-Foris never trusted. Route-authorization denials are recorded automatically (authz.denied, opt out withaudit.events.authz: false), and an audit failure can never change an authorization outcome. Every request now carries an always-on request id, so audit records and JSON log lines correlate by construction. Malformedauditsettings refuse to boot rather than leaving a compliance control silently off. - Added — an OCI image and container CLI. Building on
image:build(0.5.11), five verbs now inspect and run images on the same container host:gina image:list(aligned table or--format=jsonwith a machine-sortablesizeBytesand an RFC3339createdAt),image:rm <ref>(untags a multi-tagged image; no bulk delete; the reference is charset-gated), andimage:run <image>(runs with podman — detached by default, publishing the EXPOSEd port same:same, with--publish/--rm/--stream/--env-var/--env-file, so a${secret:KEY}baked into an image gets its value at start without ever entering argv or a shell). A newcontainer:group lists and stops running containers —container:ps [--all]andcontainer:stop <name|id>, which reports the rung the container came down on (137 = SIGKILL after the grace period, otherwise its own terms). Every verb resolves the host withimage:build's precedence and reports a build-only host (buildah without podman) honestly instead of failing opaquely. - Changed —
redirect()carries request data through the session by default. When a redirect carried the request's params they used to travel in the URL as?inheritedData=<encoded JSON>— in clear, in the address bar, browser history, and access logs, capped at 2000 characters. On a bundle with a session they now ride the session instead (a one-shot flash, consumed by the next routed GET, gone on refresh); the URL form and its cap now apply only to session-less bundles. For a bundle that halts a credential-bearing POST mid-flight (a registration or login), this removes a plaintext-secret-in-URL disclosure. The target action still reads the data fromreq.getunchanged, andresumeRequest()'s plain-XHR and full-page replays no longer drop it. - Fixed — metrics no longer double-count under the isaac engine. With
app.json metrics.enabledon, the request-lifecycle hook ran at both dispatch layers for any request reaching the router, so Prometheus counters were double-incremented and the duration histogram double-observed. The hook now records exactly once on either engine, durations are measured from engine entry, and the dev Inspector Flow timeline keeps its accurate request-start time. Metrics-enabled deployments will see counter rates roughly halve at pickup — that is the double-count disappearing, not traffic dropping. (#OBS1) - Fixed — redirect resolution. The relative-path
self.redirect('/path')form resolves its target server-side again (the route matcher is async and the historical un-awaited call could never match;redirect()is now async — preferreturn self.redirect(...)from actions), a redirect to an unresolvable target gets a clean 404 instead of crashing the process, andgetRoute()no longer throws a 500 when composing a URL for a GET route that declares norequirementsblock and receives extra params (they now land as query parameters). (#B120 / #B121) - Fixed —
image:buildfor projects that depend on gina. Images built for a project that lists gina among its own dependencies no longer fail atgina-initwithEACCES, and the pinned framework now actually winsrequire('gina')inside the image — thenode_modules/ginalink supersedes the project-extracted copy. Dependency-free projects were unaffected. (#B118 / #B119) - Fixed — two smaller corrections.
gina.emit('error', ...)on the module object no longer throws (emitis an inert stub; application events go through the controller'sself.emitEvent()), and the checkbox migration warning added in 0.5.18 now also covers the un-tick direction — markup carryingcheckedplus a false / emptyvaluethat used to render unticked and now stays ticked is flagged once per field. (#B109 / #49)
See the full Changelog and Roadmap.
Documentation
Full installation guide, tutorials, configuration reference, and API docs at gina.io/docs.
Ecosystem
| Package | Description |
|---|---|
| @rhinostone/swig | Maintained fork of the Swig template engine (upstream abandoned since 2015). CVE-2023-25345 patched. |
| gina-starter | Minimal starter project — one bundle, one route, Docker Compose included |
Governance
Gina is co-authored by Martin Luther ETOUMAN NDAMBWE (Rhinostone) and Fabrice DELANEAU (fdelaneau.com). Final decisions on direction, API design, and releases rest with Martin Luther. Community contributions and RFCs are welcome and taken seriously. See GOVERNANCE.md for details.
Supply-chain scanners
Gina is an MVC framework with a process-management CLI, so it uses Node's
child_process by design — to start and supervise application bundle processes
and the framework daemon, run local/SSH commands (lib/shell), launch the
inspector, and perform setup in the npm install scripts. Supply-chain scanners
therefore report a Shell access capability for child_process. This is
expected and intrinsic to a CLI framework, not a vulnerability: the install-time
commands are built only from local values (npm prefix, install path) and take no
network input.
License (MIT)
Copyright 2009-2026 Rhinostone
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.