Skip to main content

Building Solid Apps

Create applications that read from and write to Solid pods.

The Mental Model

Traditional apps: App stores data → User accesses via app Solid apps: User owns data → App requests access

┌─────────────┐     permission     ┌─────────────┐
│ User │◄──────────────────►│ App │
│ Pod │ │ │
└─────────────┘ └─────────────┘

This inversion means:

  • Users keep their data — Even if your app disappears
  • Multiple apps, same data — No more data silos
  • Explicit consent — Users grant specific permissions
  • Portability — Users can switch apps freely

Quick Start

1. Install Dependencies

npm install @inrupt/solid-client @inrupt/solid-client-authn-browser

2. Authentication

import {
login,
handleIncomingRedirect,
getDefaultSession
} from '@inrupt/solid-client-authn-browser';

// On page load, handle redirect from identity provider
await handleIncomingRedirect();

// Check if already logged in
const session = getDefaultSession();
if (session.info.isLoggedIn) {
console.log('Logged in as:', session.info.webId);
} else {
// Trigger login
await login({
oidcIssuer: 'https://login.inrupt.com',
redirectUrl: window.location.href,
clientName: 'My Solid App'
});
}

3. Read Data

import {
getSolidDataset,
getThing,
getStringNoLocale
} from '@inrupt/solid-client';

const dataset = await getSolidDataset('https://pod.example/profile/card');
const profile = getThing(dataset, 'https://pod.example/profile/card#me');
const name = getStringNoLocale(profile, 'http://xmlns.com/foaf/0.1/name');

4. Write Data

import {
saveSolidDatasetAt,
setThing,
setStringNoLocale
} from '@inrupt/solid-client';

const updatedProfile = setStringNoLocale(profile, FOAF.name, 'New Name');
const updatedDataset = setThing(dataset, updatedProfile);
await saveSolidDatasetAt('https://pod.example/profile/card', updatedDataset);

Key Concepts

Datasets and Things

  • Dataset — A document containing RDF data (fetched from a URL)
  • Thing — A subject within a dataset (identified by URL)
// A dataset contains multiple Things
const dataset = await getSolidDataset(profileUrl);

// Get a specific Thing by its URL
const me = getThing(dataset, profileUrl + '#me');
const friend = getThing(dataset, 'https://friend.example/profile/card#me');

Working with Properties

Different property types have different accessors:

import {
getStringNoLocale,
getInteger,
getUrl,
getDatetime,
getBoolean,
getStringWithLocale
} from '@inrupt/solid-client';

// String properties
const name = getStringNoLocale(thing, FOAF.name);

// Integers
const age = getInteger(thing, SCHEMA.age);

// URLs (references to other things)
const knows = getUrl(thing, FOAF.knows);

// Dates
const birthday = getDatetime(thing, SCHEMA.birthDate);

// Localized strings
const bio = getStringWithLocale(thing, SCHEMA.description, 'en');

Creating New Things

import {
createThing,
addStringNoLocale,
addUrl,
setThing,
createSolidDataset,
saveSolidDatasetAt
} from '@inrupt/solid-client';

// Create a new Thing
let note = createThing({ name: 'note1' });
note = addStringNoLocale(note, SCHEMA.name, 'My Note');
note = addStringNoLocale(note, SCHEMA.text, 'Note content here');
note = addUrl(note, RDF.type, SCHEMA.TextDigitalDocument);

// Add to dataset and save
let dataset = createSolidDataset();
dataset = setThing(dataset, note);
await saveSolidDatasetAt('https://pod.example/notes/note1', dataset, { fetch });

Vocabularies

Use standard vocabularies for interoperability:

PrefixNamespacePurpose
foafhttp://xmlns.com/foaf/0.1/People, social
schemahttps://schema.org/General purpose
vcardhttp://www.w3.org/2006/vcard/ns#Contact info
rdfhttp://www.w3.org/1999/02/22-rdf-syntax-ns#RDF basics
ldphttp://www.w3.org/ns/ldp#Containers
// Import vocabulary constants
import { FOAF, SCHEMA_INRUPT, VCARD, RDF } from '@inrupt/vocab-common-rdf';

// Use in your code
const name = getStringNoLocale(profile, FOAF.name);

Working with Containers

Containers are like folders. They hold resources and other containers.

Listing Container Contents

import {
getSolidDataset,
getContainedResourceUrlAll
} from '@inrupt/solid-client';

const container = await getSolidDataset('https://pod.example/notes/', { fetch });
const noteUrls = getContainedResourceUrlAll(container);

for (const url of noteUrls) {
console.log('Found note:', url);
}

Creating Containers

import { createContainerAt } from '@inrupt/solid-client';

await createContainerAt('https://pod.example/projects/', { fetch });

Authenticated Requests

For protected resources, pass the authenticated fetch:

import { fetch } from '@inrupt/solid-client-authn-browser';
import { getSolidDataset, saveSolidDatasetAt } from '@inrupt/solid-client';

// Reading protected data
const dataset = await getSolidDataset(privateUrl, { fetch });

// Writing protected data
await saveSolidDatasetAt(url, dataset, { fetch });

Error Handling

import {
getSolidDataset,
isContainer,
FetchError
} from '@inrupt/solid-client';

try {
const dataset = await getSolidDataset(url, { fetch });
} catch (error) {
if (error.statusCode === 401) {
console.log('Not authenticated');
} else if (error.statusCode === 403) {
console.log('Access denied');
} else if (error.statusCode === 404) {
console.log('Resource not found');
} else {
console.error('Unexpected error:', error);
}
}

Complete Example: Notes App

import {
login, handleIncomingRedirect, getDefaultSession, fetch
} from '@inrupt/solid-client-authn-browser';
import {
getSolidDataset, saveSolidDatasetAt,
createSolidDataset, createThing, setThing,
addStringNoLocale, addDatetime,
getContainedResourceUrlAll, getThing,
getStringNoLocale, getDatetime
} from '@inrupt/solid-client';

const NOTES_CONTAINER = 'https://mypod.example/notes/';

// Initialize authentication
async function init() {
await handleIncomingRedirect();
const session = getDefaultSession();
if (!session.info.isLoggedIn) {
await login({
oidcIssuer: 'https://login.inrupt.com',
redirectUrl: window.location.href
});
}
return session;
}

// Create a new note
async function createNote(title, content) {
let note = createThing({ name: Date.now().toString() });
note = addStringNoLocale(note, SCHEMA.name, title);
note = addStringNoLocale(note, SCHEMA.text, content);
note = addDatetime(note, SCHEMA.dateCreated, new Date());

let dataset = createSolidDataset();
dataset = setThing(dataset, note);

const noteUrl = `${NOTES_CONTAINER}${Date.now()}.ttl`;
await saveSolidDatasetAt(noteUrl, dataset, { fetch });
return noteUrl;
}

// List all notes
async function listNotes() {
const container = await getSolidDataset(NOTES_CONTAINER, { fetch });
return getContainedResourceUrlAll(container);
}

// Read a specific note
async function readNote(noteUrl) {
const dataset = await getSolidDataset(noteUrl, { fetch });
const things = getThingAll(dataset);
const note = things[0]; // Assuming one thing per note

return {
title: getStringNoLocale(note, SCHEMA.name),
content: getStringNoLocale(note, SCHEMA.text),
created: getDatetime(note, SCHEMA.dateCreated)
};
}

Best Practices

1. Use Standard Vocabularies

Your data becomes interoperable with other Solid apps.

2. Handle Offline Gracefully

Consider caching strategies for offline-first experiences.

3. Request Minimal Permissions

Only ask for access to what you need.

4. Respect User's Storage Structure

Don't assume specific folder structures exist.

5. Provide Clear Data Descriptions

Help users understand what data your app stores.

Testing Locally

Use Sandymount for local development:

npx sandymount
# Pod available at http://localhost:5420

See Also