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.
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.