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.

TypeORM Migrations Strategy — Payday v0.1.0

#Overview

Starting with v0.1.0, Payday uses TypeORM migrations instead of DB_SYNC=true. This ensures:

  • Production safety: No automatic schema mutations
  • Data integrity: Full version control of schema changes
  • Rollback capability: Down migrations for emergency reverts
  • Auditability: Complete migration history in database

#Current Migrations

##1. 1774017863432-CreateInitialSchema.ts

Status: Baseline schema (23 tables, 12 enums, all indexes & triggers) Run: First, on any new database Includes:

  • User + Authentication tables
  • Content tables (Posts, Channels, Resources, Opportunities, Events)
  • Social tables (Connections, Conversations, Messages)
  • Admin tables (Moderation Logs, Member Applications)
  • Support tables (Subscriptions, Notifications, Badges, Ratings)

##2. 1774017863433-SeedInitialData.ts

Status: Seeding (12 channels + 4 test users for dev) Run: After CreateInitialSchema Dev: Includes test users (admin@payday.dev, test2-4@payday.dev) Prod: Remove test user seeding before deploying in production


#Environment Configuration

##Development (DB_SYNC=false)

  • Uses migrations on startup
  • Full SQL logging for debugging
  • Queries printed to console
DB_SYNC=false
DB_LOGGING=true
NODE_ENV=development

##Production (DB_SYNC=false)

  • Migrations run automatically on startup
  • No SQL logging (performance, security)
  • Requires all migrations to be committed and tested
DB_SYNC=false
DB_LOGGING=false
NODE_ENV=production

#Migration Workflow

##Generating New Migrations (Development)

After modifying an entity, generate a migration:

cd server/
npm run migration:generate -- src/database/migrations/AddNewColumn

Important:

  • Always review the generated migration before committing
  • Test on a test database first
  • Ensure both up() and down() are correct

##Running Migrations Manually

# Run pending migrations (happens auto on startup)
docker compose exec api npm run migration:run

# Revert last migration  
docker compose exec api npm run migration:revert

# Show migration status
docker compose exec api npm run migration:show

##CLI Commands Available

# In server/package.json scripts:
npm run migration:generate   # Generate from entity changes
npm run migration:run        # Run pending migrations
npm run migration:revert     # Undo last migration
npm run migration:show       # Show migration history

#Deployment Checklist

##Before Deploying New Migration to Production

  • Migration tested on local dev database
  • Migration tested on staging database (full copy of production)
  • Both up() and down() methods verified
  • No data loss in down() (use cascading deletes or archive tables)
  • Performance tested: large table migrations don't lock DB for >30 seconds
  • Committed to Git with full review
  • Tagged in release notes
  • Rollback plan documented (if needed)

##Zero-Downtime Deployments

For migrations that affect production data:

  1. Expand phase: Add new column/table (backwards compatible)
  2. Migrate phase: Deploy code that uses both old & new structure
  3. Deploy phase: Code switches to new structure atomically
  4. Cleanup phase: Remove old structure in future migration

Example:

// Migration 1: Add new column
ALTER TABLE users ADD COLUMN email_verified_v2 BOOLEAN DEFAULT false;

// Code: Use both (v1 and v2)
// Code deploys first, then migration runs on startup

// Migration 2 (later): Drop old column
ALTER TABLE users DROP COLUMN email_verified;

#Troubleshooting

##"typeorm_metadata table not found"

  • Database is fresh: Run migrations normally (auto on startup)
  • Database has data but no history: This is OK, TypeORM will create metadata table

##"Migration XXX already exists"

  • Do not re-run migrations
  • Check database for corrupted migration record: SELECT * FROM typeorm_metadata

##"Migration would data" error

  • Add IF EXISTS clause in migration
  • Or provide explicit rollback in down()

##Rollback failed, can't revert

Emergency recovery:

# 1. Connect to database
docker compose exec postgres psql -U payday_user -d payday_db

# 2. View migration history
SELECT * FROM typeorm_metadata ORDER BY name DESC;

# 3. Manually delete problematic row (DANGEROUS!)
DELETE FROM typeorm_metadata WHERE name = 'Problem_Migration_Name';

# 4. Run migration again
npm run migration:run

#Production Readiness

Schema changes are now safely versionedRollbacks are automatedDowntime risk is minimized with proper migration designDatabase state is trackable in code

Next: When new features require schema changes, follow the migration workflow above. Test thoroughly before production deployment.

~ End of Document ~