Skip to content

darkroom

darkroom owns media: signed uploads, a tenant-scoped asset registry, and derived variants. It is written in Rust because it pushes pixels, and it does not want garbage-collection pauses in the middle of an image encode.

The upload flow is three calls, and the bytes never pass through

Section titled “The upload flow is three calls, and the bytes never pass through”
POST /v1/uploads ──▶ 201 { asset: pending, upload_url, storage_key, expires_in_secs }
│
client PUTs bytes ───────┘ straight to object storage
│
POST /v1/uploads/{id}/complete ──▶ 200 { asset: ready }
or 409 (object absent) / 422 (checksum mismatch)

That is the entire design: a 1 GiB upload is one request to the bucket, not a stream through an API process that has to buffer it, spool it to disk, and time out on a slow client. Two tests hold it — the byte counters behind them are asserted, not assumed.

Readiness is decided at complete, not at presign. The presign is a promise; complete is the fact, and it is where the object is read back and hashed. That is also why a checksum works identically on AWS S3 and on R2, which cannot verify one for you.

One known cost, stated by the repository rather than hidden: complete reads the object through ObjectStore::get, so a 1 GiB upload is buffered in the API process at completion time. It is released as soon as it is hashed, and the hash is available in a streaming form that is tested equivalent to the one-shot path — but the trait hands over Bytes, and the read is what it is.

Cloudflare R2 is configuration, not a second implementation

Section titled “Cloudflare R2 is configuration, not a second implementation”

There is no R2ObjectStore, and that is the point. The two backends differ in four ways, all of them request shaping, and request shaping is entirely below the storage trait:

R2 how darkroom handles it
region auto; us-east-1 and "" alias to it, but SigV4 must be signed as auto an R2 endpoint resolves to auto; a real region is refused at startup, naming the variable and the value
endpoint https://<ACCOUNT_ID>.r2.cloudflarestorage.com, account-scoped, no global endpoint always configuration, never a constant; region=auto with no endpoint is refused so a bucket-only config cannot silently resolve s3.amazonaws.com
checksums SHA-256 is COMPOSITE only; FULL_OBJECT is offered for CRC-64/NVME darkroom never asks — the object metadata type has no checksum field, and complete reads the bytes back
ACL headers rejected outright never sent

The checksum line is the one with a consequence you will feel. On R2 a client-supplied checksum cannot be verified by a header, so verification is a read-back at completion. That is true on S3 too, which is why the read-back is the only path and there is no per-backend branch.

S3ObjectStore is compiled only with --features s3, which is the deployment image’s build. The AWS SDK is not a default dependency at all, so the default build cannot construct a real object-storage client — which is what makes “tests never hit the network” a property of the dependency graph rather than a promise in a document.

Terminal window
# the database, and the service
docker compose up -d
curl -s localhost:8080/healthz

There is no POSTGRES_PORT variable here to move it — the published port is a literal, and a second compose file’s ports: list is appended rather than substituted, so adding one is not the fix. Run one service at a time, or stop whatever holds 5432. The variable that does exist is KIT_POSTGRES_PORT, and it belongs to kit’s stack rather than to this file.

The image is built from docker/Dockerfile, not ./Dockerfile, and it needs no build flags:

Terminal window
docker build -f docker/Dockerfile -t darkroom .

That file already runs cargo build --release --locked --features s3, so the default build is the S3 build. A previous version of this page printed --build-arg --features s3, which is not a command: --build-arg needs a NAME[=VALUE] after it, so Docker refuses at the flag parser before it reads a Dockerfile.

Variable Meaning
PORT Listen port. 8080.
DATABASE_URL Postgres DSN. The service owns its own database.
DARKROOM_OBJECT_STORE memory or s3. memory for development; s3 is the deployment build.
DARKROOM_S3_BUCKET · DARKROOM_S3_ENDPOINT · DARKROOM_S3_REGION Bucket, account-scoped endpoint, and region. region is optional for R2 and defaults to auto.
DARKROOM_JWKS_URL · DARKROOM_ISSUER · DARKROOM_AUDIENCE How the caller is authenticated. darkroom verifies tokens locally against identity’s key set and reads account_id and scopes from the verified token.
DARKROOM_ENV production by default. development enables the HMAC verifier, which exists only behind --features dev-auth.
DARKROOM_LOG_FORMAT human or a machine format.

darkroom authenticates locally and consumes no events. Media is produced by API calls, not by reacting to another service’s events, so consumes is omitted rather than filled with a placeholder. identity is a hard runtime requirement — a token cannot be verified without it — so dependencies is omitted rather than given a required: false value, because a field modelling “the service runs without it, degraded” has no honest value here.

./bin/prime is green on a bare machine with no Postgres and no Docker, and it runs three tiers — including cargo test --features s3, because cargo test does not compile the s3 feature at all. The database tier is separate:

Terminal window
./bin/prime # fmt, build, clippy, test, + the s3 feature
./bin/prime --db # + the #[ignore]d database tests, against TEST_DATABASE_URL

See Running the gates.

  • Repository: github.com/cafaye/darkroom
  • Language: Rust
  • Namespace / event source: darkroom — publishes darkroom.asset.ready, darkroom.asset.deleted, darkroom.variant.created
  • Container port: 8080