Installing Apps
JSS ships a built-in install subcommand that pulls a Solid app from a git repo and pushes it into your running pod at /public/apps/<name>/. One command, no clone-and-push dance, no token plumbing — the hard parts (git auto-init, ACL-gated push, working-tree extraction via updateInstead) are handled by the same git HTTP backend JSS already uses.
Quick start
jss start --provision-keys & # pod running on http://localhost:4443
jss install chrome # installs solid-apps/chrome → /public/apps/chrome/
Open http://localhost:4443/public/apps/chrome/ in a browser. That's it.
App specs
The argument to install accepts five forms:
| Input | Resolves to | Pod path |
|---|---|---|
chrome | github.com/solid-apps/chrome (default registry) | /public/apps/chrome/ |
JavaScriptSolidServer/git | github.com/JavaScriptSolidServer/git | /public/apps/git/ |
https://github.com/foo/bar | as-is | /public/apps/bar/ |
chrome#v1 | github.com/solid-apps/chrome at ref v1 | /public/apps/chrome/ |
litecut/litecut.github.io=litecut | github.com/litecut/litecut.github.io, renamed | /public/apps/litecut/ |
Two optional suffixes apply to any form:
#<branch-or-tag>— pin a ref. Usesgit clone --branch <ref>under the hood.=<name>— override the pod-path name. Useful when the repo's last segment isn't what you want under/public/apps/.
Multiple specs in one command:
jss install chrome vellum pdf hub
Each is installed independently; per-app ✓ / ⊘ / ✗ status, exit non-zero if any failed.
Authentication
The install needs write access on the target pod. Two paths:
Bearer token (default)
jss install chrome --user me --password me
# or via env (keeps the secret out of shell history):
JSS_SINGLE_USER_PASSWORD=secret jss install chrome
POST <pod>/idp/credentials returns a token, which is sent as Authorization: Bearer ... on each push.
If the pod runs in --public mode (no IDP), no token is fetched; writes are unauthenticated.
Nostr (NIP-98)
jss install chrome --nostr-privkey <64-hex>
# or via env:
NOSTR_PRIVKEY=<64-hex> jss install chrome
Each push is signed with a NIP-98 event (Schnorr signature on a kind: 27235 Nostr event). JSS verifies the signature, derives a did:nostr:<pubkey> identity, and runs WAC against that.
Pairs naturally with --provision-keys: the privkey JSS auto-generates at <pod>/private/privkey.jsonld is the natural source.
jss start --provision-keys &
PRIVKEY=$(jq -r .secretKeyMultibase pod-data/private/privkey.jsonld | sed 's/^f8126//')
NOSTR_PRIVKEY=$PRIVKEY jss install chrome
ACL on the target path must grant the corresponding pubkey:
<#owner> a acl:Authorization;
acl:agent <did:nostr:59427bb1...>;
acl:accessTo <./>;
acl:default <./>;
acl:mode acl:Read, acl:Write, acl:Control.
This is typically already true on a --provision-keys pod — JSS seeds the owner ACL to grant the provisioned key.
Targeting a different pod
jss install chrome --pod http://192.168.1.10:5544
Default is http://localhost:4443. Auth flags apply against the chosen pod.
Bundles
--bundle <source> installs a set of apps from a JSON-LD manifest. Same auth, same target, same per-app status.
jss install --bundle starter # solid-apps/bundles/HEAD/starter.jsonld
jss install --bundle media chrome # bundle + ad-hoc additions
jss install --bundle ./my-stack.jsonld # local file
jss install --bundle https://my.pod/bundles/dev.jsonld
Source resolution
| Input | Resolves to |
|---|---|
--bundle starter | https://raw.githubusercontent.com/solid-apps/bundles/HEAD/starter.jsonld |
--bundle <org>/<repo> | https://raw.githubusercontent.com/<org>/<repo>/HEAD/bundle.jsonld |
--bundle https://... | fetch as-is |
--bundle ./path.jsonld | local filesystem (absolute paths supported) |
/HEAD/ resolves to the repo's default branch — works for both gh-pages-default repos (solid-apps convention) and main-default repos.
Bundle format
JSON-LD schema:ItemList:
{
"@context": { "schema": "https://schema.org/", "app": "urn:jss:app:" },
"@id": "#bundle",
"@type": "schema:ItemList",
"schema:name": "Starter",
"schema:description": "Minimal pleasant first-run set",
"schema:itemListElement": [
"chrome",
"vellum",
{ "app:spec": "litecut/litecut.github.io=litecut", "app:label": "Litecut" }
]
}
Each item is either:
- A bare string — any spec
jss installaccepts - An object — required
app:spec, optionalapp:label/app:descriptionfor UI tooling
Curated bundles
The solid-apps/bundles repo hosts ready-made bundles:
| Bundle | Apps |
|---|---|
starter | chrome, vellum, pdf, alarm |
all | chrome, vellum, win98, pdf, hub, alarm, playlist |
media | playlist, pdf |
productivity | vellum, hub, win98 |
jss install --bundle starter
Sharing custom bundles
Bundles are JSON-LD documents — they live anywhere a JSON-LD doc can. Host yours on your pod, in a GitHub repo, or any static server:
jss install --bundle https://my.pod/bundles/dev-stack.jsonld
ACL-gated, version-controlled (if in git), pointable from a single URL. The Linux-distribution analogy is apt: apt install task-server becomes jss install --bundle task-server, but the manifests are sharable Solid resources instead of fixed-path config files.
How it works
Under the hood, jss install <name> is:
- Resolve the spec to a source URL (
github.com/solid-apps/chromefor bare names). - Authenticate. Fetch a bearer token from
<pod>/idp/credentials, OR build a NIP-98 signed event if--nostr-privkeyis set. Skipped entirely if the pod is in--publicmode. - Clone the repo to a temp directory. Full clone — no
--depth, because shallow pushes are rejected bygit-receive-pack. - Dual push to
<pod>/public/apps/<name>on bothHEAD:mainandHEAD:gh-pages. JSS auto-inits the destination repo, and whichever ref matches the server-side HEAD triggersreceive.denyCurrentBranch updateInsteadto extract the working tree onto disk where JSS serves it as static resources. The other ref is a harmless stranded reference. - Clean up the temp directory.
Idempotent on re-run. Skip-on-existing-non-repo paths (e.g. jspod's bundled pilot) report a friendly ⊘ skipped instead of an error.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
✗ <name>: invalid app name "..." | Spec doesn't match /^[a-z0-9][a-z0-9_.-]*$/i (bare-name form) or <org>/<repo> | Check the spec; review the App specs table |
✗ <name>: clone failed: Repository not found | The repo doesn't exist at the resolved URL | Verify the source — github.com/solid-apps/<name> for bare names |
✗ <name>: push failed: shallow update not allowed | Shouldn't happen with this tool — but the symptom on a manual clone-and-push is using --depth=1 | Remove --depth from the clone |
✗ <name>: push failed: HTTP 401 | Auth failed | Check --user / --password; for Nostr, check that the ACL grants the pubkey |
✗ <name>: push failed: HTTP 413 | Body exceeds JSS's bodyLimit (10 MB) | Tracked as JSS#474. Workaround: install a smaller repo or push a no-history snapshot |
Working tree empty after push (/public/apps/<name>/index.html returns 404) | Server-side HEAD doesn't match the pushed branch | JSS 0.0.197+ pins -b main on auto-init; upgrade if you see this |
⊘ <name>: skipped (path already in use) | The target path has content but no .git/ (e.g. jspod's bundled pilot) | Expected — JSS refuses to clobber non-repo content |
See also
- Git Integration — the substrate that powers
install jss install --help— the source- solid-apps/bundles — curated bundle repo
- Phased plan (JSS#464) — the design roadmap; Phases 3 (
--did) and 5 (curated no-arg default) still ahead