Skip to content

Production Checklist

filestore provides safe storage primitives, but the application still owns its upload policy, authorization, retention rules, and deployment configuration. Use this checklist before exposing an upload endpoint to untrusted clients.

Bound Every Request

Set limits at the reverse proxy or ingress and in StoreConfig. The outer HTTP limit rejects oversized requests before the application spends time parsing them; filestore's limits enforce field counts, file counts, and per-file policy.

from filestore import FileStore, LocalEngine, StoreConfig

storage = FileStore(
    "document",
    count=5,
    engine=LocalEngine(base_dir="uploads"),
    config=StoreConfig(
        max_files=5,
        max_fields=20,
        max_part_size=10 * 1024 * 1024,
        max_file_size=10 * 1024 * 1024,
        allowed_extensions={".pdf", ".txt"},
        allowed_content_types={"application/pdf", "text/plain"},
    ),
)

Choose limits for the entire request as well as each file. max_file_size is checked again by storage engines against the actual stream size, so a missing or incorrect client size hint does not bypass the final size check.

Treat Client Metadata as Untrusted

Filenames and Content-Type values come from the client. Filename sanitization is enabled by default and removes traversal segments, but MIME allow-lists do not inspect the file contents.

  • Keep sanitize_filename=True unless another trusted layer produces every name.
  • Generate server-side names when collisions, disclosure, or guessing matter.
  • For sensitive formats, add a filter that checks magic bytes or parse the file with a hardened format-specific library.
  • Scan untrusted documents for malware before making them available to other users.
  • Never render active uploads as trusted HTML or serve them from your primary application origin without appropriate download headers and isolation.

Authorize Object Operations

An object key is an identifier, not proof of permission. Before calling presign_download() or delete(), verify that the authenticated principal owns or may access that key.

from fastapi import Depends, HTTPException

@app.get("/documents/{document_id}/download")
async def download(document_id: str, user=Depends(current_user)):
    document = await repository.get(document_id)
    if document is None or document.owner_id != user.id:
        raise HTTPException(status_code=404)
    return await s3.presign_download(document.storage_key, expires_in=300)

Store the backend key returned in FileData.key with your application record. Avoid accepting arbitrary bucket keys directly from query parameters.

Keep Cloud Storage Private

Prefer private buckets and containers with short-lived presigned URLs. Configure the narrowest credentials the application needs, rotate secrets, and use workload identity or instance roles instead of long-lived keys where the provider supports them.

For browser-to-cloud uploads:

  • Restrict bucket CORS to the application origins and required methods/headers.
  • Use short expiration windows and application-generated keys.
  • Validate the requested filename and content type before signing.
  • Record the intended upload, then verify the resulting object before treating the upload as complete.
  • Add provider-side size and content constraints where the signing mechanism supports them.

Presigning is not upload validation

presign_upload() grants temporary permission to write an object. Because the bytes bypass FileStore, its filters and final stream-size validation do not run. Validate the completed object in a separate finalize step when your threat model requires it.

Plan Local Storage Deployment

LocalEngine writes atomically and prevents path traversal, but the selected directory must fit the deployment model.

  • Mount persistent storage; container-local files disappear when an instance is replaced.
  • In multi-instance deployments, use shared storage or a cloud engine when every instance must see every object.
  • Give the process write access only to the upload directory.
  • Back up durable uploads and define retention and deletion policies.
  • Serve public files through a dedicated static-file layer or object store rather than through application workers when traffic is substantial.

Handle Every Outcome

An upload can complete, partially succeed, fail, or contain no files. Map these states to an explicit API contract and persist database records only for successful objects.

from fastapi import HTTPException
from filestore import UploadStatus

if store.status is UploadStatus.FAILED:
    raise HTTPException(status_code=422, detail=store.model_dump(mode="json"))

if store.status is UploadStatus.PARTIAL:
    # Keep successful objects, or delete them to implement all-or-nothing behavior.
    ...

If the surrounding operation must be atomic, delete successfully stored objects when a later file or database operation fails. Storage and database writes are not automatically transactional.

Observe and Operate

  • Log request IDs, authenticated principals, backend names, object keys, sizes, durations, and failure classes. Do not log file bodies, credentials, or complete presigned URLs.
  • Track upload success, partial success, validation rejection, backend latency, and orphan cleanup metrics.
  • Alert on sustained storage errors and unusual upload volume.
  • Reconcile abandoned database rows and orphaned objects with a scheduled job.
  • Exercise upload, download, delete, expiration, and credential rotation in a staging environment for every enabled backend.

Release Gate

  • Dependency and lock-file versions are pinned and reviewed.
  • CI passes on every supported Python version.
  • mkdocs build --strict completes successfully.
  • Distribution metadata passes twine check for both wheel and source archive.
  • Request, file, and field limits are configured at the proxy and application.
  • Authorization is enforced before signing, downloading, or deleting objects.
  • Cloud CORS, credentials, encryption, lifecycle, and private access are reviewed.
  • Partial-success and rollback behavior is covered by integration tests.
  • Monitoring, retention, backups, and orphan cleanup have owners.

Continue with Error Handling for route-level failure patterns and the Configuration Reference for every available limit.