Skip to content

Feature: remoteStorage Protocol Support #106

@melvincarvalho

Description

@melvincarvalho

Summary

Add remoteStorage protocol support to JavaScriptSolidServer, enabling the server to serve both Solid and remoteStorage clients from the same storage backend. This would make JSS the first server to support both major per-user storage protocols.

Difficulty: 45/100
Estimated Effort: 4-6 days
Dependencies: None (can be implemented as optional plugin)


What is remoteStorage?

remoteStorage is an open protocol for per-user storage on the web, predating Solid. It enables "unhosted" web apps where:

  • Users own their data
  • Apps request scoped access via OAuth
  • Data syncs across devices automatically

The protocol is defined in IETF draft-dejong-remotestorage.


Protocol Comparison

Feature Solid (current) remoteStorage Compatibility
Discovery WebFinger → WebID WebFinger → storage URL Different format, same endpoint
Authentication Solid-OIDC + DPoP OAuth 2.0 Bearer (simpler) RS is subset
HTTP Methods GET/PUT/DELETE/PATCH GET/PUT/DELETE/HEAD RS is subset
Directory Listings Turtle/JSON-LD (LDP vocab) JSON-LD (simple format) Transform needed
ETags ✅ Supported ✅ Required Already works
Conditional Requests If-Match, If-None-Match If-Match, If-None-Match Already works
CORS ✅ Enabled ✅ Required Already works
Access Control WAC (ACL files) OAuth scopes (category:rw) Different model
Path Structure /{pod}/path/to/file /storage/{user}/{module}/ Different
Public Data ACL-based /public/ folder Map to ACL

Why Integrate?

  1. Larger ecosystem - Access to remoteStorage apps (100+ apps listed)
  2. Simpler onboarding - RS OAuth is simpler than Solid-OIDC for some apps
  3. Migration path - RS users can migrate to Solid gradually
  4. Unified storage - One server, two protocols, same data
  5. Unique positioning - First server supporting both protocols

Architecture

Proposed Plugin Structure

src/
├── remotestorage/
│   ├── index.js          # Plugin registration
│   ├── webfinger.js      # RS WebFinger format
│   ├── oauth.js          # OAuth 2.0 implicit flow
│   ├── storage.js        # GET/PUT/DELETE handlers
│   ├── listings.js       # Directory listing formatter
│   └── scopes.js         # Scope validation & mapping

Request Flow

┌─────────────────────────────────────────────────────────────┐
│                      JSS Server                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              WebFinger Handler                       │   │
│  │         /.well-known/webfinger                       │   │
│  │                                                      │   │
│  │   ?resource=acct:alice@example.com                   │   │
│  │         │                                            │   │
│  │         ├─► rel=remotestorage → RS client            │   │
│  │         └─► rel=solid → Solid client                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           remoteStorage Routes                       │   │
│  │                                                      │   │
│  │   GET  /rs/oauth/authorize  → OAuth dialog          │   │
│  │   POST /rs/oauth/token      → Token exchange        │   │
│  │   GET  /storage/{user}/*    → Read file/folder      │   │
│  │   PUT  /storage/{user}/*    → Write file            │   │
│  │   DELETE /storage/{user}/*  → Delete file           │   │
│  │   HEAD /storage/{user}/*    → Get metadata          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Solid Routes (existing)                 │   │
│  │                                                      │   │
│  │   /{pod}/*  → Solid LDP + WAC                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           Shared Storage Backend                     │   │
│  │              (filesystem.js)                         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Implementation Plan

Phase 1: WebFinger Extension (10/100)

Add remoteStorage link relation to existing WebFinger response.

Current response:

{
  "subject": "acct:alice@example.com",
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "href": "https://example.com/alice/profile/card"
    }
  ]
}

Extended response:

{
  "subject": "acct:alice@example.com",
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "href": "https://example.com/alice/profile/card"
    },
    {
      "rel": "http://tools.ietf.org/id/draft-dejong-remotestorage",
      "href": "https://example.com/storage/alice",
      "properties": {
        "http://remotestorage.io/spec/version": "draft-dejong-remotestorage-26",
        "http://tools.ietf.org/html/rfc6749#section-4.2": "https://example.com/rs/oauth/authorize",
        "http://tools.ietf.org/html/rfc6750#section-2.3": "Bearer"
      }
    }
  ]
}

Phase 2: OAuth 2.0 Implicit Flow (25/100)

Implement simplified OAuth for remoteStorage clients.

Authorization endpoint:

GET /rs/oauth/authorize
  ?client_id=https://app.example.com
  &redirect_uri=https://app.example.com/callback
  &response_type=token
  &scope=photos:rw documents:r

Authorization dialog:

┌─────────────────────────────────────────────┐
│  App "MyPhotos" is requesting access        │
│                                             │
│  ☑ photos - Read and write                  │
│  ☑ documents - Read only                    │
│                                             │
│  [Deny]                    [Allow Access]   │
└─────────────────────────────────────────────┘

Successful redirect:

https://app.example.com/callback#access_token=abc123&token_type=bearer

Scope format:

  • photos:rw - Read/write access to /storage/{user}/photos/
  • documents:r - Read-only access to /storage/{user}/documents/
  • *:rw - Full access (root scope)

Phase 3: Storage API (20/100)

Implement remoteStorage HTTP API.

GET document:

GET /storage/alice/photos/vacation.jpg
Authorization: Bearer abc123

200 OK
Content-Type: image/jpeg
ETag: "a1b2c3"
Content-Length: 12345

<binary data>

GET folder:

GET /storage/alice/photos/
Authorization: Bearer abc123

200 OK
Content-Type: application/ld+json
ETag: "folder-etag"

{
  "@context": "http://remotestorage.io/spec/folder-description",
  "items": {
    "vacation.jpg": {
      "ETag": "a1b2c3",
      "Content-Type": "image/jpeg",
      "Content-Length": 12345
    },
    "summer/": {
      "ETag": "d4e5f6"
    }
  }
}

PUT document:

PUT /storage/alice/photos/new.jpg
Authorization: Bearer abc123
Content-Type: image/jpeg
If-None-Match: *

<binary data>

201 Created
ETag: "new-etag"

DELETE document:

DELETE /storage/alice/photos/old.jpg
Authorization: Bearer abc123
If-Match: "old-etag"

200 OK
ETag: "new-folder-etag"

Phase 4: Access Control (15/100)

Map OAuth scopes to storage paths.

// src/remotestorage/scopes.js

function validateScope(token, path, method) {
  // Parse path: /storage/alice/photos/vacation.jpg
  const [, , user, category, ...rest] = path.split('/');
  
  // Check token scopes
  const scope = token.scopes.find(s => {
    const [cat, perm] = s.split(':');
    return (cat === '*' || cat === category) &&
           (perm === 'rw' || (perm === 'r' && method === 'GET'));
  });
  
  if (!scope) {
    return { allowed: false, error: 'Insufficient scope' };
  }
  
  return { allowed: true };
}

Public folder handling:

  • /storage/{user}/public/* - Readable without auth
  • Maps to ACL with acl:agentClass foaf:Agent in Solid terms

Phase 5: Testing & Compliance (15/100)

Run the official RS API Test Suite.

# Run compliance tests
npx rs-api-test-suite https://localhost:3000/storage/testuser

Test categories:

  • WebFinger discovery
  • OAuth authorization flow
  • Document CRUD operations
  • Folder listings
  • Conditional requests (ETags)
  • CORS headers
  • Public folder access

Configuration

// Proposed config additions
{
  "remotestorage": {
    "enabled": true,
    "basePath": "/storage",       // RS storage path prefix
    "oauthPath": "/rs/oauth",     // OAuth endpoints
    "publicFolder": "public",     // Public folder name
    "tokenExpiry": 86400,         // Token lifetime (seconds)
    "allowedOrigins": ["*"]       // CORS origins for RS
  }
}

CLI flag:

jss start --remotestorage        # Enable RS support
jss start --no-remotestorage     # Disable (default?)

Environment variable:

JSS_REMOTESTORAGE=true

Storage Mapping

Option A: Shared Storage (Recommended)

RS and Solid share the same filesystem:

data/
└── alice/
    ├── profile/          # Solid WebID profile
    ├── photos/           # Accessible via both protocols
    │   └── vacation.jpg
    ├── public/           # RS public folder = Solid public
    └── .acl              # Solid WAC (RS uses OAuth scopes)

Access patterns:

  • Solid: GET /alice/photos/vacation.jpg (with Solid-OIDC)
  • RS: GET /storage/alice/photos/vacation.jpg (with Bearer token)

Option B: Isolated Storage

Separate storage areas:

data/
└── alice/
    ├── solid/            # Solid-only data
    └── rs/               # RS-only data

Pros: Clean separation, no ACL conflicts
Cons: Data duplication, users manage two silos

Recommendation: Option A (shared) with clear documentation on access model differences.


Scope ↔ WAC Mapping

RS Scope Solid WAC Equivalent
photos:r acl:Read on /photos/
photos:rw acl:Read, acl:Write on /photos/
*:r acl:Read on root
*:rw acl:Read, acl:Write, acl:Control on root
(public folder) acl:agentClass foaf:Agent; acl:Read

Note: RS scopes are simpler than WAC. RS tokens can only access paths within their scopes, regardless of ACL files.


Compatibility Notes

remoteStorage Apps

Popular RS apps that would work:

remoteStorage.js Library

The official remotestorage.js client library should work out of the box once the protocol is implemented.

const rs = new RemoteStorage();
rs.access.claim('photos', 'rw');
rs.connect('alice@example.com');

Difficulty Breakdown

Component Difficulty Notes
WebFinger extension 10/100 Add link relation to existing handler
OAuth implicit flow 25/100 Simpler than Solid-OIDC
Storage GET/PUT/DELETE 15/100 Reuse existing storage backend
Directory listings 10/100 JSON-LD transformation
Scope validation 15/100 Path-based access check
HEAD requests 5/100 Trivial
Public folder 10/100 Skip auth for /public/
Compliance testing 15/100 RS test suite
Documentation 10/100 Usage guide
Total 45/100

Open Questions

  1. Default state: Should RS be enabled by default or opt-in?
  2. Storage mapping: Shared storage (Option A) or isolated (Option B)?
  3. Token storage: Reuse IdP accounts DB or separate RS tokens?
  4. Scope persistence: Store granted scopes in accounts DB?
  5. Revocation UI: Add RS token management to future admin panel?

References


Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions