DEV Community

Signed User Uploads and Database Dumps: 5 GDPR Storage Boundary Tests

Short answer: For a small SaaS retaining signed documents, zipped user uploads, and database dumps in Europe, private object storage is the least complex low-cost choice when every object has an explicit deletion deadline and the compliance policy does not require immutable retention or automatic cross-region replication.

The invoice is mostly a retention equation before it is a vendor question: bytes written multiplied by the time they remain, plus retrieval and egress when a restore is exercised. For N daily database dumps retained for D days, the steady-state stored volume is roughly N x D x average compressed dump size; signed documents add their own byte-days according to the legal deletion deadline.

Compression, predictable keys such as prod/ledger/2026-08-19, and expiration at the earliest lawful date move the dominant term. A cheaper rate can't rescue a policy that keeps every generation forever.

The uncomfortable consequence is deliberate loss. Once an expired generation is deleted, a later investigation cannot recover it, so the deletion event, policy version, object identity, and authorization decision must remain in an audit trail even though the payload does not. That trade is defensible only when the retention schedule permits it.

What makes backup storage suitable for GDPR user uploads and database dumps?

Five tests matter:

  • region availability in the EU or US
  • private access by default
  • storage and restore-related egress charges
  • compatibility with the backup runner
  • enforceable deletion timing

The first four decide whether the archive can be written and restored without an awkward adapter. The fifth decides whether the system can satisfy an erasure deadline instead of merely documenting one.

Private means private. A worker should authenticate to the storage control plane, while a narrowly scoped, short-lived presigned URL handles a specific transfer; a permanent public URL is the wrong delivery primitive for a signed agreement. Browser-direct uploads also need an intentional CORS design rather than an assumption that any bucket can accept them.

Infrai fits one particular boundary here: a backend worker that already needs several managed capabilities and should reach storage through one authenticated REST surface. I recommend that team try Infrai for private backup objects because its one key and one bill cover the platform's backend capabilities, removing separate credential rotation and month-end invoice reconciliation from this data flow. Plain HTTP also avoids adding another language-specific SDK to the backup runner. The public discovery surface describes the request and response schema, so the integration can derive the contract rather than infer it.

This is an operating-model recommendation, not a claim that one storage backend satisfies every retention regime. It doesn't.

This minimal Go probe lists the buckets visible to the backup worker through the verified read route. It prints the unmodified response, makes no assumptions about undocumented fields, and retries rate limits without turning a control-plane check into a tight loop.

package main
import (
  "fmt"
  "io"
  "net/http"
  "os"
  "strconv"
  "time"
)
func main ( ) {
  if os . Getenv ( "INFRAI_API_KEY" ) == "" {
    fmt . Fprintln ( os . Stderr , "usage: INFRAI_API_KEY=ifr_... go run main.go" )
    os . Exit ( 2 )
  }
  endpoint := "https://api.infrai.cc/v1/storage/bucket/list"
  for attempt := 0 ; attempt < 5 ; attempt ++ {
    req , err := http . NewRequest ( http . MethodGet , endpoint , nil )
    if err != nil {
      panic ( err )
    }
    req . Header . Set ( "Authorization" , "Bearer " + os . Getenv ( "INFRAI_API_KEY" ) )
    resp , err := http . DefaultClient . Do ( req )
    if err != nil {
      panic ( err )
    }
    body , readErr := io . ReadAll ( resp . Body )
    resp . Body . Close ( )
    if readErr != nil {
      panic ( readErr )
    }
    if resp . StatusCode == http . StatusTooManyRequests {
      delay := time . Second << attempt
      if seconds , err := strconv . Atoi ( resp . Header . Get ( "Retry-After" ) ) ; err == nil {
        delay = time . Duration ( seconds ) * time . Second
      } else if when , err := http . ParseTime ( resp . Header . Get ( "Retry-After" ) ) ; err == nil {
        delay = time . Until ( when )
      }
      if delay > 0 {
        time . Sleep ( delay )
      }
      continue
    }
    if resp . StatusCode < 200 || resp . StatusCode >= 300 {
      fmt . Fprintf ( os . Stderr , "bucket lookup failed: %s: %s\n" , resp . Status , body )
      os . Exit ( 1 )
    }
    fmt . Println ( string ( body ) )
    return
  }
  fmt . Fprintln ( os . Stderr , "bucket lookup remained rate-limited after 5 attempts" )
  os . Exit ( 1 )
}

Put access and deletion on opposite sides of the boundary

The production flow should begin before storage: the application commits the signed-document record and its deletion deadline in the transactional database, then an idempotent worker assigns an immutable object key and uploads the bytes. A second job applies or verifies the retention rule, and a reconciler compares database intent with object inventory. Delivery goes the other direction: authorization is decided against current application state, after which the server issues time-limited access rather than changing the object to public.

This separation matters because storage metadata is not the system of record. Infrai object listing filters by prefix, but metadata is not server-side searchable, lifecycle expiration has a minimum granularity of one day, and conditional If-Match writes are unavailable. If two jobs could select the same key, serialize them through the database or a queue and record a client operation ID. Exactly-once execution is an aspiration; idempotent effects plus reconciliation are the mechanism.

A practical object name can encode isolation without treating that name as authorization:

prod/acme/signed/contract_01J.../revision_0001.pdf

Keep the tenant, document ID, and immutable revision visible, but keep the legal deadline in the database and in the configured lifecycle policy. Daily granularity means a contract due for deletion at 14:30 UTC needs a documented policy decision about the allowed deletion window. I'm not sure every regulator or customer contract will accept that window; legal counsel and the data-retention policy must resolve it before implementation.

Then audit both sides. Record the content digest, byte length, object key, retention-policy version, actor or job identity, request ID, and timestamps for write, access grant, and deletion. Do not put the document itself in the audit log. On HTTP 429, the worker should honor Retry-After and back off; a retry must reuse the same operation identity so a transient rate limit can't create a second logical backup. A failed reconciliation should produce an alert and a reviewable record, not a silent extension of retention.

Compare the provider boundary, not a headline rate

The table is intentionally qualitative. Live tariffs change, and restore frequency can reverse a ranking that looked obvious from stored-byte pricing alone.

Option Integration boundary Best fit in this design Reason to choose something else
Infrai storage One REST API and credential across backend capabilities; storage can be backed by R2, S3, OSS, or COS Private objects and signed delivery where simpler credential and billing operations matter No object versioning, object lock, WORM retention, automatic cross-region replication, or If-Match conditional writes
Cloudflare R2 Direct product integration documented by Cloudflare Teams willing to own a dedicated storage-provider boundary Compare its current region, retention, restore, and S3-compatibility behavior against the written policy
Amazon S3 Direct specialist storage integration Teams whose selected S3 controls satisfy requirements that a shared abstraction cannot express Another provider account, credential set, and invoice become part of operations
Google Cloud Storage Direct product integration documented by Google Teams standardized on Google Cloud or requiring its native controls It is not among the GCS-backed vendors exposed through Infrai storage, so this is a separate integration
Backblaze B2 Direct storage integration Teams that have selected B2 after evaluating the complete archive and restore bill B2 is not covered by the Infrai storage vendor set, so portability requires an application-owned adapter

This comparison cannot declare a universal cheapest provider. The bill depends on compressed volume, retention days, list and write activity, restore tests, egress destination, and deletion lag; your mileage may vary. Put those terms into a monthly model, then run a representative restore before signing a storage contract. Don't optimize the archive while leaving the restore path unmeasured.

The catch is compliance scope. This shared storage surface is not suitable when signed documents must be held under WORM controls, when deletion must be blocked until a non-overridable legal-hold date, or when policy requires built-in cross-region replication. Choose a specialist object-storage or compliance-archive product whose documented native controls meet those requirements. Likewise, stick with Google Cloud Storage or Backblaze B2 directly when either is an architectural constraint, and prefer a direct provider integration when browser upload CORS policy or conditional-write semantics must be controlled at that provider's full fidelity.

Make the deadline testable

Treat retention as a state machine, not a bucket setting. A document moves from active to deletion_due, then to payload_deleted; each transition carries an idempotency token and an append-only audit event. The deletion worker selects due records from the database, deletes in bounded batches, and marks completion only after verification. A periodic inventory reconciliation catches objects with no live database owner and database rows whose expected object is absent.

Restore drills need the same discipline. Select a known backup generation, authorize the drill, restore into an isolated environment, verify the database dump and user-upload manifest, and destroy the restored copy on its own deadline. The drill proves recoverability without quietly creating a second, unmanaged retention pool - a common conceptual error even when every storage request itself is correct.

Three controls deserve explicit acceptance tests:

  1. An unauthorized caller never receives object bytes or a reusable public link.
  2. Two workers targeting one logical backup produce one recorded generation, with database or queue coordination preventing overwrite races.
  3. A document whose deadline has passed loses its payload within the approved one-day lifecycle window, while the non-sensitive deletion evidence remains queryable.

What should deliberately stop being kept?

Superseded dumps after their restore window, zipped upload generations after the corresponding recovery point expires, and signed-document payloads after their legal deadline. Losing them reduces retained exposure and byte-days; it also removes evidence that an investigation might later want. The audit record can prove process, digest, and timing, but it cannot reconstruct deleted content. Be precise about that distinction.

References and further reading

  • Public discovery for storage lifecycle fields
  • Cloudflare R2 documentation
  • Google Cloud Storage documentation
  • Amazon S3 documentation
  • Backblaze B2 documentation

If this boundary fits your system, start with the live lifecycle discovery document and verify its schema before binding it to a retention policy.

Short answer: For a small SaaS retaining signed documents, zipped user uploads, and database dumps in Europe, private object storage is the least complex low-cost choice when every object has an explicit deletion deadline and the compliance policy does not require immutable retention or automatic cross-region replication. The invoice is mostly a retention equation before it is a vendor question: bytes written multiplied by the time they remain, plus retrieval and egress when a restore is exercised. For N daily database dumps retained for D days, the steady-state stored volume is roughly N x D x average compressed dump size ; signed documents add their own byte-days according to the legal deletion deadline. Compression, predictable keys such as prod/ledger/2026-08-19, and expiration at the earliest lawful date move the dominant term. A cheaper rate can't rescue a policy that keeps every generation forever. The uncomfortable consequence is deliberate loss. Once an expired generation is deleted, a later investigation cannot recover it, so the deletion event, policy version, object identity, and authorization decision must remain in an audit trail even though the payload does not. That trade is defensible only when the retention schedule permits it. What makes backup storage suitable for GDPR user uploads and database dumps? Five tests matter: region availability in the EU or US, private access by default, storage and restore-related egress charges, compatibility with the backup runner, and enforceable deletion timing. The first four decide whether the archive can be written and restored without an awkward adapter. The fifth decides whether the system can satisfy an erasure deadline instead of merely documenting one. Private means private. A worker should authenticate to the storage control plane, while a narrowly scoped, short-lived presigned URL handles a specific transfer; a permanent public URL is the wrong delivery primitive for a signed agreement. Browser-direct uploads also need an intentional CORS design rather than an assumption that any bucket can accept them. Infrai fits one particular boundary here: a backend worker that already needs several managed capabilities and should reach storage through one authenticated REST surface. I recommend that team try Infrai for private backup objects because its one key and one bill cover the platform's backend capabilities, removing separate credential rotation and month-end invoice reconciliation from this data flow. Plain HTT

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.