Files
GearBox/.planning/phases/17-object-storage/17-01-PLAN.md

9.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
17-object-storage 01 execute 1
src/server/services/storage.service.ts
tests/services/storage.service.test.ts
docker-compose.yml
docker-compose.dev.yml
.env.example
true
IMG-01
IMG-04
truths artifacts key_links
Storage service can upload a buffer to S3-compatible storage
Storage service can delete an object from S3-compatible storage
Storage service can generate a presigned URL for an object
Docker Compose starts MinIO with automatic bucket creation
path provides exports
src/server/services/storage.service.ts S3 storage abstraction
uploadImage
deleteImage
getImageUrl
path provides
tests/services/storage.service.test.ts Storage service unit tests with mocked S3Client
path provides contains
docker-compose.dev.yml MinIO service for local development minio
path provides contains
docker-compose.yml MinIO service for production minio
from to via pattern
src/server/services/storage.service.ts @aws-sdk/client-s3 S3Client with forcePathStyle forcePathStyle.*true
from to via pattern
docker-compose.dev.yml minio-init mc init container creates bucket mc mb.*gearbox-images
Create the S3 storage service abstraction and Docker Compose MinIO infrastructure.

Purpose: Establish the foundation that all subsequent image storage refactoring depends on. The storage service wraps @aws-sdk/client-s3 and the Docker config ensures MinIO is available for development and production. Output: storage.service.ts with uploadImage/deleteImage/getImageUrl, unit tests, MinIO in both Docker Compose files.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-object-storage/17-CONTEXT.md @.planning/phases/17-object-storage/17-RESEARCH.md

@src/server/services/image.service.ts @docker-compose.yml @docker-compose.dev.yml @.env.example

Task 1: Install S3 SDK and create storage service src/server/services/storage.service.ts, tests/services/storage.service.test.ts - src/server/services/image.service.ts (current local file storage pattern) - .planning/phases/17-object-storage/17-RESEARCH.md (Pattern 1: S3 Client Singleton, Pattern 2: Presigned URL Injection) 1. Install dependencies: `bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner`
  1. Create src/server/services/storage.service.ts per D-03, D-04, D-05:

    • Create S3Client singleton at module level with forcePathStyle: true (REQUIRED for MinIO per research pitfall 4)
    • Config from env vars: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET (default: "gearbox-images"), S3_REGION (default: "us-east-1")
    • uploadImage(buffer: Buffer | ArrayBuffer, filename: string, contentType: string): Promise<void> — uses PutObjectCommand
    • deleteImage(filename: string): Promise<void> — uses DeleteObjectCommand
    • getImageUrl(filename: string): Promise<string> — uses GetObjectCommand + getSignedUrl with configurable expiry (default 1h per D-04, configurable via S3_PRESIGN_EXPIRY env var)
    • Export a helper: async function withImageUrl<T extends { imageFilename: string | null }>(record: T): Promise<T & { imageUrl: string | null }> — returns null imageUrl when imageFilename is null, presigned URL otherwise (per D-09)
    • Export a batch helper: async function withImageUrls<T extends { imageFilename: string | null }>(records: T[]): Promise<(T & { imageUrl: string | null })[]> — uses Promise.all for parallelism per research pitfall 5
  2. Create tests/services/storage.service.test.ts:

    • Mock @aws-sdk/client-s3 S3Client.send method using mock.module from bun:test
    • Mock @aws-sdk/s3-request-presigner getSignedUrl
    • Test uploadImage calls PutObjectCommand with correct Bucket, Key, Body, ContentType
    • Test deleteImage calls DeleteObjectCommand with correct Bucket, Key
    • Test getImageUrl calls getSignedUrl and returns the result
    • Test withImageUrl returns null imageUrl when imageFilename is null
    • Test withImageUrl returns presigned URL when imageFilename is present
    • Test withImageUrls processes arrays correctly bun test tests/services/storage.service.test.ts <acceptance_criteria>
    • grep -q "uploadImage" src/server/services/storage.service.ts
    • grep -q "deleteImage" src/server/services/storage.service.ts
    • grep -q "getImageUrl" src/server/services/storage.service.ts
    • grep -q "withImageUrl" src/server/services/storage.service.ts
    • grep -q "withImageUrls" src/server/services/storage.service.ts
    • grep -q "forcePathStyle.*true" src/server/services/storage.service.ts
    • grep -q "S3_ENDPOINT" src/server/services/storage.service.ts
    • grep -q "PutObjectCommand" src/server/services/storage.service.ts
    • grep -q "getSignedUrl" src/server/services/storage.service.ts
    • bun test tests/services/storage.service.test.ts passes </acceptance_criteria> Storage service exports uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls. All unit tests pass with mocked S3 client.
Task 2: Add MinIO to Docker Compose and update env config docker-compose.yml, docker-compose.dev.yml, .env.example - docker-compose.yml (current production compose) - docker-compose.dev.yml (current dev compose) - .env.example (current env vars) - .planning/phases/17-object-storage/17-RESEARCH.md (Pattern 3: Docker Compose Init Container) 1. Update `docker-compose.dev.yml` per D-13, D-14: - Add `minio` service using `quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z` (pinned per research — do NOT use latest or Docker Hub) - Command: `server /data --console-address ":9001"` - Environment: `MINIO_ROOT_USER: minioadmin`, `MINIO_ROOT_PASSWORD: minioadmin` (fixed creds for dev per D-14) - Ports: 9000:9000 (API), 9001:9001 (console) - Volume: `minio-data-dev:/data` - Healthcheck: `["CMD", "mc", "ready", "local"]` interval 5s, timeout 3s, retries 5 - Add `minio-init` service using `quay.io/minio/mc:latest` - depends_on minio with condition: service_healthy - entrypoint shell script: set alias, create bucket `gearbox-images` with --ignore-existing, exit 0 - Add `minio-data-dev` to volumes section - Add S3 env vars to app service (if app service exists in dev compose) — if not, these will be in the shell env - Add comment noting MinIO GitHub repo archived Feb 2026, S3 API abstraction makes provider swappable
  1. Update docker-compose.yml (production) per D-13, D-14:

    • Add minio service same image, same healthcheck
    • Environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY}, MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} (env vars for prod per D-14)
    • Ports: 9000:9000 only (no console in prod)
    • Volume: minio-data:/data
    • Add minio-init same pattern but using ${S3_ACCESS_KEY:-minioadmin} and ${S3_SECRET_KEY:-minioadmin} for mc alias
    • Add S3 env vars to app service: S3_ENDPOINT=http://minio:9000, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET=gearbox-images
    • Remove uploads:/app/uploads volume from app service (per D-08 — no more local file serving)
    • Remove uploads from volumes section
    • Add minio-data to volumes section
    • app service depends_on should include minio (service_healthy)
  2. Update .env.example:

    • Add S3 section with: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET, S3_REGION
    • Include defaults as comments (endpoint: http://localhost:9000, bucket: gearbox-images, region: us-east-1) grep -q "minio" docker-compose.dev.yml && grep -q "minio" docker-compose.yml && grep -q "S3_ENDPOINT" .env.example && echo "PASS" <acceptance_criteria>
    • grep -q "quay.io/minio/minio:RELEASE.2025-09-07" docker-compose.dev.yml
    • grep -q "minio-init" docker-compose.dev.yml
    • grep -q "gearbox-images" docker-compose.dev.yml
    • grep -q "quay.io/minio/minio:RELEASE.2025-09-07" docker-compose.yml
    • grep -q "minio-init" docker-compose.yml
    • grep -q "S3_ENDPOINT" docker-compose.yml
    • grep -q "S3_ACCESS_KEY" .env.example
    • grep -q "S3_SECRET_KEY" .env.example
    • grep -q "S3_BUCKET" .env.example </acceptance_criteria> Both Docker Compose files include MinIO with automatic bucket creation. Production uses env vars, dev uses fixed credentials. .env.example documents all S3 variables.
- `bun test tests/services/storage.service.test.ts` passes - `grep -r "forcePathStyle" src/server/services/storage.service.ts` confirms MinIO compatibility - `docker compose -f docker-compose.dev.yml config` validates dev compose syntax - `docker compose config` validates prod compose syntax

<success_criteria>

  • Storage service exists with all 5 exported functions (uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls)
  • Unit tests pass with mocked S3 client
  • Both Docker Compose files include MinIO + init container
  • .env.example documents S3 configuration </success_criteria>
After completion, create `.planning/phases/17-object-storage/17-01-SUMMARY.md`