Dev.to · 8 min read

Property Management SaaS: Expired Signed Links for AI Images and Object Storage

Property Management SaaS: Expired Signed Links for AI Images and Object Storage

Short answer: treat a presigned download URL as a short-lived browser credential, never as the identity of an AI-generated image. Keep the object key and retention record in your application database, mint a new URL after authorization, and make the deletion deadline a separate state transition that can never be extended by a preview refresh. That distinction matters in property management. A SaaS product may create an AI-generated inspection image, attach it to a signed lease packet, and later show both from the same dashboard. The image preview can stop loading because its URL expired while the signed document still needs to be retained until an explicit date. A blank is therefore not proof of data loss, and a successful refresh is not permission to keep the document forever. What the retention ledger must prove before a preview is served Start with the durable record, not the URL copied from the browser. For each asset, record the tenant, object key, media type, creation time, retention deadline, and current business state. The URL belongs in neither the canonical record nor a long-lived frontend cache. It is a capability with an expiry time. Here is the incident sequence I would put in a runbook. A property manager opens a portfolio at 09:00, leaves the tab idle, and returns after the image capability has expired. The page reports a failed image request, perhaps a 403, while the application database still points to tenant-17/inspections/lease-8842/front-door.webp. The backend checks that the viewer may read that tenant's object, checks the retention state, and issues one fresh capability. The browser retries once with that result. Three clocks need to be visible in logs: the browser request time, the signing service time, and the object service's expiry time. Clock skew can make a newly issued link look old, while a CDN or browser cache can make an old response look current. Log a request ID and object key hash, never the full signed URL; query strings often carry the credential itself. I'm not sure which clock will be wrong in a particular deployment, so the timestamps have to make that answer observable instead of guessed. Short key. Short credential. Do not solve the symptom by making the bucket public or by handing a storage secret to Node.js code running in the browser. Public delivery is a different product requirement. Private delivery needs an application authorization decision before the browser receives a limited download capability. Where should deletion ownership sit in a SaaS app? Signed lease documents create a harder invariant than image rendering: an explicit deletion deadline must be enforceable, auditable, and independent of a user opening the page. A preview refresh should read retention_deadline; it must not rewrite it. The cleanup worker should select records whose deadline has passed, issue a deletion request, and then record the result with an idempotency key. The same record should distinguish generated, available, previewable, retention_due, and deleted. Those are not synonyms. An AI job can finish while the object upload is incomplete. An object can be present while a viewer lacks authorization. A signed URL can be expired while the object is still available. Collapsing those states into image_url produces a UI that cannot explain what failed. For large generated assets, multipart upload also changes the cleanup problem. The storage owner has to know whether an upload completed or whether abandoned parts need removal; the application should not infer completion merely because one browser request succeeded. The multipart upload model described in the AWS documentation is useful here because it makes initiation, part transfer, completion, and abort distinct operations rather than one optimistic POST. The cleanup worker deserves its own ledger entry, because a deletion deadline is a compliance boundary rather than a background convenience. When the deadline arrives, it should claim the record, stop new preview capabilities, issue the storage deletion, persist the provider response, and emit an audit event containing the record ID and deadline. If the worker is interrupted after the storage call but before the database write, the next attempt must be safe to repeat; if the object is already gone, the record still needs an explicit terminal state. Meanwhile, a page that was opened before the deadline should not keep a link alive just because its JavaScript timer has not fired. This is why I separate previewable from retention_due, and why the deletion worker must not depend on gallery traffic to discover work. A useful alert is not merely “cleanup job ran”; it is “no authorized object remained readable past its policy deadline,” which requires checking both the ledger and the storage observation. That longer path is where most of the engineering judgment lives. The operational SLO should be stated in user terms: for example, the percentage of authorized previews that load after one refresh, measured separately from object durability and deletion completion. Set an error budget for refresh failures, then watch p95 signing latency, refreshes per page view, authorization denials, cleanup lag, and objects past their deletion deadline. A retry loop that hides those numbers is not reliability. How should a SaaS troubleshoot expired presigned downloads for generated images? The following example is intentionally provider-neutral. The Node.js service owns the credential and authorization boundary; the browser receives only the URL returned by that service. The storage adapter can implement PresignGet with the object store's official SDK or HTTP interface. No application secret crosses into browser JavaScript. package main import ( "context" "errors" "fmt" "net/http" "os" "time" ) type Asset struct { TenantID string ObjectKey string RetentionDeadline time.Time DeletedAt *time.Time } type Storage interface { PresignGet(ctx context.Context, bucket, key string, lifetime time.Duration) (string, error) } func downloadCapability(ctx context.Context, store Storage, asset Asset, viewerTenant string, now time.Time) (string, error) { if asset.TenantID != viewerTenant { return "", fmt.Errorf("%w: tenant does not own asset", os.ErrPermission) } if asset.DeletedAt != nil || !now.Before(asset.RetentionDeadline) { return "", fmt.Errorf("asset is outside its readable retention state") } // Keep this lifetime below the normal page session, and never extend retention. return store.PresignGet(ctx, "private-assets", asset.ObjectKey, 5*time.Minute) } func classifyRefreshFailure(status int) error { switch status { case http.StatusForbidden: return errors.New("authorization or expiry requires a fresh application decision") case http.StatusNotFound: return errors.New("durable record and object state disagree") default: return fmt.Errorf("storage read returned HTTP %d", status) } } The snippet's important behavior is the ordering: tenant authorization and retention state come before URL issuance. It also shows a real implementation detail that is easy to miss in a Node.js service: the browser endpoint should return a structured reason such as expired, forbidden, or missing, rather than teaching the frontend to retry every status. Replace the adapter with the storage client's documented presign operation, and keep the returned URL free of your application Authorization header. Use the application's own authorization type in production if it has one. I've kept the standard permission error visible because the error taxonomy belongs to the control plane, not to object storage. A Go compiler will catch a missing import; a production test should catch a more serious mistake, such as issuing a URL for a tenant that has crossed its deletion deadline. Which delivery boundary fits the on-call budget? The primary decision is access control versus delivery simplicity — and that is a reliability decision before it is a cost decision. Direct browser downloads reduce proxy bandwidth and can handle large objects cleanly, but the application must refresh capabilities, coalesce duplicate refreshes, and explain expiry. A backend proxy makes authorization and audit logging easier to centralize, but it adds application bandwidth, latency, and a new SLO dependency. Approach Strong fit Cost or limitation Short-lived signed download Private previews and documents with tenant authorization Refresh logic, clock checks, and URL leakage controls are required Backend download proxy Strict audit requirements or clients that cannot follow signed links Application egress and concurrency become part of the capacity plan Public asset delivery Deliberately public marketing images Unsuitable for private lease documents or tenant-scoped records Self-hosted object storage Teams that need direct control over policy and operations On-call ownership includes upgrades, durability, backup, and cleanup Managed object storage Teams that prefer a service boundary and established durability controls Policy configuration, provider coupling, and usage accounting remain your responsibility The table is a decision aid, not a ranking. Pick a proxy when auditability outweighs direct delivery, and pick signed access when the client can handle a refresh endpoint without turning it into a request fan-out. Keep a self-hosted system when operational ownership is deliberate; a managed service is not automatically cheaper once retention audits, incident response, and migration work enter the ledger. Your mileage may vary with gallery size and tenant isolation, so load-test the resume-from-idle path instead of using average request volume. For property-management records, the approach is not suitable when the requirement is immutable legal retention that must survive application-level deletion mistakes; select storage controls and an evidence process designed for that requirement. It is also the wrong fit for permanent public URLs or static-site hosting. Stick with a proxy when the browser cannot safely receive a capability, and keep the deletion worker independent from preview traffic in either design. References https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News