The Problem I Was Solving
SocialBoost started as a client request — a digital marketing agency needed a platform where their clients could log in, see analytics, schedule content, and manage subscriptions. Nothing too unusual, but the timeline was aggressive: 6 weeks from kickoff to production.
The biggest constraint wasn't time — it was that I was building it solo, alongside my full-time contract role at IVTREE.
Tech Stack Decisions
I went with Next.js App Router for the frontend, Node.js + Express for the backend API, MongoDB for the database, and Stripe for subscriptions. Here's why:
- Next.js App Router — Server Components meant I could fetch data on the server, reducing client JS bundle and improving initial load
- MongoDB — The data model wasn't fully defined upfront, so a flexible schema made sense over PostgreSQL
- Stripe — Subscription billing with webhooks is non-trivial. Stripe handles the complexity better than any alternative
The Architecture
suyogbhise.online
├── app/
│ ├── (auth)/login, signup
│ ├── (dashboard)/overview, analytics, content
│ ├── api/
│ │ ├── stripe/webhook
│ │ ├── auth/[...nextauth]
│ │ └── content/
└── components/I kept auth, billing, and content as completely separate concerns. This paid off when the client wanted to add a Google Calendar integration mid-project — I could drop it into the content layer without touching auth or billing.
The Hardest Part: Stripe Webhooks
Stripe webhooks are the source of most SaaS bugs. The order of events matters, idempotency matters, and you need to handle failures gracefully. My approach:
// Always verify the webhook signature first
const event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
// Use a processed-events collection to handle retries
const alreadyProcessed = await db.collection('webhookEvents')
.findOne({ stripeEventId: event.id });
if (alreadyProcessed) return res.json({ received: true });
// Process and mark as done atomically
await processEvent(event);
await db.collection('webhookEvents')
.insertOne({ stripeEventId: event.id, processedAt: new Date() });What Shipped in 6 Weeks
- Multi-role auth (admin, client, viewer) with Google OAuth
- Stripe subscription management with portal link
- Google Calendar integration for content scheduling
- Analytics dashboard with chart.js
- Email notifications via Resend
What I'd Do Differently
I'd invest more time upfront in a proper error boundary system and loading state management. When you're moving fast, these feel like extras — but they're what separates a polished product from one that feels rough around the edges.
Moving fast is only worth it if what you ship actually works reliably for users.