The Problem
Catarse is Brazil's leading open-source crowdfunding platform — a market where payment infrastructure is genuinely hard. Brazilian users pay through PIX (instant QR-code transfers), boleto, and card installments; the regulatory and financial landscape differs completely from Stripe-dominated markets. The platform also needed on-chain transparency for campaign funds: backers had no trustless way to verify that money raised would reach creators or be refunded if a goal was missed.
The legacy codebase couldn't handle the traffic or the complexity. We needed a ground-up API rearchitecture — one that was modular enough to own 35 distinct domains, resilient enough to process payments at scale, and extensible enough to settle funds on-chain without breaking the fiat payment flow.
The Solution
I led the backend build from the NestJS project structure through production betas. The work covered three major systems: a serverless S3/CloudFront image optimisation pipeline for campaign media, a PagarMe PIX payment integration (routed through CrowdSplit) with scheduled job processing for confirmation and expiry, and a Celo/Polygon smart contract layer that created campaign treasuries and settled confirmed payments on-chain. The API reached general availability serving 2 million users.
Platform homepage — category browsing (Literatura, Jogos, Quadrinhos) and featured campaign listings served by the API
The production API reached 2 million users across the platform's public launch and beta phases.
Domain-driven module architecture spanning payments, projects, blockchain, auth, files, jobs, and scheduling.
CampaignInfoFactory, TreasuryFactory, and PaymentTreasury — deployed on Celo and Polygon testnets.
Bull/Redis cron workers for PIX expiry, payment confirmation, treasury transfers, and campaign status reconciliation.
Modular NestJS at the Core
The backend is a single NestJS 11 application structured into 35 domain modules. Each module owns its own entities, services, controllers, and DTOs — no shared mutable state across module boundaries. MikroORM handles database interaction with PostgreSQL 17 using a migrations-first approach and snapshot-based schema evolution. Redis powers the Bull job queue for all async processing.
System architecture — NestJS API module boundaries, Bull/Redis job queue for async payment processing, S3 → Sharp compression → CloudFront CDN pipeline, CrowdSplit/PagarMe payment layer, and Celo smart contract integration
Frontend
API
MikroORM
Redis
PagarMe
Polygon
Request
URL
Bucket
Compress
CDN
Stack Overview
| Layer | Technology | Role |
|---|---|---|
| API Framework | NestJS 11, TypeScript 5.8, SWC | REST API with OpenAPI docs via Swagger + Scalar |
| Database | PostgreSQL 17, MikroORM 6 | Relational schema, 33+ entities, snapshot-based migrations |
| File Storage | AWS S3 v3, Sharp, CloudFront | Serverless upload pipeline with CDN delivery and on-demand compression |
| Payments | CrowdSplit API, PagarMe, Konduto | PIX + card processing, fraud detection, webhook reconciliation |
| Blockchain | ethers.js v6, Privy, Celo, Polygon | Campaign treasury creation, on-chain payment confirmation, wallet management |
| Job Queue | Bull v4, Redis 7, BullBoard | Async payment processing, cron jobs, treasury transfers |
| Auth | JWT (15m / 3650d), Google OAuth, Privy | Access + refresh token system, Web3 wallet auth |
| Notifications | SendGrid, Nodemailer, nestjs-i18n | Transactional email with Handlebars templates, i18n support |
| Observability | Sentry, custom logger | Error tracking with environment-scoped sampling |
| Frontend | Next.js 14, React 18, Tailwind, Radix UI | Campaign creation, backer dashboard, checkout, admin panel |
Serverless S3 / CloudFront Image Pipeline
Campaign media — thumbnails, reward images, creator photos — needed to reach users fast without degrading upload experience. The pipeline avoids synchronous resizing on the API server entirely: files are uploaded directly from the browser to S3 via presigned URLs, and a post-upload job handles compression asynchronously.
The API issues a 1-hour presigned PUT URL. The client uploads directly to S3 — no file bytes traverse the API server, so bandwidth cost is zero and upload latency is minimal.
Accepted types: JPEG, PNG, WebP, GIF, MP4, WebM, PDF, Word, Excel. File metadata — S3 key, size, type, uploader IP — is persisted to the files table immediately.
Files over 5 MB trigger a post-upload job: the file is fetched from S3, resized to a maximum 1024 px width via Sharp, and re-uploaded to the same key. The database record is updated with the new file size. Max upload limit is 20 MB.
All public URLs are served from the CloudFront distribution when CLOUDFRONT_URL is configured. URL pattern: {CLOUDFRONT_URL}/{fileKey}. This provides edge caching globally without any application-layer routing changes.
Reward creation — drag-and-drop image upload field triggers the S3 presigned URL pipeline; reward preview, availability, and shipping type configured through the rewards API
PagarMe PIX Payment Integration
In Brazil, PIX is the dominant instant payment method — a QR-code-based transfer that settles in seconds. Catarse processes PIX through CrowdSplit, a payment orchestration layer that wraps PagarMe and manages merchant authentication, subaccount creation for creators, and payout distribution. Card payments go through the same orchestration with Konduto fraud scoring applied before authorisation.
User selects PIX at checkout. The API calls CrowdSplit POST /api/v1/payments with type: 'pix', amount in cents, expiry date, and pledge metadata. Response: QR code URL, QR string, and initial AWAITING_CONFIRMATION status.
CrowdSplit fires a webhook on payment settlement. The webhook processor validates the payload, transitions the pledge from pending to confirmed, and enqueues a blockchain confirmation job if the campaign is on-chain.
A payment-confirmation cron job periodically re-checks unconfirmed payments against CrowdSplit to recover from missed webhooks. A separate pix-expiry job scans approaching expiry timestamps and marks expired pledges, triggering refund flows where applicable.
Creator payouts run through a subaccount-transfer job in batches of 20 projects, capped at R$10,000 per transfer and 25 transaction IDs per batch. PIX keys are stored and verified per creator via the payouts module. CrowdSplit handles the underlying PagarMe settlement to creator bank accounts.
PIX checkout — QR code generated by the CrowdSplit/PagarMe integration, with payment method selection and expiry countdown
Smart Contract Integration
Every campaign on Catarse can optionally be backed by on-chain contracts. This gives backers a trustless record of contributions and allows funds to be held in a smart contract treasury until campaign goals are reached — rather than sitting in a platform bank account. The contracts are deployed on Celo (primary) and Polygon (secondary), both chosen for low gas costs in the Brazilian market context.
Factory contract that deploys a CampaignInfo instance per project. Stores creator wallet, campaign ID hash, timeline (launch + deadline), goal amount in wei, NFT receipt metadata (name, symbol, image URI), and currency (bytes32 'BRL').
Deployed after CampaignInfo creation succeeds. Calls deploy(platformHash, campaignInfoAddress, feeBps) to create a treasury bound to the campaign. Address extracted from event logs and stored in the project record.
Holds accumulated campaign funds. Receives confirmPayment() calls as payments are settled off-chain. Batch-processed by the scheduler: up to 5,000 transactions per batch, 50 batches per project per run. Supports BRLA token transfers for creator payouts.
Wallet management is handled by Privy — each user gets an embedded EVM wallet on first blockchain interaction. Transaction signing goes through Privy's server-auth SDK, with the platform admin wallet as a fallback when BLOCKCHAIN_ENABLED=false. Gas is estimated with a 20% margin and all transaction hashes (campaign creation, treasury deployment, payment confirmations) are stored in the database for full auditability.
Key Engineering Decisions
NestJS Domain-Driven Module Boundaries
The 35 modules aren't organised by technical layer (controllers, services, repositories) — they're organised by domain: payments, pledges, contracts, privy, rewards, membership-tiers. Each module is self-contained and only exposes what downstream modules need via explicit imports. This made it possible to work on the PIX payment flow, smart contract layer, and S3 pipeline in parallel across the team without cross-module interference.
Presigned Upload + Async Compression Instead of Middleware
The naive approach — receive the file on the API, resize it, then upload — would have blocked request threads for every media upload and added S3 egress bandwidth for every file that passes through. Presigned URLs shift the upload entirely to the client-S3 path. The API only issues the URL and receives a notification when the upload completes. Compression happens asynchronously via a Bull job only when the file exceeds the 5 MB threshold, so small files are never processed at all. At 2 million users with frequent campaign updates, this approach eliminated a whole class of load that would have required scaling the API tier for I/O rather than compute.
CrowdSplit as an Orchestration Layer Over PagarMe
Rather than integrating PagarMe directly, Catarse routes all payments through CrowdSplit — a Brazilian payment orchestration service that wraps PagarMe and manages merchant subaccounts. This abstraction handles merchant authentication (auto-refreshing Bearer tokens every 5 minutes), subaccount creation for creators, multi-party settlement splits, and the underlying PagarMe lifecycle events. The tradeoff: an additional external dependency, but one that removes significant complexity around multi-merchant payment routing and creator payout compliance in Brazil's regulated financial environment.
Blockchain as an Optional Layer, Not a Dependency
The entire smart contract system is gated behind a BLOCKCHAIN_ENABLED feature flag. When disabled, the platform operates as a pure fiat system with no on-chain interaction. This was critical for the production beta: we could ship to real users before the contract audit was complete, then enable on-chain settlement progressively. The same flag governs which wallet signs transactions — Privy user wallets when enabled, the platform admin wallet otherwise — ensuring the code path stays identical and no production divergence accumulates between the two modes.
Batch-Bounded Scheduled Jobs for Payment Reconciliation
Every scheduled cron job processes a bounded slice of work per run. The payment confirmation job processes up to 5,000 transactions per batch with 50 batches per project per execution. The subaccount transfer job processes 20 projects per batch, 5 batches per run. This prevents any single cron execution from holding the database or blockchain connection open for an unbounded duration — a design that mattered when running on the same infrastructure as the live API during the production beta, before dedicated worker infrastructure was separated out.
AWS SSM Parameter Store for Multi-Environment Config
All environment configuration — database credentials, S3 keys, CrowdSplit secrets, contract addresses, Privy app credentials — is managed via AWS SSM Parameter Store and synced with env:pull/push/diff scripts. Environment files are never committed. This gave us clean promotions from dev to staging to beta to production with a single command per environment, and made it straightforward to rotate secrets without touching code. The secrets module integrates directly with AWS Secrets Manager for runtime secret resolution.
Production Beta and General Availability
The API reached production through a staged beta process — deploying to real users progressively while the blockchain layer remained behind a feature flag. Payment processing, PIX QR code generation, webhook reconciliation, and creator payouts were all live before on-chain settlement was enabled. The beta phase validated the payment infrastructure under real load and surfaced reconciliation edge cases that the scheduled jobs were then hardened to handle.
Backer dashboard — pledge status tracking with live PIX expiry countdown, fulfillment states (Sent, Ready for pickup, Awaiting delivery), and inline Pay PIX actions driven by the pledges and payments API
- REST API serving 2 million users, designed and built from initial NestJS project structure through production deployment
- Serverless S3/CloudFront media pipeline eliminates file I/O from the API tier entirely, with Sharp compression applied asynchronously only for files exceeding 5 MB
- PIX payment flow with webhook confirmation, cron-based reconciliation, and scheduled expiry checks — handling Brazil's most-used payment method at scale
- Three-contract on-chain system (CampaignInfoFactory, TreasuryFactory, PaymentTreasury) deployed on Celo and Polygon, with batch-bounded payment confirmation cron jobs
- Privy-managed EVM wallets for all users with platform admin wallet fallback — blockchain adoption without requiring user self-custody
- 10+ Bull/Redis cron workers managing payment lifecycle, treasury transfers, campaign status, membership billing, and creator payout distribution
- Multi-environment deployment via AWS SSM with clean dev → staging → beta → production promotion
What I Would Do Differently
The payment-confirmation cron job and the webhook handler both write to the same pledge records — there's a race condition if a webhook fires during a cron run. In production this was mitigated by the cron's batch limits, but the correct fix is an optimistic locking strategy on the pledge's paymentStatus field, rejecting writes where the status has already been advanced by a concurrent process.
The blockchain feature flag is binary: fully on or fully off. A better model would be per-campaign opt-in — creators choose whether their campaign settles on-chain — which would let us run both models simultaneously and compare settlement reliability rather than migrating the entire platform at once.