Getting Started
Prerequisites
- JDK 25. The build sets
maven.compiler.release=25, so it emits class-file version 69 and will not load on an earlier JDK. - Maven 3.9+.
Build
mvn -DskipTests package
This produces an executable target/lws-server.jar.
Run
The server refuses to start with no lws.owners: an empty owner list is open mode, where every
read, write and control decision is permitted for every client, anonymous ones included. For a quick
local try-out, ask for open mode explicitly:
java -Dlws.dev.open=true -jar target/lws-server.jar
For anything else, mint an owner first and set lws.owners.
Spring Boot (default)
java -jar target/lws-server.jar
# or
mvn spring-boot:run
Bare Eclipse Jetty (no Spring)
The same servlets and filters run on a hand-built Jetty server with no Spring on the classpath — the intended future deployment target, and the only path that can terminate TLS itself.
mvn dependency:build-classpath -Dmdep.outputFile=target/cp.txt
java -cp "target/classes:$(cat target/cp.txt)" com.ebremer.lws.server.JettyLauncher
The server listens on the port in lws.base-uri (default http://localhost:8080), or on
lws.listen-port when that is set. Configuration comes from ./lws.properties or -Dlws.* — see
Configuration.
First requests
In open mode (lws.dev.open=true) no credential is needed:
# the storage URI answers with the storage description (application/lws+cid)
curl http://localhost:8080/
# create an RDF resource in the root container
curl -i -X POST -H 'Content-Type: text/turtle' -H 'Slug: greeting' \
--data '<#it> <http://schema.org/name> "Hello LWS" .' http://localhost:8080/
# read it back as JSON-LD
curl -H 'Accept: application/ld+json' http://localhost:8080/greeting
# list the root container instead of describing the storage
curl -H 'Accept: application/lws+json' http://localhost:8080/
# create a container, add a resource, then patch it with SPARQL Update
curl -X PUT -H 'Link: <http://www.w3.org/ns/ldp#Container>; rel="type"' http://localhost:8080/box/
curl -X PUT -H 'Content-Type: text/turtle' --data '<#x> <http://schema.org/n> 1 .' http://localhost:8080/box/x
curl -X PATCH -H 'Content-Type: application/sparql-update' \
--data 'INSERT DATA { <http://localhost:8080/box/x#x> <http://schema.org/age> 42 }' http://localhost:8080/box/x
The storage description is also served at /.lws/storage-description, and every GET and HEAD
of a resource carries Link: <storage URI>; rel="https://www.w3.org/ns/lws#storage".
See the HTTP API for the full surface.
The first owner
lws.owners holds WebIDs/DIDs, but you need a credential for one before you can act as that owner.
A did:key subject of the
self-signed Controlled Identifier suite
needs no identity provider — its DID document is derived from the identifier itself — so a bundled
helper can mint the very first owner offline:
# from the packaged fat jar
java -cp target/lws-server.jar -Dloader.main=com.ebremer.lws.server.tools.DidKeyTool \
org.springframework.boot.loader.launch.PropertiesLauncher --audience http://localhost:8080
# or, from a source checkout
mvn -q exec:java -Dexec.mainClass=com.ebremer.lws.server.tools.DidKeyTool \
-Dexec.args="--audience http://localhost:8080"
It prints:
- a
did:key:…to put inlws.owners, - a private-key seed to keep secret (pass it back with
--key <seed>to mint new credentials for the same identity), and - a credential — a self-issued JWT whose
kidnames the DID’s verification method.
Options: --key <seed>, --ttl <seconds> (credential lifetime, default 3600; the server caps
self-signed credentials at lws.token.max-lifetime-seconds) and --audience <aud>.
Always pass
--audiencewith yourlws.base-uri. The server requires anaudclaim by default (lws.audience.require=true), and the token endpoint only accepts a credential whose audience names its authorization server — whose issuer islws.base-uri. A credential minted without--audienceis refused withinvalid_request.
Put the DID in lws.owners, restart without lws.dev.open, and exchange the credential for an
access token at the storage’s authorization server:
curl -s http://localhost:8080/.lws/token \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
--data-urlencode resource=http://localhost:8080/ \
--data-urlencode subject_token="$CREDENTIAL" \
-d subject_token_type=urn:ietf:params:oauth:token-type:jwt
# {"access_token":"eyJ...","issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
# "token_type":"Bearer","expires_in":300}
curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Accept: application/lws+json' http://localhost:8080/
Presenting the credential itself as Authorization: Bearer <credential> also works while
lws.oauth.accept-authentication-credentials=true (the default). See
Authentication for both routes.
Next steps
- Configuration — tune storage, auth, quotas, TLS.
- Authentication — access tokens, the credential suites and DPoP.
- Authorization — owner mode vs Web Access Control, and access grants.
- Deployment & TLS — production behind a proxy (at the root or under a path), or with built-in Let’s Encrypt.