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.

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