Media
Give files durable IDs and project-scoped libraries. Keep storage paths, browser folders and permission checks separate.
On this page
Source-backed MarkdownSet up storage and media tables
Storage owns the bytes. Media owns MediaLibrary, MediaFile and MediaItem records. The application supplies a storage disk and a database containing those tables.
The lab uses temporary storage and installs the three models into a generated test database. An application must use committed migrations and a durable disk. A browser placement is a data record; the framework does not automatically install a Vue media browser.
Copy the project-files example
Use the independent app directory and test configuration from Installation. The example uses two trusted project identities to demonstrate scoped reads.
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/media/examples/. examples/Run it
The result contains /Briefs/brief.txt, a numbered duplicate /Briefs/brief-2.txt, and true values for private visibility, cross-project rejection and deletion cleanup. The generated database and temporary files are removed afterwards.
npx tsx examples/runProjectMedia.tsGive a project its own library
libraryFor() finds or creates a library identified by scopeType, scopeId and key. Your app decides what a project is and verifies access before resolving that library.
A library can supply a default disk and path prefix. Treat scope values as trusted application inputs, not arbitrary values copied from a request body. Library identity is not an access-control check.
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 { createGeneratedTestDatabase } from '@db3.ai/app/db/test/db';
import { MediaFile, MediaItem, MediaLibrary } from '@db3.ai/app/media';
import { App } from '@db3.ai/app/server';
/**
* Stores a project brief, makes a browser placement and proves library isolation.
*
* The project identity is trusted lab input. Real endpoints must authorize the
* project before resolving its library, and scope file reads to that library.
*
* @returns Stable results after removing the lab's database and temporary files.
*/
export async function runProjectMedia() {
const root = await mkdtemp(join(tmpdir(), 'db3-media-guide-'));
try {
const database = await createGeneratedTestDatabase('media_guide');
const application = new App({ db: database.db, storage: { disks: { local: { driver: 'local', root } } } });
try {
await application.db.install(MediaLibrary, MediaFile, MediaItem);
const library = await application.media.libraryFor({ scopeType: 'project', scopeId: 'client-a', name: 'Client files', pathPrefix: 'projects/client-a' });
const otherLibrary = await application.media.libraryFor({ scopeType: 'project', scopeId: 'client-b' });
const stored = await application.media.storeVisibleFileStream({ library, name: 'brief.txt', stream: Readable.from(['Client brief']), mimeType: 'text/plain', folderPath: '/Briefs', source: 'upload' });
const duplicate = await application.media.storeVisibleFile({ library, name: 'brief.txt', contents: 'Revised brief', mimeType: 'text/plain', folderPath: '/Briefs' });
const file = await MediaFile.where({ id: stored.file.id, library }).firstOrFail();
const text = (await application.media.readFile(file)).toString('utf8');
const crossProjectRejected = await MediaFile.where({ id: file.id, library: otherLibrary }).first() === null;
const result = { text, path: stored.item.path, duplicatePath: duplicate.item.path, private: file.visibility === 'private', crossProjectRejected };
const originalPath = file.path!;
await application.media.deleteFile(file);
return { ...result, deleted: await application.storage.missing(originalPath), placementRemoved: await MediaItem.where('id', stored.item.id).first() === null };
} finally {
try { await application.close(); } finally { await database.destroy(); }
}
} 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 runProjectMedia(), null, 2));
}
Store a file or place it in a folder
storeFile() creates a managed file without a browser item. storeVisibleFile() also creates a placement and any missing folders. Use storeFileStream() or storeVisibleFileStream() for an assembled upload stream.
Browser paths can change while the file ULID remains stable. Duplicate placement names are numbered rather than replacing an existing file. ensureFolder() and attachFile() work with existing library/file records.
Do not supply a path or byte count from untrusted input without validating it. Ordinary stream storage does not inspect the file contents or implement a resumable upload protocol.
Read a file within its authorized library
Load the file with both its ID and authorized library, as the example does with MediaFile.where({ id, library }). Only then call readFile() or readFileStream().
media.file(id) is an unscoped lookup, not an authorization check. A guessed ULID must not let one project read another project’s files. Your route also owns MIME policy, response headers and download disposition.
Validate image uploads and render variants
For untrusted images, perform a cheap signature/type allowlist at the upload boundary, then use storeReconstructedVisibleImageStream(). The processor decodes and reconstructs the file, stripping metadata and trailing bytes before saving media records.
renderImage() generates responsive variants through the image processor and disposable cache. imageVariantOptionsFromSearchParams() limits the request options; supported widths are 1–4096. Decoding is bounded by a 40-megapixel input limit and 20-second processing timeout.
Configure cache disk/prefix under config.media.images. Authorize the original file before rendering a variant. The project-text-file lab does not exercise reconstruction or image caching; the framework has separate processor and renderer tests.
Choose private or public delivery deliberately
Files default to private storage visibility. A browser-visible item is not a public file, and setting metadata to public does not install a public HTTP route.
Private delivery needs an authenticated, scoped route. Public delivery needs an explicit policy for what may be published. Media does not check whether a file is safe to display as HTML, whether an SVG contains active content, or whether your app is permitted to share its contents.
Delete and recover
Before deleteFile(), check app-owned references and whether deletion is allowed. The manager removes generated variants and source bytes, then the managed row; foreign keys remove browser placements.
Database and object storage are not one transaction. Plan for partial failures and orphan reconciliation in an application that needs stronger guarantees. Reconstructed-image failures have their own partial-object cleanup, but that is not a claim of atomicity for every file operation.
Testing
Create a tests/media directory and save the test below as runProjectMedia.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. This database lab needs the same test-only SQL credentials when run through Vitest.
import { expect, it } from 'vitest';
import { runProjectMedia } from '../../examples/runProjectMedia';
it('runs the project media guide with scoped SQL reads, real streams and cleanup', async () => {
expect(await runProjectMedia()).toEqual({
text: 'Client brief', path: '/Briefs/brief.txt', duplicatePath: '/Briefs/brief-2.txt',
private: true, crossProjectRejected: true, deleted: true, placementRemoved: true,
});
});
Run and extend the test
Run with dedicated SQL credentials and local temporary storage. Try a different folder or a duplicate filename. Keep the cross-project read test: it proves the scoped lookup used by this example, not the application’s membership policy.
npx vitest run tests/media/runProjectMedia.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.tsCoverage and reference
Tested here: scoped libraries, streamed storage, browser placement, duplicate names, scoped reads, private defaults and deletion cleanup. The private-file guide adds real HTTP authentication, input limits, headers and foreign-user rejection.
Still to build: browser integration, source-backed image-processing walkthroughs and orphan reconciliation. Image APIs also have the @db3.ai/app/media/image public subpath.
Streams a project file, places it in a folder, resolves duplicate names, scopes reads and verifies file/placement cleanup.
The guide test passes against the real framework components.packages/app/src/media/tests/examples/runProjectMedia.test.tsThis test command requires the framework repository. Use the walkthrough commands in an installed application.
Environment: A dedicated MariaDB/MySQL test account and temporary local disk. No real uploads or provider requests.