PayPulse is a full-stack peer-to-peer payments app inspired by products like PayTM and Venmo. Users can create an account, sign in securely, view their wallet balance, search other users, send money, and review transaction history from a polished React dashboard.
The project is built as a portfolio-grade product demo with a modern fintech interface, cookie-based authentication, Cloudflare Turnstile bot protection, MongoDB persistence, and transaction-safe money transfers.
- Landing page for PayPulse with a clear product story, app preview, and calls to action.
- Signup and signin flows with inline validation, loading states, toast feedback, and password visibility controls.
- Authenticated dashboard with a sticky app bar, personalized greeting, balance card, user directory, and recent transaction history.
- Send-money flow with recipient context, amount validation, loading state, cancel action, and recovery UI when no recipient is selected.
- Responsive dark fintech UI using Tailwind CSS, gradients, glass-style surfaces, visible focus states, and reduced-motion-friendly animations.
- Toast notifications for success, error, loading, logout, and transfer feedback.
- New users automatically receive a starting wallet balance.
- Users can search the user directory by first or last name.
- User search supports pagination and excludes the currently signed-in user.
- Users can transfer money to another PayPulse account.
- Transfers prevent self-payment, invalid recipients, and insufficient-balance transactions.
- Transaction history shows sent and received payments with counterparty names, relative timestamps, status badges, and signed amounts.
- Passwords are hashed with
bcryptbefore being stored. - Authentication uses JWT access and refresh tokens.
- Tokens are sent through HTTP-only cookies instead of being exposed directly to frontend JavaScript.
- Protected backend routes use an authentication middleware that accepts cookie tokens or bearer tokens.
- Signup and signin can be protected by Cloudflare Turnstile.
- Backend validation uses
zodfor signup, signin, and profile update payloads. - CORS is configured for credentialed frontend requests.
- MongoDB models for users, accounts, and transactions.
- Mongoose sessions are used for signup funding and transfers so account, transaction, and ledger writes commit together.
- Every wallet money movement now writes immutable double-entry ledger rows, including signup funding and peer-to-peer transfers.
POST /account/transferrequires anIdempotency-Keyheader so client retries replay the original result instead of moving money twice.- Transactions move through an explicit state machine:
CREATED,PROCESSING,SUCCESS,FAILED,REVERSED, andEXPIRED. - Payment-provider webhooks are verified with HMAC signatures, timestamp replay windows, unique event IDs, and durable event logs.
- Failed webhook processing is retried with backoff and moved to a dead-letter queue after repeated failure.
- Reconciliation jobs detect mismatches between cached balances, ledger-derived balances, transaction ledger rows, and provider webhook state.
- Sensitive endpoints are protected by named sliding-window rate-limit policies with
X-RateLimit-*andRetry-Afterheaders. - Security, money movement, webhook, retry, reconciliation, and rate-limit events are written to an append-only audit log.
- Built-in OpenAPI documentation is available as JSON and a lightweight local docs page.
- Transfers validate positive amounts before entering the transaction flow, preventing negative-amount balance corruption.
- API responses follow a consistent
success,message, anddatashape across most endpoints. - Transaction records include sender, receiver, amount, status, and timestamps.
flowchart TB
User[User Browser] --> Frontend[React + Vite Frontend]
Frontend -->|Cookie JWT / Bearer Token| API[Express API<br/>/api/v1]
API --> RateLimit[Rate Limit Middleware]
RateLimit --> Auth[Auth Middleware]
API --> Turnstile[Turnstile Middleware]
Auth --> UserRoutes[User Routes]
Auth --> AccountRoutes[Account Routes]
Auth --> OpsRoutes[Ops / Admin Routes]
UserRoutes --> UserService[Signup / Signin / Token Flow]
AccountRoutes --> TransferService[Transfer Orchestrator]
TransferService --> Idempotency[Idempotency Key Service]
TransferService --> StateMachine[Transaction State Machine]
TransferService --> LedgerService[Double-Entry Ledger Service]
TransferService --> AuditService[Audit Log Service]
Provider[Simulated Payment Provider] -->|Signed Webhook| WebhookRoutes[Webhook Routes]
WebhookRoutes --> WebhookVerifier[HMAC Signature + Replay Protection]
WebhookVerifier --> WebhookProcessor[Webhook Processor]
WebhookProcessor --> StateMachine
WebhookProcessor --> RetryService[Retry Scheduler]
RetryService --> DLQ[Dead-Letter Queue]
WebhookProcessor --> AuditService
OpsRoutes --> Reconciliation[Reconciliation Service]
OpsRoutes --> AuditQuery[Audit Inspection]
OpsRoutes --> OpenAPI[OpenAPI Docs]
Reconciliation --> AuditService
UserService --> Mongo[(MongoDB Replica Set)]
Idempotency --> Mongo
StateMachine --> Mongo
LedgerService --> Mongo
WebhookProcessor --> Mongo
RetryService --> Mongo
DLQ --> Mongo
Reconciliation --> Mongo
AuditService --> Mongo
Turnstile --> Cloudflare[Cloudflare Turnstile]
- Transfer path: validates request, enforces rate limit, checks idempotency, creates a transaction, moves it through
CREATED -> PROCESSING -> SUCCESS, updates balances, writes double-entry ledger rows, stores replayable response, and writes an audit event inside the same transaction. - Webhook path: verifies HMAC signature and timestamp, de-duplicates provider event IDs, processes valid events idempotently, schedules failed processing for retry, and moves repeated failures to the dead-letter queue.
- Operations path: reconciliation compares cached balances, ledger totals, transaction records, and processed provider events; audit logs provide traceability for sensitive actions.
- React 18
- Vite
- React Router
- Tailwind CSS
- Axios
- React Hot Toast
- Cloudflare Turnstile via
@marsidev/react-turnstile
- Node.js
- Express
- MongoDB
- Mongoose
- JSON Web Tokens
- bcrypt
- zod
- cookie-parser
- cors
- dotenv
PayPulse/
+-- backend/
| +-- controllers/ # Route handlers for users and accounts
| +-- middlewares/ # Auth and Turnstile middleware
| +-- models/ # User, Account, and Transaction schemas
| +-- routes/ # Express routers
| +-- index.js # Express app and MongoDB connection
| +-- package.json
+-- frontend/
| +-- src/
| | +-- components/ # Appbar, balance, users, forms, transactions
| | +-- pages/ # Home, Signup, Signin, Dashboard, SendMoney
| | +-- App.jsx # Client routes
| | +-- main.jsx
| +-- package.json
+-- DESIGN.md # Visual design system notes
+-- PRODUCT.md # Product positioning and UX goals
+-- Dockerfile # MongoDB replica-set image for transactions
+-- README.md
- Node.js 18 or newer
- npm
- MongoDB
- A MongoDB replica set for signup funding and transfer transactions to work reliably with Mongoose sessions
The included Dockerfile creates a MongoDB image that starts with replica-set support.
git clone <your-repo-url>
cd PayPulse
cd backend
npm install
cd ../frontend
npm installCreate backend/.env:
PORT=4000
DATABASE_URL=mongodb://localhost:27017/paypulse?replicaSet=rs
FRONTEND_URL=http://localhost:5173
ACCESS_TOKEN_SECRET=replace-with-a-long-random-secret
ACCESS_TOKEN_EXPIRY=1d
REFRESH_TOKEN_SECRET=replace-with-another-long-random-secret
REFRESH_TOKEN_EXPIRY=10d
TURNSTILE_SECRET_KEY=
WEBHOOK_SECRET=replace-with-provider-webhook-secret
NODE_ENV=developmentTURNSTILE_SECRET_KEY is optional for local development. If it is missing, the backend middleware skips Turnstile verification and logs a warning.
WEBHOOK_SECRET is optional outside production. If it is missing locally, webhook signature verification logs the event flow but skips HMAC comparison.
Create frontend/.env:
VITE_BACKEND_URL=http://localhost:4000
VITE_TURNSTILE_SITE_KEY=VITE_TURNSTILE_SITE_KEY is optional locally. If it is not set, the Turnstile widget is not rendered.
Option A: use your own MongoDB replica set.
Option B: build and run the included MongoDB image:
docker build -t paypulse-mongo .
docker run -d --name paypulse-mongo -p 27017:27017 paypulse-mongocd backend
npm run devThe API runs on http://localhost:4000 by default.
cd frontend
npm run devThe app runs on http://localhost:5173 by default.
npm run dev # Start backend with nodemon
npm start # Start backend with node
npm test # Placeholder test scriptnpm run dev # Start Vite dev server
npm run build # Build production assets
npm run preview # Preview production build
npm run lint # Run ESLintAll application API routes are mounted under /api/v1.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/user/signup |
No | Create a user account and initial wallet balance |
POST |
/api/v1/user/signin |
No | Sign in and set access/refresh cookies |
POST |
/api/v1/user/logout |
Yes | Clear stored refresh token and auth cookies |
POST |
/api/v1/user/refresh-token |
Yes | Refresh access token using refresh token |
GET |
/api/v1/user/current-user |
Yes | Return the authenticated user |
PUT |
/api/v1/user/ |
Yes | Update password, first name, or last name |
GET |
/api/v1/user/bulk |
Yes | Search users with filter, page, and limit query params |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/v1/account/balance |
Yes | Return the authenticated user's balance |
POST |
/api/v1/account/transfer |
Yes | Transfer money to another user |
GET |
/api/v1/account/transactions |
Yes | Return sent and received transaction history |
GET |
/api/v1/account/ledger |
Yes | Return immutable debit/credit ledger entries for the authenticated user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/webhooks/payments |
Signature | Receive simulated payment-provider events with replay protection |
GET |
/api/v1/webhooks/events |
Yes | Inspect persisted webhook events and retry state |
POST |
/api/v1/webhooks/retries/process |
Yes | Process due webhook retries in a bounded batch |
GET |
/api/v1/webhooks/dead-letter |
Yes | Inspect webhook events that exceeded retry attempts |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/reconciliation/run |
Yes | Run account, ledger, transaction, and provider-event reconciliation |
GET |
/api/v1/reconciliation/reports |
Yes | List reconciliation report summaries |
GET |
/api/v1/reconciliation/reports/:reportId |
Yes | Fetch a detailed reconciliation report |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/v1/audit/logs |
Yes | Inspect audit logs with action, outcome, actor, and resource filters |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/v1/docs |
No | Lightweight API documentation page |
GET |
/api/v1/docs/openapi.json |
No | OpenAPI 3.0 JSON contract |
POST /api/v1/user/signup
Content-Type: application/json
{
"username": "aisha@example.com",
"password": "secret123",
"firstName": "Aisha",
"lastName": "Khan",
"turnstileToken": "optional-turnstile-token"
}POST /api/v1/user/signin
Content-Type: application/json
{
"username": "aisha@example.com",
"password": "secret123",
"turnstileToken": "optional-turnstile-token"
}GET /api/v1/user/bulk?filter=rahul&page=1&limit=5
POST /api/v1/account/transfer
Content-Type: application/json
Idempotency-Key: 8f0f5c7e-1f6a-4d4c-97ea-f9d55d72ef80
{
"to": "recipient-user-id",
"amount": 500
}The signature is HMAC_SHA256(WEBHOOK_SECRET, "{timestamp}.{raw_json_body}") and can be sent as either the raw hex digest or sha256=<digest>.
POST /api/v1/webhooks/payments
Content-Type: application/json
X-PayPulse-Event-Id: evt_01J5PAYPULSE001
X-PayPulse-Timestamp: <current-unix-timestamp-ms>
X-PayPulse-Signature: sha256=<hmac-sha256>
{
"type": "PAYMENT_REVERSED",
"data": {
"transactionId": "<transaction-object-id>",
"reason": "provider reversal"
}
}Supported webhook event types:
PAYMENT_SUCCESSPAYMENT_FAILEDPAYMENT_REVERSEDREFUND_PROCESSED
username: unique email-style loginpassword: hashed passwordfirstNamelastNamerefreshToken- timestamps
userId: reference toUserbalance: numeric wallet balance
type:opening_balanceortransferfromUserId: sender user referencetoUserId: receiver user referenceamountstatus:CREATED,PROCESSING,SUCCESS,FAILED,REVERSED, orEXPIREDstatusHistory: append-only transition history with reason and timestampidempotencyKey: retry-safety key used for transfer creation- timestamps
CREATED -> PROCESSINGCREATED -> FAILEDCREATED -> EXPIREDPROCESSING -> SUCCESSPROCESSING -> FAILEDSUCCESS -> REVERSED
transactionId: source transaction referenceaccountId/userId: wallet owner when the entry belongs to a user walletledgerAccount: stable ledger account identifierentryType:debitorcreditmovementType:opening_balanceortransferamount,currency,balanceAfter
key: client-provided retry keyuserId,endpoint,requestHashstatus:processing,completed, orfailedresponseStatusCodeandresponseBodyfor replaying duplicate requestslockedUntil,expiresAt
provider,eventId,eventTypepayloadHash,signature,timestampHeaderstatus:received,processing,processed,failed,retry_scheduled,dead_lettered, orignoredtransactionId,payloadattempts,lastError,processedAt,nextRetryAt,deadLetteredAt
sourceType,sourceIdprovider,eventId,eventTypeattempts,reason,payloadresolvedAt,resolutionNote
- Max attempts:
5 - Backoff schedule:
1s,5s,30s,5m,15m - Due retries are processed through
POST /api/v1/webhooks/retries/process - Events that still fail after max attempts are copied to the dead-letter queue
status:completedorfailedstartedAt,completedAt,triggeredByUserIdsummary: counts for accounts, transactions, webhook events, and discrepanciesdiscrepancies: typed findings with severity, entity reference, expected value, actual value, and metadataerrorMessage: populated when the reconciliation job fails
- Account cached balance must equal credit-minus-debit totals from wallet ledger rows.
- Successful transactions must have ledger entries, balanced debit/credit totals, and totals matching transaction amount.
- Processed provider webhook events must point to an existing transaction whose state matches the provider event.
Rate-limit responses return HTTP 429 with Retry-After, X-RateLimit-Policy, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
| Policy | Window | Limit | Applied To |
|---|---|---|---|
auth |
15 minutes | 5 | Signup and signin |
tokenRefresh |
15 minutes | 20 | Refresh token |
userSearch |
1 minute | 30 | User directory search |
transfer |
1 minute | 10 | Money transfer |
webhook |
1 minute | 120 | Payment provider webhook |
adminRead |
1 minute | 60 | Webhook/reconciliation inspection endpoints |
adminWrite |
1 minute | 5 | Retry processing and reconciliation runs |
actorUserId,actorTypeaction: event name such asuser.signin,transfer.completed,webhook.received,rate_limit.blockedresourceType,resourceIdoutcome:success,failure, orblockedipAddress,userAgent,requestIddetails: structured event metadata with secrets and passwords excluded
Audited actions currently include signup, signin success/failure, logout, completed transfers, idempotency replays/conflicts, webhook receipt/replay/conflicts, webhook retry batch processing, reconciliation runs, and rate-limit blocks.
GET /api/v1/docsserves a lightweight local API docs page.GET /api/v1/docs/openapi.jsonreturns the OpenAPI 3.0 contract for API clients and review tools.- The spec documents auth, transfers, idempotency, ledger entries, webhooks, retries, reconciliation, rate limits, audit logs, and common error responses.
- A visitor lands on the PayPulse home page and chooses to sign up.
- Signup validates the form, optionally verifies Turnstile, creates the user, and creates an account balance.
- The user signs in, receiving HTTP-only access and refresh cookies.
- The dashboard loads the user's balance, searchable user directory, and transaction history.
- The user selects another user, enters an amount, and submits a transfer.
- The backend validates the idempotency key, moves the transaction through
CREATED -> PROCESSING -> SUCCESS, updates both balances, records ledger entries, caches the response, and returns success. - The user returns to the dashboard and can see the updated history.
PayPulse uses a "Night Transit" design direction: a deep slate canvas with a warm orange-to-red pulse for primary actions. The interface emphasizes one clear focal action per screen, readable financial data, and familiar form patterns. More detail lives in DESIGN.md and PRODUCT.md.
- The frontend keeps a small
localStoragelogin flag for client-side UX, but actual authentication is handled by HTTP-only cookies. - MongoDB transactions for signup funding and transfers require replica-set support.
- The backend test script is currently a placeholder.
backend/config.jscontains an older staticJWT_SECRETexport, while the active token generation and verification paths use environment variables.