Server
Use the Storage service on the server for uploads, reads, signed URLs, and asset lifecycle operations.
Tulip's server-side storage API lives on the Storage service. Once you have added storage to your app context, backend code can upload files, read existing assets, generate signed URLs, and manage deletion and cleanup.
When to use the server API
Use the server API when:
- your app generates a PDF, CSV, image, or archive
- your backend already has the file bytes
- you need to read or process stored files on the server
- you need signed URLs in server code
- you need delete, restore, or purge operations
If the upload starts with a user selecting a file in the browser, use /docs/storage/client instead.
Access the service
Most server code uses the storage service through your app context.
const asset = await context.storage.getAssetById(assetId);In practice, context.storage is your shared Storage instance from /docs/storage/setup.
Upload directly from the server
Use storage.upload() when the file bytes already exist on the server or are generated there.
const asset = await context.storage.upload({
name: "report.pdf",
body: fileBuffer,
contentType: "application/pdf",
size: fileBuffer.byteLength,
visibility: "private",
metadata: { source: "report-job" },
});This flow:
- validates the input
- creates a
pendingasset row - uploads the bytes through the adapter
- marks the asset as
ready - marks the asset as
errorif the upload fails
Supported body types
The server upload API accepts:
stringUint8ArrayBuffer- Node.js
Readable
That makes it a good fit for generated files, stream output, and content loaded from other systems.
Read an existing object
Use getObject() when you need the file bytes on the server.
const object = await context.storage.getObject(assetId);
console.log(object.contentType);
console.log(object.size);The returned body is a Node.js readable stream. This is useful when you need to merge PDFs, inspect files, or transform stored content.
In demo-spark, this pattern is used while generating technical sheet PDFs from existing source files.
Generate a signed read URL
Use getObjectURL() when server code needs a temporary provider URL.
const url = await context.storage.getObjectURL(assetId, {
expiresIn: 3600,
disposition: "inline",
});This is useful when:
- a server-rendered document needs a temporary image URL
- backend code needs to hand off a short-lived provider URL
- you want signed access outside the normal
/api/storage/files/:idroute flow
In demo-spark, this pattern is used to resolve a temporary cover image URL for generated technical sheet PDFs.
Delete, restore, and purge assets
The storage service supports three different lifecycle operations.
Soft delete
Use deleteAsset() or deleteAssets() to mark assets as deleted by setting deletedAt.
await context.storage.deleteAssets(ids);This keeps the database rows and provider objects in place.
Restore
Use restoreAsset() or restoreAssets() to undo a soft delete.
await context.storage.restoreAsset(assetId);Purge
Use purgeAsset() or purgeAssets() to permanently remove both the provider objects and the database records.
await context.storage.purgeAsset(assetId);Use purge carefully because it is the irreversible cleanup path.
About browser upload internals
The Storage service also exposes presignUpload() and confirmUpload(), but those are usually wrapped by createStorageProcedures() and consumed through createUploadClient().
For most browser-driven upload flows, use /docs/storage/client instead of calling those methods directly in your UI.
Quick guidance
- Use
upload()when the file bytes are already on the server. - Use
getObject()when backend code needs the actual file body. - Use
getObjectURL()when backend code needs a temporary signed URL. - Use
delete,restore, andpurgeto manage asset lifecycle.
If you want to render or download assets in the UI, continue with /docs/storage/serving-files.