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 Frameworks and sign-in

Frameworks and sign-in

createDriveHandler takes a Request and gives a Response, so each framework needs a few lines: hand it the request as it came, before anything reads its body. Every example here is a file of examples/, served on a port and asked over HTTP by its tests: a folder made, a file of two parts uploaded and read back, and a change told as it happens.
Setup

One drive for all of them

The bytes in a bucket when S3_BUCKET says one, else on the disk; the entries in SQLite. The same in each example.

examples/drive.ts
import { mkdirSync } from 'node:fs';
import { type DriveServerOptions, byRoles, createDriveHandler, diskStorage, s3OptionsFrom, s3Storage, sqliteMetadata } from '@rebofiles/server';

/** Long and random, the same in every process: `openssl rand -hex 32`. */
const secret = process.env.REBOFILES_SECRET ?? (process.env.NODE_ENV === 'production' ? undefined : 'a secret for this computer only');
if (!secret) throw new Error('Set REBOFILES_SECRET: long, random, the same in every process.');

export async function createDrive(identify: DriveServerOptions['identify'], extra: Partial<DriveServerOptions> = {}) {
  const folder = process.env.REBOFILES_FOLDER ?? './storage';
  mkdirSync(folder, { recursive: true });
  return createDriveHandler({
    storage: process.env.S3_BUCKET ? s3Storage(s3OptionsFrom()) : diskStorage({ folder: `${folder}/files`, secret: secret! }),
    metadata: await sqliteMetadata(`${folder}/drive.sqlite`),
    identify,
    authorize: byRoles,
    secret: secret!,
    base: '/api/files',
    ...extra,
  });
}
App Router

Next.js

One route handler for every route, in Node: it signs with node:crypto.

app/api/files/[...path]/route.ts
import { createDrive } from '../../../../../drive';
import { identify } from '../../../../../auth/dev';

export const runtime = 'nodejs';
// Never cached: every answer is of the one asking, and the events go on as long as the page watches.
export const dynamic = 'force-dynamic';

const handle = await createDrive(identify);

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

Express

Mounted before express.json(): the drive reads its own bodies.

examples/express/server.ts
import express from 'express';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const app = express();
  app.use('/api/files', nodeHandler(await createDrive(identify)));
  // The rest of your application, its parsers among it, after.
  app.use(express.json());
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const app = await createApp();
  app.listen(3000, () => console.log('The drive is at http://localhost:3000/api/files'));
}
Node · Bun · Deno

Hono

Hono’s request is the one the drive takes.

examples/hono/server.ts
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const handle = await createDrive(identify);
  const app = new Hono();
  app.all('/api/files/*', (c) => handle(c.req.raw));
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  serve({ fetch: (await createApp()).fetch, port: 3000 }, () => console.log('The drive is at http://localhost:3000/api/files'));
}
Node

Fastify

In a plugin of its own, where no parser of Fastify’s reads the body, and the answer written on the raw response.

examples/fastify/server.ts
import Fastify from 'fastify';
import { nodeHandler } from '@rebofiles/server';
import { identify } from '../auth/dev';
import { createDrive } from '../drive';

export async function createApp() {
  const drive = nodeHandler(await createDrive(identify));
  const app = Fastify();
  await app.register(async (scope) => {
    // Every body left as it came, in this plugin only: the drive reads it.
    scope.removeAllContentTypeParsers();
    scope.addContentTypeParser('*', (_request, _payload, done) => done(null));
    scope.all('/api/files/*', (request, reply) => {
      reply.hijack();
      return drive(request.raw, reply.raw);
    });
    scope.all('/api/files', (request, reply) => {
      reply.hijack();
      return drive(request.raw, reply.raw);
    });
  });
  return app;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const app = await createApp();
  await app.listen({ port: 3000 });
  console.log('The drive is at http://localhost:3000/api/files');
}
Sign-in

Who asks, with Auth.js

examples/auth/authjs.ts
import type { User } from '@rebofiles/server';
import { auth } from '@/auth';

export async function identify(): Promise<User | null> {
  const session = await auth();
  const id = session?.user?.id;
  return id ? { id, admin: session.user?.email === process.env.DRIVE_ADMIN } : null;
}

Pass it as identify. The page sends the session cookie with every request of its own origin, so nothing else is needed in the browser.

Sign-in

Who asks, with Clerk

examples/auth/clerk.ts
import { createClerkClient } from '@clerk/backend';
import type { User } from '@rebofiles/server';

const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY, publishableKey: process.env.CLERK_PUBLISHABLE_KEY });

export async function identify(request: Request): Promise<User | null> {
  const state = await clerk.authenticateRequest(request, { authorizedParties: [process.env.APP_ORIGIN ?? 'http://localhost:3000'] });
  const { userId, orgRole } = state.toAuth() ?? {};
  return userId ? { id: userId, admin: orgRole === 'org:admin' } : null;
}
JWT

A token of your own

For an application with its own sign-in, and for the desktop application, which sends `Authorization: Bearer`.

examples/auth/jwt.ts
import type { User } from '@rebofiles/server';
import { jwtVerify } from 'jose';

const key = new TextEncoder().encode(process.env.SESSION_SECRET);

export async function identify(request: Request): Promise<User | null> {
  const bearer = /^Bearer (.+)$/.exec(request.headers.get('authorization') ?? '')?.[1];
  const cookie = /(?:^|;\s*)session=([^;]+)/.exec(request.headers.get('cookie') ?? '')?.[1];
  const token = bearer ?? cookie;
  if (!token) return null;
  try {
    const { payload } = await jwtVerify(token, key, { algorithms: ['HS256'] });
    return payload.sub ? { id: payload.sub, admin: payload.role === 'admin' } : null;
  } catch {
    return null;
  }
}

Tried, and not: the token of your own is checked by the tests, with a forged one refused. Auth.js and Clerk are written to their documentation and not run here, as they need accounts of theirs.

Errors

When something fails

The body is not JSON.

A parser of the framework read the body first: mount the drive before it, or out of its reach, as the examples do.

No GET /api/files/…

The path the framework gave is not the whole one. nodeHandler takes originalUrl when there is one (Express); otherwise mount it where base says.

Sign in first.

identify found no one: the cookie did not come (another origin needs cors and a token), or the session has no id.