BACKEND  ·  PAYMENTS  ·  WEB3  ·  CROWDFUNDING

Catarse —
Crowdfunding API at Scale

Production NestJS API serving 2 million users. Serverless S3/CloudFront image pipeline, PagarMe PIX payment processing via smart contract treasury, and on-chain campaign lifecycle management on Celo and Polygon.

NestJS 11 TypeScript PostgreSQL AWS S3 + CloudFront PagarMe PIX ethers.js Celo · Polygon Bull + Redis
Catarse campaign page — NÔMADES Primordial crowdfunding campaign with funding progress

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.

Catarse homepage — campaign discovery with category browsing and featured projects Platform homepage — category browsing (Literatura, Jogos, Quadrinhos) and featured campaign listings served by the API
2M
Users Served

The production API reached 2 million users across the platform's public launch and beta phases.

35
NestJS Modules

Domain-driven module architecture spanning payments, projects, blockchain, auth, files, jobs, and scheduling.

3
Smart Contracts

CampaignInfoFactory, TreasuryFactory, and PaymentTreasury — deployed on Celo and Polygon testnets.

10+
Scheduled Jobs

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.

Catarse system architecture — NestJS API modules, PostgreSQL RDS, S3 with compression, CloudFront CDN, CrowdSplit/PagarMe, Redis, Bull queue, and Celo blockchain 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
REQUEST PATH
Next.js
Frontend
NestJS
API
PostgreSQL
MikroORM
Bull
Redis
CrowdSplit
PagarMe
Celo /
Polygon
MEDIA UPLOAD PATH
Client
Request
Presigned
URL
S3
Bucket
Sharp
Compress
CloudFront
CDN
Primary service Edge delivery

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.

01
Presigned URL generation

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.

02
Type validation & metadata persistence

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.

03
Sharp compression for oversized images

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.

04
CloudFront CDN delivery

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 form with drag-and-drop image upload, shipping configuration, and reward preview 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.

01
QR code creation

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.

02
Webhook confirmation

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.

03
Scheduled reconciliation

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.

04
Subaccount payouts

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 QR code payment screen on mobile — checkout with QR code and payment method selection 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.

CampaignInfoFactory

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').

TreasuryFactory

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.

PaymentTreasury

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

01

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.

02

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.

03

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.

04

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.

05

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.

06

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 support dashboard — pledge tracking with PIX expiry countdown, fulfillment status, and payment actions 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.