Docs/Storage & Uploads/Storage & File Delivery

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

TypeScript
import {
    createLocalStorage,
} from "bcp/server";

export const storage =
    createLocalStorage({
        directory:
            "./uploads",
    });

Storage keys are logical relative paths:

text
avatars/user-101.webp
documents/2026/report.pdf

Absolute paths, traversal segments and BCP's reserved metadata directory are rejected.

S3-compatible storage

TypeScript
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

TypeScript
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

TypeScript
import {
    getStorageCapabilities,
} from "bcp/server";

const capabilities =
    getStorageCapabilities(
        storage
    );

The capability model currently reports:

TypeScript
{
    streamingRead,
    streamingWrite,
    ranges,
    signedUrls,
    listing,
}

createLocalStorage() and createS3Storage() in 0.1.26 report:

TypeScript
{
    streamingRead: true,
    streamingWrite: true,
    ranges: true,
    signedUrls: false,
    listing: false,
}

Signed URLs and listing are deferred to a later milestone.

Buffered writes

TypeScript
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:

TypeScript
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

TypeScript
await putStorageStream(
    storage,
    key,
    source,
    {
        maxBytes:
            50 * 1024 * 1024,
        signal:
            abortController.signal,
    }
);

If the stream exceeds maxBytes, BCP throws:

text
StorageError
code: STREAM_TOO_LARGE
status: 413

Native adapters clean up/abort incomplete writes where supported.

Streaming reads

TypeScript
import {
    readStorageStream,
} from "bcp/server";

const stream =
    await readStorageStream(
        storage,
        "videos/demo.mp4"
    );

Inclusive byte ranges are supported:

TypeScript
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:

TypeScript
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:

TypeScript
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

TypeScript
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:

  • GET and HEAD,
  • 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-Disposition filenames,
  • configurable cache control.

The default cache policy is:

text
Cache-Control: private, max-age=0, must-revalidate

Public immutable caching must be enabled explicitly.

Multiple byte ranges are intentionally unsupported in 0.1.26.

Object metadata

TypeScript
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:

text
INVALID_KEY
OBJECT_NOT_FOUND
OBJECT_EXISTS
RANGE_NOT_SATISFIABLE
STREAM_TOO_LARGE

Security 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.