Skip to content

Instantly share code, notes, and snippets.

@coopernetes
Last active April 20, 2026 14:25
Show Gist options
  • Select an option

  • Save coopernetes/626541b83a148f4ae21ae2c62c57edea to your computer and use it in GitHub Desktop.

Select an option

Save coopernetes/626541b83a148f4ae21ae2c62c57edea to your computer and use it in GitHub Desktop.
Tech stack comparison

Server Framework: Jetty/Servlet vs Node/Express

Context: git-proxy is a FINOS project that acts as a policy-enforcing proxy for git pushes. The current implementation is Node.js/Express. This document compares the server-side capabilities relevant to building a store-and-forward git proxy — where pushes land in a local repository first, run through validation hooks with real-time sideband feedback, then get forwarded upstream.

These findings come from prototyping git-proxy-java, a Java/Jetty implementation that uses JGit's native git protocol stack.

The Problem

A git proxy that does more than passively forward bytes needs to:

  1. Parse git smart HTTP protocol — packet lines, pack data, sideband channels
  2. Stream real-time feedback — sideband messages (remote: validating push...) that appear in the developer's terminal during a push, not after
  3. Intercept and transform HTTP responses — e.g. force HTTP 200 on errors so git clients read the protocol body instead of showing a generic "403 Forbidden"
  4. Cache and replay request bodies — the request body (pack data) needs to be read by multiple stages: parsing, unpacking into a local repo, and forwarding
  5. Run ordered validation pipelines — author email checks, commit message policies, GPG signature verification, external approval workflows
  6. Handle long-running operations — external approval gates (ServiceNow, risk ledger) that block for 10–60+ seconds while keeping the connection alive

Stack Comparison

Node.js / git-proxy

Component What it is
HTTP server Node.js built-in http / Express 4
Proxy express-http-proxy — transparent byte-forwarding
Git protocol None built-in — shells out to git receive-pack / git unpack-objects as child processes
Request body express.raw() buffers once; multi-consumer requires explicit PassThrough stream cloning
Response interception Monkey-patch res.status() / res.writeHead() — no standard wrapper pattern
Filter chain Express middleware (registration order); next() continues chain
Sideband streaming Not implemented — would require hand-rolling pkt-line framing + sideband bytes
Long-running hooks Event loop — synchronous blocking freezes the server; async/await required throughout
Credentials Passed to git subprocesses via URL or GIT_ASKPASS — risk of exposure in process listings

Java / git-proxy-java

Component What it is
HTTP server Jetty 12 (embedded)
Proxy ProxyServlet (transparent mode) + JGit ReceivePack (store-and-forward mode)
Git protocol JGit GitServlet — native ReceivePack, UploadPack, pack parsing, sideband channels
Request body HttpServletRequestWrapper caches byte array; all consumers re-read via ByteArrayInputStream — transparent
Response interception HttpServletResponseWrapper — standard API; intercepts setStatus() / sendError() from any downstream component
Filter chain Servlet filter registration order; FilterChain.doFilter() with request/response wrapping; typed ServletContext attributes
Sideband streaming ReceivePack.sendMessage()SideBandOutputStream → Jetty chunked encoding → developer's terminal
Long-running hooks Thread-per-request (each push on its own thread); blocks freely; Java 21 virtual threads for scale
Credentials CredentialsProvider in-memory object; never on disk or in child process environment

Capability Deep-Dive

Capability Jetty / Servlet Node / Express Impact
Git protocol stack JGit's GitServlet — native ReceivePack, UploadPack, pack parsing, sideband channels None built-in. git-proxy shells out to git receive-pack as a child process JGit gives you the full git server for free. No child processes, no manual protocol parsing
Sideband streaming ReceivePack.sendMessage() writes to SideBandOutputStream. flush() propagates through chunked transfer encoding. response.setBufferSize(256) ensures immediate dispatch You'd need to manually construct sideband packets (band 2, pkt-line framing) on top of res.write(). The event loop model also complicates long-running synchronous hooks This is the critical differentiator. Real-time sideband streaming in Jetty/JGit required ~3 lines. In Express, you'd hand-roll git protocol framing on top of raw HTTP chunked responses
Response interception HttpServletResponseWrapper — wrap the response to intercept setStatus(), sendError(), setHeader(). Standard, composable Intercepting status codes set by downstream middleware requires monkey-patching res.status() or res.writeHead(). No standard wrapper pattern Servlet's wrapper pattern is a first-class API. Express equivalent is fragile
Request body caching HttpServletRequestWrapper with cached byte array. getInputStream() returns a ByteArrayInputStream. All downstream filters transparently re-read the same body express.raw() can buffer the body, but piping to multiple consumers requires PassThrough cloning or explicit buffer management Servlet wrappers compose cleanly. Node streams are single-consume by default
Filter ordering Servlet filters have explicit registration order. FilterChain.doFilter() gives precise pre/post control. Filters can short-circuit, wrap request/response, or modify attributes Express middleware ordering is implicit (registration order). next() continues the chain. Lacks the request/response wrapping pattern and typed attributes Comparable for simple chains. Servlet filters pull ahead with response wrapping and typed attributes
Long-running hooks Thread-per-request — a hook can Thread.sleep(15_000) while sending periodic sideband keepalives without blocking other requests Event loop — a 15-second blocking operation freezes the entire server. No built-in mechanism to stream incremental sideband packets during a long-running handler Thread-per-request is natural for approval workflows. The event loop requires careful async choreography and manual protocol framing
Pack data handling JGit PackParser via ObjectInserter — unpack push objects into a local bare repo in ~5 lines. Full commit metadata via JGit's object model Shell out to git receive-pack or git unpack-objects. Commit metadata requires additional git log calls or manual object parsing JGit's in-process pack handling is dramatically simpler than shelling out
Credential handling CredentialsProvider abstraction — extract from HTTP Basic auth, pass as in-memory object to Transport.push(). Never touches disk Credentials passed via URL to git subprocesses or via GIT_ASKPASS. Risk of credentials in process listings or temp files JGit keeps credentials in-memory. Subprocess model has a larger credential exposure surface

What Express Does Better

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 / middleware npm has middleware for everything — auth, rate limiting, CORS, logging. 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 — transparent proxying is trivial in Node. Jetty's ProxyServlet works but is less commonly used
UI integration git-proxy's React UI is naturally co-hosted with the Node backend. A Java backend needs separate static file serving

Recommendation

For a policy-enforcing git proxy that needs real-time sideband feedback, store-and-forward capability, and long-running approval workflows:

Java/Jetty + JGit is the stronger foundation. The git protocol is inherently streaming and stateful — JGit handles this natively while Node requires subprocess orchestration and manual protocol framing. The sideband streaming capability (which took ~3 lines in Jetty) represents significant engineering effort in Express.

The tradeoff is contributor accessibility — Java servlet APIs have a steeper learning curve than Express middleware. Mitigated with a clean hook API that hides the servlet plumbing.


Dashboard / UI Comparison

This section compares the dashboard and web UI of finos/git-proxy (React + Material UI) with git-proxy-java's MVP dashboard (Alpine.js + Tailwind CSS), and documents the decision on how to evolve the git-proxy-java UI going forward.

Current State

finos/git-proxy — React SPA

Aspect Details
Framework React 16 (two major versions behind current)
UI kit Material UI v4 (@material-ui/core) — end-of-life, deprecated JSS styling (makeStyles/withStyles)
Template origin "Creative Tim Material Dashboard React" — free version; v5+ is now a commercial product
Bundler Vite
Routing react-router-dom v6 (client-side SPA)
HTTP client Mixed — axios in some services, native fetch in others
State management React Context (AuthContext, UserContext)
Auth (backend) Passport.js — local (bcrypt), OIDC (openid-client), Active Directory, JWT. Login page dynamically renders auth method buttons from /api/auth/config
Auth (frontend) AuthProvider with useAuth() hook, CSRF token via cookie, JWT token in localStorage
Styling 40+ JSS style files in assets/jss/material-dashboard-react/ tightly coupled to the template
Other deps moment.js (deprecated), PerfectScrollbar, font-awesome, material-design-icons

Pages (8 views + login): Login, Push Requests (tabbed: All/Pending/Approved/Canceled/Rejected/Error), Push Details (commits/diff/steps timeline/attestation form), Repo List, Repo Details (add user, delete repo), User Profile, User List (admin), Settings (JWT token)

Notable features that git-proxy-java lacks:

  • Attestation questionnaire — configurable checkbox questions with tooltip explanations, checked before approval
  • User management — CRUD for users, admin role gating
  • Repo management — add/remove repos, add authorized users per repo
  • Route guards — redirect to login when unauthenticated
  • OIDC login flow — dynamic auth method buttons, redirect-based OAuth

git-proxy-java — React + Vite SPA

Aspect Details
Framework React 19 — hooks-based functional components, no class components
Language TypeScript (strict mode) throughout
Styling Tailwind CSS v4 (via @tailwindcss/vite plugin) — utility classes, zero custom CSS files
Bundler Vite 8 — HMR in dev, optimised static build output
Routing react-router-dom v7 — client-side SPA with BrowserRouter
HTTP client Native fetch() throughout; typed response models in api.ts
Diff rendering diff2html — side-by-side with syntax highlighting
Auth Spring Security session; /api/me endpoint; login page at /login
Served by Spring MVC static resource handler (Jetty); Vite build output copied to resources/static

Pages (4 views + login): Login, Push Records (filterable by status/search, auto-refresh), Push Detail (commits, validation steps with expandable details, diff, approve/reject), Providers

What it does well:

  • Clean, modern TypeScript component model — props are typed, API responses are typed via types.ts
  • Tailwind v4 via Vite plugin — no separate CLI, no config file, purged production build
  • Vite HMR — instant feedback during development; vite dev proxies API calls to the Spring backend
  • Validation step display shows pass/fail per step with expandable error details and ANSI stripping
  • Auto-refresh on push list keeps the approval workflow live
  • diff2html rendering is feature-complete (side-by-side, syntax highlighting, file list)
  • Minimal dependency surface: react, react-dom, react-router-dom, diff2html — nothing else at runtime

Why We Didn't Port the finos/git-proxy React UI

The finos/git-proxy React UI has critical structural problems that make it unsuitable as a foundation to port from:

  1. Material UI v4 is dead. MUI v5+ (@mui/material) uses Emotion instead of JSS. Upgrading from v4 to v5 is a significant migration that touches every component. The 40+ JSS style files would all need to be rewritten.

  2. The template is now commercial. The "Creative Tim Material Dashboard React" template (which provides the sidebar, card components, grid system, and layout) is free only for v4. The v5+ version requires a paid license. Building on top of the free v4 version means no upstream fixes or improvements.

  3. React 16 is outdated. Missing concurrent features, automatic batching, useId, server components. Upgrading to React 18/19 while also migrating MUI v4→v5 is a compounding effort.

  4. Heavy dependency surface. The git-proxy UI pulls in: @material-ui/core, @material-ui/icons, @fontsource/roboto, material-design-icons, font-awesome, perfect-scrollbar, moment, history, clsx, react-html-parser, axios — all for what is fundamentally a list-of-records + detail-view + forms application.

  5. Tight coupling to the template. Components like Card, CardHeader, CardBody, CardFooter, Button, CustomTabs, GridContainer, GridItem are all wrappers around MUI with template-specific styling. They don't compose well outside the template's layout assumptions.

The answer is not "no React" — it's "no porting." git-proxy-java builds a fresh React 19 + Vite app from scratch: no MUI, no Creative Tim template, no legacy baggage. The feature set from finos/git-proxy is worth replicating; the implementation is not.

Decision: React + Vite — Fresh Modern SPA

Build a new React 19 + Vite SPA, starting from scratch. As the planned feature set grew — user management, attestation flows, SCM OAuth account linking, multi-step approval UI — the complexity outgrew what a single-file Alpine.js dashboard could cleanly support. React's component model and TypeScript's type safety make that surface area tractable.

The decision is not to adopt the finos/git-proxy React codebase (see above — MUI v4 EOL, commercial template). It's to write a clean React app with a minimal modern stack: React 19 + Vite + Tailwind CSS v4 + TypeScript. No UI framework dependencies at runtime beyond React itself.

Architecture

git-proxy-java-dashboard/
  frontend/               ← Vite project root
    src/
      api.ts              ← typed fetch wrappers for all REST endpoints
      types.ts            ← shared TypeScript types (PushRecord, Provider, etc.)
      pages/              ← one component per route
      components/         ← shared UI components (StatusBadge, etc.)
    vite.config.ts        ← dev server proxies /api/* → Spring backend on :8080
  src/main/resources/
    static/               ← Vite build output (copied by Gradle build)

The Spring backend exposes a REST API (/api/*). The React SPA calls it via fetch(). During development vite dev proxies API requests to the running Spring server. For production, Gradle builds the frontend and copies the output to resources/static/, where Spring MVC serves it as static files.

Auth is Spring Security session-based: /login is a plain HTML form (not part of the SPA), Spring Security sets a session cookie, and /api/me returns the current user for the SPA to display.

Roadmap

Phase 1 — Core approval workflow (done)

  • Push list with status filtering and auto-refresh
  • Push detail: commits, validation steps (expandable per-step errors), diff viewer, approve/reject actions
  • /api/me — current-user context in the UI; reviewer identity from Spring Security session

Phase 2 — Auth + user management

  • User profile page — view/edit own profile, SCM account linking
  • Admin user list — CRUD, role management
  • Login page — Spring Security form login; OAuth2 buttons for configured providers
  • Route guards — ProtectedRoute component wrapping authenticated pages; redirect to /login on 401

Phase 3 — Repo management + attestation

  • Repo list / details — pull from config/DB, add authorized users per repo
  • Attestation questionnaire on push detail — configurable checkbox questions from YAML config, required before approval
  • Settings page — server config display, feature toggles

Phase 4 — SCM integration

  • PR creation flow — repo picker, branch pickers (populated from SCM API), title/body form
  • Multi-file diff reviewer for pending pushes
  • OIDC login flow — dynamic auth method buttons from /api/auth/config

Why React (over continuing with Alpine.js)

Concern How React addresses it
Component reuse TypeScript React components are the reuse unit — <StatusBadge status={...} /> used everywhere, typed props prevent misuse
Complex state Approval flow state (selected commits, attestation answers, reviewer notes) is modelled with useReducer/useState — clean, testable, no DOM manipulation
SCM OAuth UI /api/auth/config returns available providers; React renders the right buttons dynamically — same pattern finos/git-proxy already proven out
Type safety types.ts models every API response; TypeScript catches API contract drift at build time, not runtime
Contributor familiarity React + TypeScript is the most common frontend stack in open source. Lower barrier than Alpine.js + Thymeleaf for contributors from a JS background
Vite DX HMR, fast cold starts, first-class TypeScript — tighter dev loop than the previous CDN-only Alpine approach

Feature parity with finos/git-proxy

The finos/git-proxy React UI proves out the feature set that matters. git-proxy-java is building toward the same coverage on a clean foundation:

Feature finos/git-proxy git-proxy-java status
Push list with status tabs Implemented (CustomTabs + PushesTable) Done — filterable list with auto-refresh
Push detail with commits/diff/steps Implemented (tabbed view) Done — per-step expandable details, diff viewer
Approve / reject / cancel actions Implemented (with auth context) Done — wired to Spring Security session via /api/me
Login page Implemented (multi-method: local + OIDC buttons) Done — Spring Security form login; separate login.html
Route guards Implemented (client-side redirect to login) Done — 401 from /api/me triggers redirect to /login
Attestation questionnaire Implemented (configurable checkbox questions from config) Planned (Phase 3) — questions from YAML config
User profile Implemented (view/edit own profile, git account linking) Planned (Phase 2)
User list (admin) Implemented (CRUD, admin-only) Planned (Phase 2)
Repo list / details Implemented (CRUD, add users, delete) Planned (Phase 3) — basic stub exists
Settings page Implemented (JWT token config) Planned (Phase 3)
OIDC login flow Implemented (dynamic auth method buttons) Planned (Phase 4)

Tech Stack Summary

Layer finos/git-proxy git-proxy-java (current) git-proxy-java (target)
Server Node.js / Express 5 Jetty 12 / Spring MVC Jetty 12 / Spring MVC
Git protocol Child processes (git receive-pack) JGit native JGit native
UI framework React 16 React 19 React 19
Language TypeScript TypeScript TypeScript
UI components Material UI v4 (EOL) Tailwind utility classes Tailwind + custom components
CSS JSS (40+ style files) Tailwind CSS v4 (via Vite plugin) Tailwind CSS v4
Bundler Vite Vite 8 Vite 8
Routing react-router-dom v6 react-router-dom v7 react-router-dom v7
Auth (server) Passport.js (local, OIDC, AD, JWT) Spring Security (form login, session) Spring Security (form, OIDC, LDAP)
Auth (UI) React Context + axios interceptors /api/me + session cookie /api/me + session cookie; ProtectedRoute
Template engine JSX JSX JSX
Diff rendering diff2html (via React wrapper) diff2html (vanilla JS) diff2html (React wrapper)

Updated April 2026. UI comparison based on finos/git-proxy main branch and git-proxy-java main branch as of this date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment