# Janindu Gayanga: Full-Stack Software Engineer > Janindu Gayanga is a full-stack software engineer in Colombo, Sri Lanka, with 3+ years on production systems in iGaming, Fintech, E-commerce and EdTech. He works mostly in Node.js, TypeScript and Go on the backend and React/Next.js on the frontend. At BotCalm he is a Software Engineer and a core member of the team rebuilding a real-money gaming platform as Go microservices. In his own projects he builds AI features (RAG, tool-calling agents, MCP servers, evals in CI) and writes Terraform for AWS and GCP, tested on LocalStack and local emulators. He is open to on-site, hybrid or remote roles. ## Summary - Name: Janindu Gayanga - Role: Full-Stack Software Engineer - Location: Colombo, Sri Lanka - Availability: Open to full-time Software Engineer and Senior Software Engineer roles, remote or in Colombo, and to freelance projects. - Current employer: BotCalm Private Limited - Education: B.Sc. (Hons) Software Engineering, Sri Lanka Technological Campus (SLTC) (2020 – 2024) - Website: https://iamjanindu.com - GitHub: https://github.com/jani358 - LinkedIn: https://www.linkedin.com/in/janindu-gayanga-02ba60217/ - Email: janindugayanga10@gmail.com ## Skills - Languages: TypeScript, JavaScript, Go, SQL - Backend: Node.js, NestJS, Express, REST / OpenAPI, GraphQL (DataLoader), WebSockets, Socket.io, Server-Sent Events, Kafka, BullMQ, Microservices - Frontend & Mobile: React, Next.js, React Native (Expo), Tailwind CSS, shadcn/ui, Zustand, TanStack Query, Redux Toolkit, PWA - AI / LLM: RAG, Tool-calling agents, MCP servers, LLM evals in CI, pgvector / hybrid search, Claude API, Gemini API, OpenAI API, Prompt-injection defence, Cursor · Claude Code · Copilot - Databases: PostgreSQL, Prisma, Redis, MongoDB, MySQL, Row-level security - Cloud & DevOps: AWS (EC2, S3, ECS Fargate, RDS, SQS), GCP (Cloud Run, Pub/Sub, BigQuery), Azure, Docker, Kubernetes (K3s), Helm, ArgoCD, Terraform, GitHub Actions CI/CD - Testing & Observability: Jest, Vitest, Playwright, Testcontainers, k6 load testing, OpenTelemetry, Prometheus, Grafana, Sentry - Security: OAuth 2.0 / OIDC (PKCE), JWT, RBAC, Multi-tenancy, Idempotency keys, Zod ## Experience ### Software Engineer, BotCalm Private Limited · Sri Lanka (July 2025 – Present) Core member of the team rebuilding a real-money iGaming platform, moving it from a Node.js/MongoDB monolith to 20 Go services on PostgreSQL, Redis and Kafka. My own pieces are the WebSocket notification service, the NOWPayments crypto integration, the vault service, the Vesant and GeoComply compliance gates (KYC, AML, geolocation) and the in-house support chat. We ship through GitHub Actions and ArgoCD to K3s on AWS. ### Associate Software Engineer, BotCalm Private Limited · Sri Lanka (August 2024 – June 2025) Built and deployed REST APIs and full-stack apps on AWS for four production client projects in Fintech, E-commerce and Education. That included crypto-exchange and payment integrations (Changelly, EasyBit, MoonPay, PayCrypto, Rubic) with signed requests and verified webhooks. ### Trainee Software Engineer (Full Stack), BotCalm Private Limited · Sri Lanka (March 2024 – July 2024) Built full-stack features with Node.js, React and PostgreSQL on production projects. Promoted to Associate after four months. ### Software Engineer Intern, NEWNOP Private Limited · Sri Lanka (May 2023 – November 2023) Built responsive pages for the Lallaloi platform in Next.js and Tailwind CSS, served assets from AWS S3, and fixed the UI defects QA found before release. ### B.Sc. (Hons) Software Engineering, Sri Lanka Technological Campus (SLTC) · Padukka (March 2020 – August 2024) Honours degree in Software Engineering. Second runners-up at CodeMania v2.0 (IEEE Computer Society SLTC). Coursera: Cloud Computing Applications (Cloud Systems and Infrastructure). ## Featured projects ### AskLK: Bilingual AI Assistant with RAG, Agents and MCP - Page: https://iamjanindu.com/projects/asklk-ai-assistant - Showcase: https://github.com/jani358/asklk-AI-assistant-showcase. Full source available on request. - Context: Personal project - Tech: TypeScript, NestJS, Next.js 15, React 19, PostgreSQL, pgvector, Prisma, Redis, RAG, LLM agents, MCP, Claude API, Gemini API, OpenTelemetry, Terraform, AWS (LocalStack), Docker, GitHub Actions, Jest, Vitest, Playwright, k6 An assistant a Sri Lankan clinic, school or company can point at its own documents. I wanted it to say "I don't know" rather than guess, so retrieval is hybrid (pgvector plus Postgres full-text, merged with Reciprocal Rank Fusion), every answer cites its sources, the agent waits for a human before any write, and an MCP server lets Claude Code search the same documents. Problem: - A tuition teacher, a clinic or a company keeps its knowledge in documents, and people ask about it in Sinhala or English. Often the document is in the other language. - Generic chatbots guess when they don't know, and they invent sources. For a clinic or a school that is worse than no answer. - Many organisations share one deployment, so one tenant must never see another tenant's documents. Architecture: - NestJS 10 API (auth, RBAC, tenants, the agent loop, SSE streaming), a Next.js 15 dashboard and chat, and an embeddable Shadow DOM widget: 17 KB raw, 5 KB gzipped. - PostgreSQL 16 with pgvector stores documents, chunks and 768-dimension embeddings. 768 because pgvector's HNSW index stops at 2,000 dimensions, and Gemini's embedding model keeps most of its quality when truncated. - Retrieval runs two queries in one transaction: a vector search, so a Sinhala question can match an English passage, and a full-text search for exact tokens. Cosine scores and ts_rank can't be added together, so the two lists are fused by rank with RRF (k = 60). - Four guards against made-up answers: a similarity floor that returns no passages for weak matches, prompt rules, citation validation, and source cards built from the retrieved rows instead of parsed out of the model's text. - A hand-written agent loop rather than LangChain. It stops after five steps, asks a human to approve any write (opening a ticket, for example), and logs every tool call to an audit table. - A provider-neutral ChatModel port with Gemini, Claude and OpenAI-compatible adapters, picked per organisation. A fast model classifies the question and a stronger one answers. - An MCP server over stdio and streamable HTTP, with tenant API keys and per-key rate limits. I've run it from Claude Code; Claude Desktop takes the same config. - Redis-backed ingestion worker (extract, chunk, embed), OpenTelemetry GenAI traces, a per-tenant LLM cost ledger with a budget, Postgres row-level security on the paths that read content, and OIDC sign-in with PKCE. - Terraform for AWS: ECS Fargate (ARM64) behind an ALB, RDS and ElastiCache in private subnets, SQS with a dead-letter queue. The GitHub OIDC deploy workflow is written but has never run, because there is no AWS account behind it yet. Results: - A 46-case golden-set eval runs on every pull request that touches the AI code. It uses a deterministic fake model, so it catches regressions; it doesn't grade a real model. Faithfulness 100%, recall@5 100%, refusal accuracy 91.7%. - One poisoned document could hijack unrelated answers: injection resistance was 0 of 10 cases. A passage sanitiser plus a stricter prompt took it to 10 of 10, and I wrote a postmortem. - 358 unit tests, 42 integration tests against a real pgvector database (tenant isolation and RLS included), 17 Vitest tests for the MCP server and 24 Playwright end-to-end tests. - k6 on my laptop with a stubbed model: p99 346 ms at 20 requests per second. That is platform overhead only, since a real model adds its own time to first token. A 20-page PDF is ingested in 1.76 s. - Terraform: a 71-resource plan, with 41 resources applied and destroyed on LocalStack. ECS, the ALB, RDS and ElastiCache were planned only. - 9 row-level security tests. One runs a query with no WHERE clause and still sees only one tenant. ### BookLK: Event-Driven Service Booking Marketplace - Page: https://iamjanindu.com/projects/booklk-booking-marketplace - Showcase: https://github.com/jani358/BookLK-showcase. Full source available on request. - Context: Personal project - Tech: TypeScript, Next.js 14, NestJS, Go, PostgreSQL, Redis, Kafka (Redpanda), MongoDB, GraphQL, BullMQ, Stripe, PayHere, Kubernetes (k3d), Helm, ArgoCD, Terraform, Prometheus, Grafana, k6, Playwright Customers book time slots with salons, clinics and repair shops, vendors get paid out, and the platform keeps a commission. It is four services plus a job worker, joined by a transactional outbox to Kafka, with a Go WebSocket service for live updates. Payments run on Stripe test mode and the PayHere sandbox, and they are reconciled every night. Problem: - When many customers press "book" on the same slot at the same moment, the system must sell it exactly once. - Booking changes must reach every connected browser in real time without losing or reordering events. - Money must stay correct when payment webhooks arrive twice, late, or never. Architecture: - NestJS API with 45 REST handlers and a public GraphQL endpoint, a Next.js 14 PWA, a Go realtime service on gorilla/websocket, an Express webhooks service, and a BullMQ job worker. - Double booking is blocked four times over: an idempotency key, a Redis SET NX lock, SELECT … FOR UPDATE inside a READ COMMITTED transaction, and a unique active-slot constraint as the database backstop. - Transactional outbox: the booking and its event commit in one Postgres transaction, and a relay publishes them to Kafka (Redpanda) using FOR UPDATE SKIP LOCKED. Consumers dedupe on eventId. Failures land in a per-topic DLQ that an admin can replay. - Payments sit behind a provider port (Stripe, PayHere and a deterministic fake). Webhooks are verified against the raw body. Money is in integer cents and commission in integer basis points; the ledger is append-only, and a refund is a reversal entry. - A nightly job compares provider transactions, payments and the ledger, records each gap, and can repair a missing webhook from the provider's own record. - Read side: MongoDB read models fed from Kafka, and GraphQL for public catalog reads with DataLoader batching, depth and cost limits, and persisted queries. - Kubernetes on a local k3d cluster: one Helm chart covering all six workloads, ArgoCD (app of apps) syncing from an in-cluster Gitea, and Prometheus and Grafana with multi-window SLO burn-rate alerts. - Terraform for a single EC2 + K3s node with a USD 10 budget alarm. It is tested with a mocked provider and applied to LocalStack; it has never touched real AWS. Results: - An integration test fires 20 parallel bookings at one slot and asserts that exactly one wins. - k6 booking spikes (six runs of 200 virtual users on 20 slots, one of 500 on 50): zero double bookings in all seven, checked directly in Postgres, and no 5xx errors. - Serializable isolation was aborting bookings for different slots (264 conflicts in one run) and leaving slots unsold. READ COMMITTED with row locks took conflicts to 0 and the median from 468–530 ms down to about 247 ms. - The latency target is not met. p95 is about 3.2 s at 200 virtual users against a 300 ms goal. cgroup CPU stats showed the API pinned at one CPU while Postgres sat mostly idle, so I moved to two API pods (85–92 req/s became 126–140). Profiling a single request is the next step. - A crash test kills the outbox relay before and after it publishes. No event is lost. ### QueueLK: Real-Time Queue Management for Clinics - Page: https://iamjanindu.com/projects/queuelk-queue-management - Showcase: https://github.com/jani358/queuelk-showcase. Full source available on request. - Context: Personal project (private repo) - Tech: TypeScript, NestJS, Next.js 14, React Native (Expo), PostgreSQL, Prisma, Redis, Server-Sent Events, Socket.io, Web Push, PWA, GCP (emulators), Pub/Sub, BigQuery, Terraform, k6, Jest, Playwright A screen on the waiting-room wall and a page on the patient's phone, for Sri Lankan dispensaries, clinics and salons. Patients scan a QR code and see how many people are ahead of them, with nothing to install. The hard part was keeping token numbers gap-free and unique when everyone scans at once. Problem: - In a Sri Lankan dispensary a patient takes a paper number and has no idea whether the queue is at 12 or at 40, so they either wait for hours or miss their turn. - Thirty patients can scan the QR code in the same second while staff issue a walk-in at the counter, and every number must still be unique and in order. - Two staff tablets pressing "call next" at the same moment must call exactly one patient. Architecture: - NestJS 10 API with Prisma 5 and PostgreSQL 16; a Next.js 14 web app with four surfaces (TV display, patient page, staff console, owner dashboard); and an Expo (React Native) patient app. - Token numbers come from one INSERT … ON CONFLICT DO UPDATE … RETURNING on the per-day counter row, which takes the lock. A UNIQUE constraint on (queue, date, number) is the backstop, and the capacity check runs in the same transaction, so a rejected token never leaves a gap. - "Call next" locks the queue row with FOR NO KEY UPDATE (plain FOR UPDATE blocked token inserts on their foreign-key check), then runs a conditional UPDATE … WHERE status = 'WAITING'. The idempotency key is written in the same transaction, so a double-tap replays the first response. - Public screens listen over Server-Sent Events through Redis pub/sub. The staff console uses Socket.io with a Redis adapter, a JWT handshake and acknowledged commands. - Web Push (VAPID) with a hand-written service worker, Expo push for the app, and an installable PWA with an offline fallback page for the token screen. - Token events carry no personal data and go to Pub/Sub after commit. In Terraform they reach BigQuery through a BigQuery subscription; the emulator can't do that, so locally a small loader stands in. The GCP Terraform (Cloud Run, Cloud SQL on a private IP, Secret Manager, Workload Identity Federation) is plan-tested and has never been applied. Results: - k6 morning rush on my laptop (300 patients in 60 s, 5 counters): no gaps, no duplicates and nobody called twice across about 5,400 tokens. - Take-a-number p95 of 71–91 ms after moving issuance to the counter row only, against 91–132 ms before, in interleaved runs. The machine's load moved p95 more than the code did; p99 dropped in every paired round. - The load test caught a regression I introduced. I stopped awaiting the display broadcast to save latency, which removed hidden back-pressure: the database pool ran dry, p95 went from 377 ms to 7.6 s and some requests failed. A per-queue coalescer fixed it, and I wrote a blameless postmortem. - The GCP Terraform has 8 plan-time tests on mocked providers: no public IP on Cloud SQL, no owner or editor roles, and a budget. - Integration tests run against real Postgres and Redis, including a real Socket.io server and a second API instance. ### ApplyWise: CV Tailoring Where Every Line Cites a Fact - Page: https://iamjanindu.com/projects/applywise - Showcase: https://github.com/jani358/applywise-showcase. Full source available on request. - Context: Personal project · pre-launch - Tech: TypeScript, Next.js 15, NestJS 11, Prisma, PostgreSQL, pgvector, Redis, BullMQ, Go, LLM evals, Anthropic / Gemini / Ollama adapters, OIDC, Chrome Extension, Terraform, AWS (LocalStack), Playwright, Jest A web app, not launched yet, that breaks a CV into small editable facts, matches them to a job description, and writes a tailored CV and cover letter where each line cites the facts behind it. A verifier (rules first, then an LLM judge) strips anything unsupported. There is also a free ATS checker, an application tracker and a Chrome extension. Problem: - Tailoring a CV by hand takes me 30 to 60 minutes per job, and generic AI tools happily add skills the candidate doesn't have. - Applicant tracking systems quietly drop CVs that use columns, tables or images. - A CV is personal data, so it has to be encrypted, exportable and deletable. Architecture: - Next.js 15 web app, a NestJS 11 API with Prisma 6, and a Go service that parses PDFs, produces the ATS report and renders an ATS-safe single-column PDF. - Clean architecture by convention: the domain classes (fact extractor, match scorer, hybrid retriever, generator, verifier) import nothing from Nest or Prisma, and controllers and repositories are thin adapters. - One LLM interface with Anthropic, Gemini and Ollama adapters plus a deterministic fake. The real adapters are unit-tested against mocks and haven't been run against a live model yet. Structured outputs, a versioned prompt registry with fingerprints, and a per-user cost ledger with a monthly budget. - Hybrid retrieval with pgvector (HNSW) and Postgres full-text search, fused with Reciprocal Rank Fusion. The vectors come from a deterministic 512-dimension hashing embedder, so there is no embedding vendor to pay. - Personal data is encrypted with AES-256-GCM before it reaches Postgres, emails are looked up through an HMAC blind index, and there are export and delete endpoints. Google sign-in uses OIDC with PKCE, state and nonce. - A BullMQ worker classifies application emails you upload or paste (.eml) and moves the card on the kanban tracker. There is no inbox integration. Plans and usage metering sit behind a payment port (only a fake provider so far), and quota is reserved under a Postgres advisory lock. - Terraform for AWS in ap-south-1: eight modules (KMS, S3, SQS, Secrets Manager, IAM, ECR, network, ECS Fargate + RDS), with least-privilege IAM checked by terraform tests. Results: - An eval gate of 30 golden CV and job-description pairs runs in CI and fails the build if 1% or more of shipped lines are fabricated. It runs on a deterministic fake model that plants three fabrications in every pair. - Latest report: the verifier removed all 90 planted claims, and 0 of 570 shipped claims were fabricated. A run on a real model comes next, once I have a paid API key. - Six parallel tailorings on a free plan give exactly 3 successes and 3 HTTP 402s, on real Postgres. - S3, SQS, Secrets Manager, KMS and IAM were applied, checked and destroyed on LocalStack. ECS, ECR, RDS and the VPC are planned only. - Go tests write golden JSON that a shared zod schema checks on the TypeScript side, and the API validates every live parser response. ### iGaming Platform: Go Microservices, Payments and Compliance - Page: https://iamjanindu.com/projects/igaming-platform - Source: not public (Production · code under NDA) - Context: Team project at BotCalm · 2024 – present · under NDA - Tech: Go, Next.js, React, TypeScript, WebSockets, Kafka, PostgreSQL, Redis, TanStack Query, Zustand, PWA, Node.js, Docker, Kubernetes (K3s), ArgoCD, GitHub Actions, AWS A real-money gaming platform with live wallets, crypto and card deposits, withdrawals, bonuses and an operator admin panel. I'm a core member of the team moving it from a Node.js/MongoDB monolith to 20 Go services behind an API gateway. My own pieces are the notification/WebSocket service, the NOWPayments crypto integration, the vault service, the Vesant and GeoComply compliance gates, and the support chat. Problem: - Balances, notifications and game state have to update live for players connected to different pods, and a balance must never be credited twice. - The old backend was a Node.js/Express/MongoDB monolith. Rebuilding it on Go and Postgres meant moving live data (12,299 users and about 584K transaction records) without losing withdrawals that were still in flight. - Money movement is regulated. Deposits, withdrawals and logins need location, identity and anti-money-laundering checks, on the crypto rail and the card rail alike. Architecture: - The platform (team-built): 20 Go services behind one API gateway with JWT auth, a circuit breaker and rate limits. PostgreSQL is the system of record, Redis holds balances, locks and pub/sub, and Kafka connects the services. - Mine: the notification service. Kafka in, WebSocket out, with Redis pub/sub so a message reaches whichever pod holds the player's socket, plus scheduled and broadcast notifications and FCM push. The client pings every 25 s because Cloudflare closes idle WebSockets after about 100 s. - Mine: the NOWPayments crypto integration in the early payment service. IPN webhooks are checked with HMAC-SHA512 over the key-sorted body, and a retry job picks up any webhook step that failed instead of dropping it. - Mine: the vault service, where players lock coins away. A per-user Redis lock (10 s TTL) stops two withdrawals from both passing the balance check. - Mine: the compliance gates. I put geolocation and KYC behind Go interfaces so we could switch from GeoComply to Vesant with an env var, then built shared KYC and face re-check gates used by both payment rails, and AML decisions that freeze the account. Every gate runs before a row is written or a balance moves. - Mine: an in-house support chat that replaced Intercom. A partial unique index allows one open conversation per player, client message ids stop retries from duplicating, and replies go out over the same WebSocket fan-out. - Player app in Next.js 16 with next-intl (21 locales), TanStack Query, Zustand and PWA support; admin panel in Next.js 15 with granular RBAC. Deploys go through GitHub Actions and ArgoCD to K3s on AWS. Results: - QA logged 13 compliance bugs against the Vesant rollout. I worked through them on both payment rails. - Fixed security holes in the gateway and the WebSocket layer, including client-IP spoofing and connecting as another user. - Fixed an admin dashboard that under-reported deposits because it counted only one of the two payment rails. - About 410 of my commits and 87 test files in the Go backend. The platform has no formal load test yet, so I don't quote concurrency numbers. ## Other work - Nexora: Contact Centre CRM & Analytics (Production client work): Contact centre platform in production for a client, with schema-driven call logging, supervisor analytics, full-text search and three roles (Admin, Supervisor, Agent). Call categories and their fields are stored as data. A field's key can't change once created, and records are deactivated rather than deleted, so old calls still read correctly in an audit. Tech: NestJS, Next.js 14, PostgreSQL, Prisma, Redis. Link: https://github.com/jani358/nexora-callcenter-showcase - Offline Retail POS System (Client product): Offline desktop POS running in a real shop. The barcode scanner is read as a keyboard and told apart from typing by keystroke timing. It handles weighing-scale barcodes, keeps money in integer cents and prints receipts through the Windows driver. SQLite runs in WAL mode, and a backup is taken after a bill (at most once a minute) and checked by reopening it. USB mirroring is optional. Licensing is tied to the machine. Tech: Electron, React, Node.js, SQLite. - GC Property: Real Estate Listings (Client work): Property listings site for Sri Lanka (gcproperty.lk). Filters live in the URL path, so "3 bedrooms in Colombo" is a real page that can rank. Listings come from a Strapi CMS on PostgreSQL, with media on S3-compatible storage. Tech: Next.js, React, Strapi, PostgreSQL. - Crypto Trading Bot (Personal project): Personal Go bot on Binance USDT-M futures. Every hour it rebuilds a six-coin watchlist from volume and volatility. DeepSeek returns a strict-JSON trade decision, Gemini adds a news-sentiment read, and about fifteen Go risk gates decide whether the order is allowed. One position at a time, with Telegram alerts and a small dashboard. Tech: Go, Binance API, DeepSeek, Gemini, MongoDB. - Hotel & Travel Sites (Client work): Two client sites. The hotel site is a React SPA whose reservation flow posts to the hotel's own booking API. The travel agency site is a static Next.js build with no third-party runtime dependencies. Tech: React, Next.js, TypeScript, Tailwind CSS. ## Contact Email janindugayanga10@gmail.com, or use the contact form at https://iamjanindu.com/#contact. Open to full-time Software Engineer and Senior Software Engineer roles, remote or in Colombo, and to freelance projects.