Skip to main content
Leonardo Serrano Leonardo Serrano

Full-stack engineer building production systems that scale

Costa Rica · 4 live products · 159K+ lines of code · real users every day. I run a recording studio and ship SaaS on the side.

Stack: Next.js 15 React 19 Tailwind 4 NestJS 10 Prisma 6 Socket.IO

What 13 years of shipping looks like

Real metrics from production codebases, not vanity numbers.

13 +
years in production FLProductions since 2013. Top 3 in Costa Rica SEO for "estudio de grabación".
~159K lines of code across 4 production products
70 NestJS modules across 3 production backends
417 REST endpoints across 3 NestJS backends
83 Prisma models 3 production schemas
500 WebSocket users concurrent load-tested (Zamr extreme)

What I'm shipping right now

Four production systems, each one a different domain, all built from scratch.

Studio Live
Featured

FLProductions

13 years turning ideas into mastertracks

Public site + private artist portal in a single monorepo. NestJS + Prisma + Next.js with real-time sync, wallet with cashback, and AI chat that parses Costa Rican Spanish. Auto-optimizes GCS storage costs by moving inactive files to Coldline after 30 days.

13+ years live
~68K LoC
28 Prisma models
202 endpoints
  • Public site + private artist portal in a single monorepo
  • Real-time reactivity: any mutation updates all open tabs instantly
  • Wallet with 5% cashback + vault with tiered pricing (standard vs VIP)
SaaS Live
Featured

Zamr

Real-time worship management for bands and churches

Multi-tenant SaaS for worship bands and churches. Leader controls the service from a tablet, projector shows the lyrics animated, every musician sees the chords in sync. All under 100 ms via a 1,023-line WebSocket gateway load-tested to 500 concurrent users.

500+ songs
100+ live events
500 WS users
156 test files
  • WebSocket gateway load-tested to 500 concurrent users on production
  • Multi-tenant: Bands × Churches × 13 church roles composed in 6 guards
  • Two distinct live views: projector (lyrics) vs musician (chords)
SaaS Live
Featured

MejorMenu

Digital menus for local restaurants, orders via WhatsApp

Multi-tenant SaaS for restaurants in Costa Rica. Each business gets its own public page at /[slug] with menu, hours, location on a map, and an order button that opens WhatsApp with a pre-formatted message. No POS, no app, no website needed. AI image generation with Gemini.

21 módulos
27 modelos
84 endpoints
3 clientes live
  • Multi-tenant SaaS for restaurants in Costa Rica (Herediana de Siquirres, Limón)
  • Each business gets its own public page at /[slug] with SEO and JSON-LD
  • AI image generation with Gemini 2.5 Flash Image (3 context types)
E-commerce Live

Ackee Beats

Exotic style, Ackee sound

Beat marketplace with PayPal licensing. Searchable catalog with YouTube previews, tiered license hierarchy (BASIC < PREMIUM < UNLIMITED) with audit trail, and PDF license generated per purchase via pdf-lib. The e-commerce extension of FLProductions.

12+ beats live
3 license tiers
1 PayPal integration
  • License hierarchy BASIC < PREMIUM < UNLIMITED with audit trail
  • PDF generated per purchase with pdf-lib
  • YouTube preview integration for every beat

What I work with

Tools I've shipped to production. Grouped by layer, ordered by how much I've used them.

Backend

  • TypeScript
    Expert
  • Node.js
    Expert
  • NestJS
    Expert
  • Express
    Proficient
  • Prisma
    Expert
  • PostgreSQL
    Proficient
  • MySQL
    Proficient
  • Socket.IO
    Expert

Frontend

  • React
    Expert
  • Next.js
    Expert
  • Astro
    Proficient
  • Tailwind CSS
    Expert
  • HeroUI
    Proficient
  • TanStack Query
    Proficient

Real-time

  • Socket.IO
    Expert
  • EventEmitter2
    Proficient
  • WebSockets
    Expert
  • SSE
    Proficient

AI / ML

  • OpenAI GPT-4o-mini
    Proficient
  • Google Gemini
    Proficient
  • Prompt engineering
    Proficient

DevOps

  • Google Cloud
    Proficient
  • Vercel
    Proficient
  • Railway
    Proficient
  • GitHub Actions
    Proficient
  • Docker
    Proficient

Tools

  • Git / GitHub
    Expert
  • VS Code
    Expert
  • Jest / Vitest
    Proficient

Real code, real patterns

A taste of what's inside the 3 codebases. No toy examples — patterns shipping in production, extracted from the actual files.

FLProductions real-time event-driven
39 lines

Unified real-time sync — the single source of truth

Service emits a domain event → SyncListener handles fan-out to WebSocket rooms. The same map drives every cache invalidation in the app. 41 @OnEvent handlers, all try/catch, multi-room broadcast.

  • Service never calls socket.to().emit() directly — emits a domain event instead
  • Each handler wraps in try/catch so sync failure never compromises a DB transaction
  • Same handler routes to multiple rooms (project room + client room + admin room)
@OnEvent('project.updated')
handleProjectUpdated(payload: {
  projectId: number;
  clientId: number;
  title: string;
  status: string;
}) {
  try {
    this.logger.log(`🔄 Syncing project ${payload.projectId}: ${payload.status}`);

    // Notify the project room (admin / uploader in detail view)
    this.assetsGateway.server
      .to(`project_${payload.projectId}`)
      .emit('dashboard_sync', {
        type: 'project_updated',
        projectId: payload.projectId,
        status: payload.status,
        projectTitle: payload.title,
      });

    // Notify the client (portal dashboard)
    this.assetsGateway.emitToClient(
      payload.clientId,
      'dashboard_sync',
      {
        type: 'project_updated',
        projectId: payload.projectId,
        projectTitle: payload.title,
        status: payload.status,
      }
    );
  } catch (error) {
    const err = error as Error;
    this.logger.error(
      `Error in handleProjectUpdated: ${err.message}`,
      err.stack
    );
  }
}
FLProductions real-time react-query
62 lines

useSync — the React Query invalidation dispatcher

One hook, one map: 38 socket events → 18 sync groups → 40+ query keys. Mount in any page, receive every relevant real-time event for that scope.

export const useSync = (currentProjectId?: number | string) => {
  const socket = useSocketConnection();
  const queryClient = useQueryClient();

  useEffect(() => {
    if (!socket) return;
    const listeners: Record<string, (p: SocketEventPayload) => void> = {};

    Object.keys(SOCKET_EVENT_MAPPING).forEach((event) => {
      const handler = (payload: SocketEventPayload) => {
        const groups = SOCKET_EVENT_MAPPING[event];
        const eventProjectId =
          payload?.projectId ||
          payload?.data?.projectId ||
          payload?.data?.id;

        groups.forEach((group) => {
          const queryKeys = GROUP_MAPPING[group];
          const shouldForceRefetch = FORCE_REFETCH_GROUPS.includes(group);

          queryKeys.forEach((key) => {
            const isGlobalKey =
              key === 'PortalDashboard' || key === 'Projects';
            const isGlobalGroup =
              group === 'PROJECTS' || group === 'FINANCE';

            if (isGlobalKey || isGlobalGroup) {
              shouldForceRefetch
                ? queryClient.refetchQueries({
                    queryKey: [key],
                    type: 'active',
                  })
                : queryClient.invalidateQueries({
                    queryKey: [key],
                    refetchType: 'all',
                  });
            } else if (
              currentProjectId &&
              eventProjectId &&
              String(eventProjectId) === String(currentProjectId)
            ) {
              // Project-scoped invalidation
              queryClient.invalidateQueries({
                queryKey: [key, currentProjectId],
                refetchType: 'all',
              });
            }
          });
        });
      };

      socket.on(event, handler);
      listeners[event] = handler;
    });

    return () => {
      Object.keys(listeners).forEach((e) =>
        socket.off(e, listeners[e])
      );
    };
  }, [socket, queryClient, currentProjectId]);
};
Zamr real-time rate-limiting
54 lines

Per-user rate limiting in a 1,023-line WebSocket gateway

30 msgs/min + 5 msgs/2s burst, with warning at 80% threshold. Returns false if exceeded; the gateway then drops the message without broadcasting.

  • Per-user-per-event scoped (not global)
  • Burst window separate from minute window
  • Logs warnings at 80% threshold for proactive ops
private checkRateLimit(
  userId: number,
  eventId: number,
  messageType: string
): boolean {
  const key = `${userId}:${eventId}`;
  const now = Date.now();
  let rateLimitInfo = this.rateLimits.get(key);

  if (!rateLimitInfo) {
    rateLimitInfo = {
      count: 1,
      resetTime: now + 60000,
      lastMessageTime: now,
    };
    this.rateLimits.set(key, rateLimitInfo);
    return true;
  }

  if (now >= rateLimitInfo.resetTime) {
    rateLimitInfo.count = 1;
    rateLimitInfo.resetTime = now + 60000;
    rateLimitInfo.lastMessageTime = now;
    return true;
  }

  // Burst window
  const timeSinceLastMessage = now - rateLimitInfo.lastMessageTime;
  if (timeSinceLastMessage < this.burstWindow) {
    if (rateLimitInfo.count >= this.burstLimit) {
      this.logger.warn(
        `Rate limit (burst) applied to user ${userId} on event ${eventId}`
      );
      return false;
    }
  }

  if (rateLimitInfo.count >= this.maxMessagesPerMinute) {
    this.logger.warn(
      `Rate limit (per minute) applied to user ${userId} on event ${eventId}`
    );
    return false;
  }

  rateLimitInfo.count++;
  rateLimitInfo.lastMessageTime = now;

  if (rateLimitInfo.count >= this.maxMessagesPerMinute * 0.8) {
    this.logger.warn(
      `User ${userId} near rate limit: ${rateLimitInfo.count}/${this.maxMessagesPerMinute}`
    );
  }
  return true;
}
Leonardo Serrano

Building software that puts food on the table

I'm Leo, a full-stack engineer based in Herediana de Siquirres, Limón, Costa Rica. I run FLProductions , a recording studio that's been live since 2013 and brings in 80% of its clients through the website.

On the side I ship multi-tenant SaaS products: Zamr for worship bands (load-tested to 500 WebSocket users) and MejorMenu for local restaurants (3 clients live and growing).

My favourite problems: real-time systems, multi-tenant architectures, and developer experience. I write the kind of docs I would want to read — 4,000+ lines across the 3 projects.

Let's build something real

Open to interesting projects, contract work, and full-time roles where the team ships production software with care.