Changelog - 2026-09-07
The asset controller serves one bucket
New FeatureIn one line. Set controller.bucket and every object route drops /buckets/{bucketName}.
controller: {
name: 'AssetController',
basePath: '/assets',
bucket: () => process.env.ASSET_BUCKET ?? 'images',
},GET /assets/objects/photo.jpg now streams the object. The function runs on every request, so the environment variable is read late, not at boot.
The problem it solves
An application that serves one bucket does not want the bucket in its URLs. Adopting the framework shape meant rewriting every asset link, including the ones already stored in a database. One consumer wrote a 456-line controller instead.
What changed
| Symbol | Change | Package |
|---|---|---|
controller.bucket | New. string or () => string, read per request | core-server |
controller.rawObjectPath | New. boolean, default false | core-server |
buildAssetDefinitions({ hasConfiguredBucket, rawObjectPath }) | New. Builds the ten route configs | core-server |
StaticAssetDefinitions | Unchanged value, now the output of buildAssetDefinitions({}) | core-server |
A configured bucket shortens upload, listObjects, getObjectByName, downloadObjectByName, deleteObject and recreateMetaLink. Those routes also lose the bucketName path param.
getBuckets, getBucketByName, createBucket and deleteBucket are not registered when a bucket is configured. They answer 404. A single-bucket application has no reason to expose bucket creation or deletion.
The generated link follows the same shape, in the upload response and in the PUT .../meta-links/... route.
Raw nested object paths
New FeatureIn one line. Set rawObjectPath: true and a folder path resolves without percent-encoding.
controller: { name: 'AssetController', basePath: '/assets', rawObjectPath: true },The problem it solves
{objectName} is one path segment. A folder path only resolved encoded, as photos%2F2024%2Ff.jpg; the raw form answered 404. Links stored years ago carry the raw form.
What changed
The object segment becomes the {objectName}{.+} catch-all. Both forms now resolve, so no existing link breaks. Every validation still runs on the joined path, maxFolderDepth included.
The four combinations
bucket | rawObjectPath | The object route |
|---|---|---|
| unset | false | /assets/buckets/images/objects/photos%2F2024%2Ff.jpg |
| unset | true | /assets/buckets/images/objects/photos/2024/f.jpg |
'images' | false | /assets/objects/photos%2F2024%2Ff.jpg |
'images' | true | /assets/objects/photos/2024/f.jpg |
#A catch-all matches everything under its prefix, and built-in routes register first. defineRoutesBefore registers your own routes ahead of them, so a literal path such as /objects/i18n still wins. defineExtraRoutes keeps registering after the built-ins.
Who is affected
- Applications that copied the asset controller to change its URLs. Delete the copy and set the two options.
- Applications that set
bucket. Move bucket creation out of the HTTP layer. Those four routes are gone. - Everyone else. No action needed. Both options are optional, and unset means today's paths, params, links and status codes.
Details
- Reference: Static Asset Component.