ReboFilesDocs
Start3
What is ReboFiles
Getting started
Demos to run
Put it in your app6
React component
Headless: your own UI
Without a framework
Styling and themes
Keyboard and accessibility
Languages
Files9
Uploads
Previews and the viewer
Search
Live changes and large folders
Comments
Starred and fields
Versions, trash and space
Download as .zip
Word documents
The server14
The route
Frameworks and sign-in
Who may do what
Storage: S3, GCS, Azure and disk
Checking uploads
Metadata: SQLite and Postgres
Database schema
Sharing and links
Activity and notifications
Hooks, logs and limits
Retention, holds and export
Desktop and other origins
Sync a folder
Going to production
Together1
AI assistants (MCP)
Reference5
Packages
Components and hooks
Client API
HTTP routes
Status and roadmap
↑↓ to move↵ to openEsc to close38 pages

The server The route

The route

createDriveHandler takes a Request and gives a Response. Every route of the drive lives under one path, and none carries the bytes but on a disk: the browser is given addresses to send them to and read them from.
Next.js

In Next.js

app/api/files/[...path]/route.ts
import { byRoles, createDriveHandler, s3OptionsFrom, s3Storage, sqliteMetadata } from '@rebofiles/server';
import { session } from '@/lib/auth'; // your own

const handle = createDriveHandler({
  storage: s3Storage(s3OptionsFrom(process.env)),
  metadata: await sqliteMetadata('drive.sqlite'),
  identify: async (request) => {
    const user = await session(request);
    return user ? { id: user.id, admin: user.isAdmin } : null;
  },
  authorize: byRoles,
  secret: process.env.REBOFILES_SECRET!,
  base: '/api/files',
});

export { handle as GET, handle as POST, handle as PATCH, handle as PUT, handle as DELETE };
Node

On node:http

nodeHandler makes it a listener, for node:http, Express and what is built on it.

server.ts
import { createServer } from 'node:http';
import { createDriveHandler, diskStorage, everyone, memoryMetadata, nodeHandler } from '@rebofiles/server';

const handle = createDriveHandler({
  storage: diskStorage({ folder: './storage', secret: process.env.REBOFILES_SECRET! }),
  metadata: memoryMetadata(),
  ...everyone(), // only on your own machine: no sign-in, everyone may do everything
  secret: process.env.REBOFILES_SECRET!,
  base: '/api/files',
});

createServer(nodeHandler(handle)).listen(3000);
Options

Every option

OptionTypeWhat it does
storageStorageWhere the bytes are: s3Storage(s3OptionsFrom(process.env)), gcsStorage(…), azureStorage(…) or diskStorage({ folder, secret }).
metadataMetadataStoreWhere the entries are: memoryMetadata(), sqliteMetadata(path) or postgresMetadata(pool).
identify(request) => User | nullWho is asking, from your session; null answers 401. { id, admin: true } may configure the drive with byRoles.
authorize(user, action, entry, drive) => booleanAsked on every route; false answers 403. byRoles, or rules of yours around it.
secretstringWhat tokens are signed with: long, random, the same in every process.
basestringThe path the routes are under: /api/files.
top'own' | 'everyone'What My files shows at the top: each person’s own (the default), or everything one may read, for one drive of a whole team.
maxSizenumberThe largest file taken, in bytes; 5 GiB unless said.
settingsPartial<DriveSettings>The defaults until someone changes them: language, theme, brand colour, pictures, quota, trashDays, and company, the name the page of a request for files shows.
cors{ origins: string[] }Pages of other origins that may use the routes: the desktop window, a page of another domain.
onShare(event) => voidEach share kept, with who, by whom and whether it is the first: to tell the person.
beforeUpload(upload, drive) => boolean | voidAsked before an upload is given addresses, with its name, type, size and folder: throw a Refused, or return false, to refuse it.
inspect(upload, drive) => boolean | voidAsked once the bytes arrived, before the file is listed, with a read of them: checkType(), clamav(), or yours. A refusal takes the bytes away.
livefalse | { poll }Changes told at /events as they happen; those of another process on the same store within poll ms (2000). false for none.
deriveDeriveWhat the server makes of a file once kept, for what the page did not: pictures, a placeholder, its text (of a PDF, a Word document, a workbook; of a picture with ocr()). deriveAll() of @rebofiles/server/derive.
onChange({ activity, entry }) => voidEach thing done to the drive, as its activity says it: for webhooks, an audit, another index, counting space.
onComment({ comment, entry, mentions, by }) => voidEach comment made, with whom it names with an @: to tell them.
onReceived({ entry, from, item, request, got }) => voidEach file that came through a request for files, with who sent it, for which document, and what came so far: to tell the owner.
onReview({ entry, status, reason, from, by }) => voidEach file accepted or returned: to tell whoever sent it.
reminders{ send(reminder) }Whom a request was sent to, reminded every remind days of what is missing, and at once: mailReminders({ mail: resend(…), url }), or yours.
checkfalse | ({ entry, item, text }) => …What is written in what came, against the words and find of its document: checkWritten unless yours; false for none.
expiringfalse | { field, days, onExpiring }The owner of a file whose field expires comes within days (15) told in the bell, and onExpiring; false for none.
onRead({ action, user, link, entries, path }) => voidEach thing opened, seen or taken: a folder listed, a file seen or downloaded, an archive; by someone or through a link. For an audit.
people{ search(query, user), names?(ids, user) }Who is who, from your directory: the people and groups that match what is typed where something is shared, and their names, shown in place of ids.
loggerLoggerWhere what went wrong and was not answered for is written: pino(), console unless given.
onRequest(done) => voidAfter every request, its route, status, time and who: for metrics, as openTelemetry(meter).
limitsLimits | falseHow many uploads, archives, text searches, wrong passwords of a link and requests each may make; a 429 past them.
limiterLimiterWhere the counts of limits are kept: in the process unless given; Redis for more than one.
clientIp(request) => stringThe address of who asks, to count wrong passwords of a link by it too.
now() => DateThe clock, for tests.
newId() => stringThe ids of new entries; random UUIDs unless given.
Speed

Held in memory

The drive is read once and held, and read again only when the store says it changed, by this process or another: a listing of 20,000 entries went from 36 ms to under 1. Fine for a team and well beyond; not for millions of entries.

API

Every route

HTTP routes lists them, as the code keeps them.