Image Guide
This guide explains how to use Vona image in Cabloy Basic with a practical, contract-first path.
Vona image is not only a file-upload helper. It is a backend image layer built around:
- image scenes for business-specific upload policy
- image providers for storage and delivery behavior
- token-based upload and signed delivery flows
- named variants and ad hoc transforms
- serializer transforms that turn stored image IDs into
DtoImageViewoutput
Use this page together with:
TIP
Image docs path
- Backend Image Guide — learn image scenes, providers, serializer transforms, delivery policy, and backend lifecycle behavior
- Frontend Image Guide — learn
basic-image:formFieldImage,basic-image:image, relation previews, and client-side image-field behavior
Use the backend guide when the question is about image scenes, providers, serializer transforms, signed delivery, or upload policy enforcement. Use the frontend guide when the question is about forms, tables, previews, or client-side upload UX.
Read this page first when your next question is:
- which image scene should own this upload flow
- which provider or delivery mode should be used
- how
resolveVieworresolveViewsfit into the DTO contract
Why the image suite exists
Real image handling usually needs more than accepting a multipart file.
A typical business flow also needs to answer questions such as:
- which upload use cases are allowed
- which provider should own the file
- what size and MIME types are allowed
- whether the client uploads through the app server, uploads directly, or uploads by URL
- how the image should be delivered later
- whether delivery should be signed
- how APIs should expose image data in DTO responses
Vona treats that as one backend capability.
Smallest correct mental model
The smallest practical model is:
@ImageScene(...)defines a business upload sceneBeanImageUploadPolicyresolves the scene into provider, client, upload limits, and temp tokensControllerImageexposes the HTTP endpointsBeanImageis the main programmatic API for upload, variant URL resolution, download, delete, and DTO-side image view resolution@ImageProvider(...)implementations map the generic image contract to concrete storage or CDN behavior- serializer transforms
a-image:resolveViewanda-image:resolveViewsconvert stored image IDs intoDtoImageViewoutput
That division is the key to reading the source correctly.
Suite boundary
The image suite lives under vona/src/suite-vendor/a-image and is split into:
a-image: the core abstraction, controller, persistence, policy, variants, and serializer transformsimage-native: local filesystem storage plus Sharp-based transformsimage-cloudflare: Cloudflare Images integration
In other words:
a-imagedefines the contract- provider modules implement the contract
Core concepts
Image provider
An image provider is the storage and delivery backend.
Providers are registered with @ImageProvider(...) and implement the IImageProviderExecute contract.
Representative built-in providers are:
image-native:nativeimage-cloudflare:cloudflare
Required provider capabilities are:
uploadgetdeletegetVariantUrl
Optional capabilities are:
uploadUrlcreateDirectUploaddownload
That means a provider can support the basic lifecycle without supporting every upload style.
Provider client
A provider can expose multiple named clients.
BeanImageProvider.getClientOptions(...) resolves client options by layering:
- caller defaults
- provider decorator
base - provider decorator
clients[clientName] - persisted
aImageProvider.clientOptions - caller runtime overrides
This lets one provider support multiple delivery or credential configurations without changing business code.
Image scene
An image scene is the business-facing upload policy.
A scene is registered with @ImageScene(...) and can define:
- provider selection
- client selection
- upload rules
- optional per-scene metadata
Representative scene shape:
@ImageScene({
upload: {
maxSize: 2 * 1024 * 1024,
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
},
})
export class ImageSceneStudentImage extends BeanBase {}This keeps business upload rules in one named backend capability instead of scattering them across controllers and services.
Image variant
A variant is how an image should be delivered.
Vona supports two modes:
- a named variant such as
originalorthumbnail - an ad hoc transform expressed through
transformOptions
The built-in named variants are:
originalthumbnailsmallmediumlargecoveravatar
The source also makes one important rule explicit:
variantNameandtransformOptionsare mutually exclusive
If neither is provided, Vona falls back to the configured default variant, which is currently original.
Signed delivery
Vona supports signed delivery in two forms:
- proxy-signed delivery: Vona creates a temporary delivery token and redirects through
/image/delivery?imageId=... - provider-signed delivery: the provider generates the final signed URL directly
The built-in defaults differ:
image-native:nativedefaults tosignedDeliveryKind: 'proxy'image-cloudflare:cloudflaredefaults tosignedDeliveryKind: 'provider'
Main backend API
The main backend surface is app.bean.image.
Representative methods are:
upload(...)uploadUrl(...)createDirectUpload(...)get(...)delete(...)getVariantUrl(...)download(...)resolveView(...)resolveViews(...)
A useful practical boundary is:
- use
BeanImageUploadPolicywhen you need scene resolution and upload policy - use
app.bean.imagewhen you need the actual image lifecycle and delivery behavior
Persistence model
The core module persists two main tables.
aImage
Each stored image records backend-facing data such as:
- provider and client
- provider resource ID
- filename and content type
- size, width, and height
- signed-delivery requirement
- variants
- custom metadata
- storage path
- delivery base URL
- image scene
aImageProvider
Provider-client configuration is stored separately and includes fields such as:
disabledproviderNameclientNameclientOptions
If a provider-client record does not exist yet, BeanImageProvider can lazily register it.
Operational flows
Authenticated multipart upload
Ordinary image upload is one authenticated multipart request:
- submit
POST /image/uploadas multipart form data - send:
imageSceneimage
- Vona reads the received file's actual size and MIME type
- Vona resolves the image scene, provider, visibility, and metadata server-side
- Vona validates actual size, MIME type, and extension against the scene policy, then uploads through the resolved provider
Representative controller shape:
@Web.post('upload')
@Core.fileUpload({
busboy: {
limits: { fields: 1, files: 1, parts: 3 },
},
})
@Api.contentType('application/json')
async upload(
@Arg.field('imageScene', v.required(), z.string()) imageScene: string,
@Arg.file('image', v.required()) file: IUploadFile,
) {
const policy = await this.bean.imageUploadPolicy.resolveUploadPolicy({
imageScene,
size: (await fse.stat(file.file)).size,
mimeType: file.info.mimeType,
});
await this.bean.imageUploadPolicy.validateUploadFile(...);
return await this.bean.image.upload(...);
}The normal passport guard remains the authorization boundary. Temporary tokens are still used for signed delivery where needed, but ordinary image upload does not issue or accept an upload token.
Direct upload
POST /image/direct-upload is for providers that can create an upload target without proxying the entire lifecycle through the shared POST /image/upload endpoint.
The shared flow is:
- resolve the image scene and upload policy
- call
app.bean.image.createDirectUpload(...) - let the provider create the direct-upload resource
- persist the draft image record
- return the public direct-upload session: Cabloy image
id, provider-hosteduploadUrl, and optional presentation fields - upload bytes to the returned
uploadUrl - finalize through
POST /image/direct-upload/finalizeusing the Cabloy imageid; the finalized response has the normal public image action shape
The built-in Cloudflare provider supports this flow by returning a provider-hosted upload target.
The native provider does not support direct upload and should use the standard authenticated /image/upload flow instead.
That provider difference is exactly why the shared API keeps direct upload split into create-upload-target and finalize steps.
Upload by URL
POST /image/upload-url is for providers that can ingest a remote URL directly.
The flow is:
- resolve the image scene and upload policy
- validate the declared
sizeandmimeType - call
app.bean.image.uploadUrl(...) - let the provider fetch or ingest the remote URL
- persist the image record and return the normal image resource result
The built-in Cloudflare provider supports this flow.
The native provider does not implement uploadUrl.
Signed delivery redirect
When delivery is signed and the provider uses proxy delivery, BeanImage.getVariantUrl(...) creates a temporary delivery token and returns a local delivery URL such as:
/image/delivery?imageId=...&token=...
The delivery controller then:
- verifies the delivery token against the fixed delivery endpoint path
- confirms the token image ID matches the request
imageIdquery value - redirects to the final target URL
This is why native signed delivery behaves differently from Cloudflare signed delivery.
Configuration defaults
The core image config currently defines:
image: {
defaultVariant: 'original',
defaultProvider: 'image-native:native',
defaultClientName: 'default',
upload: {
maxSize: 2 * 1024 * 1024,
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
},
}That gives the suite a working default even before a scene overrides anything.
Provider behavior differences
Native provider
image-native:native stores files locally and uses Sharp-backed processing.
Practical behavior to know:
- it defaults to proxy-signed delivery
- it can return local static URLs under
/api/static/... - it does not support direct upload and uses the standard authenticated multipart upload flow
- named variants produce stable variant files such as
__thumbnail - ad hoc transforms produce stable hashed files such as
__t_<hash> - repeated requests for the same custom transform reuse the same cached transformed output
- when downloading the unsigned original variant, it can return a buffer instead of a URL
- it still does not implement
uploadUrl(...)for provider-side remote URL ingestion
These behaviors are demonstrated directly in the native image tests.
Cloudflare provider
image-cloudflare:cloudflare delegates storage and delivery to Cloudflare Images.
Practical behavior to know:
- it defaults to provider-signed delivery
- it supports direct upload and upload by URL
originalresolves to Cloudflare’spublicdelivery path- ad hoc transforms become Cloudflare transform segments such as
w=320,h=180,fit=cover - signed delivery appends
expandsigquery parameters
These behaviors are demonstrated in the Cloudflare mapping tests.
Business integration patterns
The best way to understand Vona image is to follow a real business example.
Single-image pattern: training-student
The training-student module stores one image field:
@Api.field(
v.title($locale('StudentImage')),
v.optional(),
ZovaRender.field('basic-image:formFieldImage', {
imageScene: 'training-student:studentImage',
accept: ['image/png', 'image/jpeg', 'image/webp'],
maxSize: 2 * 1024 * 1024,
enableCrop: true,
cropAspectRatio: 1,
relationName: 'image',
resize: {
width: 512,
height: 512,
fit: 'cover',
format: 'jpeg',
quality: 90,
},
}),
ZovaRender.cell('basic-image:image', { relationName: 'image' }),
v.tableIdentity(),
)
imageId?: TableIdentity;The important backend point is not the frontend widget itself.
The important point is that the backend contract names a scene:
training-student:studentImage
That scene centralizes the upload policy while the entity field keeps the persisted value as imageId.
If you want the frontend-side meaning of that same field contract — how imageId pairs with image, how form previews refresh, and how the table thumbnail works — continue with Frontend Image Guide.
DTO-side image view resolution
The same module exposes the stored image ID through a resolved relation field in DTO output:
@Api.field(
ZovaRender.visible(false),
v.optional(),
v.serializerTransform('a-image:resolveView', {
fieldName: 'imageId',
imageScene: 'training-student:studentImage',
}),
v.object(DtoImageView),
)
image?: DtoImageView;This means:
- the stored field remains
imageId - the DTO adds the resolved relation field
image?: DtoImageView - serialization resolves the image lazily through
app.bean.image.resolveView(...)
Remember the serializer rule from Serialization Guide: field-level serializer metadata only runs when the target API enables serialization.
Multi-image pattern: training-record
The training-record module shows the array case.
The entity stores sceneImageIds?: TableIdentity[] and points to the scene:
training-record:sceneImage
That scene allows multiple uploads.
The DTO then resolves the stored array with:
@Api.field(
ZovaRender.visible(false),
v.optional(),
v.serializerTransform('a-image:resolveViews', {
fieldName: 'sceneImageIds',
imageScene: 'training-record:sceneImage',
}),
v.array(DtoImageView),
)
sceneImages?: DtoImageView[];This is the cleanest pattern when business storage should keep image IDs, while API output should expose resolved relation fields with view-ready image objects.
Extending the system
Add a new image scene
When the business needs a new image use case, add a new @ImageScene(...) bean.
A scene is the right place for:
- upload limits
- provider routing
- provider client selection
- scene-specific metadata
- the decision about whether the use case is single-image or multi-image
The core module already provides a boilerplate path for image scenes:
vona/src/suite-vendor/a-image/modules/a-image/cli/imageScene/boilerplate/
A practical sequence is:
- create the scene bean
- define
uploadrules and optional provider/meta resolvers - point entity or DTO metadata at the new scene name
- verify authenticated multipart upload and serializer behavior end to end
Add a new image provider
When the storage backend changes, add a new @ImageProvider(...) bean that implements IImageProviderExecute.
The provider boilerplate lives under:
vona/src/suite-vendor/a-image/modules/a-image/cli/imageProvider/boilerplate/
Model new providers after the built-in implementations:
image-nativefor local filesystem plus transform behaviorimage-cloudflarefor remote CDN-backed delivery behavior
A practical provider checklist is:
- implement the required methods
- decide whether to support
uploadUrl,createDirectUpload, anddownload - only implement
createDirectUploadwhen the provider can hand the client a real external upload target instead of routing bytes back through the local app - define sensible base options in the decorator
- verify variant URL behavior, signed delivery behavior, and delete behavior
Test-backed reading map
The current repo already contains strong executable examples under:
vona/src/suite-vendor/a-test/modules/test-image/test/
The most useful tests are:
imageUpload.test.tsfor authenticated multipart upload, native direct-upload rejection, Cloudflare direct-upload, and upload-url request flowsimageNative.test.tsfor local upload, variant URLs, signed delivery, and delete behaviorimageCloudflareMapping.test.tsfor Cloudflare mapping, custom transforms, signed delivery, direct upload, and upload-by-URL behavior
When the source and a prose explanation seem to disagree, prefer the current source and tests.
Verification checklist
When writing or changing image-related docs, verify in this order:
- check the controller flow in
ControllerImage - check scene and token behavior in
BeanImageUploadPolicy - check delivery and DTO view behavior in
BeanImage - check provider-specific differences in
image-nativeandimage-cloudflare - check business examples in
training-studentandtraining-record - check the test-image suite for executable confirmation
- run the docs build and confirm the page appears in the sidebar
A good final question is:
- does this explanation stay backend-first and source-backed, or has it drifted into a generic image-upload article?
If it stays centered on scenes, providers, variants, signed delivery, and serializer transforms, it is aligned with how Vona image actually works.