Skip to content
Return to Projects
Case Study

Payday Chat

A real-time, members-only network for online business founders.

The Problem

Founders needed a private, high-signal place to network, build teams, and scale revenue — not another noisy public feed. Payday Chat is invite-only and built around real-time collaboration.

My Role

Built solo with AI-assisted development — Flutter client, NestJS API, data model, real-time layer, and deployment.

Highlights

  • Real-time messaging with presence and live events over WebSockets
  • One NestJS API powering both a Flutter mobile app and a web client
  • PostgreSQL domain model with Redis for caching and pub/sub fan-out
  • NGINX gateway, Stripe payments, and OAuth authentication

Stack

FlutterDartNestJSPostgreSQLRedisNGINXStripe

Constraints

  • Invite-only product — auth and access control had to be correct from day one.
  • One backend, two clients (Flutter + web) — schema and contracts had to be shared, not duplicated.
  • Realtime is a feature, not a nice-to-have — presence and live events must survive reconnects.

System Architecture

Clients
Flutter Mobile App
Web App
Gateway
NGINX
API
NestJS REST
WebSocket Gateway
Data
PostgreSQL
Redis (cache + pub/sub)
External
Stripe
OAuth

Key Trade-offs

The decisions worth defending — what I chose, what I turned down, and why.

Realtime transport

Chose

WebSockets with Redis pub/sub fan-out

Rejected

Long-polling or third-party realtime SaaS

Predictable latency, no per-message vendor cost, and Redis already in the stack for caching — one fewer moving part.

Mobile client framework

Chose

Flutter (single codebase for iOS + Android)

Rejected

Native Swift + Kotlin clients

Solo build — two native clients would have doubled the surface area and slowed iteration on the API.

API style

Chose

REST + dedicated WebSocket gateway

Rejected

GraphQL subscriptions

Simpler operational story, easier to cache at the NGINX layer, and the realtime channel stays an explicit, observable component.

What I'd Do Differently

An honest retrospective — the stuff I'd change with more time, more users, or a second pass.

  1. 1Introduce contract tests between the NestJS API and the Flutter client earlier — a few breakages were caught only at runtime.
  2. 2Move long-lived sockets to a dedicated process so API deploys don't drop client connections.
  3. 3Add structured event versioning from day one instead of retrofitting it once the schema started moving.

Technical Deep-Dive

Architecture, specifications, and implementation details.

Pre-Launch Checklist

Go through every item before opening Payday to real members.


#Security

  • All JWT secrets are 64+ characters and randomly generated
  • BCRYPT_SALT_ROUNDS=12 (not lower)
  • SWAGGER_ENABLED=false in production .env
  • DB_LOGGING=false in production (prevents PII in logs)
  • DB_SYNC=false (must always be false)
  • Redis requires password (requirepass set)
  • PostgreSQL not exposed on public port in production
  • Redis not exposed on public port in production
  • Nginx rate limiting active (limit_req_zone configured)
  • CORS ALLOWED_ORIGINS set to actual frontend domain only
  • HTTPS enabled (SSL certificate on Nginx)
  • All OAuth redirect URIs updated to production domain
  • STRIPE_SECRET_KEY is sk_live_... not sk_test_...
  • Stripe webhook endpoint registered with production URL
  • Apple Sign-In domain verified in Apple Developer Console

#Infrastructure

  • Production docker-compose.prod.yml tested
  • PostgreSQL data volume is on a persistent disk (not ephemeral)
  • Redis AOF persistence enabled (appendonly yes)
  • Uploads volume backed up or pointed to S3
  • Health check endpoint responding: GET /health → 200
  • All containers set to restart: always in production
  • Portainer secured with strong password (or disabled)
  • Docker socket not exposed to public network
  • Log rotation configured (max-size limits on all containers)

#Database

  • All 18 tables created: \dt in psql shows full list
  • pgvector extension loaded
  • pg_trgm extension loaded
  • All indexes created (check \di in psql)
  • All triggers active
  • Default channels seeded (12 channels)
  • Admin user created with strong password
  • Seed test data removed (no test@example.com accounts)

#Application

  • Registration flow works end-to-end (register → email → verify → login)
  • Google OAuth login tested
  • Apple Sign-In tested (requires HTTPS)
  • Stripe checkout flow tested (use test cards first, then live)
  • Stripe webhook receiving events (check Stripe Dashboard)
  • File upload tested (image upload to profile)
  • Real-time messaging tested (send DM, verify instant delivery)
  • Socket.io connecting without errors
  • Email sends arriving (not going to spam)
  • Password reset flow tested
  • Member application flow tested (apply → admin reviews → approve → new user can login)

#Email

  • From address MAIL_FROM set to your verified sending domain
  • Welcome email sends correctly
  • Verify email link works
  • Password reset email works
  • Test all emails for spam score (mail-tester.com)
  • SPF, DKIM, DMARC DNS records configured for sending domain

#Payments

  • Stripe account verified (not in test mode)
  • All 3 pricing tiers created in Stripe (Starter, Pro, Elite)
  • Price IDs copied to production .env
  • Stripe webhook created for production URL
  • Webhook secret copied to production .env
  • Test a real $1 charge before launch
  • Billing portal configured in Stripe Dashboard
  • Cancellation/downgrade flow tested

#Performance

  • docker stats shows containers within memory limits
  • PostgreSQL slow query log reviewed (log_min_duration_statement=500)
  • Redis memory usage below maxmemory limit
  • Response times acceptable: curl -w "%{time_total}" http://yourdomain/health

#Monitoring

  • Container health checks all passing: docker compose ps
  • Log aggregation set up (CloudWatch, Datadog, or Papertrail)
  • Uptime monitor set up (UptimeRobot free tier is fine)
  • Alert set up for health check failures

#Legal / Compliance

  • Privacy Policy published on frontend
  • Terms of Service published
  • Cookie consent implemented (if EU users expected)
  • GDPR data deletion flow implemented (DELETE /api/v1/members/me/account)
~ End of Document ~