Перейти к основному содержимому

S3-compatible object storage, accessed via @aws-sdk/client-s3. See the db-files skill for storage patterns.

Why was it chosen?

  • Zero egress fees — the primary differentiator over AWS S3.
  • S3-compatible API — the same SDK code ports to any S3 provider.

See vendors.md for pricing, free tier limits, and provider alternatives.

Known limitations

  • No versioning, object locking, bucket notifications, or replication.
  • No KMS encryption (SSE-C only); no Glacier-equivalent cold tier.

Connection

Uses the S3-compatible API via @aws-sdk/client-s3. The endpoint is derived from the account ID — no separate endpoint env var needed.

Environment variables:

Variable Purpose
R2_ACCOUNT_ID Cloudflare account ID (endpoint computed: https://{id}.r2.cloudflarestorage.com)
R2_ACCESS_KEY_ID S3 API token access key
R2_SECRET_ACCESS_KEY S3 API token secret
R2_BUCKET_NAME Target bucket name

Client setup: $lib/server/store/index.ts creates an S3Client with region: 'auto' and endpoint derived from the account ID. Returns null when credentials are missing (graceful degradation via feature flags).

Implementation

The store layer lives at $lib/server/store/ with the same structure as the db and graph layers:

src/lib/server/store/
├── index.ts           # S3Client setup, BUCKET export
├── types.ts           # ObjectInfo, ObjectDetail, BucketStats, PresignedUrlResult, etc.
├── errors.ts          # StoreError class, classifyS3Error()
└── showcase/
    ├── queries.ts     # verifyConnection, listShowcaseObjects, getObjectDetail, generateDownloadUrl, getObjectRange
    ├── mutations.ts   # generateUploadUrl, confirmUpload, deleteShowcaseObject
    ├── seed.ts        # reseedBucket() — 11 seed objects
    └── guards.ts      # assertShowcaseKey, checkObjectLimit

Error handling: classifyS3Error() normalizes AWS SDK errors into typed StoreError kinds: credentials, not_found, forbidden, timeout, limit, unavailable, unknown.

Safety guards:

  • All operations scoped to showcase/ prefix via assertShowcaseKey()
  • Max 20 objects in showcase namespace via checkObjectLimit()
  • Upload validation: allowlisted MIME types, 2 MB size cap
  • UUID-based key generation prevents collisions

Upload flow:

Client                    Server                    R2
  │                         │                        │
  ├── Request upload URL ──▶│                        │
  │                         ├── Validate + presign ─▶│
  │◀── Presigned PUT URL ───┤                        │
  │                         │                        │
  ├── PUT file directly ────┼───────────────────────▶│
  │                         │                        │
  ├── Confirm upload ──────▶│                        │
  │                         ├── HeadObject verify ──▶│
  │◀── Upload result ───────┤                        │

Content-Type is locked into the presigned URL signature — the client must send the exact MIME type that was validated server-side.

Latency tiers

R2 is always-on — no cold starts like Neon (PostgreSQL) or Aura (Neo4j). Round-trip time reflects only network distance to the R2 edge:

Tier Latency Meaning
Fast < 150ms Low network distance to the R2 edge
Slow 150–500ms Higher network distance, or listing many objects
Degraded > 500ms Network issues or R2 service degradation

Live at /showcases/db/storage/connection.

← Back to Stack

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв