Introduction
Upload files to object storage, track them in your database, and serve them through your app.
Tulip Storage is the low-level file module in @tulip-systems/core. It gives you a typed asset catalog in your database, server utilities for presigned and direct uploads, client upload helpers, and a file route for serving private or public files through your app.
What the module includes
storage_assets, the canonical table for uploaded filesStorage, the server service for presigning, confirming, reading, deleting, restoring, and purging assetscreateStorageProcedures(), the RPC procedures for browser upload flowscreateStorageRouteHandler(), the/api/storage/[[...rest]]route for serving filescreateUploadClient(),UploadZone, andStorageImagefor browser uploads and renderinggetAssetURL()for stable app URLs such as/api/storage/files/:id
Entry points
Use the shared package entry points depending on where the code runs:
import { getAssetURL } from "@tulip-systems/core/storage";
import { StorageImage, UploadZone, createUploadClient } from "@tulip-systems/core/storage/client";
import {
Storage,
createStorageProcedures,
createStorageRouteHandler,
storageS3Adapter,
} from "@tulip-systems/core/storage/server";How storage works
Tulip Storage is built around a simple upload lifecycle:
- Your app creates a
Storageinstance with a database client and adapter. - The browser asks your app to presign an upload.
- Tulip creates a
pendingrow instorage_assets. - The browser uploads the file directly to object storage.
- Your app confirms the upload and the asset becomes
ready. - UIs render the file through
/api/storage/files/:id, which redirects to a short-lived signed read URL.
The storage catalog uses three statuses:
pending: the asset exists in the database but the upload is not confirmed yetready: the file is uploaded and can be servederror: the upload flow failed and the asset needs cleanup or retry
What Storage is good at
Use Storage when you need a file layer for domain records such as product images, email attachments, generated PDFs, or import files. It is especially useful when you want:
- database-backed asset metadata
- presigned browser uploads
- typed server-side file operations
- private file delivery through your own app routes
- a reusable upload flow across multiple features
Storage vs Drive
| Use case | Choose |
|---|---|
| Attach files to your own records | Storage |
| Upload and render images or documents in feature UIs | Storage |
| Need folders, namespaces, and a file manager workflow | Drive |
| Need a structured content tree on top of uploaded assets | Drive |
Storage manages flat objects and their metadata. Drive builds folder-like structure, tree operations, and content organization on top of Storage.
If you are working on Drive-specific UIs, continue with /docs/storage/drive for the shared client view config APIs.
Serving files
Tulip does not expect your UI to store raw provider URLs. Instead, the UI works with stable app URLs:
import { getAssetURL } from "@tulip-systems/core/storage";
const src = getAssetURL(assetId);That route is handled by createStorageRouteHandler(). When a file is requested, Tulip resolves the asset, checks visibility, and redirects to a signed object URL.
- Public assets can be requested without a session.
- Private assets require an authenticated session before the redirect happens.
dispositioncan be used to request inline display or attachment download behavior.
Current scope
- Tulip currently ships with one built-in adapter:
storageS3Adapter(). - The module tracks uploaded objects in
storage_assets; object keys stay internal to the storage service. - Asset metadata is a string-to-string record.
- Storage is intentionally low-level. Authorization rules beyond basic private/public checks still belong in your app.
The next step is wiring the schema, service, procedures, and route into your app.