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.
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.
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 hubEach is installed independently; per-app ✓ / ⊘ / ✗ status, exit non-zero if any failed.
The install needs write access on the target pod. Two paths:
jss install chrome --user me --password me
# or via env (keeps the secret out of shell history):
JSS_SINGLE_USER_PASSWORD=secret jss install chromePOST <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.
jss install chrome --nostr-privkey <64-hex>
# or via env:
NOSTR_PRIVKEY=<64-hex> jss install chromeEach 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 chromeThe target ACL 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.
jss install chrome --pod http://192.168.1.10:5544Default is http://localhost:4443. Auth flags apply against the chosen pod.
--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| 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.
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
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 starterBundles 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.jsonldACL-gated, version-controlled (if in git), pointable from a single URL.
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.
| Symptom | Cause | Fix |
|---|---|---|
✗ <name>: invalid app name "..." |
Spec doesn't match the validation regex | 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 |
A manual clone-and-push used --depth=1 (the install itself never does this) |
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 (/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 |
- Git Support — the substrate that powers
install jss installsource- solid-apps/bundles — curated bundle repo
- Phased plan (JSS#464) — design roadmap; Phases 3 (
--did) and 5 (curated no-arg default) still ahead