Coperniq
SaaS for US solar-industry project management. Serves 200+ contractor companies, scaled to 3000+ concurrent users.
Outcomes
Context
Coperniq is a US-based SaaS for managing solar-panel installation projects — from sales lead through permitting, install, and post-install service. The customer base is 200+ contractor companies, mostly mid-market, each with their own crews running 5–50 active projects in parallel.
When I joined, the platform was a single Node.js monolith. Deploys took 90 minutes, MongoDB queries on critical endpoints were 1.2s p50, and adding a new dev to the team produced more conflicts than features for the first month. The product was working, but each new contractor onboarded was making the next deploy harder.
What I built
- Designed and led the monolith → microservices migration. Carved out 5 services: auth, projects, scheduling, billing, notifications. Each owns its database, schema, and deploy cycle.
- Built the real-time field-sync layer on WebSocket. Field crews update project status from mobile; updates propagate to office dashboards in <300ms. Tested up to 3000 concurrent users without queue backlog.
- Owned the MongoDB performance work end-to-end: profiled slow queries, designed compound indexes, rewrote aggregation pipelines. p50 on the critical endpoints dropped from 1.2s to 180ms — an 85% improvement.
- Introduced blue-green deploys in GitLab CI with health checks, automatic rollback, and a 'canary' phase routing 5% of traffic to the new version for 10 minutes before full cutover.
- Integrated with QuickBooks (accounting), Stripe (billing), Google Maps (routing), and DocuSign (contracts) — all the external systems contractors actually use day-to-day.
- Tech-led a team of 4 fullstack devs: code review, architecture reviews, weekly 1:1s, hired 2 mids. Onboarding time for new devs dropped from 'first PR in 3 weeks' to 'first PR in 4 days'.
Technical decisions
Service boundaries were chosen by data ownership, not by 'feature' — billing owns invoices and payment intents, scheduling owns crew calendars and timeslots. Avoided the 'distributed monolith' antipattern where services share a database.
Inter-service communication: synchronous REST for read-paths that need consistency, RabbitMQ events for write-paths that can be eventually consistent. Saga pattern for cross-service workflows like 'project completed → invoice → notification'.
Frontend stayed as a single React app with Redux Toolkit + React Query. RTK Query for cached server state, Redux only for genuine client state (modals, drafts, optimistic updates). Eliminated ~40% of the legacy Redux boilerplate.
Observability: Prometheus for metrics, Grafana for dashboards, structured JSON logs shipped to a centralized stack. Every microservice exposes the same /metrics, /health, /ready trio — uniformity matters more than picking the perfect stack.
From the code
// Before: full collection scan on every dashboard load (~1.2s p50)
db.projects.find({
organizationId: ObjectId("..."),
status: { $in: ["in_progress", "permitting"] },
scheduledStart: { $gte: ISODate("2026-01-01") },
}).sort({ scheduledStart: 1 }).limit(50);
// Compound index covering the filter + sort
db.projects.createIndex(
{ organizationId: 1, status: 1, scheduledStart: 1 },
{ name: "org_status_scheduled_v2", background: true }
);
// After: IXSCAN, ~180ms p50, no in-memory sort.
io.on("connection", (socket) => {
const { orgId, userId } = socket.data.session;
socket.join(`org:${orgId}`);
socket.join(`user:${userId}`);
});
projectsEvents.on("status_changed", async (evt) => {
// Push only to clients in the affected org, not all 3000.
io.to(`org:${evt.orgId}`).emit("project.updated", {
projectId: evt.projectId,
status: evt.status,
updatedBy: evt.actorId,
ts: evt.timestamp,
});
});
Result
Team grew from 5 to 12 devs without proportional drop in velocity. Deploy time: 90 min → 12 min. Production incidents: −60%. API latency: 1.2s → 180ms.
I still own the architecture review for any cross-service work and run tech interviews for fullstack hires.