nomad.fs is the API for hyper:// drives — reading and writing files, drive lifecycle
(create/fork/configure), and multi-writer collaboration, all through one surface. Every drive is an
Autobase (multi-writer-capable, with a stable URL for life).
stat() returns real mtime, ctime, and size, and get(path, 'json')
parses JSON for you.
API
nomad.fs.drive(url)
Create a scoped drive handle. Its methods accept paths relative to the drive rather than full URLs.
var drive = nomad.fs.drive('hyper://abc123../')
await drive.list('/')
Returns an FsDrive instance with all the methods below (scoped to the drive).
Every method also exists as a top-level, URL-first helper — e.g. nomad.fs.readFile('hyper://abc123../index.html').
Reading
- getInfo([opts]) — Drive info:
{ key, url, writable, title, description, type, collaborative }.collaborativeis whether the drive currently accepts writer-access requests (see Collaboration). - stat(path[, opts]) —
{ isFile(), isDirectory(), size, mtime, ctime, metadata }. Real timestamps. - entry(path[, opts]) — The raw view entry
{ key, value: { blob, metadata } }. - get(path[, opts]) — Read a file.
opts.encodingis one ofutf8(default),binary,base64,hex, orjson(parses and returns the object). - readFile(path[, opts]) — Alias of
get. - list(path[, opts]) — Entries under
path(a flat, recursive key listing). - readdir(path[, opts]) — Immediate children. Pass
{ includeStats: true }for{ name, stat }. - query(path[, opts]) — Backend-agnostic listing under a path/prefix.
- diff(other[, opts]) — Changes vs another version (Hyperdrive only; empty on Autobase for now).
var drive = nomad.fs.drive('hyper://abc123../')
var manifest = await drive.get('/index.json', 'json') // parsed object
var st = await drive.stat('/index.json') // st.mtime, st.ctime, st.size
var html = await drive.readFile('/index.html')
var posts = await drive.query('/posts/')
Writing
- put(path, data[, opts]) — Write a file.
datamay be a string, Buffer, or (withencoding:'json') an object. - writeFile(path, data[, opts]) — Alias of
put. - del(path[, opts]) / unlink(path[, opts]) — Delete a file.
- mkdir(path[, opts]) / rmdir(path[, opts]) — Create / remove a directory.
- copy(src, dst[, opts]) / rename(src, dst[, opts]) — Works within and across drives.
var drive = nomad.fs.drive('hyper://abc123../')
await drive.writeFile('/notes.txt', 'hello')
await drive.put('/data.json', { hi: true }, { encoding: 'json' })
await drive.rename('/notes.txt', '/notes-2.txt')
await drive.del('/data.json')
Writes require the drive to be writable — a Hyperdrive you own, or an Autobase drive you are a writer of.
Watching
nomad.fs.watch(url[, pathSpec][, onChanged])
Watch a drive (or a path prefix) for changes. Returns an EventTarget that emits changed.
var drive = nomad.fs.drive('hyper://abc123../')
drive.watch('/posts/', () => rerender())
// or url-first:
nomad.fs.watch('hyper://abc123../', () => rerender())
Drive lifecycle
These are top-level nomad.fs methods. createDrive/createCollaborativeDrive/forkDrive return a scoped FsDrive.
- nomad.fs.createDrive([opts]) — Create a new drive.
opts:{ title, description, collaborative }. Locked / single-writer unlesscollaborative: true. - nomad.fs.createCollaborativeDrive([opts]) — Same as
createDrivebut defaults to accepting writers; equivalent tocreateDrive({ ..., collaborative: true }). - nomad.fs.forkDrive(url[, opts]) — Fork an existing drive into a new one.
- nomad.fs.configure(url, settings[, opts]) — Update the drive manifest (
title,description,type,thumb,links) and/orcollaborative(see below). Also on the scoped handle asdrive.configure(settings). - nomad.fs.isCollaborativeDrive(url) — Boolean.
// Every new drive is a multi-writer-capable Autobase, but "collaborative" is a policy flag.
var drive = await nomad.fs.createDrive({ title: 'Notes' }) // locked (single-writer)
await nomad.fs.configure(drive.url, { collaborative: true }) // unlock later — SAME URL
Collaboration
Every drive is an Autobase and can gain writers without its URL ever changing. Whether it accepts
writers is a policy flag, collaborative, locked by default:
- A locked drive ignores writer-access requests entirely (it doesn’t advertise the request channel).
- Unlock it any time via
configure(url, { collaborative: true })— or automatically by inviting/approving a writer. The URL is unchanged, so a drive can start private and open up later. - The owner is always the sole gate on who becomes a writer;
collaborativejust controls whether unsolicited requests are accepted.
Writer management (owner-side unless noted; also available on the scoped drive handle):
- createInvite(url[, opts]) — Returns an invite URL to share. Unlocks the drive.
- claimInvite(inviteUrl[, opts]) — Recipient redeems an invite to request write access.
- requestAccess(url[, opts]) — Ask an already-collaborative drive for write access.
- listRequests(url) — Pending requests
[{ writerKey, profileUrl }]. - watchRequests(url[, onChanged]) —
EventTargetemittingchangedwhen a request arrives. - approveRequest(url, writerKey[, opts]) / denyRequest(url, writerKey) — Owner accepts/rejects.
- removeWriter(url, writerKey) — Revoke a writer.
- listWriters(url) —
[{ writerKey, profileUrl }].
var drive = nomad.fs.drive('hyper://abc123../')
var invite = await drive.createInvite() // share this; drive is now collaborative
// …recipient: await nomad.fs.claimInvite(invite)
await drive.approveRequest(writerKey)
Draft Mode
Stage edits to a drive privately — synced across your own devices but invisible to every other
peer — until you Publish. Backed by your Vault (ADR-0012), so a Draft never enters the drive’s
replicated log. While Draft Mode is on, put/writeFile/del stage instead of going live; reads stay
on the published view unless you pass { draft: true } to preview the merge.
- beginDraft(url) / endDraft(url) — Turn a drive’s Draft Mode on/off.
{ on }. - draftStatus(url) —
{ mode, changes: [{ path, op: 'put'|'del', conflict }] }.conflictis true when the base moved under a staged path since it was staged. - publishDraft(url[, opts]) — Fold the Draft onto the drive.
opts.pathsrestricts to a subset/subtree;opts.forceapplies conflicting paths anyway.{ published, conflicts }. - discardDraft(url[, opts]) — Drop the Draft (or
opts.pathssubset).{ discarded }. - watchDraft(url[, onChanged]) —
EventTargetemittingchangedwhen the Draft mutates. - setDraftPreview(url, on) — Render the merged Draft in this tab (used by the browser chrome’s “Preview Draft” toggle). Local-only; never replicated.
var drive = nomad.fs.drive('hyper://abc123../')
await drive.beginDraft()
await drive.writeFile('/posts/2026-07-04-hi/post.json', '{"title":"Hi"}') // staged, not replicated
var preview = await drive.readFile('/posts/2026-07-04-hi/post.json', { draft: true })
await drive.publishDraft({ paths: ['/posts/2026-07-04-hi/'] }) // this post goes live
Publish works only on a device that can write the drive; on a device you merely paired into, Publish is rejected — draft on your phone, Publish from the owning device.
Import / export
- importFromFilesystem(opts) / exportToFilesystem(opts) / exportToDrive(opts) — Bulk copy between a drive and the local filesystem (or another drive).
Notes
- Nomad
hyper://is a Nomad dialect. Nomad drives are Autobase-backed and are not readable by generichyper://clients.