Skip to content

Asset Service

Most admin frameworks ship “upload to a local directory and return a URL.” That works in development and breaks in production: files vanish when a container restarts or scales out, private files have no access check, URLs leak, every project re-writes its own OSS/S3 wiring, and large uploads saturate the application server.

StackRivet treats files as a first-class Asset Service: it manages asset metadata, storage adapters, upload/download security and business references in one place.

Business tables store an assetId, never a raw file URL. A URL is fetched on demand through the Asset API — so it can be short-lived, permission-checked, and independent of which storage backend you use.

business row → assetId → Asset API → (permission check) → short-lived signed URL

This is what keeps private files private and lets you switch storage backends without rewriting business code.

A single StorageAdapter interface covers Local Dev, S3-compatible (e.g. MinIO, AWS S3) and Aliyun OSS. The adapter:

  • Never leaks a cloud-vendor SDK object into the business layer.
  • Converts errors into one unified exception type.
  • Takes endpoint / bucket / region / credentials from configuration (env vars, never committed).
  • Issues short-lived signed URLs by default.

Switching from local storage to S3 or OSS is a configuration change (STACKRIVET_STORAGE_TYPE), not a code change. See the Configure object storage guide.

The storage object key is generated by the system — the user never controls it, and the original filename is kept only in metadata:

{tenantId}/{yyyy}/{MM}/{assetId}.{extension}
# e.g. default/2026/05/01JABC.pdf

Private files never get a permanent URL.

An asset moves through a small state machine:

stateDiagram-v2
  [*] --> pending
  pending --> active: upload completed
  pending --> deleted: aborted / expired
  active --> deleted: soft delete
  deleted --> [*]

Community implements pending, active and deleted. Enterprise environments can extend the lifecycle with compliance and security states when required.

The client reads GET /api/v1/assets/capabilities before choosing a path. It must not infer direct-upload support from a backend name or file size.

Advertised capabilityPath
Transit (all backends)The backend enforces quota and content policy, then writes the exact bytes through the storage adapter. Local and Aliyun OSS always use this path.
supportsPresignedUpload (S3 only today)A short-lived PUT capability binds the declared Content-Length; completion verifies the object before the asset becomes active.
supportsMultipartUpload (S3 only today)Initiate / complete / abort sessions use a deterministic part plan and byte-bound part URLs.

If either direct capability is absent, the client falls back to quota-enforced server transit. This fail-closed rule prevents Local or OSS from being treated as safe client-direct paths merely because their SDKs expose presign primitives.

Upload responses return a stable assetId, so clients can retry around the returned asset record instead of storing raw URLs or vendor-specific object keys.

A private file’s download URL is only minted after a permission check, via the AssetAccessPolicy SPI:

public interface AssetAccessPolicy {
boolean canRead(
AssetEntity asset,
String callerUserId,
Collection<? extends GrantedAuthority> authorities
);
}

By default public assets are readable, the owner can read, and a privileged administrator authority can read within the tenant. Unknown or unauthorized private downloads are handled as not found, so callers do not learn whether another tenant’s private asset exists; oversized or forbidden uploads return 413 or 415.

Community includes Local / S3-compatible / Aliyun OSS, private signed downloads, capability-driven upload routing, security validation and business references. Recycle-bin, CDN, image-processing, malware-scanning, retention and additional-adapter work is Roadmap or explicit contract scope — see the pricing page.