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
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:
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
invoices/{merchantId}/{invoiceId}.pdfmerchants/{merchantId}/logo.{ext}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