Build apps on Solid pods using standard HTTP. Resources are RDF (JSON-LD or Turtle), organized in LDP containers, protected by WebACL.
| Method | Purpose | Container | Resource |
|---|---|---|---|
GET |
Read | Listing | Content |
HEAD |
Headers only | ✓ | ✓ |
PUT |
Create/overwrite | ✓ | ✓ |
POST |
Create child | ✓ | ✗ |
DELETE |
Remove | ✓ | ✓ |
PATCH |
Partial update | ✗ | ✓ |
OPTIONS |
Capabilities | ✓ | ✓ |
Containers end with /. Resources don’t. This is mandatory.
https://alice.pod/docs/ ← container
https://alice.pod/docs/note1 ← resource
/{user}/
├── profile/
│ └── card WebID profile (HTML + JSON-LD)
├── Settings/
│ ├── Preferences.ttl
│ ├── publicTypeIndex.ttl
│ └── privateTypeIndex.ttl
├── inbox/ ActivityPub / LDN inbox
├── public/
├── private/
└── .acl Root ACL
A WebID like https://alice.pod/profile/card#me identifies a person. The document at https://alice.pod/profile/card contains:
{
"@context": {
"foaf": "http://xmlns.com/foaf/0.1/",
"solid": "http://www.w3.org/ns/solid/terms#",
"pim": "http://www.w3.org/ns/pim/space#",
"ldp": "http://www.w3.org/ns/ldp#",
"schema": "http://schema.org/"
},
"@id": "https://alice.pod/profile/card#me",
"@type": "foaf:Person",
"foaf:name": "Alice",
"pim:storage": { "@id": "https://alice.pod/" },
"solid:oidcIssuer": { "@id": "https://alice.pod/" },
"ldp:inbox": { "@id": "https://alice.pod/inbox/" },
"pim:preferencesFile": { "@id": "https://alice.pod/Settings/Preferences.ttl" },
"solid:publicTypeIndex": { "@id": "https://alice.pod/Settings/publicTypeIndex.ttl" },
"solid:privateTypeIndex": { "@id": "https://alice.pod/Settings/privateTypeIndex.ttl" }
}
| Predicate | Purpose | Example |
|---|---|---|
foaf:name |
Display name | "Alice" |
pim:storage |
Pod root URI | https://alice.pod/ |
solid:oidcIssuer |
Auth provider | https://alice.pod/ |
ldp:inbox |
Notification inbox | https://alice.pod/inbox/ |
pim:preferencesFile |
User settings | https://alice.pod/Settings/Preferences.ttl |
solid:publicTypeIndex |
Public type registry | https://alice.pod/Settings/publicTypeIndex.ttl |
solid:privateTypeIndex |
Private type registry | https://alice.pod/Settings/privateTypeIndex.ttl |
Find a user’s pod root from their WebID:
Link header for rel="http://www.w3.org/ns/pim/space#storage"pim:storage predicate from the profileLink: <https://alice.pod/>; rel="http://www.w3.org/ns/pim/space#storage"
| Content-Type | Format | Notes |
|---|---|---|
application/ld+json |
JSON-LD | Default, always supported |
text/turtle |
Turtle | Requires server support (conneg) |
text/n3 |
Notation3 | Used for PATCH |
application/sparql-update |
SPARQL | Used for PATCH |
Request a format with the Accept header:
GET /docs/note1 HTTP/1.1
Accept: text/turtle
Write with Content-Type:
PUT /docs/note1 HTTP/1.1
Content-Type: application/ld+json
{"@context": {...}, ...}
Content-Type is required on all write operations. Missing it → 400 Bad Request.
GET /docs/note1 HTTP/1.1
Accept: application/ld+json
GET /docs/ HTTP/1.1
Accept: application/ld+json
Response:
{
"@context": { "ldp": "http://www.w3.org/ns/ldp#" },
"@id": "https://alice.pod/docs/",
"@type": ["ldp:Container", "ldp:BasicContainer", "ldp:Resource"],
"ldp:contains": [
{ "@id": "https://alice.pod/docs/note1" },
{ "@id": "https://alice.pod/docs/photos/" }
]
}
PUT /docs/note1 HTTP/1.1
Content-Type: application/ld+json
{
"@context": { "schema": "http://schema.org/" },
"@type": "schema:TextDigitalDocument",
"schema:text": "Hello world"
}
→ 201 Created (new) or 204 No Content (overwrite)
POST /docs/ HTTP/1.1
Content-Type: application/ld+json
Slug: my-note
{...}
→ 201 Created with Location: /docs/my-note
The Slug header suggests a name. Server may modify it if taken. Without Slug, server generates a UUID.
POST /docs/ HTTP/1.1
Content-Type: application/ld+json
Link: <http://www.w3.org/ns/ldp#BasicContainer>; rel="type"
Slug: photos
Or simply PUT with a trailing slash:
PUT /docs/photos/ HTTP/1.1
DELETE /docs/note1 HTTP/1.1
→ 204 No Content
PATCH /profile/card HTTP/1.1
Content-Type: text/n3
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
_:patch a solid:InsertDeletePatch;
solid:deletes {
<#me> foaf:name "Old Name" .
};
solid:inserts {
<#me> foaf:name "Alice" .
}.
PATCH /profile/card HTTP/1.1
Content-Type: application/sparql-update
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
DELETE { <#me> foaf:name ?old . }
INSERT { <#me> foaf:name "Alice" . }
WHERE { <#me> foaf:name ?old . }
→ 204 No Content (updated) or 201 Created (new resource)
Every response includes an ETag header. Use it for safe concurrent updates.
PUT /docs/note1 HTTP/1.1
Content-Type: application/ld+json
If-Match: "abc123"
{...}
→ 412 Precondition Failed if resource changed since you read it.
PUT /docs/note1 HTTP/1.1
Content-Type: application/ld+json
If-None-Match: *
{...}
→ 412 Precondition Failed if resource already exists.
GET /docs/note1 HTTP/1.1
If-None-Match: "abc123"
→ 304 Not Modified if unchanged (use cached copy).
Every response includes Link headers describing the resource:
Link: <http://www.w3.org/ns/ldp#Resource>; rel="type",
<http://www.w3.org/ns/ldp#Container>; rel="type",
<https://alice.pod/docs/.acl>; rel="acl",
<https://alice.pod/>; rel="http://www.w3.org/ns/pim/space#storage"
| Relation | Purpose |
|---|---|
rel="type" + ldp:Resource |
This is an LDP resource |
rel="type" + ldp:Container |
This is an LDP container |
rel="type" + ldp:BasicContainer |
This is a basic container |
rel="acl" |
ACL file controlling access |
rel="http://www.w3.org/ns/pim/space#storage" |
Pod root |
Allow: GET, HEAD, PUT, DELETE, PATCH, OPTIONS, POST
Accept-Patch: text/n3, application/sparql-update
Accept-Put: application/ld+json, text/turtle
Accept-Post: application/ld+json, text/turtle
Vary: Accept, Authorization, Origin
Content-Type: application/ld+json
ETag: "hash"
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Accept-Patch, Accept-Post, Accept-Put,
Allow, Content-Type, ETag, Link, Location, WAC-Allow
| Code | Meaning |
|---|---|
200 |
GET/HEAD success |
201 |
Resource created (PUT/POST/PATCH) |
204 |
Success, no body (PUT/DELETE/PATCH) |
301 |
Redirect (missing trailing slash on container) |
304 |
Not modified (ETag match) |
400 |
Bad request (missing Content-Type, malformed body) |
401 |
Not authenticated |
403 |
Forbidden (ACL denies access) |
404 |
Not found |
405 |
Method not allowed |
409 |
Conflict |
412 |
Precondition failed (ETag mismatch) |
415 |
Unsupported media type |
507 |
Insufficient storage (quota exceeded) |
Content-Type on PUT/POST/PATCH → 400409405If-Match on updates → risk of overwriting concurrent changesSee webacl.md for full WebACL reference.
Quick summary: every resource has a .acl file. The WAC-Allow header shows current permissions:
WAC-Allow: user="read write control", public="read"
AGPL-3.0