Skip to content

API: Implement Cloudflare R2 File Storage Service #111

@0xdevcollins

Description

@0xdevcollins

Severity: HIGH — Invoice PDFs, Logo Uploads All Fail

Problem

There is zero Cloudflare R2 code in the codebase. No upload service, no client, no bucket config. This blocks:

  • Invoice PDF storage (PDF generated but nowhere to save it)
  • Merchant logo uploads in Settings > Branding
  • KYB document uploads

Implementation Required

1. R2 Storage Service

```typescript
// apps/api/src/modules/storage/storage.service.ts
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

@Injectable()
export class StorageService {
private client: S3Client;
private bucket: string;

constructor(private config: ConfigService) {
this.client = new S3Client({
region: 'auto',
endpoint: config.get('R2_ENDPOINT'), // https://.r2.cloudflarestorage.com
credentials: {
accessKeyId: config.get('R2_ACCESS_KEY_ID'),
secretAccessKey: config.get('R2_SECRET_ACCESS_KEY'),
},
});
this.bucket = config.get('R2_BUCKET_NAME');
}

async upload(key: string, body: Buffer, contentType: string): Promise {
await this.client.send(new PutObjectCommand({
Bucket: this.bucket, Key: key, Body: body, ContentType: contentType,
}));
return `${this.config.get('R2_PUBLIC_URL')}/${key}`;
}

async getSignedDownloadUrl(key: string, expiresIn = 3600): Promise {
return getSignedUrl(this.client, new GetObjectCommand({
Bucket: this.bucket, Key: key,
}), { expiresIn });
}

async delete(key: string): Promise {
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
}
}
```

2. Key naming convention

  • Invoice PDFs: invoices/{merchantId}/{invoiceId}.pdf
  • Merchant logos: merchants/{merchantId}/logo.{ext}
  • KYB documents: kyb/{merchantId}/{docType}.{ext}

3. Add to .env.example

```
R2_ENDPOINT=https://.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=useroutr-assets
R2_PUBLIC_URL=https://assets.useroutr.io
```

4. Wire into InvoicesService and MerchantsService

Acceptance Criteria

  • StorageService uploads files to R2 bucket
  • Signed download URLs generated for private files (PDFs)
  • Public URL used for merchant logos
  • Invoice PDF upload wired in InvoicesService
  • Merchant logo upload wired in MerchantsService branding update
  • Files deleted when invoice/merchant is deleted
  • Unit tests with mocked S3Client

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend API workinfrastructureDevOps, Docker, deployment, monitoring

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions