Object Storage
tsdevstack provides unified object storage across all cloud providers. Add a bucket, import StorageModule, and your code works identically on local MinIO, AWS S3, GCP Cloud Storage, and Azure Blob Storage.
Quick start
Add a storage bucket to your project:
This updates config.json. Then sync to regenerate docker-compose, secrets, and start MinIO:
Import the module in your NestJS service:
Use it in a service:
How it works
The StorageProvider interface is the same across all environments. The adapter is selected automatically at runtime based on the SECRETS_PROVIDER environment variable:
No code changes are needed between environments. The same StorageProvider calls work everywhere.
Local development (MinIO)
When storage buckets are configured, docker-compose.yml includes MinIO — an S3-compatible object storage server.
Default credentials: minioadmin / minioadmin
What happens on docker compose up
- MinIO container starts on ports 9000 + 9001
minio-initjob runs automatically — creates all configured buckets using themcCLI- Bucket names follow the cloud naming pattern:
{project}-{name}-dev
Data persistence
MinIO data is stored in ./data/minio/ (bind mount), the same pattern used by PostgreSQL (./data/{name}/) and Redis (./data/redis/). Data persists across container restarts.
StorageModule setup
Static configuration
Use forRoot when bucket names are known at compile time:
Async configuration
Use forRootAsync when you need dynamic configuration (e.g., reading from SecretsService during initialization):
Both register the module globally — import once in AppModule and @InjectStorage works everywhere.
Multi-bucket access
For services that need multiple buckets, inject each one separately:
Or use StorageService to access any bucket dynamically:
File Uploads
There are two approaches to handling file uploads. Choose based on your requirements.
Traditional upload (through backend)
Files flow through the full stack: Client → Next.js → Kong → NestJS → Storage.
Use when: You need server-side validation, custom naming, synchronous post-processing, or atomic database + file operations.
Size limits:
Configure the Kong limit in infrastructure.json:
Presigned URL upload (direct to storage)
Files go directly from the client to the storage provider, bypassing WAF, load balancer, Kong, and NestJS entirely.
Use when: Files are large (>10MB), you want to avoid server-side memory/CPU load, or you need to exceed provider WAF/LB size limits.
Key details:
- Works across all providers (GCS v4 signatures, S3 presigned URLs, Azure SAS tokens)
- Maximum URL expiration: 7 days (all providers), configurable via
expiresInSeconds - No size limit imposed by WAF or Kong — the file goes directly to the storage bucket
- The backend only handles URL generation (fast, no file data in memory)
StorageProvider API
All methods are available on every provider adapter.
Types
Bucket naming
Cloud bucket names are derived automatically. You only specify the logical name.
Secrets
Storage secrets are framework-managed and shared-scope — they are auto-assigned to all services via secret-map.json. You never need to manually add them to .secrets.user.json.
Local secrets
Cloud secrets
In cloud environments, only STORAGE_BUCKET_{NAME} secrets exist. Credentials are handled by IAM roles — no access keys in containers:
For deeper coverage, see How Secrets Work.
Cloud deployment
Running infra:deploy handles everything:
- Terraform creates cloud buckets — S3 buckets (AWS), GCS buckets (GCP), or Azure Blob containers
- IAM permissions are granted — service roles get read/write access to buckets
- Post-Terraform sync pushes
STORAGE_BUCKET_*secret values to the cloud secret manager - Services are deployed with access to the new bucket names via SecretsService
Per-provider details
CORS is set to * on all providers. Buckets are private — presigned URLs are the security boundary, not CORS. Browser access requires a presigned URL obtained via your API (which is protected by Kong's CORS).
For provider-specific architecture details, see GCP Architecture, AWS Architecture, or Azure Architecture.
Removing a bucket
What it does locally:
- Removes bucket from
config.json - Regenerates
docker-compose.yml(removes MinIO entirely if this was the last bucket) - Regenerates secrets (removes
STORAGE_*secrets if this was the last bucket) - Does NOT delete local MinIO data — it remains in
./data/minio/
What it does NOT do:
- Does not remove cloud resources (S3 buckets, GCS buckets, Azure Blob containers)
- Does not remove
STORAGE_BUCKET_*secrets from cloud secret managers - Does not delete any stored files
To clean up cloud resources:
- Run
infra:deploy --env {env}to apply Terraform changes
This permanently deletes all data in the cloud bucket. Back up your data first if needed.
- Remove cloud secrets:
cloud-secrets:remove STORAGE_BUCKET_UPLOADS --env {env} - If this was the last bucket, also remove
StorageModuleimports from your NestJS code