Tulip Logo IconTulip
Storage

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 files
  • Storage, the server service for presigning, confirming, reading, deleting, restoring, and purging assets
  • createStorageProcedures(), the RPC procedures for browser upload flows
  • createStorageRouteHandler(), the /api/storage/[[...rest]] route for serving files
  • createUploadClient(), UploadZone, and StorageImage for browser uploads and rendering
  • getAssetURL() 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:

  1. Your app creates a Storage instance with a database client and adapter.
  2. The browser asks your app to presign an upload.
  3. Tulip creates a pending row in storage_assets.
  4. The browser uploads the file directly to object storage.
  5. Your app confirms the upload and the asset becomes ready.
  6. 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 yet
  • ready: the file is uploaded and can be served
  • error: 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 caseChoose
Attach files to your own recordsStorage
Upload and render images or documents in feature UIsStorage
Need folders, namespaces, and a file manager workflowDrive
Need a structured content tree on top of uploaded assetsDrive

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.
  • disposition can 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.

On this page