Package Exports
- javascript-solid-server
- javascript-solid-server/src/index.js
- javascript-solid-server/src/server.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (javascript-solid-server) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
JavaScript Solid Server
A minimal, fast, JSON-LD native Solid server.
Features
Implemented
- Live Reload - Auto-refresh browser on file changes (
--live-reload) - Read-Only Mode - Disable write operations for static hosting (
--read-only) - Public Mode - Skip WAC for open read/write access (
--public) - Schnorr SSO - Passwordless login via BIP-340 Schnorr signatures using NIP-07 browser extensions (Podkey, nos2x, Alby)
- Passkey Authentication - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
- HTTP Range Requests - Partial content delivery for large files and media streaming
- Single-User Mode - Simplified setup for personal pod servers
- ActivityPub Federation - Fediverse integration with WebFinger, inbox/outbox, HTTP signatures
- Mastodon-compatible API - Dynamic client registration, instance info, account verification
- OAuth 2.0 Authorization - Shared auth flow for Mastodon clients, remoteStorage apps, and third-party panes
- remoteStorage Protocol - draft-dejong-remotestorage-22 file sync (requires
--activitypubfor WebFinger discovery + OAuth) - LDP CRUD Operations - GET, PUT, POST, DELETE, HEAD
- N3 Patch - Solid's native patch format for RDF updates
- SPARQL Update - Standard SPARQL UPDATE protocol for PATCH
- Conditional Requests - If-Match/If-None-Match headers (304, 412)
- CLI & Config -
jsscommand with config file/env var support - SSL/TLS - HTTPS support with certificate configuration
- WebSocket Notifications - Real-time updates via solid-0.1 protocol (SolidOS compatible)
- Container Management - Create, list, and manage containers
- Multi-user Pods - Path-based (
/alice/) or subdomain-based (alice.example.com) - Subdomain Mode - XSS protection via origin isolation
- Mashlib Data Browser - Optional SolidOS UI (CDN or local hosting)
- WebID Profiles - HTML with JSON-LD data islands, rendered with mashlib-jss + solidos-lite
- Web Access Control (WAC) -
.aclfile-based authorization with relative URL support - Solid-OIDC Identity Provider - Built-in IdP with DPoP, RS256/ES256, dynamic registration
- Solid-OIDC Resource Server - Accept DPoP-bound access tokens from external IdPs
- NSS-style Registration - Username/password auth compatible with Solid apps
- Nostr Authentication - NIP-98 HTTP Auth with Schnorr signatures, did:nostr → WebID resolution
- WebID-TLS - Client certificate authentication for backend services and CLI tools
- Simple Auth Tokens - Built-in token authentication for development
- Content Negotiation - Turtle <-> JSON-LD conversion, including HTML data islands
- CORS Support - Full cross-origin resource sharing
- Git HTTP Backend - Clone and push to containers via
gitprotocol - Nostr Relay - Integrated NIP-01/NIP-11/NIP-16 relay on the same port (
wss://your.pod/relay) - Invite-Only Registration - CLI-managed invite codes for controlled signups
- Storage Quotas - Per-user storage limits with CLI management
- HTTP 402 Paid Access - Monetize API endpoints with per-request sat payments (
--pay) - Security - Blocks access to dotfiles (
.git/,.env, etc.) except Solid-specific ones
HTTP Methods
| Method | Support |
|---|---|
| GET | Full - Resources and containers |
| HEAD | Full |
| PUT | Full - Create/update resources |
| POST | Full - Create in containers |
| DELETE | Full |
| PATCH | N3 Patch + SPARQL Update |
| OPTIONS | Full with CORS |
Getting Started
Prerequisites
- Node.js 18+
Android/Termux
JSS runs on Android via Termux (uses pure JavaScript bcryptjs for compatibility):
pkg install nodejs git
npm install -g javascript-solid-server
jss start --port 8080 --nostr --gitUse PM2 for persistence:
npm install -g pm2
pm2 start jss -- start --port 8080 --nostr --git
pm2 saveInstallation
npm install
# Or install globally
npm install -g javascript-solid-serverQuick Start
# Initialize configuration (interactive)
jss init
# Start server
jss start
# Or with options
jss start --port 8443 --ssl-key ./key.pem --ssl-cert ./cert.pemCLI Commands
jss start [options] # Start the server
jss init [options] # Initialize configuration
jss invite <cmd> # Manage invite codes (create, list, revoke)
jss quota <cmd> # Manage storage quotas (set, show, reconcile)
jss --help # Show helpStart Options
| Option | Description | Default |
|---|---|---|
-p, --port <n> |
Port to listen on | 3000 |
-h, --host <addr> |
Host to bind to | 0.0.0.0 |
-r, --root <path> |
Data directory | ./data |
-c, --config <file> |
Config file path | - |
--ssl-key <path> |
SSL private key (PEM) | - |
--ssl-cert <path> |
SSL certificate (PEM) | - |
--conneg |
Enable Turtle support | false |
--notifications |
Enable WebSocket | false |
--idp |
Enable built-in IdP | false |
--idp-issuer <url> |
IdP issuer URL | (auto) |
--subdomains |
Enable subdomain-based pods | false |
--base-domain <domain> |
Base domain for subdomains | - |
--mashlib |
Enable Mashlib (local mode) | false |
--mashlib-cdn |
Enable Mashlib (CDN mode) | false |
--mashlib-module <url> |
Enable ES module data browser from a URL | - |
--mashlib-version <ver> |
Mashlib CDN version | 2.0.0 |
--solidos-ui |
Enable modern SolidOS UI (requires --mashlib) | false |
--git |
Enable Git HTTP backend | false |
--nostr |
Enable Nostr relay | false |
--nostr-path <path> |
Nostr relay WebSocket path | /relay |
--nostr-max-events <n> |
Max events in relay memory | 1000 |
--invite-only |
Require invite code for registration | false |
--webid-tls |
Enable WebID-TLS client certificate auth | false |
--default-quota <size> |
Default storage quota per pod (e.g., 50MB) | 50MB |
--activitypub |
Enable ActivityPub federation | false |
--ap-username <name> |
ActivityPub username | me |
--ap-display-name <name> |
ActivityPub display name | (username) |
--ap-summary <text> |
ActivityPub bio/summary | - |
--ap-nostr-pubkey <hex> |
Nostr pubkey for identity linking | - |
--public |
Allow unauthenticated access (skip WAC) | false |
--read-only |
Disable PUT/DELETE/PATCH methods | false |
--live-reload |
Auto-refresh browser on file changes | false |
--pay |
Enable HTTP 402 paid access for /pay/* | false |
--pay-cost <n> |
Cost per request in satoshis | 1 |
--pay-mempool-url <url> |
Mempool API URL for deposit verification | (testnet4) |
--pay-address <addr> |
Address for receiving deposits | - |
--pay-token <ticker> |
Token to sell (enables primary market + withdrawal) | - |
--pay-rate <n> |
Sats per token for buy/withdraw | 1 |
--pay-chains <ids> |
Multi-chain deposits + AMM (e.g. "tbtc3,tbtc4") | - |
--mongo |
Enable MongoDB-backed /db/ route | false |
--mongo-url <url> |
MongoDB connection URL | mongodb://localhost:27017 |
--mongo-database <name> |
MongoDB database name | solid |
-q, --quiet |
Suppress logs | false |
Environment Variables
All options can be set via environment variables with JSS_ prefix:
export JSS_PORT=8443
export JSS_SSL_KEY=/path/to/key.pem
export JSS_SSL_CERT=/path/to/cert.pem
export JSS_CONNEG=true
export JSS_SUBDOMAINS=true
export JSS_BASE_DOMAIN=example.com
export JSS_MASHLIB=true
export JSS_MASHLIB_MODULE=https://example.com/mashlib.js
export JSS_NOSTR=true
export JSS_INVITE_ONLY=true
export JSS_WEBID_TLS=true
export JSS_DEFAULT_QUOTA=100MB
export JSS_ACTIVITYPUB=true
export JSS_AP_USERNAME=alice
export JSS_PUBLIC=true
export JSS_READ_ONLY=true
export JSS_LIVE_RELOAD=true
export JSS_SOLIDOS_UI=true
export JSS_PAY=true
export JSS_PAY_COST=10
export JSS_PAY_ADDRESS=your-address
export JSS_PAY_TOKEN=PODS
export JSS_PAY_RATE=10
export JSS_MONGO=true
export JSS_MONGO_URL=mongodb://localhost:27017
export JSS_MONGO_DATABASE=solid
jss startConfig File
Create config.json:
{
"port": 8443,
"root": "./data",
"sslKey": "./ssl/key.pem",
"sslCert": "./ssl/cert.pem",
"conneg": true,
"notifications": true
}Then: jss start --config config.json
Creating a Pod
Single-User Mode
For personal pod servers where only one user needs access:
# Basic single-user mode (creates pod at /me/)
jss start --single-user --idp
# Custom username
jss start --single-user --single-user-name alice --idp
# Root-level pod (pod at /, WebID at /profile/card#me)
jss start --single-user --single-user-name '' --idp
# Via environment
JSS_SINGLE_USER=true jss start --idpFeatures:
- Pod auto-created on first startup with full structure (inbox, public, private, profile, Settings)
- Registration endpoint disabled (returns 403)
- Login still works for the single user
- Proper ACLs generated automatically
curl -X POST http://localhost:3000/.pods \
-H "Content-Type: application/json" \
-d '{"name": "alice"}'Response:
{
"name": "alice",
"webId": "http://localhost:3000/alice/#me",
"podUri": "http://localhost:3000/alice/",
"token": "eyJ..."
}Using the Pod
# Read public profile
curl http://localhost:3000/alice/
# Write to pod (with token)
curl -X PUT http://localhost:3000/alice/public/data.json \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/ld+json" \
-d '{"@id": "#data", "http://example.org/value": 42}'
# Read back
curl http://localhost:3000/alice/public/data.jsonPATCH with N3
curl -X PATCH http://localhost:3000/alice/public/data.json \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: text/n3" \
-d '@prefix solid: <http://www.w3.org/ns/solid/terms#>.
_:patch a solid:InsertDeletePatch;
solid:inserts { <#data> <http://example.org/name> "Updated" }.'PATCH with SPARQL Update
curl -X PATCH http://localhost:3000/alice/public/data.json \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/sparql-update" \
-d 'PREFIX ex: <http://example.org/>
DELETE DATA { <#data> ex:value 42 } ;
INSERT DATA { <#data> ex:value 43 }'Conditional Requests
Use If-Match for safe updates (optimistic concurrency):
# Get current ETag
ETAG=$(curl -sI http://localhost:3000/alice/public/data.json | grep -i etag | awk '{print $2}')
# Update only if ETag matches
curl -X PUT http://localhost:3000/alice/public/data.json \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/ld+json" \
-H "If-Match: $ETAG" \
-d '{"@id": "#data", "http://example.org/value": 100}'Use If-None-Match: * for create-only semantics:
# Create only if resource doesn't exist (returns 412 if it does)
curl -X PUT http://localhost:3000/alice/public/new-resource.json \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/ld+json" \
-H "If-None-Match: *" \
-d '{"@id": "#new"}'Philosophy: JSON-LD First
This is a JSON-LD native implementation. Unlike traditional Solid servers that treat Turtle as the primary format and convert to/from it, this server:
- Stores everything as JSON-LD - No RDF parsing overhead for standard operations
- Serves JSON-LD by default - Modern web applications can consume responses directly
- Content negotiation is optional - Enable Turtle support with
{ conneg: true }when needed - Fast by design - Skip the RDF parsing tax when you don't need it
Why JSON-LD First?
- Performance: JSON parsing is native to JavaScript - no external RDF libraries needed for basic operations
- Simplicity: JSON-LD is valid JSON - works with any JSON tooling
- Web-native: Browsers and web apps understand JSON natively
- Semantic web ready: JSON-LD is a W3C standard RDF serialization
When to Enable Content Negotiation
Enable conneg: true when:
- Interoperating with Turtle-based Solid apps
- Serving data to legacy Solid clients
- Running conformance tests that require Turtle support
import { createServer } from './src/server.js';
// Default: JSON-LD only (fast)
const server = createServer();
// With Turtle support (for interoperability)
const serverWithConneg = createServer({ conneg: true });Configuration
createServer({
logger: true, // Enable Fastify logging (default: true)
conneg: false, // Enable content negotiation (default: false)
notifications: false, // Enable WebSocket notifications (default: false)
subdomains: false, // Enable subdomain-based pods (default: false)
baseDomain: null, // Base domain for subdomains (e.g., "example.com")
mashlib: false, // Enable Mashlib data browser - local mode (default: false)
mashlibCdn: false, // Enable Mashlib data browser - CDN mode (default: false)
mashlibVersion: '2.0.0', // Mashlib version for CDN mode
});Mashlib Data Browser
Enable the SolidOS Mashlib data browser for RDF resources. Two modes are available:
CDN Mode (recommended for getting started):
jss start --mashlib-cdn --connegLoads mashlib from unpkg.com CDN. Zero footprint - no local files needed.
Local Mode (for production/offline):
jss start --mashlib --connegServes mashlib from src/mashlib-local/dist/. Requires building mashlib locally:
cd src/mashlib-local
npm install && npm run buildES Module Mode (for custom or next-gen mashlib builds):
jss start --mashlib-module https://example.com/mashlib.jsLoads an ES module-based data browser from any URL. Uses <script type="module"> and <div id="mashlib"> (self-initializing). CSS is auto-derived by replacing .js with .css. Content negotiation is auto-enabled.
How it works:
- Browser requests
/alice/public/data.ttlwithAccept: text/html - Server returns Mashlib HTML wrapper
- Mashlib fetches the actual data via content negotiation
- Mashlib renders an interactive, editable view
Note: Mashlib works best with --conneg enabled for Turtle support.
Modern UI (SolidOS UI):
jss start --mashlib --solidos-ui --connegServes a modern Nextcloud-style UI shell while reusing mashlib's data layer. The --solidos-ui flag swaps the classic databrowser interface for a cleaner, mobile-friendly design with:
- Modern file browser with breadcrumb navigation
- Profile, Contacts, Sharing, and Settings views
- Path-based URLs (browser URL reflects current resource)
- Responsive design for mobile devices
Requires solidos-ui dist files in src/mashlib-local/dist/solidos-ui/. See solidos-ui for details.
Profile Pages
Pod profiles (/alice/) use HTML with embedded JSON-LD data islands and are rendered using:
- mashlib-jss - A fork of mashlib with
getPod()fix for path-based pods - solidos-lite - Parses JSON-LD data islands into the RDF store
This allows profiles to work without server-side content negotiation while still providing full SolidOS editing capabilities.
WebSocket Notifications
Enable real-time notifications for resource changes:
const server = createServer({ notifications: true });Clients discover the WebSocket URL via the Updates-Via header:
curl -I http://localhost:3000/alice/public/
# Updates-Via: ws://localhost:3000/.notificationsProtocol (solid-0.1, compatible with SolidOS):
Server: protocol solid-0.1
Client: sub http://localhost:3000/alice/public/data.json
Server: ack http://localhost:3000/alice/public/data.json
Server: pub http://localhost:3000/alice/public/data.json (on change)Git Support
Enable Git HTTP backend to clone and push to pod containers:
jss start --gitInitialize a Repository
# Create a git repo in a pod container
cd data/alice/myrepo
git init
echo "# My Project" > README.md
git add . && git commit -m "Initial commit"Clone and Push
# Clone (public read access)
git clone http://localhost:3000/alice/myrepo
# Push (requires write access via WAC)
cd myrepo
echo "New content" >> README.md
git add . && git commit -m "Update"
git pushGit operations respect WAC permissions - clone requires Read access, push requires Write access.
Auto-checkout: After a successful push to a non-bare repository, JSS automatically updates the working directory - no post-receive hooks needed.
Git Push with Nostr Authentication
Git push supports NIP-98 authentication via Basic Auth. Install the credential helper:
npm install -g git-credential-nostr
git-credential-nostr generate
git config --global credential.helper nostr
git config --global nostr.privkey <key-from-generate>Create an ACL for your repo (includes public read for clone + owner write for push):
cd myrepo
git-credential-nostr acl > .acl
git add .acl && git commit -m "Add ACL"See git-credential-nostr for more details.
ActivityPub Federation
Enable ActivityPub to federate with Mastodon, Pleroma, Misskey, and other Fediverse servers:
jss start --activitypub --ap-username alice --ap-display-name "Alice" --ap-summary "Hello from JSS!"Endpoints
| Endpoint | Description |
|---|---|
/.well-known/webfinger |
Actor discovery (Mastodon searches here) |
/.well-known/nodeinfo |
NodeInfo discovery |
/profile/card |
Actor (returns JSON-LD when Accept: application/activity+json) |
/inbox |
Shared inbox for receiving activities |
/profile/card/inbox |
Personal inbox |
/profile/card/outbox |
User's activities |
/profile/card/followers |
Followers collection |
/profile/card/following |
Following collection |
How It Works
- Discovery: Mastodon looks up
@alice@your.servervia WebFinger - Actor: Returns ActivityPub Actor JSON-LD with public key
- Follow: Remote servers POST Follow activities to inbox
- Accept: JSS auto-accepts follows and sends Accept back
- Delivery: Posts are signed with HTTP Signatures and delivered to follower inboxes
Identity Linking
Your WebID (/profile/card#me) becomes your ActivityPub Actor. Link to Nostr identity:
jss start --activitypub --ap-nostr-pubkey <64-char-hex-pubkey>This adds alsoKnownAs: ["did:nostr:<pubkey>"] to your Actor profile, creating a verifiable link between your Solid, ActivityPub, and Nostr identities (the SAND stack).
Programmatic Usage
import { createServer } from 'javascript-solid-server';
const server = createServer({
activitypub: true,
apUsername: 'alice',
apDisplayName: 'Alice',
apSummary: 'Building the decentralized web!',
apNostrPubkey: 'abc123...' // Optional: links to did:nostr
});Testing Federation
# Check WebFinger
curl "http://localhost:3000/.well-known/webfinger?resource=acct:alice@localhost:3000"
# Get Actor (AP format)
curl -H "Accept: application/activity+json" http://localhost:3000/profile/card
# Check NodeInfo
curl http://localhost:3000/.well-known/nodeinfo/2.1Mastodon-compatible API
JSS exposes Mastodon API endpoints so that Mastodon clients (Elk, Phanpy, Ice Cubes) can connect:
jss start --activitypub --idpEndpoints
| Endpoint | Description |
|---|---|
POST /api/v1/apps |
Dynamic client registration |
GET /api/v1/accounts/verify_credentials |
Current user profile |
GET /api/v1/instance |
Instance metadata |
GET /oauth/authorize |
OAuth authorize page |
POST /oauth/authorize |
Process login |
POST /oauth/token |
Exchange code for Bearer token |
OAuth 2.0 Flow
The OAuth layer is shared between Mastodon clients, remoteStorage apps, and third-party Solid panes:
- Client registers via
POST /api/v1/apps(getsclient_id+client_secret) - Client redirects user to
GET /oauth/authorize?client_id=...&redirect_uri=...&response_type=code - User logs in, JSS redirects back with
?code=... - Client exchanges code for Bearer token via
POST /oauth/token - Bearer token works with all JSS endpoints (Solid, ActivityPub, remoteStorage)
Supports out-of-band (OOB) redirect for CLI/desktop clients.
Testing
# Register a client
curl -X POST http://localhost:3000/api/v1/apps \
-H "Content-Type: application/json" \
-d '{"client_name": "Test App", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob"}'
# Check instance info
curl http://localhost:3000/api/v1/instanceremoteStorage
JSS implements the remoteStorage protocol. The storage routes are always available, but WebFinger discovery and OAuth require --activitypub (which provides the WebFinger and OAuth endpoints). Any remoteStorage-compatible app can store and sync data on your pod.
jss start --activitypub --idpDiscovery
remoteStorage clients discover the storage endpoint via WebFinger:
curl "http://localhost:3000/.well-known/webfinger?resource=acct:me@localhost:3000"The response includes a remotestorage link relation pointing to /storage/me/.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/storage/:user/* |
Read file or list folder (JSON-LD) |
HEAD |
/storage/:user/* |
Get metadata (ETag, Content-Type, size) |
PUT |
/storage/:user/* |
Write file (creates parent folders) |
DELETE |
/storage/:user/* |
Delete file |
How It Works
- Auth: Bearer token via OAuth 2.0 (same flow as Mastodon clients)
- Public folder:
/storage/me/public/*is readable without auth - Conditional requests: If-Match, If-None-Match (uses shared ETag utilities)
- Dotfile protection:
.acl,.meta, and other dotfiles are blocked - Read-only mode: Respects
--read-onlyflag - Streaming: Large files are streamed, not buffered
Testing
# Write a file (needs Bearer token from OAuth flow)
curl -X PUT http://localhost:3000/storage/me/documents/hello.txt \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: text/plain" \
-d "Hello, remoteStorage!"
# Read it back
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/storage/me/documents/hello.txt
# List a folder
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/storage/me/documents/
# Read from public folder (no auth needed)
curl http://localhost:3000/storage/me/public/readme.txtLinking Nostr to WebID (did:nostr)
Bridge your Nostr identity to a Solid WebID for seamless authentication:
Step 1: Add your WebID to your Nostr profile (kind 0 event):
{
"name": "alice",
"alsoKnownAs": ["https://solid.social/alice/profile/card#me"]
}Step 2: Add the did:nostr link to your WebID profile:
{
"@id": "#me",
"owl:sameAs": "did:nostr:<your-64-char-hex-pubkey>"
}How it works:
- NIP-98 signature is verified (existing flow)
- DID document is fetched from
nostr.social/.well-known/did/nostr/<pubkey>.json alsoKnownAsis checked for a WebID URL- WebID profile is fetched and
owl:sameAsverified - If bidirectional link exists → authenticated as WebID
This enables Nostr users to access their Solid pods using existing NIP-07 browser extensions.
Invite-Only Registration
Control who can create accounts by requiring invite codes:
jss start --idp --invite-onlyManaging Invite Codes
# Create a single-use invite
jss invite create
# Created invite code: ABCD1234
# Create multi-use invite with note
jss invite create -u 5 -n "For team members"
# List all active invites
jss invite list
# CODE USES CREATED NOTE
# -------------------------------------------------------
# ABCD1234 0/1 2026-01-03
# EFGH5678 2/5 2026-01-03 For team members
# Revoke an invite
jss invite revoke ABCD1234How It Works
| Mode | Registration | Pod Creation |
|---|---|---|
| Open (default) | Anyone can register | Anyone can create pods |
| Invite-only | Requires valid invite code | Via registration only |
When --invite-only is enabled:
- The registration page shows an "Invite Code" field
- Invalid or expired codes are rejected with an error
- Each use decrements the invite's remaining uses
- Depleted invites are automatically removed
Invite codes are stored in .server/invites.json in your data directory.
Storage Quotas
Limit storage per pod to prevent abuse and manage resources:
jss start --default-quota 50MBManaging Quotas
# Set quota for a user (overrides default)
jss quota set alice 100MB
# Show quota info
jss quota show alice
# alice:
# Used: 12.5 MB
# Limit: 100 MB
# Free: 87.5 MB
# Usage: 12%
# Recalculate from actual disk usage
jss quota reconcile aliceMongoDB Storage (/db/ Route)
Optional MongoDB-backed route for JSON-LD documents that need scale (social feeds, posts, follows). All other routes continue using the filesystem unchanged.
# Install the optional MongoDB driver
npm install mongodb
# Start with MongoDB enabled
jss start --mongo --mongo-url mongodb://localhost:27017 --mongo-database solidOperations
# Store a document
curl -X PUT http://localhost:3000/db/alice/notes/1 \
-H "Content-Type: application/ld+json" \
-H "Authorization: Bearer <token>" \
-d '{"@context": "https://schema.org/", "@type": "Note", "text": "Hello"}'
# Read it back
curl http://localhost:3000/db/alice/notes/1
# List container (derived from URI prefixes)
curl http://localhost:3000/db/alice/
# Delete
curl -X DELETE http://localhost:3000/db/alice/notes/1 \
-H "Authorization: Bearer <token>"How It Works
GET /db/:path— retrieve a document by URI, or list a virtual containerPUT /db/:path— create or update (upsert) a JSON-LD documentDELETE /db/:path— remove a document- Returns standard LDP headers (Link, ETag, WAC-Allow, CORS)
- Supports conditional requests (If-Match, If-None-Match)
- Container listings are computed from URI prefix queries — no directory management needed
- Auth: pod owner can write (
/db/{podName}/...), reads are public - MongoDB is an optional dependency — the server runs without it
How It Works
- Quotas are tracked incrementally on PUT, POST, and DELETE operations
- When quota is exceeded, the server returns HTTP 507 Insufficient Storage
- Each pod stores its quota in
/{pod}/.quota.json - Use
reconcileto fix quota drift from manual file changes
Size Formats
Supported formats: 50MB, 1GB, 500KB, 1TB
HTTP 402 Paid Access
Monetize API endpoints with per-request satoshi payments. Resources under /pay/* require NIP-98 authentication and a positive balance.
jss start --pay --pay-cost 10 --pay-address your-address --pay-token PODS --pay-rate 10Routes
| Method | Path | Description |
|---|---|---|
| GET | /pay/.info |
Public: cost, token info, chains, pool |
| GET | /pay/.balance |
Check your balance (NIP-98 auth) |
| POST | /pay/.deposit |
Deposit sats via TXO URI or MRC20 state proof |
| POST | /pay/.buy |
Buy tokens with sat balance (requires --pay-token) |
| POST | /pay/.withdraw |
Withdraw balance as portable tokens (requires --pay-token) |
| GET | /pay/.offers |
List open sell orders (secondary market) |
| POST | /pay/.sell |
Create a sell order (requires --pay-token) |
| POST | /pay/.swap |
Execute a swap against a sell order |
| GET | /pay/.pool |
AMM pool state (requires --pay-chains) |
| POST | /pay/.pool |
AMM swap, add/remove liquidity |
| GET | /pay/* |
Paid resource access (deducts balance) |
How It Works
- Authenticate with NIP-98 (Nostr HTTP Auth)
- Check balance at
/pay/.balance - Deposit sats by POSTing a TXO URI to
/pay/.deposit - Access paid resources — each request deducts the configured cost
- Optionally buy tokens (
/pay/.buy) or withdraw as portable tokens (/pay/.withdraw) - Balance tracked in a Web Ledger at
/.well-known/webledgers/webledgers.json
Example
# Check balance
curl -H "Authorization: Nostr <base64-event>" http://localhost:3000/pay/.balance
# Deposit (post a confirmed transaction output)
curl -X POST -H "Authorization: Nostr <base64-event>" \
http://localhost:3000/pay/.deposit \
-d "txid:vout"
# Access paid resource
curl -H "Authorization: Nostr <base64-event>" http://localhost:3000/pay/my-resource
# Buy tokens with sat balance
curl -X POST -H "Authorization: Nostr <base64-event>" \
-H "Content-Type: application/json" \
http://localhost:3000/pay/.buy \
-d '{"amount": 100}'
# Withdraw entire balance as portable tokens
curl -X POST -H "Authorization: Nostr <base64-event>" \
-H "Content-Type: application/json" \
http://localhost:3000/pay/.withdraw \
-d '{"all": true}'Deposit verification uses the mempool API (default: testnet4). The X-Balance and X-Cost headers are returned on successful paid requests. Buy and withdraw return portable MRC20 proofs with Bitcoin anchor data for independent verification.
Secondary Market
Users can trade tokens peer-to-peer through the pod. Sell orders are created via /pay/.sell and filled via /pay/.swap. The pod acts as escrow — transferring tokens on the Bitcoin-anchored MRC20 trail and settling sats in the webledger.
Multi-Chain AMM
Enable multi-chain deposits and an automated market maker:
jss start --pay --pay-chains "tbtc3,tbtc4"Deposits detect the chain from the TXO URI prefix (txo:tbtc3:txid:vout). Each chain's balance is tracked separately. The AMM uses a constant-product formula (x × y = k) with a 0.3% fee.
# Add liquidity
curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
http://localhost:3000/pay/.pool \
-d '{"action": "add-liquidity", "tbtc3": 1000, "tbtc4": 5000}'
# Swap
curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
http://localhost:3000/pay/.pool \
-d '{"action": "swap", "sell": "tbtc3", "amount": 100}'
# Check pool state
curl http://localhost:3000/pay/.poolSupported chains: btc, tbtc3, tbtc4, ltc, signet.
Authentication
Simple Tokens (Development)
Use the token returned from pod creation:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/alice/private/Built-in Identity Provider (v0.0.12+)
Enable the built-in Solid-OIDC Identity Provider:
jss start --idpWith IdP enabled, pod creation requires email and password:
curl -X POST http://localhost:3000/.pods \
-H "Content-Type: application/json" \
-d '{"name": "alice", "email": "alice@example.com", "password": "secret123"}'Response:
{
"name": "alice",
"webId": "http://localhost:3000/alice/#me",
"podUri": "http://localhost:3000/alice/",
"idpIssuer": "http://localhost:3000",
"loginUrl": "http://localhost:3000/idp/auth"
}OIDC Discovery: /.well-known/openid-configuration
Programmatic Login (CTH Compatible)
For automated testing and scripts, use the credentials endpoint:
curl -X POST http://localhost:3000/idp/credentials \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "password": "secret123"}'Response:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"webid": "http://localhost:3000/alice/#me"
}For DPoP-bound tokens (Solid-OIDC compliant), include a DPoP proof header.
Passkey Authentication (v0.0.77+)
Enable passwordless login with WebAuthn/FIDO2:
jss start --idpHow it works:
- User logs in with username/password
- Prompted to add a passkey (Touch ID, Face ID, security key)
- Future logins: tap "Sign in with Passkey" → biometric → done!
Benefits:
- Phishing-resistant (bound to domain)
- No passwords to remember or leak
- Works on mobile and desktop
Passkeys are stored per-account and work across devices via platform sync (iCloud Keychain, Google Password Manager, etc.).
Schnorr SSO (v0.0.79+)
Sign in with your Nostr key using NIP-07 browser extensions:
jss start --idpHow it works:
- User clicks "Sign in with Schnorr" on the login page
- NIP-07 extension (Podkey, nos2x, Alby) signs a NIP-98 auth event
- Server verifies BIP-340 Schnorr signature
- User authenticated via linked did:nostr identity
Requirements:
- Account must have a
did:nostr:<pubkey>WebID linked - User needs a NIP-07 compatible browser extension
Benefits:
- No passwords - cryptographic authentication
- Works with existing Nostr identity
- Single sign-on across Solid and Nostr ecosystems
Solid-OIDC (External IdP)
The server also accepts DPoP-bound access tokens from external Solid identity providers:
curl -H "Authorization: DPoP ACCESS_TOKEN" \
-H "DPoP: DPOP_PROOF" \
http://localhost:3000/alice/private/WebID-TLS (Client Certificates)
For backend services, CLI tools, and automated agents that need non-interactive authentication:
jss start --ssl-key key.pem --ssl-cert cert.pem --webid-tlsHow it works:
- Client presents X.509 certificate during TLS handshake
- Certificate's
SubjectAlternativeNamecontains a WebID URI - Server fetches the WebID profile
- Server verifies the certificate's public key matches one in the profile
Testing with curl:
# Generate self-signed cert with WebID in SAN
openssl req -x509 -newkey rsa:2048 -keyout client-key.pem -out client-cert.pem -days 365 \
-subj "/CN=Test" -addext "subjectAltName=URI:https://example.com/alice/#me" -nodes
# Make authenticated request
curl --cert client-cert.pem --key client-key.pem https://localhost:8443/alice/private/Profile requirement: Your WebID profile must contain the certificate's public key:
@prefix cert: <http://www.w3.org/ns/auth/cert#> .
<#me> cert:key [
a cert:RSAPublicKey;
cert:modulus "abc123..."^^xsd:hexBinary;
cert:exponent 65537
] .Use cases:
- Enterprise backend services with existing PKI
- Server-to-server communication
- CLI tools and scripts
- IoT devices with embedded certificates
Pod Structure
/alice/
├── index.html # WebID profile (HTML with JSON-LD)
├── .acl # Root ACL (owner + public read)
├── inbox/ # Notifications (public append)
│ └── .acl
├── public/ # Public files
├── private/ # Private files (owner only)
│ └── .acl
└── settings/ # User preferences (owner only)
├── .acl
├── prefs
├── publicTypeIndex
└── privateTypeIndexSubdomain Mode (XSS Protection)
By default, JSS uses path-based pods (/alice/, /bob/). This is simple but has a security limitation: all pods share the same origin, making cross-site scripting (XSS) attacks possible between pods.
Subdomain mode provides origin isolation - each pod gets its own subdomain (alice.example.com, bob.example.com), preventing XSS attacks between pods.
Why Subdomain Mode?
| Mode | URL | Origin | XSS Risk |
|---|---|---|---|
| Path-based | example.com/alice/ |
example.com |
Shared origin - pods can XSS each other |
| Subdomain | alice.example.com/ |
alice.example.com |
Isolated - browser's Same-Origin Policy protects |
Enabling Subdomain Mode
jss start --subdomains --base-domain example.comOr via environment variables:
export JSS_SUBDOMAINS=true
export JSS_BASE_DOMAIN=example.com
jss startDNS Configuration
You need a wildcard DNS record pointing to your server:
*.example.com A <your-server-ip>Pod URLs in Subdomain Mode
| Path Mode | Subdomain Mode |
|---|---|
example.com/alice/ |
alice.example.com/ |
example.com/alice/public/file.txt |
alice.example.com/public/file.txt |
example.com/alice/#me |
alice.example.com/#me |
Pod creation still uses the main domain:
curl -X POST https://example.com/.pods \
-H "Content-Type: application/json" \
-d '{"name": "alice"}'Comparison
| Server | Size | Deps | Notes |
|---|---|---|---|
| JSS | ~14K LoC | 14 | Minimal, JSON-LD native |
| NSS | 777 KB | 58 | Original Solid server |
| CSS | 5.8 MB | 70 | Modular, configurable |
| Pivot | ~6 MB | 70+ | Built on CSS |
Security
Root ACL Required
JSS uses restrictive mode by default: if no ACL file exists for a resource, access is denied. This prevents unauthorized writes to unprotected containers.
You must create a root .acl file in your data directory. Example (JSON-LD format):
{
"@context": {
"acl": "http://www.w3.org/ns/auth/acl#",
"foaf": "http://xmlns.com/foaf/0.1/"
},
"@graph": [
{
"@id": "#owner",
"@type": "acl:Authorization",
"acl:agent": { "@id": "https://your-domain.com/profile/card#me" },
"acl:accessTo": { "@id": "https://your-domain.com/" },
"acl:default": { "@id": "https://your-domain.com/" },
"acl:mode": [
{ "@id": "acl:Read" },
{ "@id": "acl:Write" },
{ "@id": "acl:Control" }
]
},
{
"@id": "#public",
"@type": "acl:Authorization",
"acl:agentClass": { "@id": "foaf:Agent" },
"acl:accessTo": { "@id": "https://your-domain.com/" },
"acl:default": { "@id": "https://your-domain.com/" },
"acl:mode": [
{ "@id": "acl:Read" }
]
}
]
}Save this as data/.acl (replacing your-domain.com with your actual domain).
See Issue #32 for background.
Performance
This server is designed for speed. Benchmark results on a typical development machine:
| Operation | Requests/sec | Avg Latency | p99 Latency |
|---|---|---|---|
| GET resource | 5,400+ | 1.2ms | 3ms |
| GET container | 4,700+ | 1.6ms | 3ms |
| PUT (write) | 5,700+ | 1.1ms | 2ms |
| POST (create) | 5,200+ | 1.3ms | 3ms |
| OPTIONS | 10,000+ | 0.4ms | 1ms |
Run benchmarks yourself:
npm run benchmarkRunning Tests
npm testCurrently passing: 289 tests (including 27 conformance tests)
Conformance Test Harness (CTH)
This server passes the Solid Conformance Test Harness authentication tests:
# Start server with IdP and content negotiation
JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true jss start
# Create test users
curl -X POST http://localhost:4000/.pods \
-H "Content-Type: application/json" \
-d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
curl -X POST http://localhost:4000/.pods \
-H "Content-Type: application/json" \
-d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
# Run CTH authentication tests
docker run --rm --network=host \
-e SOLID_IDENTITY_PROVIDER="http://localhost:4000/" \
-e USERS_ALICE_WEBID="http://localhost:4000/alice/#me" \
-e USERS_ALICE_PASSWORD="alicepassword123" \
-e USERS_BOB_WEBID="http://localhost:4000/bob/#me" \
-e USERS_BOB_PASSWORD="bobpassword123" \
solidproject/conformance-test-harness:latest \
--filter="authentication"CTH Status (v0.0.15):
- Authentication tests: 6/6 passing
Project Structure
src/
├── index.js # Entry point
├── server.js # Fastify setup
├── handlers/
│ ├── resource.js # GET, PUT, DELETE, HEAD, PATCH
│ ├── container.js # POST, pod creation
│ ├── git.js # Git HTTP backend
│ └── pay.js # HTTP 402 paid access
├── storage/
│ ├── filesystem.js # File operations
│ └── quota.js # Storage quota management
├── auth/
│ ├── middleware.js # Auth hook
│ ├── token.js # Simple token auth
│ ├── solid-oidc.js # DPoP verification
│ ├── nostr.js # NIP-98 Nostr authentication
│ ├── did-nostr.js # did:nostr → WebID resolution
│ └── webid-tls.js # WebID-TLS client certificate auth
├── wac/
│ ├── parser.js # ACL parsing
│ └── checker.js # Permission checking
├── ldp/
│ ├── headers.js # LDP Link headers
│ └── container.js # Container JSON-LD
├── webid/
│ └── profile.js # WebID generation
├── patch/
│ ├── n3-patch.js # N3 Patch support
│ └── sparql-update.js # SPARQL Update support
├── notifications/
│ ├── index.js # WebSocket plugin
│ ├── events.js # Event emitter
│ └── websocket.js # solid-0.1 protocol
├── idp/
│ ├── index.js # Identity Provider plugin
│ ├── provider.js # oidc-provider config
│ ├── adapter.js # Filesystem adapter
│ ├── accounts.js # User account management
│ ├── credentials.js # Credentials endpoint
│ ├── keys.js # JWKS key management
│ ├── interactions.js # Login/consent handlers
│ ├── passkey.js # WebAuthn/FIDO2 passkey support
│ ├── views.js # HTML templates
│ └── invites.js # Invite code management
├── ap/
│ ├── index.js # ActivityPub plugin
│ ├── keys.js # RSA keypair management
│ ├── store.js # SQLite storage (followers, activities)
│ └── routes/
│ ├── actor.js # Actor JSON-LD
│ ├── inbox.js # Receive activities
│ ├── outbox.js # User's activities
│ ├── collections.js # Followers/following
│ ├── mastodon.js # Mastodon API (apps, instance, verify_credentials)
│ └── oauth.js # OAuth 2.0 authorize/token flow
├── webledger.js # Web Ledger balance tracking (webledgers.org)
├── mrc20.js # State chain verification
├── remotestorage.js # remoteStorage protocol (draft-dejong-remotestorage-22)
├── rdf/
│ ├── turtle.js # Turtle <-> JSON-LD
│ └── conneg.js # Content negotiation
├── mashlib/
│ └── index.js # Mashlib data browser plugin
└── utils/
├── url.js # URL utilities
├── conditional.js # If-Match/If-None-Match
└── ssrf.js # SSRF protectionDependencies
14 direct dependencies for a fast, secure server:
- fastify - High-performance HTTP server
- @fastify/middie - Express/Connect middleware bridge (for IdP)
- @fastify/rate-limit - Rate limiting for API endpoints
- @fastify/websocket - WebSocket support for notifications
- @simplewebauthn/server - Passkey/WebAuthn authentication
- bcryptjs - Password hashing (pure JS, works on Termux/Android)
- commander - CLI command parsing
- fs-extra - Enhanced file operations
- jose - JWT/JWK handling for Solid-OIDC
- microfed - ActivityPub primitives (only when activitypub enabled)
- n3 - Turtle parsing (only used when conneg enabled)
- nostr-tools - Nostr protocol and Schnorr signature verification
- oidc-provider - OpenID Connect Identity Provider (only when IdP enabled)
- sql.js - SQLite storage for federation data (WASM, cross-platform)
License
AGPL-3.0-only
This project is licensed under the GNU Affero General Public License v3.0. If you run a modified version as a network service, you must make the source code available to users of that service.