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

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