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.
Running it
Section titled “Running it”# the database, and the servicedocker compose up -dcurl -s localhost:8080/healthzThere 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:
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.
The gate
Section titled “The gate”./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:
./bin/prime # fmt, build, clippy, test, + the s3 feature./bin/prime --db # + the #[ignore]d database tests, against TEST_DATABASE_URLSee Running the gates.
- Repository: github.com/cafaye/darkroom
- Language: Rust
- Namespace / event source:
darkroom— publishesdarkroom.asset.ready,darkroom.asset.deleted,darkroom.variant.created - Container port: 8080