File storage
Initialize storage in your application with a local memory adapter or your own persistence.
@mp-lb/inproduct-storage is an independent ESM package for storing objects by key.
It requires Node.js 22.13 or newer. It does not require the InProduct backend scaffold,
an account, environment variables, or a hosted runtime. React is optional for the headless hook.
Install and initialize
Install the published package into your application:
pnpm add @mp-lb/inproduct-storageFor development within this repository, use the workspace dependency and build it:
pnpm --filter <consumer-package> add @mp-lb/inproduct-storage@workspace:*
pnpm --filter @mp-lb/inproduct-storage buildimport { createStorage, createMemoryStorageAdapter } from '@mp-lb/inproduct-storage';
const storage = createStorage({
adapter: createMemoryStorageAdapter(),
prefix: 'receipts',
maxBytes: 5 * 1024 * 1024,
});
await storage.put('2026/order-123.txt', {
body: new TextEncoder().encode('Order 123'),
contentType: 'text/plain',
});
const receipt = await storage.get('2026/order-123.txt');
if (receipt) console.log(new TextDecoder().decode(receipt.body));
await storage.delete('2026/order-123.txt');Configure storage
Pass an adapter to createStorage. The optional prefix and maxBytes values
are captured at initialization; changing the original options later has no effect.
prefixnamespaces keys: the example storesreceipts/2026/order-123.txtin the adapter. Omit it for no namespace. An empty prefix is invalid.maxByteslimits each write's byte length. It must be a nonnegative safe integer; zero allows empty objects only. Omit it for no limit. It does not limit reads or total stored bytes.
Keys and prefixes are case-sensitive, nonempty slash-separated names. Empty segments,
. and .. segments, backslashes, and control characters are rejected. Percent escapes
are literal and Unicode is not normalized. Adapters must treat keys as opaque names,
not decode them into filesystem paths. Prefixes are naming conveniences, not tenant
isolation; your application must authorize access before calling storage.
Read, replace, and delete
put stores a Uint8Array body and optional caller-supplied contentType. It replaces
the complete object at an existing key. Content type is metadata, not file validation.
get returns the object, or undefined when absent. delete succeeds even when the
key is already absent. All three operations are asynchronous.
Invalid configuration throws synchronously. Invalid keys reject operations with
TypeError; writes exceeding maxBytes reject with RangeError. Adapter failures
propagate unchanged, so permission or network failures must not be treated as absence.
The shipped TypeScript declarations and declaration maps are the API reference.
Memory adapter and persistence
Memory storage is ephemeral. All objects are lost when the process exits. Use
createMemoryStorageAdapter for local development and tests. Each instance owns a
separate store; share an adapter between facades when you want them to share data.
Bytes and metadata are copied on writes and reads, including Node Buffer input, so
caller mutations cannot change stored objects. Total memory use is unbounded.
For durable storage, implement the exported StorageAdapter contract and pass it to
createStorage. Your adapter owns credentials, persistence, concurrency, and transport.
It must preserve bytes and content type, isolate stored data from caller mutation,
replace objects on put, make delete idempotent, and return undefined only for
missing objects on get.
The low-level object API has no listing. It does not provide streaming, signed URLs, retries, or metadata database. Objects are buffered in memory. Your application owns authentication, authorization, and file validation.
Build a backend-backed file manager
Create the storage service on your Node backend and mount the library's HTTP handler:
import { createServer } from 'node:http';
import { createFileStorage } from '@mp-lb/inproduct-storage';
import { createDiskFileStorageAdapter, createFileStorageHandler } from '@mp-lb/inproduct-storage/node';
const storage = createFileStorage({ adapter: createDiskFileStorageAdapter({ directory: './data/files' }) });
const server = createServer(createFileStorageHandler({
storage,
authorize: (request) => yourExistingAdminCheck(request),
}));yourExistingAdminCheck is your own authorization function, not a library export.
The handler requires authorization for every operation. Use your application's
configured port and existing authentication; scope storage to the appropriate tenant.
The default path is /api/files. Serve it on the frontend's origin or behind your proxy.
The frontend only needs an HTTP client, a hook and your table/forms:
import { createFileStorageClient } from '@mp-lb/inproduct-storage';
import { useFileStorage } from '@mp-lb/inproduct-storage/react';
const client = createFileStorageClient('/api/files');
export const FileStorageSection = () => {
const storage = useFileStorage(client);
// Render storage.files, loading, busy and error.
// Wire uploads to storage.uploadFile(file), filters to storage.setFilters(...),
// and actions to storage.edit(...), storage.download(id), storage.remove(id).
return <p>{storage.usage?.count ?? 0} files</p>;
};The hook handles refresh, stale responses, pagination and browser transfers. React
18 or 19 is required only for /react. The base entry point does not import React
or Node built-ins. Import /node only on the backend. Operations return a success
boolean and expose failures through error; no repeated event-handler catches needed.
Durable local files
The disk adapter keeps bytes and metadata in the configured directory across browser reloads and backend restarts. Use one adapter instance per directory in a single process. Writes use atomic replacement and are serialized within the instance. It buffers file contents and scans records for listing; use another adapter for large collections, distributed processes or shared/network filesystems. Keep data outside public/static roots. Atomic replacement is not a backup or a power-loss guarantee.
The service defaults to a 10 MiB file limit; the handler bounds request bodies to 11 MiB including multipart overhead. Increase both limits together when necessary. Duplicate names produce separate IDs. Deletion removes bytes immediately. Edits change metadata. Search covers names/tags; exact category/tag filters combine with search. Usage totals remain independent of filters. Files are not malware-scanned; no audit ledger, retention policy or total-memory/disk quota is supplied.
For tests and ephemeral prototypes, createFileStorage() without an adapter still
uses isolated memory storage. MyShop uses the disk adapter on its backend, never
in-memory file operations in the browser. See MyShop for the complete
frontend/backend demo and its local-only access boundary.