ShipNext

Storage Tables

The storage_files table, upload records, usage tracking, and expiration cleanup.

Storage table schemas live in:

src/core/db/schema/sqlite/storage.schema.ts
src/core/db/schema/pg/storage.schema.ts

Object storage itself is handled by src/modules/storage. Browser uploads use presigned multipart URLs, then write storage_files records for usage tracking, file lookup, and cleanup.

Business boundary

storage_files stores metadata only. File bytes live in S3-compatible storage such as Cloudflare R2 or AWS S3.

Related files:

FileResponsibility
src/modules/storage/provider.tsS3-compatible provider and URL generation
src/modules/storage/services/browser-upload.service.tsClient upload flow
src/modules/storage/services/multipart-upload.service.tsPresigned parts and multipart completion
src/modules/storage/repositories/storage-file.repository.tsFile records, deletion, expiration queries
app/api/storage/presigned-url/*Browser upload APIs
app/api/storage/usage/route.tsUser storage usage API

storage_files

FieldDescription
idFile record ID
user_idOwner user
object_keyUnique object storage key
file_nameDisplay filename
file_sizeSize in bytes
mime_typeOptional MIME type
expires_atExpiration timestamp; null means never expires
created_atRecord creation time

object_key is not a full URL. It is a storage key such as:

userId/avatar/uuid-file.png

Resolve it to a public URL when displaying files.

Upload write flow

Create presigned upload

The client calls POST /api/storage/presigned-url. The server checks auth, file size, expiration, and quota, then returns objectKey, uploadId, and part URLs.

Upload directly to object storage

The browser sends file parts directly to object storage with PUT.

Complete and insert record

The client calls POST /api/storage/presigned-url/complete. The server completes the multipart upload, reads the actual size with HeadObject, then inserts storage_files.

Track usage

getUserStorageUsage(userId) sums storage_files.file_size. GET /api/storage/usage returns used bytes and quota.

Expiration and cleanup

Upload expires is in hours:

  • -1: never expires, expires_at = null
  • positive integer: current time plus that many hours

The repository provides:

getExpiredFiles(batchSize);
deleteFileRecordByKey(objectKey);

ShipNext does not include a cleanup cron by default. Add one if you need automatic object deletion.

User avatars

Profile avatar upload stores the returned key in user.image. Display code uses:

import { resolveUserImageUrl } from "@/modules/storage";

const src = user.image ? resolveUserImageUrl(user.image) : null;

So user.image can be either a full URL or a storage object key.

Billing quota relationship

ConfigLocationPurpose
plan.storageLimitsrc/modules/billing/config/plan.tsDashboard usage display
EntitlementResourceType.Storageprices[].entitlementsUpload API quota enforcement

If upload API returns quota: null, no API-level storage quota is enforced.

Extension guidance

  • Continue storing object_key, not only full URLs.
  • Delete both object storage files and storage_files records when removing files.
  • Keep field meanings stable if you add a non-S3 provider.
  • Trust server-side HeadObject size over client-provided file size.

On this page