## Template Version Alignment
- Go: 1.23 → 1.25 across all templates (go.work, go.mod, Dockerfiles, CI)
- Alpine: latest → 3.19 (explicit version pinning)
- Woodpecker: failure:retry → failure:ignore (invalid syntax fix)
## SDLC Tree Fixes (slackpath-5-full-lifecycle)
Fixed merge failures by correcting lifecycle flow:
1. **Branch Creation**: Added missing create-branch step (planned → ready)
- Bug: Merge command requires feature.Branch field to be set
- Fix: POST /projects/{id}/sdlc/features/{slug}/branch
2. **Artifact Status**: Changed approval to pass for execution artifacts
- Bug: Review/audit/QA need status="passed" not "approved"
- Fix: /artifacts/{type}/approve → /artifacts/{type}/pass
- Added: pass-qa step after wait-qa
3. **Phase Transition Order**: Reordered merge phase transition
- Bug: Merge command checks if phase == "merge" first
- Fix: transition-to-merge BEFORE merge-feature (not after)
## GCS Provisioner Fix
- Replaced deprecated option.WithCredentialsFile with env var approach
- Now uses GOOGLE_APPLICATION_CREDENTIALS for ADC (Application Default Credentials)
- Avoids security risk from deprecated credential options
- Fixed test: Added ComponentTypeGCS to ValidComponentTypes test
## Critical Rules Added
- Version alignment: All template versions must stay in sync
- When updating versions, grep entire templates/ tree
## Files Changed
- 27 template files: Go version + Woodpecker syntax
- 1 tree file: SDLC lifecycle flow corrections
- 1 CLAUDE.md: Version alignment rule
- 1 GCS provisioner: Deprecated API fix
- 1 test file: Added missing component type
Root cause: Skeleton templates lagged behind Go 1.25 release and had
invalid Woodpecker syntax. SDLC tree skipped required branch creation
and used wrong artifact approval endpoints.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
Media Handling Specification
Version: 1.0 Status: Implementation Owner: Platform Team Last Updated: 2026-02-08
Overview
This specification defines comprehensive media handling for rdev, enabling generated projects to store and serve user-uploaded files (images, videos, documents) via Google Cloud Storage. The implementation follows rdev's established patterns for infrastructure provisioning, skeleton packages, and component templates.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ rdev Platform Layer │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ GCS Provisioner (internal/adapter/gcs) │ │
│ │ - Creates per-project buckets │ │
│ │ - Generates service account credentials │ │
│ │ - Stores credentials in rdev credential store │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓ credentials
┌─────────────────────────────────────────────────────────────┐
│ Generated Project Layer │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ pkg/storage (skeleton package) │ │
│ │ - Storage interface abstraction │ │
│ │ - GCS implementation │ │
│ │ - Memory implementation (testing) │ │
│ │ - Signed URL generation │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ media-upload Component (optional) │ │
│ │ - HTTP upload/download/delete endpoints │ │
│ │ - File validation │ │
│ │ - Path management │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Google Cloud Storage (External) │
│ - Per-project buckets (project-{name}-media) │
│ - Per-project service accounts │
│ - IAM bindings (objectAdmin) │
│ - Lifecycle rules (temp/ auto-cleanup) │
│ - CORS configuration │
└─────────────────────────────────────────────────────────────┘
Storage Patterns
Path Conventions
Projects should organize objects using consistent path patterns:
uploads/{user_id}/{timestamp}-{filename} # User-uploaded files
avatars/{user_id}.jpg # User avatars
temp/{session_id}/{filename} # Temporary files (auto-delete after 24h)
public/{category}/{filename} # Public assets (logos, etc.)
private/{user_id}/{document_id}/{filename} # Private documents (signed URLs only)
Content Types
Supported MIME types:
- Images:
image/jpeg,image/png,image/gif,image/webp,image/svg+xml - Videos:
video/mp4,video/webm,video/quicktime - Documents:
application/pdf,application/msword,application/vnd.openxmlformats-officedocument.* - Archives:
application/zip,application/x-tar,application/gzip
Size Limits
- Default max upload: 100MB per file
- Component template: Configurable via
MAX_UPLOAD_SIZEenv var - GCS bucket: No hard limit (quota-based)
TTL and Expiry
Lifecycle rules automatically delete objects:
temp/*paths: 24 hours- User can configure custom rules in GCS console
Security Model
Authentication Flow
-
Provisioning Time (rdev API):
- Create GCS bucket:
project-{name}-media - Create service account:
project-{name}-storage@{gcp-project}.iam.gserviceaccount.com - Grant IAM role:
roles/storage.objectAdminon bucket - Generate service account JSON key
- Store credentials in rdev credential store (encrypted)
- Create GCS bucket:
-
Runtime (generated project):
- Read credentials from env vars:
GCS_BUCKET,GCS_SERVICE_ACCOUNT_JSON - Initialize
pkg/storageclient with service account JSON - Client uses ADC (Application Default Credentials) with service account
- Read credentials from env vars:
IAM Roles
Per-project service accounts have isolated permissions:
- Bucket-scoped:
roles/storage.objectAdmin(CRUD on objects) - No cross-project access: Service account A cannot access bucket B
- No IAM permissions: Cannot modify IAM policies or create resources
Signed URLs
For temporary access without service account credentials:
// Generate read URL (1 hour expiry)
signedURL, _ := storageClient.SignURL(ctx, "uploads/photo.jpg", time.Hour, false)
// Generate write URL (15 min expiry) for client-side uploads
uploadURL, _ := storageClient.SignURL(ctx, "uploads/photo.jpg", 15*time.Minute, true)
Use cases:
- Direct browser downloads (avoid proxying through API)
- Client-side uploads (POST directly to GCS, not API)
- Sharing files with external users (time-limited links)
CORS Configuration
Buckets are created with CORS rules:
MaxAge: 3600
Methods: [GET, POST, PUT, DELETE, OPTIONS]
Origins: ["https://*.threesix.ai"]
ResponseHeaders: ["Content-Type", "ETag"]
Projects should override origins for custom domains.
API Standards
Upload Endpoint
Request:
POST /api/media-upload/upload
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
<binary data>
--boundary--
Response (201 Created):
{
"data": {
"url": "https://storage.googleapis.com/project-myapp-media/uploads/1706889600-photo.jpg",
"path": "uploads/1706889600-photo.jpg",
"filename": "photo.jpg",
"size": 245678
}
}
Error Responses:
400 Bad Request: File too large, invalid form, missing file500 Internal Server Error: Upload failed (GCS error)
Download Endpoint
Request:
GET /api/media-upload/download/uploads/1706889600-photo.jpg
Response (307 Temporary Redirect):
HTTP/1.1 307 Temporary Redirect
Location: https://storage.googleapis.com/project-myapp-media/uploads/1706889600-photo.jpg?X-Goog-Algorithm=...
Error Responses:
404 Not Found: File does not exist
Delete Endpoint
Request:
DELETE /api/media-upload/delete/uploads/1706889600-photo.jpg
Response (204 No Content):
HTTP/1.1 204 No Content
Error Responses:
404 Not Found: File does not exist500 Internal Server Error: Delete failed
Rate Limiting
Component template should include rate limiting:
- Upload: 10 requests/minute per IP
- Download: 100 requests/minute per IP
- Delete: 10 requests/minute per IP
Use github.com/go-chi/httprate middleware.
Testing Strategy
Unit Tests
Platform (GCS Provisioner):
TestSanitizeForGCP: Validate bucket name sanitizationTestBucketNameFor: Validate bucket naming conventionTestServiceAccountEmailFor: Validate service account email format
Skeleton (pkg/storage):
TestMemoryStorage: Verify in-memory implementationTestUploadOptions: Validate option handlingTestErrorHandling: Verify error types (ErrNotFound, etc.)
Integration Tests
GCS Provisioner:
// Requires GCS_TEST_PROJECT_ID and GCS_TEST_CREDENTIALS_PATH env vars
func TestGCSProvisionerIntegration(t *testing.T) {
// Create bucket
creds, err := provisioner.CreateProjectBucket(ctx, "test-project-123")
// Verify bucket exists in GCS
// Verify service account exists
// Verify IAM bindings
// Cleanup
provisioner.DeleteProjectBucket(ctx, "test-project-123", true)
}
pkg/storage:
// Requires test GCS bucket
func TestGCSStorageIntegration(t *testing.T) {
// Upload file
// Verify file exists
// Download file
// Delete file
}
E2E Tests (Cookbook Tree)
See cookbooks/trees/media-upload-flow.yaml:
- Create project
- Provision GCS component
- Add media-upload service component
- Wait for CI/CD pipeline
- Test upload endpoint
- Verify file in GCS bucket
- Test download endpoint
- Cleanup (delete project, bucket)
Mocks
For projects using pkg/storage, provide mock implementation:
// pkg/storage/mock.go (generated projects can create this)
type MockStorage struct {
UploadFunc func(ctx context.Context, path string, r io.Reader, opts UploadOptions) (string, error)
DownloadFunc func(ctx context.Context, path string) (io.ReadCloser, *ObjectAttrs, error)
// ... other methods
}
Operational Concerns
Bucket Lifecycle
Creation:
- Triggered by
POST /projects/{id}/componentswithtype=gcs - Returns immediately after bucket + credentials created
- Credentials stored in rdev credential store
Deletion:
- Triggered by
DELETE /project/{id} - Deletes all objects first (if
force=true) - Deletes bucket
- Deletes service account and keys
Orphan Prevention:
- Project deletion hook cleans up all infra (postgres, redis, gcs)
- If cleanup fails, logs warning but continues (manual cleanup required)
Cost Management
Estimates (per project):
- Storage: $0.020/GB/month (Standard class, US region)
- Operations: $0.005/10k reads, $0.05/10k writes
- Network: $0.12/GB egress (to internet)
Typical project (1k users, 10GB media):
- Storage: $0.20/month
- Operations: $0.10/month (10k reads, 1k writes)
- Total: ~$0.30/month
Cost optimization:
- Use lifecycle rules to auto-delete temp files
- Serve images via CDN (reduce GCS egress)
- Use signed URLs (avoid API proxy overhead)
Monitoring
Metrics to track (Prometheus):
rdev_gcs_buckets_total: Total buckets createdrdev_gcs_provision_duration_seconds: Bucket creation latencyrdev_gcs_provision_errors_total: Provisioning failuresstorage_upload_duration_seconds: Upload latency (in generated projects)storage_upload_errors_total: Upload failuresstorage_upload_bytes_total: Total bytes uploaded
Logs to monitor:
- Provisioning errors (insufficient permissions, quota exceeded)
- Upload errors (file too large, invalid content type)
- Download 404s (broken links, deleted files)
Quotas
GCS limits:
- Bucket creation: 100/day per GCP project (sufficient for small deployments)
- Service accounts: 100 per GCP project (shared quota with other services)
- IAM policies: 1500 bindings per bucket (one per service account)
Scaling beyond limits:
- Use multiple GCP projects (shard by project ID hash)
- Use single bucket with path prefixes (less isolation, not recommended)
Backup and Recovery
Bucket versioning:
- Enable versioning for critical projects:
gsutil versioning set on gs://bucket - Allows recovery from accidental deletions
Cross-region replication:
- For high-availability projects, enable dual-region buckets
- Example:
Location: "US"→Location: "NAM4"(multi-region)
Implementation Phases
Phase 1: Platform - GCS Provisioner ✅
- Define
port.StorageProvisionerinterface - Implement
adapter/gcs.Provisioner - Wire into
ComponentService - Add GCS config to
cmd/rdev-api
Phase 2: Skeleton - pkg/storage ✅
- Define
storage.Storageinterface - Implement
GCSStorage - Implement
MemoryStorage(testing) - Add to skeleton templates
Phase 3: Component - media-upload ✅
- Create component template with upload/download handlers
- Add Woodpecker build/deploy steps
- Add to component registry
Phase 4: Testing - Cookbook Tree ✅
- Write E2E cookbook tree
- Run in CI pipeline
- Document in guides
Security Checklist
- Service accounts have minimal IAM roles (objectAdmin only)
- Credentials stored encrypted in rdev credential store
- Bucket names do not expose sensitive project details
- CORS origins restricted to *.threesix.ai (or custom domains)
- Signed URLs have reasonable expiry times (≤1 hour for reads, ≤15 min for writes)
- File size limits enforced (prevent DoS via large uploads)
- Content-Type validation (prevent malicious file uploads)
- Public read ACLs only set when explicitly requested (
Public: true)
Future Enhancements
- Multi-Backend Support: Add S3, MinIO, R2 adapters
- Image Processing: Automatic thumbnail generation, format conversion
- CDN Integration: Cloudflare R2 + cache purging
- Quota Management: Per-project storage limits, alerting
- Virus Scanning: ClamAV integration for uploaded files
- Resumable Uploads: For large files (>100MB)
- Streaming: Direct browser-to-GCS uploads (bypass API)
References
- GCS Client Docs: https://cloud.google.com/go/docs/reference/cloud.google.com/go/storage/latest
- IAM Best Practices: https://cloud.google.com/iam/docs/best-practices
- Signed URLs: https://cloud.google.com/storage/docs/access-control/signed-urls
- rdev Postgres Provisioner:
internal/adapter/postgres/provisioner.go - rdev Redis Provisioner:
internal/adapter/redis/provisioner.go