R2 STORAGE ARCHITECTURE
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Cloudflare R2 Storage Architecture
Session 198.1 | March 10, 2026
Overview
GoGrab uses Cloudflare R2 (S3-compatible object storage) alongside MongoDB GridFS in a hybrid storage architecture. R2 handles user-facing media (chat images, uploads, generation artifacts) with global CDN delivery. GridFS remains for small transactional files linked to agent handoffs.
R2 Bucket Architecture
| Bucket | Purpose | TTL | Access |
|---|---|---|---|
gograb-chat-media |
Chat images from all channels (WhatsApp, Telegram, web, groups) | Permanent | System-only, signed URLs |
gograb-assets |
GoGrab internal assets (platform branding, defaults, seed data) | Permanent | Admin-only |
gograb-spaces |
Tenant-uploaded media (space owners) | Permanent | Tenant admins + GoGrab admins |
gograb-space-production |
End-user uploaded media (tenant's customers) | Permanent | Rate-limited, moderated |
gograb-gen |
AI generation artifacts (module source, build outputs) | 30 days | System-only |
gograb-backups |
Database exports, migration dumps | 90 days | System-only, encrypted |
pulse-music |
GoGrab Pulse music player (pre-existing) | Permanent | Wrangler CLI |
Key Format
{spaceSlug}/{channelType}/{yyyy-mm}/{messageId}_{filename}
Examples:
my-space/whatsapp/2026-03/SMabc123-0_media.jpg
_global/web/2026-03/1710000000-a7b3c_upload.png
my-space/telegram/2026-03/12345_photo.jpg
spaceSlugis guaranteed unique via MongoDB unique index (collision appends timestamp)_globalprefix used when no space context existsyyyy-mmpartitioning for efficient listing and lifecycle rules
Library: @libs-api/r2-storage-core
Location: libs/libs-api/r2-storage-core/
Module Registration
// In app.module.ts
import { R2StorageModule } from '@libs-api/r2-storage-core';
@Module({
imports: [
R2StorageModule.forRoot(), // Reads R2_* env vars via ConfigService
],
})
Service API
import { R2StorageService, R2_BUCKETS } from '@libs-api/r2-storage-core';
// Upload a buffer
await r2Storage.upload(R2_BUCKETS.chatMedia, key, buffer, 'image/jpeg');
// Download from external URL and upload to R2 (with retry)
await r2Storage.uploadFromUrl(R2_BUCKETS.chatMedia, key, twilioUrl, authHeaders);
// Generate signed URL (default 7 days)
const url = await r2Storage.getSignedUrl(bucket, key);
// Build structured key
const key = r2Storage.buildKey({
spaceSlug: 'my-space',
channelType: 'whatsapp',
messageId: 'SMabc123-0',
filename: 'media.jpg',
});
// Check configuration status
if (r2Storage.isConfigured()) { ... }
Graceful Degradation
R2StorageService logs a warning and skips operations when credentials are missing. Media pipeline methods wrap R2 calls in catch blocks — R2 failure never blocks chat flow.
Environment Variables
# .env.local
R2_ACCOUNT_ID=<cloudflare-account-id> # Same as CLOUDFLARE_ACCOUNT_ID
R2_ACCESS_KEY_ID=<r2-s3-access-key> # From R2 API Token page
R2_SECRET_ACCESS_KEY=<r2-s3-secret-key> # Shown once at creation
These are S3-compatible credentials (separate from CLOUDFLARE_API_TOKEN used by Pages/Wrangler).
Storage Decision Matrix
| Data Type | Storage | Why |
|---|---|---|
| Chat images (all channels) | R2 | CDN delivery, LLM vision, Twilio URLs expire |
| AI generation artifacts | R2 | Large files, ephemeral, deployed elsewhere |
| Tenant uploads (future) | R2 | Needs CDN, presigned URLs for direct upload |
| Agent output files | GridFS | Transactional with handoff docs, metadata queries |
| Per-space files | GridFS | Existing working system, rich query support |
R2 vs GridFS Comparison
| Criteria | R2 | GridFS |
|---|---|---|
| CDN | Global edge (300+ cities) | None (direct MongoDB) |
| Egress cost | $0 | Atlas charges $0.01-0.20/GB |
| Presigned URLs | Native S3-compatible | Not supported |
| Max file size | 5GB (5TB multipart) | Unlimited (chunked) |
| Metadata queries | List by prefix only | Full MongoDB queries |
| Transactions | No | Yes (participates in MongoDB transactions) |
| Vendor lock-in | S3 API universal | MongoDB-specific |
Future: Premium Tenant Buckets
Premium subscribers will get isolated buckets:
gograb-tenant-{spaceId} // Separate R2 access token per tenant
The R2StorageService interface stays identical — bucket name changes based on subscription tier.
Related Files
- Library:
libs/libs-api/r2-storage-core/ - Constants:
libs/libs-api/r2-storage-core/src/lib/r2-storage.constants.ts - Service:
libs/libs-api/r2-storage-core/src/lib/services/r2-storage.service.ts - Module import:
APIs/backend-vitto-api/src/app/app.module.ts - Channel extraction:
APIs/backend-vitto-api/src/app/connectivity/channel-gateway.service.ts - GridFS (existing):
APIs/backend-api/src/app/spaces/space-database.service.ts