Storage

Write files to a named disk, stream large payloads and keep paths relative to storage. Add Media when files need durable identities and ownership metadata.

On this pageSource-backed Markdown

Configure a disk

Use storage.default and storage.disks in App options. The example creates a local exports disk in a temporary directory. In an application, choose a persistent absolute root or deliberately resolve one relative to the process working directory.

Without configuration, the local disk writes under storage/app relative to the current working directory. That is convenient locally, but a container filesystem is not automatically durable storage. A standalone script can also construct Storage directly.

Copy the export example

Use the independent app directory from Installation. This lab does not need a database, an S3 account or an HTTP server.

Copy the shipped example
bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/storage/examples/. examples/

Run it

The script writes a JSON report and streamed CSV, reads them, lists their relative paths and deletes the report. The result should include traversalRejected: true and deleted: true. Its temporary disk is removed afterwards.

Run the lab
bash
npx tsx examples/runStorage.ts

Write and read files

write() and put() write text, bytes or supported stream contents. Use readToString() for text and readToBuffer() or get() when you deliberately want the whole file in memory. read() returns a stream, not a string.

Choose a named disk with app().storage.disk("exports"); normal calls on app().storage use the default. drive() is an alias for disk().

examples/runStorage.ts
ts
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Readable } from 'node:stream';
import { pathToFileURL } from 'node:url';
import { App } from '@db3.ai/app/server';

/**
 * Writes, reads, lists and removes an export on an isolated real local disk.
 *
 * This runnable lab owns its temporary directory, not application storage.
 * The same storage calls can use a configured app().storage in feature code.
 *
 * @returns Stable observations; no temporary absolute path is exposed.
 */
export async function runStorage() {
	const root = await mkdtemp(join(tmpdir(), 'db3-storage-guide-'));
	const application = new App({ storage: { default: 'exports', disks: { exports: { driver: 'local', root } } } });
	try {
		const disk = application.storage.disk('exports');
		await disk.write('reports/latest.json', JSON.stringify({ status: 'ready' }), { mimeType: 'application/json' });
		const report = JSON.parse(await disk.readToString('reports/latest.json'));
		await disk.writeStream('reports/export.csv', Readable.from(['name\n', 'Ada\n']), { mimeType: 'text/csv' });
		const csv = await disk.readToString('reports/export.csv');
		const files = (await disk.list('reports').toArray()).filter(entry => entry.isFile).map(entry => entry.path);
		let traversalRejected = false;
		try { await disk.write('../outside.txt', 'not allowed'); } catch { traversalRejected = true; }
		await disk.delete('reports/latest.json');
		return { report, csv, files, traversalRejected, deleted: await disk.missing('reports/latest.json') };
	} finally {
		try { await application.close(); } finally { await rm(root, { recursive: true, force: true }); }
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
	console.log(JSON.stringify(await runStorage(), null, 2));
}

Stream uploads and exports

Use writeStream() and readStream() for large payloads. Consume the returned stream or pass it to your HTTP response, with proper stream-error handling. The wrapper does not buffer the whole file before passing it to the driver.

The example uses a short CSV stream so the result is easy to inspect. Apply upload limits, client-disconnect handling and temporary-upload cleanup in the HTTP adapter. A streamed API alone does not provide those policies.

List and inspect stored files

exists() and missing() check a path. size(), lastModified() and mimeType() expose file metadata. list() returns a lazy async iterable; toArray() intentionally collects it. Use deep: true only when descendants are needed.

delete() removes the specified file. Treat listing paths as disk-relative names, not application IDs or proof that the caller may read them. Use a scoped Media record when you need that ownership layer.

Use S3-compatible storage

Configure driver: "s3", bucket and region. Set endpoint for an S3-compatible service, and forcePathStyle when that provider requires it. Keep credentials in your environment or the supported provider credential chain, never browser configuration.

The service-owned reference includes the full S3 disk configuration. The local lab tests the framework workflow, not credentials, permissions, listing consistency or multipart behaviour on your remote provider. Run a small upload/read/delete smoke test in a dedicated bucket before production.

Paths, URLs and private delivery

File paths are relative to the disk root. Absolute paths, parent traversal and null bytes are rejected. Empty paths are allowed for listing the root, not for writing a file. This is path validation, not a sandbox against untrusted filesystem changes or an authorization system.

path() returns a local filesystem path; remote disks cannot supply one. url() needs a configured public URL or provider support and only constructs a URL. It does not create a route, sign a private download or grant permission.

For private downloads, authorize the caller first, then stream the allowed file with deliberate content type and disposition headers. Never publish a private storage root just to make url() work.

Testing

Create a tests/storage directory and save the test below as runStorage.test.ts. It imports the example you copied into examples/. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.

tests/storage/runStorage.test.ts
ts
import { expect, it } from 'vitest';
import { runStorage } from '../../examples/runStorage';

it('runs the storage guide on a real isolated local disk', async () => {
	expect(await runStorage()).toEqual({
		report: { status: 'ready' }, csv: 'name\nAda\n',
		files: ['reports/export.csv', 'reports/latest.json'],
		traversalRejected: true, deleted: true,
	});
});

Run and extend the test

The copied test uses a real temporary local disk and removes it in finally. Add a named disk or another file and assert the result. Use dedicated test storage, not a production bucket, for destructive cases.

Run your copied test and check types
bash
npx vitest run tests/storage/runStorage.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts

Coverage and reference

Tested here: named local disks, JSON/text, streamed writes, listing, traversal rejection and deletion. The linked export recipe tests producer interruption and recovery; the private-file guide tests authenticated downloads.

Still to build: socket-disconnect and provider-specific S3 integration trials. Those are not implied by local labs passing.

Behaviour tested Executed by the documentation maintenance gate
What this does

Writes JSON and a streamed CSV, lists the files, rejects traversal and removes a file on a real local disk.

Expected outputThe guide test passes against the real framework components.
Behaviour testpackages/app/src/storage/tests/examples/runStorage.test.ts

This test command requires the framework repository. Use the walkthrough commands in an installed application.

Environment: Node.js and an isolated temporary directory; no database or remote storage account.