Lesson 32 — Distributing packages: publish, install, update, fork
Goal: by the end of this lesson you can publish a namespace as an immutable package version, then browse the registry and install / update / roll back / uninstall / fork packages — all without leaving the graph. Placement follows intent: install is a build act, so it lives on the Build surface's packages context-bar chip; publish is an authoring act on what you built, so it lives as a ⬆ action on the namespace in the Explorer.
Concepts introduced: registry, :package-version,
publish, pin (:package-install), reference-install vs
fork (copy-on-write), version constraint / latest /
rollback, withdraw, the packages chip (Build surface) and the
per-namespace ⬆ publish action, publish visibility
(org-private vs public) and the publish-packages capability,
and the Organization surface's governance view.
Authoring vs distributing
Lesson 31 was about authoring a package — the fns.edn /
impls.clj / package.edn files on disk that load at startup.
This lesson is about distributing one: taking a namespace
that already lives in the graph and turning it into a versioned,
installable artifact other branches (and, later, other people)
can pull in.
Two different things share the word "package":
| On disk (Lesson 31) | In the registry (this lesson) |
|---|---|
| A directory loaded at boot | A :package-version row: an immutable snapshot of a namespace's fn-defs |
| One copy, shared by the whole install | Named + semver-versioned; many versions coexist |
| Changes when you edit the files + rebuild | Frozen once published — re-publishing the same (name, version) is rejected |
Publish — freeze a namespace into the registry
Publishing exports the fn-def subtree rooted at a namespace and
stores it as a :package-version. Take a mycorp namespace holding
one fn, :greet (a namespace you make in the Explorer; Lesson 31's
on-disk package gives you mycorp.hello, which works the same way):
curl -X POST http://localhost:9002/api/packages/publish \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"mycorp-hello","version":"1.0.0","ns-root":"mycorp"}'
# → {"ok":true,"name":"mycorp-hello","version":"1.0.0","fn-count":1,...}
ns-root is the namespace to snapshot; name + version are
how the registry indexes it. Change :greet and publish again as
1.1.0 — now the registry holds both versions.
GET /api/packages lists the index.
The version number is checked, not just recorded. Before writing the
row, publish diffs the bundle against the newest version already
below the one you named: a public fn-def that disappeared, an arg
that is gone or newly required or narrower, a binding you dropped
(so callers must now supply that arg), a wider return type. If the
bundle breaks 1.1.0 and you called it 1.2.0, the answer is
{"ok":false,"reason":"breaking-change","previous":"1.1.0","changes":[…]}
with each change spelled out (kind, fn, arg, old, new), and
nothing is written. Call it 2.0.0 and it publishes — a break has to
leave the previous version's ^ range, so a consumer pinned with
^1.1 never auto-advances into it. Adding fn-defs, optional args or
wider types is compatible and publishes under any higher version.
You can also publish from the editor — and because publishing
is an authoring act on a namespace, it lives on the namespace.
In the Explorer, hover a namespace row and click its ⬆ button
(next to rename / add / hide). A small popover opens with the
package name (pre-filled from the namespace's last segment) and
a version; the namespace itself is the ns-root. Click
Publish — the same export-and-freeze step, no curl needed, and
it confirms with the published fn-count. The curl above is the
programmatic equivalent for scripts / CI. Either way, publishing is
a deliberate "author decides to release" action; installing happens
on the packages chip below.
(The ⬆ action shows only when the optional registry package
is installed on the deployment, only when you're signed in — and,
on a multi-tenant cloud, only when you hold the
publish-packages org capability. The org owner always holds
it; grant it to other members from Roles or Grants on the
Organization surface. On a self-hosted single-tenant install there
is no capability system, so publishing is simply open.)
Who sees what you publish — visibility
On a multi-tenant cloud, a published version is private to your
organization by default: it lands in the registry stamped with
your org, and only your org's members see it when browsing. The ⬆
popover has a "Public — visible outside your organization"
checkbox for the explicit opt-in. When moderation is enabled, a public
version waits for operator approval before other organizations see it;
your own org can use it while pending. See
Lesson 40. In the packages chip's browse list, private
versions carry a private badge. (The programmatic
equivalent: "public": true in the publish JSON body.)
On a single-tenant install there is no org boundary to be private within — the checkbox isn't shown, and every published version is visible to every user.
The packages chip — browse & install
Installing is a build act — you're adding a building block to your project — so it lives with the other project-context chips on the Build surface, not on an admin page. In the context bar (top of the Build surface, alongside the workspace and branch chips) click packages. A popover opens:
Packages
Package Version
mycorp-hello 1.0.0 [1.0.0 ↑] × ← installed pins on THIS branch
▾ + Install a package ← native <details>, click to open
mycorp-hello 1.0.0 [Install] [Fork] ← the registry index
mycorp-hello 1.1.0 [Install] [Fork]
The top table is what's installed on the current branch
(remember branches from Lesson 23 — pins are
per-branch, so dev and prod can run different versions). The <details> below it
is the registry — every published version, with an action
per row. (The packages chip appears only when the optional
registry package is installed on the deployment.)
Install — by reference, not by copy
Click Install next to mycorp-hello 1.1.0. Graphden:
- Materializes the version's fns under a version-qualified
namespace —
mycorp@1-1-0(dots in the version become dashes). Idempotent: a second install is a no-op. - Writes a pin — a
:package-installrow saying "this branch usesmycorp-helloat1.1.0". The pin, plus the visible materialized fns, IS the install.
Nothing is copied into your own namespaces — you reference
mycorp@1-1-0. The panel refreshes to show the new pin in
the installed table.
Update / rollback — repoint the pin, rewrite your refs
Each installed row has a version input prefilled with the current
version and an ↑ button. Type a different version and click
↑:
1.1.0→ forward to a newer version.1.0.0→ rollback — the same button, an older version. The operation is symmetric.latestor a constraint like>=1.1— the highest published match is resolved.
Update doesn't just move the pin: it rewrites your project's own
references from the old version-qualified namespace to the new
one (so fns you built on top of mycorp@1-0-0 now point at
@1-1-0). Package-internal refs are left alone. Same version =
no-op.
Fork — copy-on-write when you want to edit
Install references fns you can't change (they're the
package's). When you want to modify a package, click Fork
instead. Fork copies the version's fns into the graph at
their original namespace (mycorp, not the versioned one), so
they become ordinary editable fn-defs — and writes no
pin (it's a copy, not a reference). A short notice confirms it;
reload to see the copied fns in the explorer tree.
When Fork refuses. The copy lands at the original namespace on
the same deterministic ids the package loader would use. If that
namespace is a package synced from disk on this instance
(Lesson 31), those ids are the platform's own: the "copy" would land
on rows the editor keeps read-only, and the next boot's sync would
overwrite it. So the fork is refused —
{"ok":false,"reason":"package-owned","owned":["greet", …]} naming
the fns, and the panel shows the same as a notice. A built-in
package is changed in its fns.edn; Fork is for a package that
arrived through the registry — the case
Lesson 41 walks end to end.
| Install | Fork | |
|---|---|---|
| Rows | Referenced (shared) | Copied (yours) |
| Namespace | ns@version (qualified) |
ns (original) |
| Editable? | No | Yes |
| Writes a pin? | Yes | No |
| Refused when | a dependency is missing | a dependency is missing, or the namespace is a built-in package here |
Uninstall
The × on an installed row drops the pin for this branch. The
materialized ns@version fns stay (another branch may still
reference them) — uninstall only removes this branch's claim on
the package. Remove the last pin and the table collapses to the
empty-state notice.
Withdraw — retract a published version
Uninstall is the consumer's act; withdraw is the publisher's. A published version is immutable, but not eternal — publishing the wrong namespace or the wrong number shouldn't be permanent. The publisher can delete the registry row:
curl -X DELETE "http://localhost:9002/api/packages/withdraw?name=mycorp-hello&version=1.0.0" \
-H "Authorization: Bearer $AUTH_TOKEN"
# → {"ok":true,"withdrawn":"mycorp-hello"}
Rules:
- Refused with 409
still-installedwhile any branch still pins the package at that version — a pin resolves through its version row, so withdrawing under it would break installs. Consumers unpin (uninstall or update away) first; a branch that already moved to1.1.0does not keep1.0.0alive. - 404
no-such-versionwhen the(name, version)pair is unknown. - Withdrawing is gated by the same
publish-packagescapability as publishing, and only on your own org's rows — a non-publisher can't erase what your org shipped. - The materialized
ns@versionfns are not removed — by then they're ordinary graph content, deleted like any other namespace if you want them gone too.
There's no panel button for this — withdraw is an API-only, deliberate act.
Governance — the Organization surface's packages view
Publish and install are everyday Build-surface acts; oversight lives on the Organization surface. Its packages section is a read-only governance view with three parts:
- a who-may-publish note — states the
publish-packagescapability rule and the default visibility on this deployment; - the catalog — every version your org published (Package / Version / Visibility / Published);
- the install audit — every pin: which package, at which version, on which branch, installed when.
It is deliberately not an install surface — there's no Install button here. Reviewing what your org ships and consumes happens on Organization; acting on it happens on the Build packages chip.
Try it
- At the bottom of the Explorer click New namespace and type
mycorp. Hover its row, click + → New graph…, name itgreet(parentconst,:valuebound to"Hello"). Hovermycorpagain, click ⬆: package namemycorp-hello, version1.0.0, Publish (or use thecurlabove). (If you built Lesson 31's on-disk package,mycorpalready exists — publish itsmycorp.hellonamespace instead and readmycorp.hello@…formycorp@…below.) - Change
:greet's value, then publish again as1.1.0(the ⬆ popover again). On a disk package that is afns.ednedit and abb rebuildfirst. - Open the packages chip in the Build context bar, expand
+ Install a package, click Install on
mycorp-hello 1.1.0. Watch the pin appear — andmycorp@1-1-0in the Explorer. - Type
1.0.0in the installed row's version box, click↑— you've rolled back. Type1.1.0,↑— forward again. - Fork is the other door, and not one to open here: the copy
lands at the original namespace on the same ids — the very
mycorprows you just published from — and on a namespace synced from disk it is refused outright (package-owned, § Fork above). Fork is for a package whose source is not on your branch; Lesson 41 plays that with two branches. - Click
×— the pin's gone. - Open the Organization surface's packages section — the
governance catalog lists both published
mycorp-helloversions with their visibility; the install audit shows a row per pin (re-install first if you removed the pin in step 6). - Withdraw
mycorp-hello 1.0.0with thecurlabove — it's unpinned, so the row disappears from the registry browse. Now try withdrawing1.1.0while its pin from step 7 exists: a 409still-installedrefusal. Uninstall first, and the withdraw goes through.
The Marketplace — browsing with more than a name
The chip's browse list is the quick door: every version, an Install button. Searching, filtering, ratings and install counts are the Marketplace surface (Browse marketplace → at the bottom of the chip) — lesson 40.
Installing from ANOTHER graphden's registry
The browse <details> has one more affordance under the local table:
a small form — registry URL / package name / version — that pulls
a published package from a different graphden (say, graphden.dev
into your self-hosted install). Behind the scenes the version is
mirrored into your local registry first (an immutable local copy,
never re-published as public), then installed exactly as above —
reference, pin, secrets manifest and all. If the remote registry
requires auth, the server presents its GRAPHDEN_REGISTRY_TOKEN — only
to the registry the operator named in GRAPHDEN_REGISTRY_URL, never to a
URL someone types into the form; the browser never handles that
credential. The copy also snapshots the
origin's marketplace numbers (rating, installs) and shows them read-only
— reviews stay where the package was published
(lesson 40).
Beyond the registry — an external package from its own git repo
Everything above lives inside one graphden install. A package that
ships its own base-fn impls is code and enters through the build
instead — pulled by git coord via executor-packages.edn:
Lesson 31 § An external package from its own git repo.
What we glossed over
- The programmatic API — every panel action has a JSON
sibling:
POST /api/packages/{install,update,fork}andGET /api/packages[/:name/:version]. The panel's own endpoints (/api/packages/panel-*) return refreshed HTML instead; the JSON ones return{ok, …}for scripts and CI. See docs/PACKAGE_DISTRIBUTION.md. - Dependencies — a published bundle records the external fn-names it depends on; install/fork reject if a dependency is absent from the target graph.
Next
Lesson 33 — Working across organizations. For the whole loop between a package's author and its consumers — fixing someone else's package, sending the fix back, accepting it and shipping the next version — jump to Lesson 41.