ProxyPay is a monorepo combining a multi-tenant payment-orchestration backend and its React client library:
| Project | Path | Description |
|---|---|---|
| Backend API | backend/ |
.NET 8 multi-tenant API orchestrating PIX billings, invoices, QR codes and AbacatePay webhooks (REST + GraphQL) |
| Frontend library | frontend/ |
proxypay-react β a React + TypeScript component library (npm package) to consume the API, plus an example-app/ admin dashboard |
The backend is a multi-tenant API that orchestrates PIX billings, invoices, QR codes and payment webhooks for merchant stores, acting as a thin integration and persistence layer on top of AbacatePay. Each merchant tenant gets its own PostgreSQL database, its own JWT signing key and its own AbacatePay webhook URL β isolation is physical, not row-level. It is written in .NET 8 following Clean Architecture and exposes both REST and GraphQL endpoints.
The frontend (proxypay-react) is a lightweight component library that
provides ready-to-use components for PIX QR Code payments, invoice payments,
and recurring billing subscriptions β fully typed, shipping as ESM + CJS with
zero runtime dependencies beyond React. See frontend/README.md
for the full library reference.
Today the platform runs three active tenants: emagine (primary), monexup,
and fortuno. New tenants can be added by configuration (no code change)
through the Tenants:{tenantId} section of appsettings.*.json or the
matching Tenants__{tenantId}__* environment variables injected by Docker
Compose.
Authentication is delegated to the external NAuth service; each tenant
validates tokens with its own secret via a custom
NAuthTenantSecretProvider, so a token emitted for tenant A is never
accepted under tenant B.
- π’ Physical multi-tenancy β one PostgreSQL database per tenant resolved per request via
TenantDbContextFactory - π Per-tenant JWT secrets β NAuth validates each token with the secret of the tenant on the request, cross-tenant tokens are rejected
- π§ Header-driven tenant resolution β
X-Tenant-Idon every request (REST, GraphQL, webhooks via path segment) - πΈ AbacatePay integration β create billings, PIX QR codes, check status, and receive payment webhooks
- π§Ύ Invoice & transaction lifecycle β full PIX payment journey from QR generation through status polling and webhook confirmation
- π GraphQL admin schema β HotChocolate endpoint (
/graphql) with[UseProjection], filtering and sorting over invoices and transactions - π Swagger / OpenAPI β interactive API docs enabled in Development and Docker environments
- π Basic-token authentication β NAuth
Authorization: Basic {token}flow with[Authorize]controllers - ποΈ EF Core 9 migrations β schema managed via
dotnet ef, per-tenant physical isolation prevents row-level bleed - π§± Clean Architecture β eight .NET projects with explicit dependency direction and centralized DI bootstrap in
ProxyPay.Application
- .NET 8 + ASP.NET Core β runtime, REST controllers, middleware pipeline
- Clean Architecture β Domain / Application / Infra / DTO / Infra.Interfaces / API / GraphQL layering
- PostgreSQL 17 β one physical database per tenant
- Entity Framework Core 9.0.8 with Npgsql β ORM, lazy-loading proxies, migrations via
dotnet ef
- HotChocolate 14.3.0 β admin schema on
/graphql,[Authorize]at class level, IQueryable projection for EF Core
- NAuth 0.5.9 β Basic-token auth with dynamic per-tenant
ITenantSecretProvider - zTools β external service for S3 uploads, e-mail (MailerSend) and utilities
- AbacatePay β PIX billing, QR-code and webhook provider
- FluentValidation 12.1.1 β request validation (e.g.,
QRCodeRequestValidator) - AutoMapper 13.0.1 β entity β DTO mapping (Invoice, Transaction, Customer, Store, Billing profiles)
- Newtonsoft.Json β AbacatePay wire format (camelCase contract resolver)
- Serilog β structured logging, console sink with template output
- Swashbuckle 9.0.4 β OpenAPI v2 Swagger generation
- xUnit 2.9.2 + Moq 4.20.72 β test project (
ProxyPay.Tests) scaffolded; integration harness yet to be filled in
- Docker + Docker Compose β two compose files (
docker-compose.ymlfor dev,docker-compose-prod.ymlfor prod) - GitHub Actions β
deploy-prod.yml,create-release.yml,version-tag.yml - GitVersion β semantic versioning configuration in
GitVersion.yml - Bruno β API collection in
bruno/for manual/regression testing
ProxyPay/
βββ backend/ # All .NET code (solution, projects, schema snapshot)
β βββ ProxyPay.API/ # ASP.NET Core host: Controllers, Startup, Middleware, appsettings
β β βββ Controllers/ # StoreController, PaymentController, WebhookController, GraphQLController
β β βββ Middlewares/ # TenantMiddleware (reads X-Tenant-Id β HttpContext.Items)
β β βββ Filters/ # GlobalExceptionFilter
β β βββ Dockerfile # Container image definition
β β βββ appsettings.{Development,Docker,Production}.json
β βββ ProxyPay.Application/ # DI bootstrap + multi-tenant plumbing
β β βββ Startup.cs # ConfigureProxyPay extension (DI composition root)
β β βββ TenantContext.cs # ITenantContext + AsyncLocal EnterScope(...)
β β βββ TenantResolver.cs # ITenantResolver (ConnectionString, JwtSecret, BucketName per tenant)
β β βββ TenantCatalog.cs # ITenantCatalog (active tenant ids, existence check)
β β βββ TenantDbContextFactory.cs # Per-request DbContext with tenant's connection string
β β βββ NAuthTenantSecretProvider.cs # Dynamic per-tenant JWT secret for NAuth
β β βββ NAuthTenantProvider.cs # Tenant provider for outbound NAuth calls
β βββ ProxyPay.Domain/ # Business logic and entities
β β βββ Models/ # Store, Customer, Invoice, InvoiceItem, Billing, BillingItem, Transaction
β β βββ Services/ # InvoiceService, TransactionService, CustomerService, StoreService, BillingService
β β βββ Interfaces/ # ITenantContext, ITenantResolver, ITenantCatalog, service contracts
β β βββ Validators/ # FluentValidation validators (QRCodeRequestValidator, ...)
β βββ ProxyPay.DTO/ # Shared DTOs and settings
β β βββ AbacatePay/ # Wire-format types for AbacatePay integration
β β βββ Billing/ Invoice/ Customer/ # Request/response DTOs per domain
β β βββ Settings/ # NAuthSetting, zToolsetting, AbacatePaySetting
β βββ ProxyPay.Infra/ # EF Core, repositories, AppServices
β β βββ Context/ProxyPayContext.cs # EF Core DbContext (7 DbSets)
β β βββ Migrations/ # EF Core migrations (PostgreSQL)
β β βββ Repository/ # 7 repositories (one per aggregate root)
β β βββ AppServices/ # AbacatePayAppService (outbound HTTP)
β β βββ Mappers/ # AutoMapper profiles
β β βββ UnitOfWork.cs # Transaction coordination
β βββ ProxyPay.Infra.Interfaces/ # Repository + AppService contracts
β βββ ProxyPay.GraphQL/ # HotChocolate schema
β β βββ Admin/AdminQuery.cs # Admin queries (requires [Authorize])
β β βββ Types/ # InvoiceTypeExtension, TransactionTypeExtension
β β βββ GraphQLServiceExtensions.cs # AddProxyPayGraphQL DI
β β βββ GraphQLErrorLogger.cs # Diagnostic listener
β βββ ProxyPay.Tests/ # xUnit test project (scaffold)
β βββ proxypay.sql # Raw schema snapshot (complementary to EF migrations)
β βββ ProxyPay.sln # Solution file
βββ frontend/ # proxypay-react β React component library (npm package)
β βββ src/ # Library source (the published package)
β β βββ types/ # TypeScript interfaces, enums, component props
β β βββ services/ # proxyPayService β REST API client
β β βββ contexts/ # ProxyPayContext provider
β β βββ hooks/ # useProxyPay consumer hook
β β βββ components/ # PixPayment, InvoicePayment, BillingPayment
β β βββ index.ts # Public API surface (all exports)
β βββ example-app/ # Full admin dashboard demo (consumes the lib via file:..)
β βββ docs/ # Frontend docs (API_REFERENCE.md, system-design)
β βββ vite.config.ts # Library build (lib mode, ESM + CJS + .d.ts)
β βββ package.json # Package metadata + scripts
β βββ README.md # Library reference (published to npm)
βββ bruno/ # Bruno API collection (GraphQL, Payment, Store, Webhook)
βββ docs/ # Backend documentation + architecture diagram
βββ specs/ # GitHub spec-kit feature specs (dev tooling)
βββ .specify/ # Spec-kit templates, memory and scripts
βββ .github/workflows/ # GitHub Actions: deploy-prod, create-release, version-tag
βββ docker-compose.yml # Local dev (API + Postgres) β build context ./backend
βββ docker-compose-prod.yml # Production (API only, external DB) β build context ./backend
βββ GitVersion.yml # SemVer tooling
βββ LICENSE # MIT
βββ README.md # This file (unified backend + frontend)
The following diagram illustrates the high-level architecture of ProxyPay:
A request flows through TenantMiddleware, which reads X-Tenant-Id from
the HTTP header (or from the path segment for AbacatePay webhooks) and
publishes it into the request scope. TenantResolver reads the current
tenant and exposes its ConnectionString, JwtSecret and BucketName
from IConfiguration. TenantDbContextFactory builds a per-request
ProxyPayContext pointing at the correct physical database.
NAuthTenantSecretProvider resolves the JWT secret dynamically, so NAuth
only accepts tokens signed by the tenant of the current request.
Outbound calls to AbacatePay and NAuth are issued from
ProxyPay.Infra.AppServices and the NAuth ACL clients registered in DI.
π Source: the editable Mermaid source is available at
docs/system-design.mmd.
| Document | Description |
|---|---|
| API_REFERENCE.md | Comprehensive REST + GraphQL reference: endpoints, payloads, response shapes |
| ABACATE_PAY.md | Integration guide for AbacatePay β PIX flows, webhooks, error handling |
| system-design.mmd | Mermaid source for the architecture diagram |
ProxyPay reads its configuration from appsettings.{Environment}.json
(for dotnet run) and from environment variables prefixed with
Tenants__{tenantId}__* (for Docker). A template is provided.
cp .env.example .envFor production:
cp .env.prod.example .env.prod# Database (used by the docker-compose Postgres service)
POSTGRES_USER=proxypay_user
POSTGRES_PASSWORD=your_secure_password_here_change_this
POSTGRES_DB=proxypay_db
# Tenant: emagine (default)
EMAGINE_CONNECTION_STRING=Host=proxypay-db;Port=5432;Database=proxypay_emagine_dev;Username=proxypay_user;Password=your_secure_password_here_change_this
EMAGINE_JWT_SECRET=generate_a_base64_256bit_secret_here
# Tenant: monexup
MONEXUP_CONNECTION_STRING=Host=proxypay-db;Port=5432;Database=proxypay_monexup_dev;Username=proxypay_user;Password=your_secure_password_here_change_this
MONEXUP_JWT_SECRET=generate_a_distinct_base64_256bit_secret_here
# Tenant: fortuno
FORTUNO_CONNECTION_STRING=Host=proxypay-db;Port=5432;Database=proxypay_fortuno_dev;Username=proxypay_user;Password=your_secure_password_here_change_this
FORTUNO_JWT_SECRET=generate_a_distinct_base64_256bit_secret_here
# Shared services
PROXYPAY_BUCKET_NAME=proxypay
NAUTH_API_URL=https://your-nauth-host/auth-api
NAUTH_BUCKET_NAME=nauth
ZTOOLS_API_URL=https://your-ztools-host/tools-api
# AbacatePay (moves to per-tenant in a follow-up)
ABACATEPAY_API_KEY=your_abacatepay_api_key
ABACATEPAY_WEBHOOK_SECRET=your_abacatepay_webhook_secret
# App
APP_PORT=5000- Never commit
.envor.env.prodwith real credentials β only.env.example/.env.prod.exampleare version-controlled - Each tenant MUST have a distinct
JwtSecret; sharing secrets breaks the isolation guarantee - Each tenant MUST point at a different physical database β startup fails if two tenants share the same ConnectionString
- Rotate all default passwords and secrets before deployment
The compose file expects an external network named emagine-network.
Create it once per host:
docker network create emagine-networkThen copy the env template (see section above).
docker-compose up -d --buildThis spins up two containers:
proxypay-dbβ PostgreSQL 17 (port5432)proxypay-apiβ the API (host port defined byAPP_PORT, default5000β container80)
docker-compose ps
docker-compose logs -f api| Service | URL |
|---|---|
| API (health check) | http://localhost:5000/ |
| Swagger UI (Docker env) | http://localhost:5000/swagger/ui |
| GraphQL endpoint | http://localhost:5000/graphql |
| Action | Command |
|---|---|
| Start services | docker-compose up -d |
| Start with rebuild | docker-compose up -d --build |
| Stop services | docker-compose stop |
| View status | docker-compose ps |
| View logs | docker-compose logs -f |
| Remove containers | docker-compose down |
| Remove containers and volumes ( |
docker-compose down -v |
- .NET 8 SDK
- PostgreSQL 17 running locally or reachable over the network
dotnet-efCLI tool:dotnet tool install --global dotnet-ef
psql -U proxypay_user -c "CREATE DATABASE proxypay_emagine_dev;"
psql -U proxypay_user -c "CREATE DATABASE proxypay_monexup_dev;"
psql -U proxypay_user -c "CREATE DATABASE proxypay_fortuno_dev;"All .NET CLI commands below assume you are inside the backend/ directory
(cd backend):
# emagine
dotnet ef database update --project ProxyPay.Infra --startup-project ProxyPay.API \
--connection "Host=localhost;Port=5432;Database=proxypay_emagine_dev;Username=proxypay_user;Password=..."
# monexup
dotnet ef database update --project ProxyPay.Infra --startup-project ProxyPay.API \
--connection "Host=localhost;Port=5432;Database=proxypay_monexup_dev;Username=proxypay_user;Password=..."
# fortuno
dotnet ef database update --project ProxyPay.Infra --startup-project ProxyPay.API \
--connection "Host=localhost;Port=5432;Database=proxypay_fortuno_dev;Username=proxypay_user;Password=..."cd backend
dotnet build ProxyPay.sln
dotnet run --project ProxyPay.APIThe API will be available on the HTTPS/HTTP ports declared by
ASPNETCORE_URLS (by default https://localhost:44374 in Development).
All tests (from backend/):
cd backend
dotnet test ProxyPay.slnA single project:
dotnet test ProxyPay.Tests/ProxyPay.Tests.csprojThe ProxyPay.Tests project targets .NET 9 (test host) and uses
xUnit with Moq for mocks.
ProxyPay.Tests/
βββ Domain/ # (planned) domain-level unit tests
βββ Integration/ # Integration tests β currently scaffolded,
β # infrastructure (WebApplicationFactory,
β # per-tenant DB fixtures) still pending
βββ ProxyPay.Tests.csproj # xUnit 2.9.2 + Moq 4.20.72
Integration tests live in ProxyPay.Tests/Integration/ and are marked
[Fact(Skip=...)] until the WebApplicationFactory harness and per-tenant
database fixtures are in place.
The frontend/ directory holds the React client library and its
example app. The library wraps the API surface below into ready-to-use
components (PixPayment, InvoicePayment, BillingPayment) and a
useProxyPay() hook.
cd frontend
npm install
npm run build # β dist/ (ESM + CJS + .d.ts)
npm run dev # watch-mode build
npm run typecheck # tsc --noEmit
npm run lint # eslint src/cd frontend/example-app
cp .env.example .env # then edit the VITE_* values
npm install # links the library via "proxypay-react": "file:.."
npm run dev # Vite dev serverπ Full component reference, usage examples and the exported API are documented in
frontend/README.md. Frontend-specific docs (API reference, architecture diagram) live infrontend/docs/.
ProxyPay exposes REST and GraphQL surfaces. Both require the
X-Tenant-Id header so requests can be routed to the correct physical
database.
1. Client obtains Basic token from NAuth for a specific tenant
β 2. Client calls ProxyPay with:
Authorization: Basic {token}
X-Tenant-Id: {tenant}
β 3. TenantMiddleware stores the tenant in HttpContext
β 4. NAuth validates the token with the tenant-specific JwtSecret
β 5. Controller executes against the tenant's own PostgreSQL database
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/ |
Health check (JSON with statusApplication) |
No |
GET |
/swagger/ui |
Swagger UI (Dev + Docker only) | No |
POST |
/graphql |
HotChocolate admin schema (Invoices, Transactions) | Yes |
POST |
/store/insert |
Create a merchant store | Yes |
POST |
/store/update |
Update a merchant store | Yes |
POST |
/payment/billing |
Create an AbacatePay billing | No (tenant header + ClientId) |
POST |
/payment/invoice |
Create an invoice | No (tenant header + ClientId) |
POST |
/payment/qrcode |
Create a PIX QR code | No (tenant header + ClientId) |
GET |
/payment/qrcode/status/{invoiceId} |
Check QR-code status | No |
POST |
/webhook/abacatepay |
Receive AbacatePay webhooks | No (secret query param) |
π Full reference: detailed payloads, response shapes and error behavior are documented in
docs/API_REFERENCE.md. AbacatePay-specific flows are covered indocs/ABACATE_PAY.md.
A runnable Bruno collection is committed in bruno/ with folders for
GraphQL, Payment, Store, and Webhook requests plus per-environment
variables under bruno/environments/. Open it in
Bruno to exercise the API interactively.
- Physical tenant isolation β each tenant owns a dedicated PostgreSQL database; cross-tenant data leakage is prevented at the storage layer rather than relying on row-level filters
- Per-tenant JWT secrets β
NAuthTenantSecretProviderresolves the secret dynamically per request, so a token from tenant A is rejected under tenant B [Authorize]on sensitive controllers βStoreControllerand the admin GraphQL schema require authentication- Startup invariant β
Program.csrefuses to boot if two tenants share the sameConnectionString, catching misconfiguration before any traffic is served - Secrets kept out of responses β no connection string, JWT secret, or stack trace is ever returned to clients (follows the project constitution Β§V)
cd backend
dotnet run --project ProxyPay.APIProduction uses the docker-compose-prod.yml stack (API only β database
is an external managed PostgreSQL instance), pointing at the tenant
databases defined in .env.prod:
docker-compose -f docker-compose-prod.yml up -d --buildThe deploy pipeline (.github/workflows/deploy-prod.yml) automates this
over SSH β see the CI/CD section below.
| Workflow | Purpose |
|---|---|
deploy-prod.yml |
On push to main (or manual dispatch), SSH into the production host, pull the latest main, rebuild and restart the compose stack |
create-release.yml |
Create a GitHub Release from tags |
version-tag.yml |
Manage version tagging (driven by GitVersion.yml) |
Required repository secrets for deploy-prod.yml:
PROD_SSH_HOST,PROD_SSH_USER,PROD_SSH_PASSWORD,PROD_SSH_PORT(optional)
The deploy script expects the repo to live under /opt/proxypay on the
remote host and uses git reset --hard origin/main for idempotent updates.
Contributions are welcome β especially around filling in the
ProxyPay.Tests integration harness and adding new tenants.
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Follow the project constitution in
.specify/memory/constitution.mdβ especially the five non-negotiable principles on stack, naming and multi-tenancy - Run the build and tests (
cd backend && dotnet build ProxyPay.sln && dotnet test) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- .NET conventions β PascalCase, file-scoped namespaces,
_camelCaseprivate fields,[JsonPropertyName("camelCase")]on DTOs - PostgreSQL conventions β
snake_casetables and columns,{entity}_idbigint PKs,ClientSetNulldelete behavior - Clean Architecture layering β new entities/services/repositories must follow the
dotnet-architectureskill - Stack is frozen β EF Core is the only ORM; no Docker commands in the local dev loop
Developed by Rodrigo Landim Carneiro and contributors.
This project is licensed under the MIT License β see the LICENSE file for details.
- Built on .NET 8 and ASP.NET Core
- GraphQL powered by HotChocolate
- PIX payments via AbacatePay
- Authentication via NAuth; utilities via zTools
- Data access via Entity Framework Core and Npgsql
- Issues: GitHub Issues
- Discussions: GitHub Discussions
β If you find this project useful, please consider giving it a star!
