Anonymous India
A social product where people post anonymously. I own all of it: the Flutter app on both stores, the NestJS API, the public site, the admin console, the database design and the infrastructure it runs on. Nobody reviews my pull requests and nobody else gets paged.
- Codebases
4
app, API, site, admin console
- Engineers
1
product decisions included
- Data models
29
under Prisma, one schema
- API modules
24
NestJS, one service

Six decisions, and what each one cost
Every one of these has a downside. A case study that only lists the upsides is a brochure.
No email, no phone, no password
How do you run accounts on a product where anonymity is the point?
Auth is a hashed device identifier and nothing else. The device generates a UUID, the server stores a SHA-256 hash of it, and that hash is the account. Tokens are JWTs signed RS256 with refresh tokens that can be invalidated the moment somebody is banned.
The tradeoff
Lose the device, lose the account. There is no recovery flow because there is nothing to recover against. That is a genuine cost, and I took it deliberately: a product about confessions should not be holding a table of phone numbers. The best way to not leak personal data is to never collect it.Fastify instead of Express under NestJS
Why bother swapping the HTTP adapter?
NestJS runs on Express by default. It also supports Fastify, which handles roughly twice the requests per second on the same hardware for JSON workloads like this one.
The tradeoff
A handful of Express-specific middlewares do not work, so anything reaching for req or res directly has to be written against Fastify. That is a real constraint on a solo project where nobody else has to learn the difference. For a team I would think harder about it.Redis carries the Socket.io adapter
What happens to realtime when there is more than one API container?
Socket.io keeps connections in memory. Two containers means two sets of connections that cannot see each other, so a reaction on one is invisible on the other. The Redis adapter puts a pub/sub layer between them so an event published anywhere is delivered everywhere.
The tradeoff
It is infrastructure I do not need today, on one container. I built it in anyway, because retrofitting it after you have scaled out is a migration and building it before is an afternoon. This is the same containment argument I make about ECS task definitions, applied to my own thing.Moderation runs on the server, not the client
The profanity filter already existed in the Flutter app. Why move it?
Anything enforced in the client is a suggestion. The filter, the rate limits, the reports, the bans and the audit log all moved server side. Rate limits are Redis backed and scoped per action, so posting, commenting and reacting each have their own budget.
The tradeoff
Every write now costs a Redis round trip and the app cannot give instant local feedback on a blocked word. Slower and less pleasant, and correct. A moderation system a determined user can disable by editing the client is not a moderation system.Counters live in transactions, not in application code
Why is a comment count so hard?
Comment counts, reaction counts and the rest are updated inside Prisma transactions alongside the write that caused them. Read, add one, write is a race the moment two people comment at the same time, and the numbers drift quietly for weeks before anyone notices.
The tradeoff
Longer transactions and more contention on hot rows. Worth it. A social product whose counts are visibly wrong loses trust faster than one that is briefly slower.Multi-stage Docker, non-root, four services
How does it actually run?
A three stage build on node:20-alpine: dependencies, builder, runner. The final image drops to a non-root user and carries no build toolchain. Production composes four services: Postgres 16, Redis 7, the API, and Nginx in front doing TLS and rate limiting at the edge.
The tradeoff
One host, so it is rung one on my own ladder, and that is on purpose. This product does not have the traffic to justify a cluster, and putting it on one to look impressive would contradict everything else I have written.
Honestly
What I would do differently.
- The Flutter app talks to Firebase directly for storage and to my API for everything else. Two sources of truth for one product is a seam, and seams are where things break. I would put all of it behind the API.
- 29 models is more than this product needed at the start. Subscriptions, referrals and feature flags all landed before anybody was asking for them. Building for a scale you have not reached is the exact mistake I write about.
- I did not write a runbook until months in, because it was mine and I thought I would remember. I did not remember.
Owning one layer teaches you the layer. Owning all of them teaches you the seams, and the seams are where the expensive problems live.
The whole stack
- Mobile
- Flutter
- Dart
- Firebase Auth
- Crashlytics
- Analytics
- RevenueCat
- AdMob
- API
- NestJS 10
- Fastify
- Prisma 5
- Swagger
- JWT RS256
- Socket.io
- Data
- PostgreSQL 16
- Redis 7
- Firebase Storage
- Infrastructure
- Docker
- Docker Compose
- Nginx
- node:20-alpine
- Web
- Next.js
- React
- TanStack Query
- Tailwind
Want to go through it properly?
I will walk through the schema, the auth flow or the deployment, whichever is more useful. Including the parts I got wrong.