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
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
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.
- 1Introduce contract tests between the NestJS API and the Flutter client earlier — a few breakages were caught only at runtime.
- 2Move long-lived sockets to a dedicated process so API deploys don't drop client connections.
- 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()anddown()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()anddown()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:
- Expand phase: Add new column/table (backwards compatible)
- Migrate phase: Deploy code that uses both old & new structure
- Deploy phase: Code switches to new structure atomically
- 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 EXISTSclause 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 versioned ✅ Rollbacks are automated ✅ Downtime risk is minimized with proper migration design ✅ Database state is trackable in code
Next: When new features require schema changes, follow the migration workflow above. Test thoroughly before production deployment.