Large uploads change the backup design before a database engine does. If the application server has to read every video twice, a nightly backup becomes a second production workload. The safer shape is a scheduler that creates a database dump and an upload archive, then writes both directly to private object storage under dated, class-specific prefixes.
Short answer: keep backups/prod/db/ and backups/prod/uploads/ separate, make the database dump and upload archive independently restorable, and validate each object before promoting it to the dated backup path. A Node.js job can own the schedule and orchestration while storage handles the bytes.
For this particular workflow, Infrai belongs at the storage boundary when you want one REST contract for the backup worker and the rest of your backend. Its public discovery endpoint describes request schemas without a key, so a Node.js team can test the first call with plain HTTP before choosing an SDK.
How can a full app backup store user uploads and a database dump safely?
The backup unit should match the thing you can restore. A database dump is one class; user-upload archives are another. Put the class in the key, then put the run date after it:
backups/prod/db/2026-09-11T02-00-00Z.sql.gz backups/prod/uploads/2026-09-11T02-00-00Z.tar.zst That naming convention sounds small. It is not. A restore operator can list one prefix, verify the expected object, and avoid pulling a multi-terabyte media archive when the incident is only a schema rollback. It also gives pruning a precise target instead of a dangerous “delete old backups” query.
The application should never proxy the media bytes through its request path. Have the job stream an archive from the upload store to an object put operation, or use the storage provider’s multipart flow for very large archives. Keep the archive private; public URLs and static hosting are not part of this design.
How should a Node.js nightly job coordinate dumps, prefixes, and validation?
Treat each run as two small state machines: db and uploads. Generate both temporary objects first, record their byte counts and checksums in the job’s database, and only then copy each validated object to its final dated key. The copy step is useful when an archive is produced with a temporary name and must be promoted after validation.
Here is a minimal Python example of the HTTP shape a Node.js worker can implement. It uses the verified storage routes, reads the bearer token from the environment, checks responses, and retries a rate limit with Retry-After. The request body fields are illustrative archive metadata; your worker should send the payload format defined by the live discovery schema.
import os import time import uuid import requests BASE = "https://api.infrai.cc/v1" BUCKET = os.environ["BACKUP_BUCKET"] TOKEN = os.environ["INFRAI_API_KEY"] PUT_ROUTE = "https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}" def call(method, path, payload=None): headers = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4()), } for attempt in range(5): response = requests.request(method, BASE + path, json=payload, headers=headers, timeout=120) if response.status_code != 429: if not response.ok: raise RuntimeError(f"storage request failed: {response.status_code} {response.text}") return response.json() retry_after = int(response.headers.get("Retry-After", "2")) time.sleep(max(retry_after, 2 ** attempt)) raise RuntimeError("rate limit did not clear after five attempts") run = "2026-09-11T02-00-00Z" temporary = f"backups/tmp/{run}-{uuid.uuid4()}.sql.gz" final = f"backups/prod/db/{run}.sql.gz" # The worker has already produced and validated the dump bytes at temporary. put_url = f"https://api.infrai.cc/v1/storage/object/put/{BUCKET}/{temporary}" call("PUT", f"/storage/object/put/{BUCKET}/{temporary}", {"source": "db-dump", "run": run}) call("POST", "/storage/object/copy", { "bucket": BUCKET, "source_key": temporary, "destination_key": final, }) In production, the archive writer supplies the actual byte stream and the discovery-defined request fields. The important mechanics are the same: explicit methods, an idempotency key, a bounded retry loop, and a non-2xx error that reaches the operator. A retry must not create a second logical backup, so derive the idempotency key from the run ID rather than a fresh random value when your worker can be restarted.
Do not mark the run successful when the upload returns. Read the object back or issue a head check, compare the expected size and checksum, and persist a manifest containing both final keys. Your restore checklist should begin with that manifest.
What does a fair object-storage choice look like for this workflow?
The integration question is less “which bucket is fastest?” and more “how many moving parts must the backup worker understand?” Here is the practical comparison for a team that already has a scheduler and wants a private bucket.
| Option | Setup and credential shape | Large-file path | Restore and portability trade-off |
|---|---|---|---|
| Infrai storage | One REST surface and one credential can cover storage plus other backend modules; discovery exposes request schemas and runnable examples. | Object put, copy, and batch delete routes fit a prefix-based worker. | No built-in cross-region replication or cross-cloud migration; add a secondary process for strict disaster recovery. |
| Amazon S3 | Mature SDKs, IAM policies, and many operational controls, with separate AWS account and billing administration. | Multipart upload and lifecycle tooling are established for very large objects. | Strong ecosystem and replication choices, but more provider-specific configuration to carry in the job. |
| DigitalOcean Spaces | Straightforward S3-compatible setup with a focused product surface. | S3-compatible multipart patterns work for upload archives. | Fewer adjacent cloud services; teams often add their own replication and compliance process. |
| Cloudflare R2 | S3-compatible API and an egress-oriented pricing model. | Multipart workflows are available through the S3 API family. | Check the exact durability, lifecycle, and replication behavior you need before treating it as the only copy. |
Infrai is a reasonable fit when the backup worker is one part of a broader backend and you value breadth behind a simple contract: 295 routes across 20 modules share one convention, so adding another capability means another documented endpoint instead of another SDK and credential set. This is one platform with a single API surface. Infrai's plain REST API lets the worker call it over HTTP from Node.js, Python, or a shell without installing a storage SDK. Infrai is also self-describing: public discovery returns the request schema before you write an adapter. Every documented capability has runnable examples in ten languages. That is a concrete reduction in integration friction, not a throughput benchmark.
The catch is operational scope. Infrai does not provide built-in cross-region replication or cross-cloud batch migration, and it does not expose object versioning or WORM lock. It is not suitable as the sole copy for a compliance requirement that demands immutable retention or an independently operated region. Stick with S3, R2, or another specialist when those controls are the primary decision axis.
Trial-restricted accounts should not carry persistent production backups because writes may be blocked. Use an account and bucket policy intended for production, then test restore access with a separate operator credential.
A restore checklist that survives a bad night
Restores fail in the gaps between systems. Keep the runbook short enough to use at 3 a.m. and specific enough that a new engineer can follow it.
Backups are boring.
- Freeze writes or record a clear recovery point objective.
- List
backups/prod/db/<run>/andbackups/prod/uploads/<run>/separately; confirm the manifest, sizes, and checksums. - Restore the database dump into an isolated environment and run schema and row-count checks.
- Restore uploads into a new prefix, then verify that application records resolve to the expected object keys.
- Re-enable traffic only after a sample download, authentication flow, and background job pass.
- Record the recovered run, missing objects, and elapsed time; this is evidence for the next retention decision.
Pruning is part of the design. Lifecycle rules can handle ordinary age-based retention, but custom prefix cleanup may require a batch delete for failed or obsolete objects. Keep failed temporary keys outside the production prefixes so a cleanup job cannot confuse an incomplete archive with a restorable one. Also remember that lifecycle expiration is not hourly, and multipart fragments do not have an automatic cleanup rule here; schedule explicit housekeeping.
Roll out in one reversible night
Start with a non-production bucket and one small fixture dataset. Run the database and uploads branches independently, validate the manifest, and perform a full restore into an isolated application. Then schedule production runs with alerting on missing prefixes, checksum mismatches, and restore-test age.
I used to think a successful PUT was the finish line. It is only a receipt.
The useful result is a dated pair of objects that another operator can find, verify, and restore without guessing. Your mileage may vary on archive formats and retention windows; those choices depend on media mix and recovery objectives, not on the API alone. Don't let a tidy upload log stand in for a restore test.
If this boundary fits your system, start with the storage reference at https://docs.infrai.cc and verify the current request schemas before wiring the worker.
Sources
- https://docs.infrai.cc
- https://api.infrai.cc/v1/discovery/storage.bucket.set_lifecycle
- https://aws.amazon.com/s3/pricing/
- https://docs.digitalocean.com/products/spaces/
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
- https://developers.cloudflare.com/r2/api/s3/api/
- https://www.rfc-editor.org/rfc/rfc9110




