Golit
Reactive data apps that actually ship.
A high-performance reactive Directed Acyclic Graph (DAG) framework for Python. Golit maps your data dependencies once, then on every interaction recomputes only the nodes that changed — not your whole script.
Documentation · Changelog · Examples · Benchmarks · Architecture · Deployment
Golit pairs the authoring speed of a notebook-style data app with the runtime discipline of a compiled reactive graph. Your dashboard is a set of plain Python functions; Golit infers the dependency graph between them, and a Rust kernel ensures a user interaction re-runs the minimal subgraph it touched and ships only the affected HTML fragments over the wire. The guiding thesis: update cost is proportional to the change, not the size of the program.
Features
- Rust reactive kernel (PyO3) — dirty tracking, topological scheduling, memoized propagation.
- Polars data held Python-side; only node ids/hashes cross the FFI boundary. Nodes may return LazyFrames — plans fuse across node boundaries and collect once at the view, with an opt-in path to the streaming engine for larger-than-memory data.
- SQL nodes — reactive nodes written as in-process DuckDB SQL over Polars frames (
golit.sql). - Litestar orchestration with HTMX server-rendered fragment transport — no client framework.
- Charts — Lets-Plot static SVG, plus interactive Plotly / Altair / Bokeh / AnyChart.
- Tables — styled tables from Polars, or return a Great Tables
GTobject for a polished, auto-rendered display table. - Maps — native MapLibre GL: GeoDataFrame vector, rioxarray/xarray raster, and DuckDB spatial SQL (
golit.gis). - Components — reactive input widgets plus a shadcn-styled
golit.uilibrary, server-rendered with Tailwind. - Realtime — SSE push with pluggable pub/sub (in-memory or Redis), WebSocket chat, video (server-side MJPEG + browser-camera CV), and audio (mic recorder).
- Live sources —
@app.pollstreams external data that changes on its own (a Google Sheet, an API); only real changes re-render and hit the wire. - Scales horizontally — N single-worker instances behind a sticky load balancer with Redis fan-out.
Installation
Golit ships prebuilt wheels for Python 3.11+ (the abi3 stable ABI):
pip install golit # core
pip install "golit[charts]" # interactive Plotly / Altair / Bokeh
pip install "golit[sql]" # DuckDB SQL nodes over Polars frames
pip install "golit[gis]" # native MapLibre maps from GeoDataFrames
pip install "golit[gis-raster]" # raster maps from rioxarray/xarray arrays
pip install "golit[tables]" # Great Tables display tables (auto-rendered from a view)
pip install "golit[vision]" # webcam / MJPEG video streams (Pillow)
pip install "golit[vision-cv]" # + OpenCV for real CV models (face detection)
pip install "golit[redis]" # Redis fan-out for multi-worker
Quickstart
make dev # uv venv (3.11) + deps + build the Rust kernel (maturin)
make test # cargo test + pytest
make run # golit run examples/sales_explorer/app.py
Then open http://127.0.0.1:8000.
Programming model
Nodes are plain Python functions. Dependencies are inferred from parameters: a parameter named after another node is an edge; a parameter defaulting to a widget is an input.
import polars as pl
from golit import App, create_app, slider, upload
from golit.charts import aes, geom_bar, ggplot, ggsize
app = App(title="Sales Explorer")
SAMPLE = pl.DataFrame({"region": ["North", "South", "East", "West"], "revenue": [120, 90, 70, 40]})
@app.source
def data(file=upload("Upload CSV")) -> pl.DataFrame:
return SAMPLE if file is None else pl.read_csv(file) # renders before any upload
@app.reactive
def filtered(data: pl.DataFrame, threshold: int = slider(0, 100, default=20)) -> pl.DataFrame:
return data.filter(pl.col("revenue") > threshold) # re-runs only when data/threshold change
@app.view
def chart(filtered: pl.DataFrame):
return ggplot(filtered, aes("region", "revenue")) + geom_bar(stat="identity") + ggsize(640, 360)
application = create_app(app) # an ASGI app; `golit run app.py` serves it
Moving the slider dirties threshold → filtered → chart. The data node is never touched; only
the chart fragment is re-rendered and swapped. A view that depends only on data (e.g. a dataset
overview) is not re-rendered on a slider move — that selective recompute is the whole point. See
examples/sales_explorer/app.py.
How a change flows
- POST in — a committed input (
hx-post) runs the dirty subgraph; the response carries only the affected view fragments as out-of-band HTMX swaps. - SSE out — nodes dirtied server-side (streaming sources, background jobs, shared nodes) are
pushed over
/eventsas namednode:<id>events. - Memoization — a node re-executes only when its inputs hash differently; an unchanged output cascades into memo hits downstream (nothing on the wire).
Performance
The thesis is update cost is proportional to the change, not the program — and the benchmark harness measures it (dev laptop, loopback; reproducible, not yet the published cloud figure). The honest summary:
- A single filter → chart updates in ~2 ms over HTTP — the same as Dash. Idiomatic Dash is a hand-wired reactive DAG, so on one chain both do identical work and tie. Saying otherwise would be a strawman.
- The gap opens on the shape real dashboards have: shared upstream work. When one expensive step (a load, a join, a sort) feeds several views, moving a control that touches only one view makes Golit re-run only that view — the shared upstream is memoized and executes zero times — while Dash recomputes it every callback. Over real HTTP that's ~1.6× faster at 100K rows, ~5.5× at 1M, ~8.3× at 2M, and the lead widens with the app.
chart_specskips the figure object. Returning a raw spec dict instead of ago.Figurecuts the per-update round-trip to ~1.5 ms and ~635 B (vs ~6.9 KB) — ~1.4× faster than figure-returning Dash with a ~10× smaller payload, same chart.
See bench/README.md for the methodology and one-command repros.
SQL nodes
A reactive node can be written as SQL instead of Polars. golit.sql(query, **frames) runs DuckDB
in-process over the named upstream frames and returns Polars, so the node memoizes and renders like
any other — inputs feed straight into the query.
from golit import sql
@app.reactive
def by_region(data: pl.DataFrame, threshold: int = slider(0, 200, default=40)):
return sql(
"SELECT region, sum(revenue)::BIGINT AS revenue "
f"FROM d WHERE revenue > {int(threshold)} GROUP BY region ORDER BY region",
d=data,
)
DuckDB exchanges data with Polars zero-copy. A raw duckdb.sql(...) relation returned from a node is
auto-detected and materialized too. Optional dependency: pip install "golit[sql]"; it is imported
only inside sql(), never at framework import time. See
examples/duckdb_sql/app.py.
Charts
Lets-Plot renders to static SVG (no client runtime). For interactivity, return a Plotly, Altair,
or Bokeh figure — Golit auto-detects it and renders a client-side chart that hydrates on the
initial load and across POST/SSE swaps. AnyChart (no Python package) is available via
anychart().
@app.view
def chart(by_region):
import plotly.express as px
return px.bar(by_region, x="region", y="revenue") # Polars frame in directly
For a view that rebuilds its chart on every interaction, chart_spec(lib, dict) hands Golit the raw
wire-format spec directly, skipping the figure-object build and to_json (see
Performance). See examples/charts_gallery/app.py.
Maps
A map is a reactive view like any other — a control rebuilds it. Return a GeoPandas
GeoDataFrame and Golit renders a native MapLibre GL map; golit.gis.geo_map adds
choropleths, tooltips, and basemaps, and golit.gis.spatial_sql runs DuckDB ST_* queries that feed
it. No client map framework — the server ships the GeoJSON and the style rules; the GPU draws. For
large vector data, golit.gis.vector_tiles keeps the GeoDataFrame server-side and streams MVT
vector tiles (pip install "golit[gis-vector-tiles]") so 100k+ features render without inlining
the whole GeoJSON.
import golit.gis as gis
@app.view
def map(regions): # regions is a filtered GeoDataFrame
return gis.geo_map(regions, color="revenue", tooltip=["name", "revenue"])
Raster works too: gis.raster(dataarray) colormaps a georeferenced rioxarray/xarray array (or
GeoTIFF) to a MapLibre image layer, gis.rgb(stack, bands=…) renders a multiband raster as a
true/false-color satellite composite (pip install "golit[gis-raster]"), and
gis.tiles("scene.tif") streams a very large COG as on-demand z/x/y tiles via rio-tiler
(pip install "golit[gis-tiles]") — only the visible window crosses the wire. And
gis.terrain(dem, "hillshade") runs WhiteboxTools terrain analysis (slope, flow accumulation, …)
into a renderable raster (pip install "golit[gis-terrain]"), and gis.ee_layer(image, vis=…)
overlays Google Earth Engine imagery as live tiles (pip install "golit[gis-ee]"). Install
vector with pip install "golit[gis]" (DuckDB spatial rides on the sql extra). Moving a control
re-runs only the filter + map node — the fragment swaps in place on the initial load and after a
POST/SSE. See
examples/geo_explorer/app.py (vector),
examples/vector_tiles/app.py (60k-feature vector tiles),
examples/raster_explorer/app.py (raster),
examples/rgb_composite/app.py (RGB composite),
examples/tiled_raster/app.py (tiled COG),
examples/terrain_analysis/app.py (terrain), and
examples/earth_engine/app.py (Earth Engine).
Realtime: video and audio
Some views don't re-render on a change — they hold a live connection. golit.ui.chat(channel)
opens a WebSocket-backed chat panel (@app.on_message adds bot/moderation logic). For computer
vision, @app.stream(name) + ui.webcam(name) push a server-side MJPEG feed the browser plays
in a plain <img> — a host camera, a detector drawing boxes, a synthetic animation — and
shared=True fans one producer out to many viewers. The mirror, @app.on_frame(name) +
ui.camera(name), streams the visitor's own webcam up over a WebSocket, runs your handler on each
frame server-side, and paints the annotated result back.
import numpy as np
import golit.ui as ui
@app.on_frame("faces")
def detect(frame: np.ndarray) -> np.ndarray: # (H, W, 3) uint8 RGB in and out
... # run your model, draw boxes
return frame
@app.view
def live() -> str:
return ui.camera("faces", title="Your camera")
Frames are JPEG bytes or (H, W, 3) RGB arrays (encoded with Pillow). Sync handlers run in a
worker thread; one frame is in flight at a time, so a slow model lowers the rate instead of backing
up, and a producer/handler that errors is logged without dropping the stream. pip install "golit[vision]" (or [vision-cv] for OpenCV). See
examples/webcam_stream (server feed),
examples/browser_camera (browser camera), and
examples/face_detect (real OpenCV face detection).
For audio, ui.recorder(name) captures the visitor's mic and uploads each clip as 16-bit WAV
(with inline playback + a download link for the clip); the @app.on_audio(name) handler decodes it
(Python's stdlib wave — no ffmpeg) and returns a result to show, or audio to play back. See
examples/audio_recorder and
Audio recording.
Components
Inputs (reactive) — slider, number, select, text, checkbox, upload, radio,
multiselect, switch, date, textarea, button.
Display (golit.ui) — card, columns, grid, tabs, expander, accordion, divider,
metric, scorecard, alert, badge, progress, skeleton, spinner, table, markdown,
code, json_view, heading, caption.
Realtime (golit.ui) — chat, webcam, camera, recorder.
import golit.ui as ui
@app.view
def panel(by_region, total):
return ui.card(
ui.columns([ui.metric("Revenue", f"${total:,}", delta="+8%"), chart_fig]),
title="Overview",
)
Components compose through the renderer, so any argument can be a DataFrame, a chart figure, another
component, or trusted HTML. See
examples/components_gallery/app.py.
Page layout
By default views stack under one controls panel. golit.layout arranges the reactive view fragments
into a sidebar, rows, tabs, etc. — the layout is static scaffold, so each view keeps its id and
still swaps in place on POST/SSE.
from golit import layout as L
app.layout = L.Sidebar(
L.Controls(), # all inputs, in the sidebar
L.Stack(
L.Row(L.View("kpi"), L.View("status")),
L.Tabs({"Chart": L.View("chart"), "Data": L.View("table")}),
),
)
References are validated at build time: every View/Control must resolve to a real view/input and
be placed at most once.
Deploying and scaling
A single process needs nothing extra. To scale horizontally, set GOLIT_REDIS_URL and run N
single-worker instances behind a sticky (session-cookie) load balancer — session state is
worker-local by design, and Redis fans server-side invalidations across the fleet. A runnable
podman/nginx stack lives in deploy/.
pip install "golit[redis]"
export GOLIT_REDIS_URL=redis://localhost:6379
golit run examples/sales_explorer/app.py
See DEPLOYMENT.md for the full topology and why uvicorn --workers can't provide
session affinity.
Architecture
| Tier | Role | Technology |
|---|---|---|
| 0 | Reactive kernel | Rust + PyO3 (src/, golit._golit) |
| 1 | Orchestrator | Litestar + SSE/Redis fan-out (golit.server) |
| 2 | Transport | HTMX fragments; static SVG, interactive charts, native maps (golit.rendering) |
| 3 | Local shield | Alpine.js (widget immediacy, tab state) |
See project_scope.md for the full architecture and
golit_benchmark.md for the benchmark methodology.
Status
v1.0.0 — first stable release (see the CHANGELOG).
Built end-to-end and green (17 cargo + 209 pytest, ruff + mypy clean): Rust kernel, reactive
engine, rendering (static and interactive charts, native MapLibre maps, auto-rendered Great
Tables), the golit.ui component library, page layout, DuckDB SQL nodes, GIS (vector maps + MVT
vector tiles for large data; single-band, RGB-composite, tiled-COG raster maps; WhiteboxTools
terrain; Earth Engine overlays; spatial SQL — golit.gis), Litestar server (POST + SSE), Redis
pub/sub fan-out, multi-worker deployment, live polled sources (@app.poll), realtime WebSocket chat,
video (server-side MJPEG streams + browser-camera CV), and audio (mic recorder), the benchmark
harness (bench/, with measured Golit-vs-Dash results), and the examples.
Deferred: a standard-cloud-instance benchmark publication and the wider design suite in
golit_pages/.
Development
make dev # set up venv + build extension
make test # cargo test + pytest
make lint # ruff + mypy
make build # release wheel
License
Golit is released under the Apache License 2.0. © 2026 Daniel Boadzie.
Metadata
Release files for golit 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| golit-1.2.0.tar.gz | 4.2 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| golit-1.2.0-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details |
| golit-1.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| golit-1.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| golit-1.2.0-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 5.6 MB
Release files / golit-1.2.0.tar.gz
| Download URL | golit-1.2.0.tar.gz |
|---|---|
| Size | 4.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3cc103f2ce8ba248608e99f37b41570d62618d474b80892addd5580920e428ea
|
|
BLAKE2b-256 checksum How to use checksums |
cc1d1c741d2e267452e0961fd2de57d882e959a2e2c092c0318b7bf7938dc1c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / golit-1.2.0-cp311-abi3-win_amd64.whl
| Download URL | golit-1.2.0-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 275.7 kB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2fd387b7a22b98cf5834f5bba928efa560e07ba9280be9161c6b824d1702967a
|
|
BLAKE2b-256 checksum How to use checksums |
fb57430e9a68b54864df573c98c01c70f92b73dec3667a2b5f855ceb5dc5db20
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / golit-1.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | golit-1.2.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 399.2 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
48e9181659714f942607cb3569905d5fd8eaffed4b61e99471794ff2bd730a50
|
|
BLAKE2b-256 checksum How to use checksums |
6c3e530cb29c174881afc8285911c7acdf17c7445d2c4499131bb6994d7dd59e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / golit-1.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | golit-1.2.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 399.5 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
e12734c19dfdb16d70cff92c93a3c0b725a5ee8a741a788931efba151f9b11f8
|
|
BLAKE2b-256 checksum How to use checksums |
052004128040db4d41aae78e05ea5bac13b0257f9bf9cc9eeda3e0fc0cc28972
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / golit-1.2.0-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | golit-1.2.0-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 371.8 kB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
f92b2c255ba6d6e5a76ebe2ab10096652af887b16c9211243f871126b28f1d7b
|
|
BLAKE2b-256 checksum How to use checksums |
b565429c18c05834f2654f9033fc6e0b091026980e7a9ada0b1eb2a1b083bb1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency log