High Performace IDE for Jupyter Notebooks https://zasper.io
  • TypeScript 59.6%
  • Go 30.3%
  • SCSS 8.9%
  • JavaScript 0.6%
  • Makefile 0.3%
  • Other 0.2%
Find a file
Prasun Anand 07d5fba3c0
fix(tabs): a project's tabs are restored once the server has said which project it is (#137)
Starting Zasper in a new folder logged "could not dial: zmq4: could not dial to tcp://127.0.0.1:…:
operation was canceled" right after the banner. The strip was seeded at module load from the project
written last, so that it could be painted before any request went out, and swapped for this project's
own once /api/info answered. But the seeded front tab mounted at once, and when it was a notebook it
created a session and started a kernel for its path in the new folder. When /api/info named a
different directory the swap unmounted it, the kernel was stopped while its sockets were still
connecting, and the cancelled dial was what reached the log. Where the path happened to exist here as
well, the wrong file was opened for a moment too.

The strip now starts as the Launcher alone and useRememberTabs restores this project's strip on its
first run with a directory, the way useRememberRecentFiles already reads recent files. A tab someone
opens in the moment before the directory arrives is kept rather than replaced. readStoredTabs takes
the directory it is asked about, and the record's "last" field, which existed only for the seed, is no
longer written or required; version 3 has not shipped, 1.1.0 wrote version 2, so nothing out there
depends on it.

tabSeed.test.ts now asserts the seed is the Launcher alone with a strip remembered, which fails against
the old seed, and the ordering and load cases moved onto rememberedGroups. The restore tests no longer
mock rememberedDirectory, which is gone, and cover the strip staying the Launcher until the directory
is known.
2026-09-22 20:36:37 +05:30
.github go tests gate on windows (#132) 2026-09-18 10:34:19 +05:30
assets fix: update README link for self-hosting instructions 2026-09-11 20:32:40 +05:30
bin Keep bin directory 2025-05-17 22:48:43 +05:30
docker code organisation, phase six: update GitHub Actions workflows and dependencies 2026-09-15 00:25:42 +05:30
docs Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
e2e Refactor comments to remove story references and improve clarity across various components 2026-09-21 15:26:29 +05:30
internal Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
resources feat(desktop): implement macOS desktop app with server integration and build process (#133) 2026-09-22 18:33:01 +05:30
scripts Release 2.0.0 (#135) 2026-09-22 19:30:01 +05:30
snap Release 2.0.0 (#135) 2026-09-22 19:30:01 +05:30
ui fix(tabs): a project's tabs are restored once the server has said which project it is (#137) 2026-09-22 20:36:37 +05:30
.dockerignore feat(analytics): add tests for PostHog API key fallback and injection 2026-09-10 12:02:08 +05:30
.gitattributes go tests gate on windows (#132) 2026-09-18 10:34:19 +05:30
.gitignore refactor: Revamp login page layout and functionality 2026-09-11 17:01:56 +05:30
.goreleaser.yml code organisation, phase six: update GitHub Actions workflows and dependencies 2026-09-15 00:25:42 +05:30
.nvmrc feat: add .nvmrc for Node.js version management and update CONTRIBUTING.md and README.md for improved setup instructions 2026-09-14 09:07:02 +05:30
app.go Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
app_test.go Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
browser.go feat: add --no-browser option to prevent automatic browser launch on startup 2026-09-11 18:16:38 +05:30
browser_test.go feat: add --no-browser option to prevent automatic browser launch on startup 2026-09-11 18:16:38 +05:30
CHANGELOG.md Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
CLA.md feat: update license references to AGPL-3.0-only, add Contributor License Agreement, and implement CLA workflow 2026-09-11 19:56:02 +05:30
cliff.toml feat(release): update version to 1.0.0 and enhance changelog process (#128) 2026-09-10 13:37:05 +05:30
CODE_OF_CONDUCT.md Create CODE_OF_CONDUCT.md 2024-09-20 11:44:32 +05:30
CONTRIBUTING.md feat: update jotai to version 2.20.3 and adjust related types 2026-09-14 21:30:52 +05:30
desktop.go Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
go.mod feat(desktop): implement macOS desktop app with server integration and build process (#133) 2026-09-22 18:33:01 +05:30
go.sum feat(desktop): implement macOS desktop app with server integration and build process (#133) 2026-09-22 18:33:01 +05:30
LICENSE Update License 2024-12-19 21:53:11 +05:30
main_cli.go Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
Makefile feat(desktop): implement macOS desktop app with server integration and build process (#133) 2026-09-22 18:33:01 +05:30
PRIVACY.md feat(banner): improve banner display with color support and update privacy notice 2026-09-21 16:08:47 +05:30
PUBLISHING.md docs: the checksums file by the name releases actually give it (#131) 2026-09-14 10:17:28 +05:30
README.md Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
serve.go Realease 2.0.0. fix: Let a loopback server answer to a reverse proxy's host name (#136) 2026-09-22 20:14:28 +05:30
spa.go Refactor API responses and improve terminal handling 2026-09-14 20:51:58 +05:30
spa_apiserver.go Refactor API responses and improve terminal handling 2026-09-14 20:51:58 +05:30
spa_handler.go Refactor API responses and improve terminal handling 2026-09-14 20:51:58 +05:30
spa_handler_test.go Refactor API responses and improve terminal handling 2026-09-14 20:51:58 +05:30
version.txt Release 2.0.0 (#135) 2026-09-22 19:30:01 +05:30

Zasper

High Performance IDE for Jupyter notebooks.
Any Jupyter kernel, one static binary, a fraction of JupyterLab's resource use.

Latest release Build status conda-forge License: AGPL-3.0-only

Documentation · Downloads · Installation · Slack

Zasper running a Jupyter notebook

Overview

Zasper is an IDE for Jupyter notebooks, designed from the ground up for concurrency and a small footprint. It implements Jupyter's wire protocol, so it runs any Jupyter kernel, and it reads and writes the .ipynb format directly, so notebooks move between Zasper and JupyterLab unchanged.

Zasper is a single static binary with no runtime dependencies. It serves its interface to your browser, on your own machine or on a server you share.

  • Notebooks on any Jupyter kernel, with Plotly, ipywidgets, HTML and Markdown with LaTeX rendered inline. A save is byte-for-byte what Jupyter would have written.
  • Editor, terminal and version control in the same window as your notebooks.
  • Command palette that finds files and every command in one search.
  • Light and dark themes, and window zoom.
  • Self-hosting built in: every session is protected by an access token.
More screenshots

Editor

Editor

Terminal

Terminal

Launcher

Launcher

Version control

Version control

Command palette

Command palette

Dark theme

Dark theme

Notebook in the dark theme

Contents

Performance

In benchmarks against JupyterLab, Zasper uses:

  • up to 5× less CPU,
  • up to 40× less memory,

with higher throughput and lower latency, and it stays responsive under very high load.

Resource use, Zasper compared with JupyterLab

The methodology and full results are in the benchmark report.

Installation

Current release version: v2.0.0

Platform Support
macOS Supported
Linux Supported
Windows Binaries are published and notebooks run, but terminals are not available yet and some kernel paths are less well exercised. For terminals and the best experience, use WSL.

Requirements. Zasper runs notebooks on Jupyter kernels but does not install one: pip install ipykernel is enough to start (see Jupyter kernels). You also need a modern browser, since Zasper serves its interface locally rather than being a separate desktop application. Without a kernel, Zasper still starts and the Launcher tells you what to install.

Homebrew

brew tap zasper-io/tap
brew trust zasper-io/tap
brew install zasper-io/tap/zasper

Homebrew 6 loads nothing from a third-party tap until you trust it, which is what brew trust records. If you installed 0.x through Homebrew, run brew uninstall zasper first: from 1.0, Zasper is published as a cask rather than a formula.

Snap

sudo snap install zasper

Get it from the Snap Store

conda

conda install -c conda-forge zasper

Prebuilt binaries

Every release ships a signed, notarized macOS build and static binaries for Linux and Windows. The Linux archives are plain tarballs: one build serves every distribution. There is no .deb or .rpm yet.

Platform Architecture Archive
macOS Apple Silicon zasper-webapp-<version>-darwin-arm64.tar.gz
macOS Intel zasper-webapp-<version>-darwin-amd64.tar.gz
Linux x86-64 zasper-webapp-<version>-linux-amd64.tar.gz
Linux ARM64 zasper-webapp-<version>-linux-arm64.tar.gz
Linux i386 zasper-webapp-<version>-linux-386.tar.gz
Windows x86-64 zasper-webapp-<version>-windows-amd64.zip
Windows ARM64 zasper-webapp-<version>-windows-arm64.zip
Windows i386 zasper-webapp-<version>-windows-386.zip

Each release carries a zasper_<version>_checksums.txt. Verify a download with sha256sum -c zasper_<version>_checksums.txt --ignore-missing. The same archives are listed on the downloads page.

Docker

An image can be built from docker/; see docker/README.md.

Getting started

Run zasper in the directory you want to work in:

$ zasper
==========================================================
     ███████╗ █████╗ ███████╗██████╗ ███████╗██████╗
     ╚══███╔╝██╔══██╗██╔════╝██╔══██╗██╔════╝██╔══██╗
       ███╔╝ ███████║███████╗██████╔╝█████╗  ██████╔╝
      ███╔╝  ██╔══██║╚════██║██╔═══╝ ██╔══╝  ██╔══██╗
     ███████╗██║  ██║███████║██║     ███████╗██║  ██║
     ╚══════╝╚═╝  ╚═╝╚══════╝╚═╝     ╚══════╝╚═╝  ╚═╝

                    Zasper Server
                Version: 1.0.0
----------------------------------------------------------
 ✅ Server started successfully!
 📡 Bound to:            127.0.0.1:8048
 🖥️  Webapp available at: http://127.0.0.1:8048
 🔐 Server Access Token: 14be1b674a3b9196a82c01129028d0dd
 🔗 Sign in with:        http://127.0.0.1:8048/?token=14be1b674a3b9196a82c01129028d0dd
==========================================================

Zasper opens the Sign in with link in your default browser, so you arrive already signed in. It does this on macOS and Windows, and on Linux when there is a display. Pass --no-browser to leave the browser closed, for example on a machine you reach over SSH, and open the link yourself.

Self-hosting

Zasper always runs in protected mode: every route except the health check needs a session, and a session comes from the access token printed at startup.

1. Start the server

zasper --host=0.0.0.0 --no-browser

--host makes the server reachable from other machines. Zasper binds 127.0.0.1 by default; widen it only when you mean to. Bound to 127.0.0.1, it answers only to localhost, 127.0.0.1 and [::1], which stops a web page that points its own domain at your machine from reaching it. Behind a reverse proxy on the same machine, keep the default and name the proxy's host instead: --allow-host=zasper.example.com. With --host=0.0.0.0 the banner reports the wider binding:

 📡 Bound to:            0.0.0.0:8048
 🖥️ Webapp available at: http://localhost:8048
 🔐 Server Access Token: 14be1b674a3b9196a82c01129028d0dd
 🔗 Sign in with:        http://localhost:8048/?token=14be1b674a3b9196a82c01129028d0dd

2. Sign in

On your own machine there is nothing to do: Zasper opens your browser already signed in. Otherwise, sign in either way:

  • With the link. Open the Sign in with link from the banner. It signs you in straight away, and the page removes the token from the address bar as soon as it has used it. Treat the link like a password.
  • With the token. Open Zasper in a browser, which takes you to the sign-in page, and paste the Server Access Token there.

The banner's links say localhost, because they are written for a browser on the server itself. From another machine, replace localhost with the server's hostname or IP address, for example http://my-server:8048/?token=….

When Zasper's output is not a terminal (under Docker, systemd, or piped to a file), it logs one JSON line instead of the banner; the token is that line's access_token field. ZASPER_LOG_FORMAT=console brings the banner back.

A session lasts 24 hours, after which you sign in again. It is an HttpOnly cookie, so no script running in the page can read it, and Sign out ends it on the server, so a copy of it stops working too. A client that is not a browser can send the token from /auth/login's answer as Authorization: Bearer … instead. After ten wrong tokens in a minute, sign-in from that address waits out the rest of the minute.

The Zasper sign-in page

3. Keep the token across restarts (optional)

A new access token is generated every time the server starts, and open sessions are signed with a key derived from it — so a restart both changes what you type and signs everyone out. Set the token yourself to keep a link you have handed out working, and to leave signed-in browsers signed in:

export ZASPER_ACCESS_TOKEN=your-access-token

There is nothing else to configure: sessions follow the access token, and changing it signs everyone out on purpose. Sign-outs are remembered only while the server runs, so with the token pinned, a session signed out before a restart works again after it until its 24 hours are up.

The file browser and the editor follow symbolic links, including ones that lead out of the project, so a data folder linked to a large mount opens like any other. It also means a project containing a link to ~/.ssh shows those files, as a terminal in the project would. Saving over a file that is itself a link out of the project is refused.

Jupyter kernels

Zasper works with any kernel that implements the Jupyter protocol, including:

  • Python, in any virtual environment, conda environment or uv project
  • R (IRkernel)
  • Julia (IJulia)
  • Ruby (IRuby)
  • JavaScript and TypeScript (Deno)
  • Go (GoNB)

To see the kernels installed on your machine, run jupyter kernelspec list. The quickest way to get a Python kernel is:

pip install ipykernel

conda environments

conda create --name my-env -c conda-forge ipykernel
conda activate my-env
python -m ipykernel install --user --name=my-env

uv projects

uv init my-project && cd my-project
uv add ipykernel
uv run python -m ipykernel install --user --name=my-project

Once registered, the environment appears in the Launcher and in each notebook's kernel picker.

Language servers

Errors as you type, completion, hover, go to definition and formatting come from a language server you already have installed. For Python that is basedpyright, pyright or pylsp — whichever is found first, in that order — and for Go gopls, with seven languages served in all. Zasper ships none of them and starts the one it finds, in PATH and in the usual per-project places. The status bar item at the right names the server, its state and, for Python, the interpreter it reads imports with; its menu restarts it, shows its log, or names the command to install one.

Notebook cells are served too: the cells are given to the server as one document, IPython's magics and ! escapes are hidden from it, and the interpreter it reads imports with is the one the notebook's kernel runs.

Python type checking is off by default, as it is in VS Code: undefined names and imports that cannot be found are still reported, and nothing else is. Raise it under Settings → Language servers when you want a type checker's opinion on code you maintain.

See docs/LANGUAGE-SERVERS.md for those modes, what each server is told, and what to do when a correct line is squiggled.

Notebook compatibility

Zasper reads and writes the .ipynb format directly. Two guarantees are worth stating outright:

  • A save produces no spurious diff. A notebook Zasper opens and saves comes out byte-for-byte the file Jupyter would have written, so saving does not manufacture merge conflicts.
  • A file keeps its own nbformat minor version. A 4.2 notebook is written back as 4.2 rather than silently upgraded to the newest revision, which is what JupyterLab does. Notebooks in formats 2 and 3 are converted to 4.5 on read.

What renders

Output Status
text/plain, stdout/stderr, tracebacks
text/html
image/png
Plotly figures (application/vnd.plotly.v1+json)
ipywidgets (application/vnd.jupyter.widget-view+json)
application/json
Markdown cells: GFM tables, task lists, raw HTML, LaTeX via KaTeX
image/svg+xml
text/latex
image/jpeg

text/latex — what SymPy emits under init_printing() — is typeset with KaTeX, the same renderer markdown cells use. image/svg+xml, which graphviz and networkx produce and which matplotlib produces under %config InlineBackend.figure_format = 'svg', is inlined rather than wrapped in an <img>, so a figure that sizes itself to the cell still can.

Known limitations

  • Widget state is not written into the notebook. Closing and reopening a tab is fine: the kernel keeps running, and the widgets redraw from it. But the state lives only in the kernel, so once that is gone — after the server restarts, or when the file is opened anywhere else, such as on GitHub — the widget shows a placeholder until the cell is run again. JupyterLab can save widget state into the file so it renders without a kernel; Zasper does not write it yet.
  • Notebooks are not signed or trusted. Zasper does not yet implement Jupyter's signature database, and stored text/html output can carry scripts that run when the notebook is opened, which is how Plotly and Bokeh outputs draw themselves. Treat a notebook you did not write the way you would treat any downloaded file.

Keyboard shortcuts

Every action listed here is also in the command palette (⇧⌘P / Ctrl+Shift+P), which shows each command's chord beside it, so the palette rather than this table is the thing to reach for when you have forgotten one.

Where a row gives two chords for macOS, both work: is the usual editor convention, and is what Zasper was bound to first.

Global

Action macOS Windows / Linux
Show All Commands ⇧⌘P or ⌃⇧P Ctrl+Shift+P
Go to File ⇧⌘O or ⌃⇧O Ctrl+Shift+O
Zoom In ⌘= or ⌘+ Ctrl+= or Ctrl++
Zoom Out ⌘- Ctrl+-
Reset Zoom ⌘0 Ctrl+0

Zoom scales the whole window, chrome included. To change only the size of code, terminal text and cell output, use Increase / Decrease Font Size from the palette; those have no chord.

Notebook

Action macOS Windows / Linux
Save Notebook ⌘S or ⌃S Ctrl+S
Run Cell ⌃⏎ Ctrl+Enter
Run Cell and Select Next ⇧⏎ Shift+Enter
Insert Cell Above ⌃⇧A Ctrl+Shift+A
Insert Cell Below ⌃⇧B Ctrl+Shift+B
Move Cell Up ⌃⇧↑ Ctrl+Shift+Up
Move Cell Down ⌃⇧↓ Ctrl+Shift+Down
Delete Cell ⌃⇧D Ctrl+Shift+D
Undo Cell Operation ⇧⌘Z Ctrl+Shift+Z
Change Cell to Code ⌃⇧Y Ctrl+Shift+Y
Change Cell to Markdown ⌃⇧M Ctrl+Shift+M

Cell operations are on ⌃⇧ rather than a bare on purpose: ⌃A, ⌃B, ⌃E and ⌃K are the system text-editing bindings on macOS, and Ctrl+A is select-all everywhere else, so a bare chord would be swallowed before the cell's editor saw it.

Undo Cell Operation is the notebook's own history: it takes back an inserted, deleted, cut, pasted or retyped cell, and a cleared output. ⌘Z inside a cell is the editor's, and still undoes the text you typed there.

Move Cell Up / Down reorders the notebook and is not the same as Select Next / Previous Cell, which only moves the selection. The cell's hover toolbar has both pairs: chevrons move the selection, arrows move the cell. The focus travels with the cell, so the chord can be held to carry one cell several places.

These have no chord and live in the palette: Run All Cells, Cut / Copy / Paste Cell, Select Next / Previous Cell, Change Cell to Raw, Expand or Collapse Output, Clear Cell Output, Clear All Outputs, Interrupt Kernel, Restart Kernel, Restart Kernel and Run All Cells, Reconnect to Kernel and Change Kernel. The notebook toolbar and a cell's hover toolbar reach most of them in one click.

Inside a cell

These belong to the editor rather than to a command, because what they do depends on where the cursor is.

Key What it does
Tab Accepts the highlighted completion; with a word to the left of the cursor, asks the kernel for completions; otherwise indents
on the first line, on the last Moves to the cell above or below
Double-click, or Enter on a selected markdown cell Opens its source for editing
Escape, or running the cell, in a markdown cell Renders it again

A single click on a rendered markdown cell only selects it; it stays rendered.

Configuration

Command-line flags

Flag Default Description
--cwd . Base directory of the project
--host 127.0.0.1 Interface to bind; 0.0.0.0 puts the server on the network
--port :8048 Port to start the server on
--allow-host Other host names a server on loopback answers to, comma-separated, such as a reverse proxy's
--no-browser off Do not open the app in a browser on startup
--tracking true Send anonymous usage data; see Logging and privacy
--debug off Set the log level to debug
--version Print the version and exit

--protected is still accepted but ignored: Zasper always runs in protected mode.

Environment variables

Variable Description
ZASPER_ACCESS_TOKEN A fixed access token, instead of a new one on every start; open sessions survive a restart when it is set
ZASPER_ALLOWED_HOSTS Host names added to --allow-host's, comma-separated
ZASPER_TELEMETRY 0 or 1 to turn anonymous usage data off or on for this run
ZASPER_LOG_FORMAT json or console; by default, console on a terminal and JSON otherwise
ZASPER_ACCESS_LOG 1 to log every request, not only the ones that failed

Logging and privacy

The server writes logs to standard output. Run it with --debug to raise the log level.

Zasper sends a small amount of anonymous usage data: counts of things like notebooks opened, cells run and terminals started. It never sends file names, paths, code, project names, your username or your IP address, and there is no session recording or autocapture. PRIVACY.md lists every event and every property, and explains how the allowlist that enforces it works. To turn tracking off:

zasper --tracking=false     # this run
ZASPER_TELEMETRY=0 zasper   # this run, from the environment

Or clear Settings → Privacy → Send anonymous usage data, which is remembered. With tracking off nothing is collected and no request is made.

Architecture

Zasper architecture

Building from source

Requires Go 1.26+ and Node.js 22.12+ (.nvmrc pins the Node version, so nvm use picks it up).

git clone https://github.com/zasper-io/zasper
cd zasper
make install   # install the frontend's dependencies, build, and install the zasper binary

make install puts zasper in your Go binary directory, so make sure that is on your PATH, then check the install with zasper --version. make build builds the binary in the repository instead, make dev runs the frontend and backend in development mode, and make test runs both test suites. make on its own lists the targets. CONTRIBUTING.md covers the development workflow.

Roadmap

  • Rendering for the outputs listed as not yet under What renders, first after 1.0.
  • Support for data apps, beyond Jupyter notebooks.
  • Easier integration with existing tools.
  • Zasper Hub, for self-hosted deployment in the cloud.

Documentation

  • zasper.io/docs: the user documentation.
  • CHANGELOG.md: what changed in each release.
  • docs/API.md: the HTTP and WebSocket API, covered by semantic versioning from 1.0.0 onwards.
  • docs/LANGUAGE-SERVERS.md: which language servers Zasper starts, what they are told, and what they give a notebook that a file does not get.
  • PRIVACY.md: what anonymous usage data is collected, event by event.
  • PUBLISHING.md: how releases are cut.

Community and support

  • Questions and discussion: join the Zasper community on Slack.
  • Bugs and feature requests: open an issue.
  • Sponsorship: support Zasper's development through GitHub Sponsors.

Acknowledgements

Zasper's development has been supported by a grant from FOSS United and Zerodha.

FOSS United      Zerodha

Zasper would not exist without the Jupyter community. It uses the Jupyter wire protocol and draws on Jupyter's architecture, and our thanks go to everyone who has built that foundation.

Contributing

Contributions of every kind are welcome: bug reports, documentation, and pull requests or reviews of them. CONTRIBUTING.md explains how to build Zasper and submit changes. Before your first pull request is merged, you will be asked to sign the Contributor License Agreement. Everyone taking part is expected to follow the Code of Conduct.

Contributors

License

Copyright © 20242026 Prasun Anand.

Zasper is dual-licensed. You can use it under either of these:

  • Open source: the GNU Affero General Public License, version 3 only (AGPL-3.0-only). See LICENSE. Running Zasper, on your own machine or on a server, needs nothing more. If you distribute Zasper, or let people use a modified version over a network, the AGPL requires you to make the corresponding source, including your changes, available under the same license.
  • Commercial: for organizations that want to build Zasper into a proprietary product, keep their modifications private, or need terms the AGPL does not offer. Write to prasun@zasper.io.

Contributions are accepted under the Contributor License Agreement, which is what lets Zasper be offered under both licenses.