Programming Model: JGit + Jetty vs Express + child-process git
Context: finos/git-proxy is a FINOS project that acts as a policy-enforcing proxy for git pushes. The current implementation is Node.js/Express. fogwall is a Java/Jetty implementation that uses JGit's native git protocol stack.
This document compares the programming models — how each codebase structures request handling, validation, streaming, and extensibility.
finos/git-proxy proves you can block or allow a push with a single buffered response as part of a stateful, transparent proxy with a processing & enrichment layer from git wire data for the purposes of enforcing a company's policy around source code movement between trusted and untrusted zones (primarily - or rather originally - intended to enable organizations with stringent security policies to contribute code to public open source projects). That ability to implement policy enforcement itself doesn't require a new software architecture. The Express & express-http-proxy middleware implementation proves that.
The gap which fogwall intends to fill is store-and-forward as an architecture: running a real git server (JGit's ReceivePack) that takes possession of the push into a local repository before deciding whether to forward it. That possession is what makes deferred forwarding possible — queue an approved push and forward it later, fully decoupled from the client's original connection. A transparent byte-forwarding proxy architecturally cannot do this: it only relays bytes between client and upstream in real time. While not impossible to extend an HTTP connection to instrument more and more complex validation or other logic within Express' paradigm, the git client ultimately expects back a timely response. Most HTTP servers will complete a connection after a status code & response body is written to and with a short timeout (60 seconds is a usual default).
During the course of a push through a git-aware proxy or gateway and in certain organizations, there may be long running tasks or operations, such as dispatching to an external ticket system, which have to occur throughout the lifecycle of code moving from zone to zone. The ability to asynchronously initiate a process such as this is both a simple quality-of-life enhancement to the client experience and allows for more possibilities of complex orchestration of pre-receive & post-receive tasks as part of a single git push's lifecycle.
Sideband streaming and live feedback follow from holding that session open — they're a corollary, not the headline reason for building a store-and-forward proxy.
fogwall runs two proxy modes simultaneously for every provider:
- Store-and-forward (
/push/...) — JGitReceivePackreceives the push into a local bare repo. A pre-receive hook chain (FogwallHookimplementations sorted bygetOrder()) runs validation.ForwardingPostReceiveHookpushes upstream using the client's credentials held in-memory viaCredentialsProvider. - Transparent proxy (
/proxy/...) — JettyProxyServletforwards the request. A servlet filter chain (FogwallFilterimplementations sorted bygetOrder()) inspects the pack data before it reaches upstream.
Both modes share the same validation logic and mirrored order ranges (0–199 authorization, 200–399 content, 400–499 post-validation). A validation rule written once works in both modes.
finos/git-proxy uses a single transparent proxy model: express-http-proxy relays bytes between client and upstream, gated by a filter(req, res) callback that can short-circuit the proxy. For inspection, it implements pkt-line and pack header parsing (pktLineParser.ts, parsePush.ts), then shells out to git receive-pack as a child process to write objects. The validation chain (chain.ts) is a plain ordered array of async functions which also have an implicit ordering & dependency between steps.
fogwall (store-and-forward): rp.sendMessage() writes to JGit's sideband-2 channel. The hook chain explicitly flushes rp.getMessageOutputStream() after each hook, so messages stream to the git client immediately as remote: … lines. The non-trivial part is the protocol infrastructure JGit provides underneath: SideBandOutputStream multiplexes pack data, progress, and errors across three channels as pkt-line framed packets over the same stream (PacketLineOut, SideBandOutputStream). In Node, that framing layer does not exist and would need to be written — correctly implementing the pkt-line framing and channel multiplexing that git clients expect.
fogwall (transparent proxy): No mid-chain streaming — the HTTP response is a single buffered reply. ValidationSummaryFilter collects all failures and writes one combined sideband error response at the end using JGit's SideBandOutputStream.
finos/git-proxy: Validation outcomes surface only in the final response. executeChain in chain.ts is a plain for...of with await — strictly sequential, each step mutates and returns the Action object for the next. Adding per-step sideband streaming would require implementing git pack protocol sideband framing on top of chunked HTTP responses; and in a transparent proxy there is no local processing session to stream progress about — the bytes are already on their way to upstream.
fogwall creates PushContext and ValidationContext per-request. No shared mutable state between concurrent pushes. Credentials live on PushContext, never on cached Repository config. Configuration is snapshotted at push start via Supplier.get(), so a config reload mid-push doesn't affect in-flight operations.
finos/git-proxy passes an Action object through the chain per-request, which serves a similar isolation purpose. For the inspection clone, credentials are extracted from the HTTP Authorization header and passed as an in-memory onAuth callback to isomorphic-git's clone(). For upstream delivery, the original HTTP request (including the Authorization header) is forwarded transparently by express-http-proxy.
fogwall: Thread-per-request — a hook can block freely while sending periodic sideband keepalives. ApprovalPreReceiveHook holds the connection open (configurable, up to 30 minutes by default using virtual threads), streaming approval status updates to the client in real time. When a reviewer approves, the push auto-forwards without requiring the developer to re-push.
finos/git-proxy: blockForAuth.ts avoids blocking entirely: it sends a "push received, visit this dashboard link" message and lets the HTTP request complete, requiring the developer to push again once approved. This is a deliberate design choice given the transparent proxy architecture, not a limitation of Node's event loop.
| Component | Node.js / finos/git-proxy | Java / fogwall |
|---|---|---|
| HTTP server | Node.js built-in http / Express 5 |
Jetty 12 (embedded) |
| Proxy | express-http-proxy — transparent byte-forwarding, gated by a filter(req, res) callback |
ProxyServlet (transparent mode) + JGit ReceivePack (store-and-forward mode) |
| Git protocol | Hand-rolled pkt-line/pack-header parsing for inspection + git receive-pack child process to write objects |
JGit — native ReceivePack, UploadPack, pack parsing, sideband channels; inspection and writing through one in-process library |
| Request body | Dual PassThrough streams (extractRawBody) — piped to a buffered consumer and a re-exposed req.pipe. Arguably more memory-efficient since only one consumer fully materializes the body |
HttpServletRequestWrapper caches the full byte array up front; all consumers re-read via ByteArrayInputStream. Simpler code, eagerly buffers |
| Response interception | express-http-proxy's filter option: return false and the library never proxies, leaving the caller free to respond directly — a documented extension point |
HttpServletResponseWrapper — intercepts setStatus() / sendError() from any downstream component. More general, though git-proxy's use case doesn't need that generality |
| Filter chain | Express middleware (registration order); next() continues chain. Validation chain in chain.ts is a plain ordered array of async functions with short-circuit support |
Servlet filter registration order; FilterChain.doFilter() with request/response wrapping; typed ServletContext attributes. Dual-mode: same validation as both JGit hooks and servlet filters |
| Sideband streaming | Not implemented | ReceivePack.sendMessage() → SideBandOutputStream → Jetty chunked encoding. Native to JGit |
| Long-running hooks | Event loop — async/await required throughout; current approval flow avoids blocking by returning immediately | Thread-per-request; blocks freely; virtual threads for scale |
| Credentials | Inspection clone uses isomorphic-git onAuth callback (in-memory). Upstream delivery forwards the original Authorization header transparently. SSH path shells out to system git |
CredentialsProvider in-memory object in S&F mode; Authorization header forwarded by Jetty ProxyServlet in transparent mode. Never on disk, never in a subprocess in either mode |
| SSH transport | ssh2 ~1.17.0 — agent forwarding uses ssh2's undocumented private APIs (_protocol, _chanMgr) with explicit acknowledgment these lack semver guarantees |
Not yet implemented — roadmap via Apache MINA SSHD (stable Apache project API) |
| Authentication | Passport.js — simple, composable middleware with a large strategy ecosystem (passport-local, openid-client, passport-activedirectory, etc.). Each strategy is independently maintained, which means quality and release cadence varies across providers. |
Spring Security — form login, OIDC, LDAP under one release cadence, but configuration complexity is a real cost; wiring up a non-trivial auth flow requires understanding its filter chain, security context, and bean lifecycle |
| Feature | finos/git-proxy | fogwall status |
|---|---|---|
| SSH transport | Substantial implementation (~2,500 lines) using ssh2. Handles git-upload-pack and git-receive-pack over SSH. Agent forwarding via ssh2's private APIs (sshInternals.ts wraps _protocol, _chanMgr, _handlers with an explicit warning about semver instability). Upstream SSH connections shell out to system git rather than using ssh2 |
Roadmap for 1.2/1.3 via Apache MINA SSHD. JGit already provides SshGitProtocol and typed command handling; MINA SSHD is an established Apache project with a stable, documented API — no private-API coupling |
| Pre-receive hook registry | preReceive.ts shells out to ./hooks/pre-receive.sh via spawnSync. 3-state exit code: 0 = auto-approve, 1 = auto-reject, 2 = manual approval. Not Windows-compatible |
Not implemented. Partially addressed if/when SPI plugin system ships — external hooks could be wrapped as a plugin |
| CLI tool | @finos/git-proxy-cli — separate npm package. Commands: login, logout, ls (list/filter pushes), authorise, reject, cancel, reload-config, create-user. Thin REST API client |
Not implemented. All interaction via dashboard UI or direct REST API calls |
| Plugin system | Runtime plugin loader (PluginLoader) via load-plugin. Plugins implement PushActionPlugin/PullActionPlugin with exec(req, action), spliced into the push chain after parsePush |
Not implemented. OrderableFogwallFilter supports runtime order changes but registration is hard-coded. ServiceLoader-based SPI is on the roadmap |
finos/git-proxy's SSH implementation works today, and that's worth crediting. The architectural concern is sustainability: sshInternals.ts accesses ssh2's undocumented internal APIs for agent forwarding, with the authors explicitly acknowledging these lack semver guarantees. A patch bump to ssh2 could break agent forwarding with no deprecation warning.
fogwall's planned approach uses Apache MINA SSHD via JGit's existing SSH integration. No private or internal API coupling is anticipated — the same ReceivePack hook chain that handles HTTP pushes today would handle SSH pushes through the same path.
| Capability | Express advantage |
|---|---|
| Startup time | Node starts in milliseconds. Jetty + JGit is ~1 second. Not meaningful for a long-running server but matters for test iteration |
| Ecosystem breadth | npm has middleware for everything — rate limiting, CORS, logging, niche integrations. Java has equivalents but they're heavier |
| Developer familiarity | More developers know JavaScript/TypeScript than Java servlet APIs. Lower barrier to contribution |
| Simple proxy passthrough | express-http-proxy makes transparent proxying trivial. Jetty's ProxyServlet works but is less commonly used |
| UI co-hosting | React UI naturally co-hosted with the Node backend. Java backend needs separate static file serving or a build step |
This section describes capabilities that fogwall's architecture enables — some ship today, some are roadmap items. The distinction matters.
- Dual-mode validation: same rules work as both JGit hooks (streaming feedback) and servlet filters (buffered response). Write a validation once, deploy in either proxy mode.
- Live sideband progress: per-hook progress messages stream to the developer's terminal during a push. Not just pass/fail — per-step status.
- Held-connection approval: the push session stays open while waiting for reviewer approval, with heartbeat keepalives and disconnect detection. Auto-forwards on approval without requiring the developer to re-push.
- Per-request config snapshot: config is read once at push start; mid-push reloads don't affect in-flight operations.
- Disconnect detection:
HeartbeatSenderdetects when the client disconnects mid-push and marks the push CANCELED in the database.
- ServiceLoader-based SPI: the
FogwallHookandFogwallFilterinterfaces are designed for it —getOrder(),getName(),shouldFilter()predicates. Registration is currently hard-coded; making it dynamic via ServiceLoader is a mechanical change, not an architectural one. - SSH transport via MINA SSHD: JGit's existing MINA SSHD integration means the same
ReceivePackhook chain handles SSH pushes. No separate validation path. - Concurrent/DAG pipeline execution: the ordered hook/filter chain could be extended to allow parallel execution of independent validation steps (e.g., secret scanning and commit message validation simultaneously).
| Capability | fogwall | finos/git-proxy | Assessment |
|---|---|---|---|
| Git protocol stack | JGit — parse and write through one in-process library | Hand-rolled parser for inspection + git receive-pack subprocess for writing |
JGit gives both jobs from one maintained library. finos/git-proxy maintains its own parser for inspection and subprocesses the other half — it works, but two mechanisms covering one job is more surface area for bugs. |
| Sideband streaming | ReceivePack.sendMessage() + explicit flush per hook. JGit handles sideband framing |
Not implemented. Building it would mean hand-rolling sideband packet framing on top of chunked HTTP responses | Real differentiator today. JGit provides this as part of its protocol implementation; in Express it would be significant engineering effort |
| Response interception | HttpServletResponseWrapper — intercepts calls from any downstream component |
express-http-proxy's filter option — pre-flight gate, documented extension point |
Different scope. For policy enforcement (decide pass/fail before proxying), the pre-flight gate is sufficient and is what ships |
| Request body caching | HttpServletRequestWrapper with cached byte array |
Dual PassThrough streams — one buffered consumer, one re-exposed pipe |
Two working solutions. Servlet wrapper is simpler code; Node approach uses less peak memory |
| Filter ordering | Servlet filters with explicit registration order + JGit hooks with getOrder(). Dual-mode: same validation in both paths |
Express middleware ordering + chain.ts ordered array with short-circuit |
Comparable for simple chains. fogwall's dual-mode (hook + filter) is structurally distinctive |
| Long-running hooks | Thread-per-request, blocks freely, virtual threads for scale. Approval gate holds connection open with heartbeat | Event loop, async/await. Approval flow returns immediately, requires re-push | Real UX difference (auto-forward vs. manual re-push), not a statement about what Node can or can't do |
| Pack data handling | JGit PackParser — unpack into local bare repo, full commit metadata via typed APIs |
writePack.ts shells out to git receive-pack; metadata extracted by hand-rolled parser |
JGit does both through one API. finos/git-proxy uses two mechanisms — works, but more moving parts |
| Credential handling | S&F: CredentialsProvider in-memory object. Transparent: Authorization header forwarded by Jetty. Never on disk or in a subprocess in either mode |
HTTPS: isomorphic-git onAuth callback (in-memory) for inspection clone; Authorization header forwarded transparently for upstream delivery. SSH: shells out to system git |
Both keep credentials in-memory for the HTTPS path. The SSH path (PullRemoteSSH) shells out to system git, which is a larger exposure surface |
| SSH transport | Not yet implemented (MINA SSHD roadmap) | Implemented via ssh2; agent forwarding uses undocumented private APIs | finos/git-proxy is ahead here today. Architectural question is API stability of the ssh2 internals dependency |
| Authentication | Spring Security — OIDC, LDAP, form login under one release cadence | Passport.js — separate packages per strategy, each independently maintained | Real packaging difference. Not evidence of insecurity without checking CVE history |
| Plugin extensibility | Not yet implemented (ServiceLoader SPI roadmap) | Runtime plugin loader via load-plugin; plugins splice into push/pull chains |
finos/git-proxy is ahead here today. fogwall's hook/filter interfaces are designed for SPI but registration is still hard-coded |
| CLI | Not implemented | @finos/git-proxy-cli — push management, user creation, config reload |
finos/git-proxy is ahead here |
| Pre-receive hooks | Not implemented | Shells out to external script; 3-state exit code protocol | finos/git-proxy is ahead here. Niche feature, partially addressed by SPI if/when it ships |
| API standardization | Jakarta EE spec — Filter, HttpServletRequestWrapper portable across Jetty, Tomcat, Undertow |
Express middleware convention: (req, res, next). Library-specific hooks on express-http-proxy |
Spec-level standardization means portable patterns. Express's answer is narrower but working |
finos/git-proxy supports hot reload via ConfigLoader.ts with three source types (file/http/git), polling on reloadIntervalSeconds. On detected change it calls proxy.stop() then proxy.start() — a full in-process restart of the proxy module. Multi-source merge uses a config-flag-controlled strategy: recursive deepMerge (later source wins on leaf conflicts) or full override. Schema validation via quicktype-generated types.
fogwall supports hot reload for validation config via file-watch (WatchService) or git-repo polling. Provider/server/database changes log a warning and require a JVM restart. Config is snapshotted per-push via Supplier.get(). Environment variable overrides are generic (FOGWALL_-prefixed, mapped to any config path via Gestalt), while finos/git-proxy hardcodes six specific overridable env vars.
Where finos/git-proxy is ahead: explicit merge-or-override conflict resolution across multiple sources is a more deliberately specified model than fogwall's current approach, where if both file-watch and git-poll are enabled, whichever fires last re-overlays the full stack with no defined precedence.
Where fogwall is ahead: env var override coverage is generic rather than hardcoded to specific fields. Per-push config snapshotting ensures in-flight operations aren't affected by reloads.
The choice between the two models reduces to one question: does the proxy need to take possession of a push before deciding what to do with it?
If the answer is no — policy enforcement on the wire data is sufficient, and a developer re-pushing after a dashboard approval is an acceptable workflow — the transparent proxy model is simpler. Express middleware, a filter callback, and a buffered response covers the use case with less infrastructure.
If the answer is yes — deferred forwarding, held-connection approval, or lifecycle-gated workflows where the proxy must own the pack across multiple steps — then a store-and-forward architecture is the appropriate model. JGit's ReceivePack provides that possession as a first-class primitive. Building the equivalent on top of a transparent proxy requires implementing most of what ReceivePack provides from scratch, against a connection the proxy doesn't own.
Neither model is universally better. They solve different problems.
Updated June 2026. Comparison checked against finos/git-proxy main and fogwall main branch source code as of this date.