Storage & Uploads
Storage & File Delivery
Storage & File Delivery — Storage & Uploads documentation for BCP Framework.
Last updated: 2026-08-29T01:11:41
Storage and File Delivery
BCP Framework 0.1.25 introduced the server-only StorageAdapter abstraction and production file-delivery helpers through bcp/server.
BCP Framework 0.1.26 completes the next storage milestone with:
- additive streaming reads/writes,
- capability discovery,
- local streaming I/O,
- S3-compatible storage,
- production multipart-to-storage streaming,
- streamed full/ranged file delivery.
Existing adapters written for 0.1.25 remain valid because putStream() and readStream() are optional.
Local storage
import {
createLocalStorage,
} from "bcp/server";
export const storage =
createLocalStorage({
directory:
"./uploads",
});Storage keys are logical relative paths:
avatars/user-101.webp
documents/2026/report.pdfAbsolute paths, traversal segments and BCP's reserved metadata directory are rejected.
S3-compatible storage
import {
createS3Storage,
} from "bcp/server";
export const storage =
createS3Storage({
bucket:
process.env.S3_BUCKET!,
region:
process.env.S3_REGION!,
endpoint:
process.env.S3_ENDPOINT,
accessKeyId:
process.env.S3_ACCESS_KEY_ID,
secretAccessKey:
process.env.S3_SECRET_ACCESS_KEY,
});The same application-facing StorageAdapter surface works with local or S3-compatible backends.
Read more: S3-Compatible Storage.
Storage adapter contract
interface StorageAdapter {
capabilities?: Partial<
StorageAdapterCapabilities
>;
put(
key: string,
value: StorageWriteValue,
options?: StoragePutOptions
): Promise<StorageObjectMetadata>;
putStream?(
key: string,
stream: ReadableStream<Uint8Array>,
options?: StoragePutStreamOptions
): Promise<StorageObjectMetadata>;
stat(
key: string
): Promise<StorageObjectMetadata | null>;
read(
key: string,
options?: StorageReadOptions
): Promise<Uint8Array>;
readStream?(
key: string,
options?: StorageReadOptions
): Promise<StorageReadableStream>;
exists(
key: string
): Promise<boolean>;
delete(
key: string
): Promise<boolean>;
}The original buffered methods remain required. Streaming methods are additive optional capabilities.
Capabilities
import {
getStorageCapabilities,
} from "bcp/server";
const capabilities =
getStorageCapabilities(
storage
);The capability model currently reports:
{
streamingRead,
streamingWrite,
ranges,
signedUrls,
listing,
}createLocalStorage() and createS3Storage() in 0.1.26 report:
{
streamingRead: true,
streamingWrite: true,
ranges: true,
signedUrls: false,
listing: false,
}Signed URLs and listing are deferred to a later milestone.
Buffered writes
const stored =
await storage.put(
"notes/hello.txt",
"Hello BCP",
{
contentType:
"text/plain",
}
);put() does not overwrite an existing object unless overwrite: true is supplied.
Local storage persists content type and SHA-256 identity in sidecar metadata. Buffered S3 writes persist SHA-256 in S3 user metadata.
Streaming writes
Use the generic helper so application code works with native or legacy adapters:
import {
putStorageStream,
} from "bcp/server";
await putStorageStream(
storage,
"imports/data.bin",
request.body!,
{
contentType:
"application/octet-stream",
maxBytes:
100 * 1024 * 1024,
}
);When an adapter provides putStream(), BCP uses it. Legacy adapters fall back to collecting the stream and calling put().
The local adapter writes chunks directly to disk and calculates SHA-256 while streaming.
The S3 adapter uses the AWS SDK multipart upload helper so streams whose final size is not known at the start can still be uploaded without buffering the complete object.
Size limits and abort
await putStorageStream(
storage,
key,
source,
{
maxBytes:
50 * 1024 * 1024,
signal:
abortController.signal,
}
);If the stream exceeds maxBytes, BCP throws:
StorageError
code: STREAM_TOO_LARGE
status: 413Native adapters clean up/abort incomplete writes where supported.
Streaming reads
import {
readStorageStream,
} from "bcp/server";
const stream =
await readStorageStream(
storage,
"videos/demo.mp4"
);Inclusive byte ranges are supported:
const stream =
await readStorageStream(
storage,
"videos/demo.mp4",
{
start: 0,
end: 1023,
}
);Legacy adapters fall back to read() and expose the result as a stream.
Multipart uploads
Small forms can continue using the buffered FormData API:
const formData =
await parseMultipartFormData(
request
);
const file =
requireUploadedFile(
formData,
"file"
);
await storeUploadedFile(
file,
{
storage,
}
);For large production uploads, 0.1.26 adds direct multipart-to-storage streaming:
import {
storeMultipartFile,
} from "bcp/server";
const stored =
await storeMultipartFile(
request,
{
storage,
fieldName:
"file",
key:
"documents/report.pdf",
maxBytes:
100 * 1024 * 1024,
constraints: {
maxBytes:
80 * 1024 * 1024,
allowedTypes: [
"application/pdf",
],
allowedExtensions: [
".pdf",
],
},
}
);The multipart parser and storage write operate concurrently, so the complete file is not converted to a File / ArrayBuffer first.
Read more: File Upload.
File delivery
import {
createStorageResponse,
} from "bcp/server";
export function GET(
request: Request
) {
return createStorageResponse(
request,
storage,
"documents/report.pdf",
{
disposition:
"attachment",
downloadName:
"report.pdf",
}
);
}createStorageResponse() uses readStorageStream() and supports:
GETandHEAD,ETag/If-None-Match,Last-Modified/If-Modified-Since,If-Range,- single byte ranges with
206 Partial Content, 304 Not Modified,416 Range Not Satisfiable,- safe
Content-Dispositionfilenames, - configurable cache control.
The default cache policy is:
Cache-Control: private, max-age=0, must-revalidatePublic immutable caching must be enabled explicitly.
Multiple byte ranges are intentionally unsupported in 0.1.26.
Object metadata
interface StorageObjectMetadata {
key: string;
size: number;
contentType: string;
lastModified: Date;
etag: string;
checksumSha256?: string;
}checksumSha256 is optional because not every cloud provider exposes a portable whole-object SHA-256 value for multipart objects.
Do not assume an S3 ETag is an MD5 hash.
Storage errors
Current codes:
INVALID_KEY
OBJECT_NOT_FOUND
OBJECT_EXISTS
RANGE_NOT_SATISFIABLE
STREAM_TOO_LARGESecurity boundary
Storage keys are not authorization. Authenticate and authorize requests before allowing access to user-selected keys.
MIME type and extension checks are metadata validation, not file-signature verification.
Streaming limits do not replace the framework/proxy request body limit. Configure server.bodyLimit / BCP_BODY_LIMIT large enough for intended uploads while keeping an explicit outer request boundary.
For S3/object storage:
- keep credentials server-only,
- prefer private buckets,
- scope IAM/provider permissions,
- use provider-side encryption/retention policies where required,
- validate provider CORS only when direct browser access is intentionally enabled.