CLI tool
djblue/portal avatar
djblue/portal

Portal turns tap> output into a UI you can click through

A clojure tool to navigate through your data.

1,039 stars93 forksClojureMIT

At a glance

What is it?
Portal is a Clojure tool for navigating through data. It attaches to the standard tap> target, renders values in a separate window that can be launched inside VS Code, IntelliJ or Emacs, and can also run as a standalone server or a web build. Its one documented hazard is that it holds on to objects until you clear them.
Who is it for?
Adopt Portal when you work in Clojure or ClojureScript and want to look at a value rather than read a printed form, especially when the value is a large map, a lazy sequence or something you cannot print without flooding the terminal. Do not adopt it as a production dependency, because it is a development tool that keeps a reference to everything you submit, and it can hold objects out of garbage collection until the UI is cleared.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A tap target with a window attached to it

Portal is described as a Clojure tool to navigate through your data, and the mechanism is built on something Clojure already has. The standard library ships a tap mechanism, the tap> function, and anything that adds itself to the tap target set receives every value that gets tapped out. Portal adds itself as a tap target and renders those values in a UI you can click through.

The API makes the shape clear. You open an inspector, which returns an object, and you add portal's submit function to the tap target set. From that point, every tap> call in your REPL shows up in the Portal window, and the value stays there until you clear it.

The audience is Clojure developers working with data that is awkward to read in printed form. A map with forty keys, a lazy sequence that would print a thousand lines, a nested structure you want to walk into one key at a time. The point is not that the data is hard to print, it is that printing it is not the same as examining it.

The UI is a separate application surface. The README links an in-depth explanation of the UI concepts, and a live demo recording exists for London Clojurians, so the intended mental model is a window next to your editor rather than output in the terminal.

Starting a REPL with Portal takes one dependency declaration

The usage section covers six different starting points, which tells you how wide the support is for how people actually run Clojure.

For a Clojure REPL, the Clojure CLI of at least version 1.10.0 is used with a dependency override.

bash
clj -Sdeps '{:deps {djblue/portal {:mvn/version "0.68.1"}}}'

For babashka of at least 0.2.4, the shape is the same with a different binary.

bash
bb -Sdeps '{:deps {djblue/portal {:mvn/version "0.68.1"}}}'

Leiningen users add the dependency to a project.clj profile.

clojure
{:profiles {:dev {:dependencies [[djblue/portal "0.68.1"]]}}}

The same block can go into ~/.lein/profiles.clj as a global profile, and the documentation carries a warning about it that is easy to miss. If you add Portal to a profile other than :dev, you must start the REPL with with-profiles +portal, and the note says the plus sign is important. Without it, the profile is not active and Portal is simply not there.

There is also a Clojure CLR example project in the examples directory, and a set of examples showing how to integrate Portal into an existing project. The examples directory is large, and it covers a lot of ground: jvm, node, web, bb, clr, basilisp, nbb, remote, git, parse-and-tap, mulog, timbre, tufte, plotly-viewer, portal-present and more.

The API is six functions and one tap

The API file in the source tree is small enough to read in one sitting, and the README shows the whole surface.

clojure
(require '[portal.api :as p])

(def p (p/open))

(add-tap #'p/submit)

(tap> :hello)

(p/clear)

(tap> :world)

(prn @p)

(remove-tap #'p/submit)

(p/close)

The interesting line is prn @p, which brings the selected value back into the REPL. The atom returned by p/open holds the value you have selected in the UI, so the UI is not only a viewer, it is a way to pick a value and bring it back into code.

The rest of the surface is lifecycle. p/clear empties the view, remove-tap takes Portal out of the tap target set so that subsequent tap> calls no longer go there, and p/close closes the inspector. p/docs opens the documentation locally, which works on the JVM and on node but not on the web.

There is a second entry point for the browser. For a web build you require portal.web instead of portal.api, and the comment there warns that you might need to enable popups for the Portal UI to work in the browser. That is a browser restriction, not a Portal one, and it is the kind of detail that costs an afternoon if you meet it unprepared.

The launcher and editor options decide where the UI lives

The options table for portal.api/open is short, and two of the entries are about editors rather than about the data.

The launcher option chooses which editor the UI is launched in, with the allowed values being vs-code, intellij and emacs. The editor option enables editor commands while using a separate UI window, with the same three choices. The difference between them is whether the UI appears inside the editor or beside it.

That distinction is the most interesting design decision in the tool. A separate window gives you a large surface to navigate a big value, and it works the same in every editor. An embedded UI gives you the editor commands, the key bindings and the tight loop of an integrated inspector, and it depends on the editor extension being installed and working.

The repository carries the extensions for both major editors, an extension-intellij directory and an extension-vscode directory, plus doc/editors pages for vs-code, intellij and emacs, and the emacs documentation points at an xwidget webkit embed. The other entries are more conventional. The window-title option sets the title, the theme option defaults to portal.colors/nord, the value option sets the root value of the UI as an atom holding a list, the app option decides whether to launch in a Chrome app window and defaults to true, and port and host set the HTTP server for the UI, with port defaulting to 0 and host to localhost.

The port default of 0 means the operating system picks a free port, which is the right default for a development tool that may be running several times.

The standalone build is a Python server, not a Clojure one

There is a second distribution of the same tool, and it is not obvious from the fact that Portal is a Clojure project.

The project publishes a Python package named djblue.portal, with a pyproject file that requires Python 3.8 or newer and depends on aiohttp, asyncio and basilisp. The package description is the same one line, a Clojure tool to navigate through your data, and the project urls point at a Standalone link that is the hosted portal site. The requirement list is fully pinned, and the build backend is poetry-core.

So the standalone version is a server that hosts the Portal UI, and it can be run without a Clojure runtime at all. The documentation has a dedicated guide for it, in doc/guides/standalone.md, and the README notes that Portal can also be used without a runtime via the standalone version.

That is a meaningful architectural choice, because it decouples the UI from the language. A client can then submit values to it over the network, and the examples directory has a remote example, along with a git example, which suggests the intended pattern of inspecting something you are not running locally.

The web build sits alongside it as a third option, the ClojureScript one that needs popups enabled. Three ways to run the same inspector is unusual, and it is worth picking one deliberately rather than assuming the default.

The web UI is React, Vega and a Markdown renderer

The JavaScript side of the project is in package.json under the name @djblue/portal, and the dependency list explains what the UI is made of.

React and React DOM at version 19 are the rendering layer. The fontawesome packages provide the icons. For data presentation there is vega, vega-embed and vega-lite, so charts in the UI are Vega specifications rather than a bespoke chart widget. Text handling comes from marked, which is a Markdown renderer, and highlight.js, which does the syntax highlighting for code shown in the UI. papaparse handles CSV, and the diff package compares values, which is what lets the UI show what changed between two versions of a map. anser is there for formatted output, and idiomorph is a DOM morphing library, which is the technique for updating the UI in place rather than re-rendering it.

The dev dependencies show how the front end is built and tested. browserify is the bundler, nbb is the Node based ClojureScript environment, and ws provides the websocket server that the browser build talks to. The repository also has shadow-cljs.edn, nbb.edn, bb.edn and deps.edn, so there are several ClojureScript build paths, and a .clj-kondo directory for the linter configuration.

The result is a UI that is a normal modern web application, which matters for a tool that has to render arbitrary user data.

Portal holds your objects until you clear them

The README carries one warning in a blockquote, and it is the operational risk you have to plan around.

Portal will keep objects from being garbage collected until they are cleared from the UI.

The reason is the design. To render a value, Portal has to hold a reference to it, and holding a reference keeps it alive. A large map, a big lazy sequence or a cached result stays in memory for as long as it sits in the UI, and the collection of everything you have tapped during a session is the total. p/clear is the release valve, and the fact that it exists as an explicit call rather than a background sweep tells you the cost is real.

In practice this bites in a long REPL session where you tap values while chasing a bug and never clear. The symptom is memory that keeps growing while the data you care about gets smaller. The mitigation is to clear when the previous investigation is over.

The other limitation is a scope one. Portal is a development tool that shows you values, and the values it shows are the values your process holds. It is not a query tool, it is not a remote data browser for a service you are not connected to, and it does not persist anything between sessions. The examples directory shows the shape of what it is good at, with integrations for tap-based logging libraries and for plotting libraries, where a value that is already in memory becomes a chart you can look at.

Editorial conclusion

Adopt Portal when you work in Clojure or ClojureScript and want to look at a value rather than read a printed form, especially when the value is a large map, a lazy sequence or something you cannot print without flooding the terminal. Do not adopt it as a production dependency, because it is a development tool that keeps a reference to everything you submit, and it can hold objects out of garbage collection until the UI is cleared. Verify first which build you want, since the web version needs popups enabled and the standalone version needs Python with basilisp and aiohttp, and read the option table before opening the UI, because the launcher and editor options decide where the window appears.

Frequently asked questions

How do I start a Clojure REPL with Portal?

Run clj -Sdeps '{:deps {djblue/portal {:mvn/version "0.68.1"}}}' with the Clojure CLI of 1.10.0 or newer. For babashka the equivalent is bb -Sdeps with the same dependency map, and for Leiningen you add the dependency to a profile in project.clj.

What does the p/open API return and how do I use it?

p/open returns an object, and you add p/submit to the tap target set with add-tap. Values tapped with tap> then appear in the UI, prn @p brings the selected value back into the REPL, and p/clear, remove-tap and p/close manage the lifecycle.

Can I use Portal inside VS Code or IntelliJ?

Yes. The launcher option opens the UI in vs-code, intellij or emacs, and the editor option enables editor commands while keeping a separate UI. The repository ships the VS Code and IntelliJ extensions, and the documentation has per editor pages.

What happens to memory when I use Portal?

The README warns that Portal will keep objects from being garbage collected until they are cleared from the UI, so you should call p/clear when you finish with a set of values rather than leaving them in the inspector for a whole session.

Does Portal work in the browser or without Clojure?

There is a web build that uses the portal.web namespace and may need popups enabled in the browser, and there is a standalone version that runs without a Clojure runtime. The standalone build is published to PyPI as djblue.portal and depends on aiohttp, asyncio and basilisp.

What are the available options for portal.api/open?

The options include window-title, theme defaulting to portal.colors/nord, value, app defaulting to true for a Chrome app window, launcher and editor for vs-code, intellij and emacs, port defaulting to 0 and host defaulting to localhost.

Official sources

  1. djblue/portal on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/djblue-portal.svg)](https://hysenlabs.com/projects/djblue-portal)

Was this useful?