This guide explains how to integrate tb-solid-pod into your app. For why (principles, local-first, data sovereignty), see PRINCIPLES_AND_GOALS.md. For practical "how do I…" questions, see USE_CASES.md.
Terms: TinyBase = reactive store + persistence. JSON-LD = data format for personas, contacts, groups. CLI = terminal (browser or Node).
- Install as a dependency —
npm install github:devalbo/tb-solid-pod, then import schemas, components, or the CLI. Your app needs a TinyBase store (and indexes if you use the file browser or CLI); wrap your app in TinyBase’sProvider. See Use as a library and the sections below for store setup and usage. - Copy what you need — Copy
src/schemas, and optionallysrc/storeLayout.ts,src/utils,src/components, andsrc/cli, into your repo. Install the same dependencies (TinyBase, Zod, vocab packages). Use this when you want to customize or avoid a package dependency. See Option 1: Copy components for the file list and store setup.
For answers to “how do I access or manage users, groups, and documents?”, see USE_CASES.md.
Install from GitHub (replace devalbo/tb-solid-pod with your fork if needed):
npm install github:devalbo/tb-solid-podThen import schemas, CLI, or components:
import { createPersona, createContact, PersonaSchema } from 'tb-solid-pod';
import { CliTerminal } from 'tb-solid-pod';
import { PersonaList, PersonaForm } from 'tb-solid-pod';JSON Schema: The project generates JSON Schema from Zod types (no canonical Solid JSON Schema; the ecosystem uses SHACL/ShEx). See the Schemas tab on the live demo for the schema list, links to each JSON file, and example code. To emit static .json files, run npm run generate:schemas; output is in schema/ and public/schema/.
The sections below describe store setup, Provider, components, and CLI for both the install and copy paths.
This project is structured as a standalone app. To integrate by copying into your project:
npm install tinybase zod @inrupt/vocab-common-rdf @inrupt/vocab-solid-commonsrc/
├── storeLayout.ts # Table and index names (STORE_TABLES, STORE_INDEXES) – required for store setup
├── schemas/ # Copy entire folder – Zod schemas + factory functions
│ ├── base.ts # JSON-LD base types and context
│ ├── persona.ts # Identity/profile schema
│ ├── contact.ts # Contact/agent schema
│ ├── group.ts # Organization/team schema
│ ├── typeIndex.ts # Type index and type registration schema
│ ├── preferences.ts # Solid preferences document schema
│ └── file.ts # File metadata schema
├── utils/
│ ├── settings.ts # Settings utilities and SETTINGS_KEYS (needed for components)
│ ├── typeIndex.ts # Type index helpers (register, lookup, defaults)
│ └── storeExport.ts # Import/export helpers
└── components/ # Copy what you need
├── PersonaList.tsx / PersonaForm.tsx
├── ContactList.tsx / ContactForm.tsx
├── GroupList.tsx / GroupForm.tsx / MembershipManager.tsx
└── FileMetadataPanel.tsx
If you installed from GitHub, use import { STORE_TABLES, STORE_INDEXES, SETTINGS_KEYS } from 'tb-solid-pod' (and optionally storeLayout). If you copied files, use import { STORE_TABLES, STORE_INDEXES } from './storeLayout' and import { SETTINGS_KEYS } from './utils/settings' (or your equivalent paths).
The store layout (table and index names) is the library’s stable contract: it will not change in a way that requires you to migrate. Use the exported constants so your app stays compatible when the library adds features (e.g. sync to a Solid pod).
If you installed from GitHub:
import { createStore, createIndexes } from 'tinybase';
import { createLocalPersister } from 'tinybase/persisters/persister-browser';
import { STORE_TABLES, STORE_INDEXES } from 'tb-solid-pod';
const store = createStore();
const indexes = createIndexes(store);
const persister = createLocalPersister(store, 'my-app-pod');
await persister.load();
await persister.startAutoSave();
indexes.setIndexDefinition(STORE_INDEXES.BY_PARENT, STORE_TABLES.RESOURCES, 'parentId');If you copied files: Use the same code but import from your copy of storeLayout:
import { STORE_TABLES, STORE_INDEXES } from './storeLayout';
import { Provider } from 'tinybase/ui-react';
function App() {
return (
<Provider store={store} indexes={indexes}>
<YourApp />
</Provider>
);
}import PersonaList from './components/PersonaList';
import PersonaForm from './components/PersonaForm';
import { STORE_TABLES, SETTINGS_KEYS } from 'tb-solid-pod'; // or from your storeLayout + utils/settings
function ProfilePage() {
const [formOpen, setFormOpen] = useState(false);
const [editingId, setEditingId] = useState<string>();
return (
<>
<PersonaList
store={store}
onSelect={(id) => console.log('Selected:', id)}
onEdit={(id) => { setEditingId(id); setFormOpen(true); }}
onDelete={(id) => store.delRow(STORE_TABLES.PERSONAS, id)}
onCreate={() => { setEditingId(undefined); setFormOpen(true); }}
onSetDefault={(id) => store.setValue(SETTINGS_KEYS.DEFAULT_PERSONA_ID, id)}
/>
{formOpen && (
<PersonaForm
store={store}
baseUrl="https://myapp.com/users/"
personaId={editingId}
onSave={() => setFormOpen(false)}
onCancel={() => setFormOpen(false)}
/>
)}
</>
);
}import { createPersona } from './schemas/persona'; // or 'tb-solid-pod'
import { createContact } from './schemas/contact';
import { createGroup } from './schemas/group';
import { STORE_TABLES } from 'tb-solid-pod'; // or your storeLayout
const persona = createPersona({
name: 'Alice Smith',
email: 'alice@example.com',
bio: 'Software developer'
}, 'https://myapp.com/users/');
store.setRow(STORE_TABLES.PERSONAS, persona['@id'], persona);
const contact = createContact({
name: 'Bob Jones',
email: 'bob@example.com',
isAgent: false
}, 'https://myapp.com/contacts/');
store.setRow(STORE_TABLES.CONTACTS, contact['@id'], contact);
const group = createGroup({
name: 'Engineering Team',
type: 'team',
description: 'Core engineering'
}, 'https://myapp.com/groups/');
store.setRow(STORE_TABLES.GROUPS, group['@id'], group);A key architectural principle (see PRINCIPLES_AND_GOALS.md) is that all app operations should flow through the CLI command layer. This ensures consistent validation, error handling, and behavior across UI, terminal, and programmatic use.
Instead of calling pod.handleRequest() or store.setRow() directly from UI code, route operations through the CLI executor:
| Direct Approach (Avoid) | CLI Approach (Preferred) |
|---|---|
UI code calls pod.handleRequest() |
UI code calls useCliExecutor().createFile() |
| Validation duplicated in UI and CLI | Validation happens once in the command |
| Different error handling paths | Unified error codes and messages |
| Hard to test UI operations | Commands testable in isolation |
import { useCliExecutor, useCommandHandler } from 'tb-solid-pod'; // or from your hooks
function FileActions() {
const { createFile, deleteResource, navigate } = useCliExecutor();
const handle = useCommandHandler();
const handleCreate = async (name: string, content: string) => {
await handle(
() => createFile(name, content, 'text/plain'),
{
successMessage: `Created ${name}`,
onError: (err) => console.error(err.code, err.message),
}
);
};
return (
<button onClick={() => handleCreate('notes.txt', 'Hello')}>
Create File
</button>
);
}The CLI command logic is agnostic to browser vs. Node:
- The same command implementation runs in both environments.
- Platform-specific behavior (file dialogs, clipboard) is handled by adapters, not command code.
- Tests written for one environment work in the other.
This means an app built with the CLI works without modification in:
- Browser (React/web-ink)
- Node terminal (Ink)
- E2E tests (Playwright driving the browser or Node)
- Future environments (Electron, React Native)
| UI Operation | CLI Command | Executor Method |
|---|---|---|
| Create file button | touch <name> |
createFile(name, content, type) |
| Create folder button | mkdir <name> |
createFolder(name) |
| Delete button | rm <path> |
deleteResource(path) |
| Navigate to folder | cd <path> |
navigate(path) |
| Set file title | file set-title <path> <title> |
setTitle(path, title) |
| View metadata | file info <path> |
getInfo(path) |
For the full operation mapping, see CLI_COMMAND_UNIFICATION.md.
If you only need the data structures without the UI:
import { PersonaSchema, createPersona } from './schemas/persona'; // or 'tb-solid-pod'
import { ContactSchema, createContact } from './schemas/contact';
import { GroupSchema, createGroup } from './schemas/group';
// Validate external data
const result = PersonaSchema.safeParse(untrustedData);
if (result.success) {
const persona = result.data;
}
// Create new records with proper JSON-LD structure
const newPersona = createPersona({ name: 'Test' }, baseUrl);
// Returns: { '@context': {...}, '@id': 'https://...#me', '@type': 'foaf:Person', ... }The library uses these TinyBase tables:
| Table | Purpose | Key Fields |
|---|---|---|
personas |
User identities | @id, foaf:name, foaf:mbox, foaf:bio |
contacts |
Address book | @id, vcard:fn, vcard:hasEmail, @type |
groups |
Organizations/teams | @id, vcard:fn, vcard:hasMember, @type |
typeIndexes |
Type index registrations | forClass, instance/instanceContainer, indexType (public/private) |
resources |
Files and folders | URL as key, type, body, contentType, parentId |
Settings are stored in TinyBase values (not tables):
defaultPersonaId— Default personatheme— Color theme preferencecliHistorySize— CLI history length
The CLI runs in two environments: (1) in-app Terminal tab in the browser, and (2) from a real terminal (Node.js).
App-neutral data access: In the Solid vision, your data belongs to you—not to any particular application. Whether your users prefer a browser UI (no install, instant access from any device) or a terminal app (scripting, automation, CI/CD pipelines), they work with the same data through the same commands. The interface is a matter of preference; the data is theirs regardless of how they access it.
import { CliTerminal } from 'tb-solid-pod'; // or from your cli
<CliTerminal
store={store}
pod={virtualPod} // VirtualPod instance for file operations
currentUrl={currentUrl}
setCurrentUrl={setCurrentUrl}
baseUrl="https://myapp.com/pod/"
/>From the repo: npm run cli. Same commands as the browser. Data is stored in ~/.tb-solid-pod/data/store.json (override with TB_SOLID_POD_DATA_PATH). Interactive mode supports ↑/↓ history and Tab completion for command names. Use exit to quit. Export: export prints JSON to the terminal; export --download writes a file to the current directory (Node has no clipboard). Single-command mode: npm run cli -- help or npm run cli -- contact list.
The baseUrl parameter controls the IRI namespace for your data:
// For a multi-tenant app
const baseUrl = `https://myapp.com/users/${userId}/`;
// All created resources will have IRIs like:
// https://myapp.com/users/123/personas/abc-def#me
// https://myapp.com/users/123/contacts/xyz-789- Create a new schema in
schemas/:
// schemas/project.ts
import { z } from 'zod';
import { JsonLdBase, NodeRef, nowISO, POD_CONTEXT } from './base';
export const ProjectSchema = JsonLdBase.extend({
'@type': z.literal('https://schema.org/Project'),
'https://schema.org/name': z.string(),
'https://schema.org/member': z.array(NodeRef).optional(),
});
export function createProject(input: { name: string }, baseUrl: string) {
const id = `${baseUrl}projects/${crypto.randomUUID()}`;
return {
'@context': POD_CONTEXT,
'@id': id,
'@type': 'https://schema.org/Project',
'https://schema.org/name': input.name,
'https://schema.org/dateCreated': nowISO(),
};
}-
Add CLI commands in
cli/commands/project.tsx(follow existing patterns). -
Create UI components as needed.