Projects

The work behind the job titles

1club

Founding Engineer · Jan 2026 - Present

1club is an AI-native platform for gyms, studios and sports clubs: one system for the people who run a club and for its members, from the schedule and the front desk to payments, the door and the club’s own website.

I joined as a founding engineer, and the work covers the whole product - the API and the data model behind it, the admin and member apps, the AI assistants and the MCP server, the Google Cloud platform it runs on, and the bridges to on-site hardware that has no cloud API. Much of it is built with AI coding agents in the loop, so the codebase is kept legible to them as well as to people: a domain glossary, decision records, and architecture tests that fail CI when a convention is broken.

Club-management platform

Sports & fitness

Staff run their club from an admin app, members use a mobile app, a member portal and the club’s own website, and the front desk checks people in on tablets and kiosks. Behind all of them is one TypeScript API on Express and Prisma over PostgreSQL, organised by business domain. The tenant is the organization, and a club is one of its locations. Every Prisma query passes through a client extension that adds the active organization from the request context or throws, so a cross-tenant read has to opt out explicitly. Authorization is by permission - module, action and scope, from own records to a club to the whole organization - and roles are only default bundles that an organization can change.

A club sells memberships, passes and drop-ins from one catalogue. Memberships pause, resume and carry a minimum term stamped at sale, and required documents are snapshotted when they are signed, so a later edit never changes an agreement. Recurring classes become individual occurrences: a bounded series in full, an open-ended one on a rolling window that a nightly job extends. A booking is written as a hold before any money moves, so every policy and conflict check runs first. Cancellation terms are bands of notice hours, and the band decides the refund, whether the session goes back on the membership and how much of the instructor’s pay is kept. Waitlists either fill a freed seat automatically or open a 15-minute claim window, instructor pay accrues to a ledger, and every check-in records where it came from - the front desk, a kiosk, the app, an assistant - with duplicates detected per booking or visit rather than per day.

Members pay the club, never 1club. Card payments are Stripe Connect direct charges on the club’s own account, next to card terminals, myPOS and DatecsPay hosted checkouts, cash, and a wallet kept as an append-only ledger. An organization can trade through several companies: each sale is stamped with the one that sells it, each company numbers its own invoices, and a basket that would span two is refused rather than split. A per-payment database lock makes sure at most one attempt ever settles, whichever payment method it came through. Renewals are charged a few days ahead on the saved card; a failure is flagged to staff, and saving a new card retries only the attempts known to have moved no money. 1club bills the clubs through Stripe as well, with a plan per club and a member allowance pooled across the organization, and Stripe is the source of truth: the subscription is read back on every webhook, every change made in the app and a nightly drift check, so an edit made in the Stripe dashboard is recorded instead of overwritten.

Domain writes add events to an outbox in the same transaction, and a processor claims them with SKIP LOCKED and hands them to automations, notifications, member tagging and outgoing webhooks. Notifications go out by email, by SMS from the club’s own number, over WhatsApp and as web and mobile push, under per-category preferences. About fifty scheduled jobs run inside every API replica, and a lease table in a separate PostgreSQL database makes each run happen once, with heartbeat renewal and a CI test that fails if a job skips the lock. Schema changes follow expand-contract: a destructive migration fails CI unless it states why, and migrations run as their own job before the new release takes traffic.

  • TypeScript
  • Node.js
  • Express
  • Prisma
  • PostgreSQL
  • Valkey
  • Zod
  • Passport
  • Stripe Connect
  • Stripe Billing
  • Stripe Terminal
  • myPOS
  • BORICA
  • SendGrid
  • Twilio
  • WhatsApp Cloud API
  • Web Push
  • Cloudinary
  • React
  • Vite
  • MUI
  • TanStack Query
  • Vitest

AI assistants & MCP server

Staff can ask one admin assistant to do most of what the admin app does. It runs its own bounded tool-calling loop on OpenAI’s Responses API: up to twelve rounds, a forced text answer if the rounds run out, and a guard that stops the model from repeating a call that already failed with the same arguments. Its tools are the platform’s own operations - about two hundred of them, defined once and shared with the public API and the MCP server - so the assistant goes through the same validation and permission checks as every other client. Thirty core operations are bound directly and the rest are found through a capability search, which keeps the tool list under the model’s limit. A tool is offered only when the user’s permissions cover it, and destructive operations are held instead of run: the model is told nothing has changed, and only a Confirm button replays the exact stored call, with permissions checked again. This one assistant replaced a router that sent each message to one of five specialist assistants and broke down on requests that crossed two of them.

The other assistants are narrower. The website builder edits pages, sections and themes through its own tools, with undo. A member assistant on the portal and on club websites answers questions and can book or cancel for a signed-in member, with every tool bound to that one organization, and a marketplace concierge works across clubs only through the marketplace’s visibility rules. Elsewhere AI is a single structured call: the import assistant reads a CSV’s headers and five sample rows and proposes the entity type and field mapping, after which the import runs as an ordinary preview and execute; invoices and documents are scanned into drafts; and replies to member messages are drafted in a tone the club chooses.

The MCP server gives outside assistants such as Claude and ChatGPT the same operations. The admin endpoint signs in with OAuth 2.1 - dynamic client registration, PKCE, a consent screen where the user picks the organization, and hashed, rotating tokens - or with a platform API key, and scopes every request to that organization. Its scopes are generated from the operations, and tests check that the two stay in step. An anonymous, read-only discovery endpoint searches the marketplace and renders results as MCP Apps widgets. A report-theme tool gives assistants 1club’s palette and a self-contained HTML template, so a report drawn in a Claude artifact or a ChatGPT canvas looks like 1club’s own. The team and customers use it to chart their data and to find wrong or false payments, and moving a new club’s data across from its old systems now takes an hour or two instead of about two days of staff time.

  • TypeScript
  • OpenAI API
  • Model Context Protocol (MCP)
  • MCP Apps
  • OAuth 2.1
  • JSON Schema
  • Zod
  • Server-Sent Events
  • Vercel
  • Vitest

Member apps & check-in

Members use an Expo app on iOS and Android to find clubs on a map, book classes and spaces, buy memberships and passes, sign documents, message the club and show a QR code at the desk. The app has no login form of its own: it opens the central auth app in a browser session, keeps the long-lived refresh token in the Keychain or Keystore, and detects a reinstall on iOS, where the Keychain survives, to force a fresh login. Every booking, paid or covered by a membership, goes through one order endpoint. Cards are taken with Stripe’s payment sheet, with Apple Pay and Google Pay, on the club’s own account; for hosted checkouts the app polls the order rather than trusting the redirect back, so an unconfirmed payment shows as pending, never failed. Push notifications go through Expo, and tapping one can switch organization before opening the message or the waitlist spot it points to. For clubs on Salto KS, a native module wraps Salto’s SDK so the member’s digital key is created on the phone.

The member portal runs per organization, on its own subdomain or the club’s domain, resolved from the host. Every product signs in through one auth app - email first, then a password, a phone code, or Google, Facebook or Apple - which authenticates but never owns a session. It hands the product a single-use code that is valid for sixty seconds, and the product’s server redeems it for first-party HttpOnly cookies, so no token ever travels in a URL.

At the front desk, a tablet runs the check-in app: a Next.js app on the club’s own host inside an Expo shell that keeps the screen awake, follows the device’s orientation and shows a native offline state. A club chooses between class and open-gym check-in and the order of identification methods - QR code, PIN or name search. QR scanning uses the browser’s BarcodeDetector and falls back to ZXing on iOS, and the text colour adapts to the brightness of the club’s background photo. Kiosks add card readers that type like a keyboard, and Web NFC. Both resolve first and commit second: identification returns what the check-in would attach to - a booking in the next hour, a membership or a paid walk-in - or sends the case to staff, and confirmation checks everything again before writing it. Mobile releases go through EAS, and a fingerprint diff on each pull request decides whether a change can ship over the air or needs a new store build.

  • React Native
  • Expo
  • Expo Router
  • EAS
  • TanStack Query
  • Stripe
  • Mapbox
  • Next.js
  • React
  • Tailwind CSS
  • Radix UI
  • ZXing
  • Web NFC
  • Sentry
  • Vercel

Club websites & 1club.me marketplace

Each club can publish its own website from the admin app. A site is a set of pages made of sections - hero, plans, classes, booking, FAQ, gallery and about twenty more - stored as a small DSL. Every field of every section is described once, in one schema, and that schema drives the visual editor, the public API and the website assistant. Publishing snapshots the draft into a versioned release that can be amended or rolled back, and clears the site’s cache. One multi-tenant Next.js app serves every site: it resolves the organization from the host, redirects platform hosts to the club’s verified domain, returns a real, non-indexed 404 for a host with no site, and shows an unbranded holding page when a site’s add-on lapses. Custom domains are attached to the Vercel project through its API, with DNS instructions and verification handled in the admin app. Every site also publishes files for AI agents: llms.txt, an agent discovery document, an MCP server card and its current availability.

1club.me is the public marketplace: search clubs by sport and city on a map, look at their classes, events and plans, and book. Results come in two layers - clubs on 1club, and places discovered from OpenStreetMap and Overture data that their owners can later claim. Availability search first filters in SQL by what each club offers, then checks live availability for a bounded set of clubs under a two-second deadline, with a cache and a kill switch. A marketplace booking makes the person a member of the club, takes the organization from the booked class, and leaves payment to the venue; the marketplace itself never charges a card. Directory pages for each sport, each city and each combination, with slugs fixed per language, carry the search traffic.

  • Next.js
  • React
  • next-intl
  • Tailwind CSS
  • Mapbox GL JS
  • PostgreSQL
  • OpenStreetMap
  • Overture Maps
  • Vercel
  • TypeScript

Google Cloud platform & delivery

1club moved from Heroku to Google Cloud in July 2026, onto infrastructure defined in Terraform: an admin project for state, the container registry and identity federation, separate staging and production projects with their own networks, and thin local modules around the official Google ones, split into small roots that each keep their own state. Organization policies forbid service-account keys, public database addresses and external VM addresses. The API runs on Cloud Run with direct VPC egress, tuned for WebSockets: high concurrency, so open sockets do not force new instances, and a request timeout that limits how long an old revision can hold them. PostgreSQL is Cloud SQL on a private address with regional high availability and point-in-time recovery, and Valkey holds sessions, rate limits and the cache. Cloudflare provides DNS, the firewall and rate limiting in front of a global load balancer, Turnstile protects signup and the public assistant, and the thirteen frontend projects on Vercel are managed in Terraform too.

Every pull request passes formatting, linting, typechecks of the affected workspaces and the architecture tests. A merge builds the API image once, runs migrations as a Cloud Run job, deploys staging and tags the commit with the image digest. Production is a gated promotion of that exact digest, with no rebuild, and a rollback is the promotion of an older commit’s image. GitHub Actions deploys through Workload Identity Federation without stored keys, and the Vercel frontends are built from the same commit, with Sentry releases tied to it.

The database moved as a dump loaded into Cloud SQL from Cloud Storage, rehearsed on staging first. Cloudflare went in front of Heroku weeks earlier, so the cutover itself was a maintenance window, a final load and a DNS change, and rolling back meant reverting the record. Logs are structured Pino JSON in Cloud Logging, linked to request traces, with credentials and personal data redacted. Nineteen uptime checks run from three continents and alert when two regions fail, and a weekly workflow renews the wildcard certificates through DNS challenges in Cloudflare, added after two expired certificates caused outages. The public status page runs on Vercel, reads the uptime checks through federated identity, and keeps its history outside the environment it reports on.

  • Google Cloud
  • Terraform
  • Cloud Run
  • Cloud SQL for PostgreSQL
  • Memorystore for Valkey
  • VPC
  • Cloud Load Balancing
  • Secret Manager
  • Artifact Registry
  • Workload Identity Federation
  • Cloud Monitoring
  • Cloud Logging
  • Cloudflare
  • Vercel
  • GitHub Actions
  • Docker
  • Pino
  • Sentry
  • WireGuard

Door access & Access Bridge

Clubs open their doors through access-control systems that 1club does not own, and many of them sit on the club’s private network with no cloud API. The platform turns bookings and check-ins into access grants with PostgreSQL triggers and pushes them to the club’s system: a dispatcher sends each change within a second, and every five minutes a full roster sync reconciles everything the platform manages.

The Access Bridge is a small program installed on the club’s network that turns one such system into a standard HTTP API. Its OpenAPI 3.1 contract is vendor-neutral, designed against two systems that disagree: clients branch on advertised capabilities rather than versions, unsupported operations and fields are refused instead of silently dropped, and assumptions not yet proven on real hardware are published as unverified. An unlock needs an idempotency key, recorded before the vendor is called, so a crash leaves an in-flight record instead of opening the door twice. Bulk roster sync is paced for vendors that allow a single API session, runs as an asynchronous task, and refuses to remove anyone unless the caller states that the roster is complete, so one page of results never makes almost everyone look absent; memberships the bridge did not create are never touched. A TypeScript SDK, a Go simulator with a deliberately constrained profile, and contract tests that drive the real simulator binary through the SDK keep both sides of the contract in step. The first integration, in Go, talks to Impro Access Portal and ships as a static binary that runs as a systemd or Windows service with its own API key, shown once.

Salto KS is a cloud system, so the platform connects to it directly: each area becomes a Salto access group, and the platform keeps its members current. For the Salto digital key on members’ phones, Salto’s identity server federates sign-in to a small OpenID Connect provider in 1club’s auth app - authorization code with PKCE, RS256 ID tokens with published, rotating keys, and only verified email addresses asserted - so the key is created on the phone and 1club never holds key material.

  • Go
  • TypeScript
  • OpenAPI 3.1
  • Redocly
  • Vitest
  • Ajv
  • Docker
  • systemd
  • GitHub Actions
  • GitHub Packages
  • Impro Access Portal
  • Salto KS
  • OpenID Connect
  • PostgreSQL

Payment Bridge

In Bulgaria a club takes cards on a payment terminal and prints receipts on a certified fiscal device, and neither has a cloud API. The Payment Bridge is the on-site half of card-present payments for them: a Kotlin core with a driver per device, shipped as an Android app that runs on the Datecs payment terminal itself and, later, a desktop app on the reception PC for external pinpads and fiscal printers. Drivers receive byte streams from the app, so the same driver serves both.

The bridge only connects outwards, so a club opens no ports. It pairs with a one-time code and then long-polls the API for commands, chosen over WebSockets because the API runs on many Cloud Run instances with a command queue in PostgreSQL. It keeps no state of its own: the platform resends open commands, and the terminal’s transaction log or the printer’s last receipt number is the authority. The rules are written around money: an unclear outcome is unknown, never declined; the card is only touched after the platform accepts the command, so a cancel at the till cannot race a payment; every charge says whether it must be fiscalized; and no card data leaves the device except a masked number and references. The core, the drivers and the apps run against simulators and byte-level fakes of the devices, and the platform side and bench tests on real hardware are in progress.

  • Kotlin
  • Android
  • Gradle
  • Ktor
  • Kotlin Coroutines
  • jSerialComm
  • JUnit 5
  • OpenAPI
  • Datecs
  • BORICA
  • GitHub Actions

Campaign landing pages

Marketing

Paid campaigns get their own landing pages, set up so that a new one can ship without touching the others. Each campaign is one folder - configuration, page, English and Bulgarian copy, and assets - and becomes one static Astro site, one Vercel project and one subdomain. A build bundles only the campaign named in its environment, so nothing leaks between pages. One command scaffolds a campaign from a template and starts a preview, and Claude Code commands build the page from a Figma frame through the Figma MCP server and keep it in sync with the design, under rules that copy arrives in English, is translated into natural Bulgarian, and never invents claims or prices.

When a new campaign lands on main, a provisioning script creates its Vercel project, points the build at that campaign, attaches the subdomain with a redirecting alias so only one host is indexed, and triggers the first deploy; it can run any number of times and never deletes anything. Deploys are selective: a script diffs the push and builds only the campaigns whose files changed, or all of them when shared code did, and Vercel applies the same rule on its side. The root page sends visitors to their browser’s language, calls to action carry UTM tags for the campaign and the placement, and the Google Ads, Meta, Microsoft Clarity and OpenAI conversion tags wait for consent from the HubSpot cookie banner before they load.

  • Astro
  • TypeScript
  • Tailwind CSS
  • React
  • Vercel
  • GitHub Actions
  • Claude Code
  • Figma MCP
  • HubSpot
  • Google Ads
  • Meta Pixel
  • Microsoft Clarity

OfficeRnD

Senior Platform Engineer · 2023 - Jan 2026

Somewhere in the DIV Motion years I ran out of steam. It was a mix of things: working from home all the time, sleepless nights, too many projects at once, managing people, and sitting somewhere between a staff engineer, a consultant and an engineering manager. I think it was a burnout, and what I wanted was to join a startup and build something significant for the long run - not advise on it for a few months and move on.

Around the same time, a friend of mine, one of OfficeRnD’s co-founders, told me they were putting together a small, senior team to take on some serious problems and get the platform, and the company, to the next level. I was in.

OfficeRnD makes software for running office spaces, and it had been built the old-fashioned way: one large monolith, hard to scale and hard to upgrade, with everything self-managed inside it and microservices out of reach. Earlier infrastructure work had added complexity rather than removed it, and the company was heading for the point where almost all engineering time would go on maintenance and firefighting instead of new functionality.

For quite a while it was just me, the technical co-founder and the CTO, rebuilding the platform - this time properly: more modern, more robust, and with less maintenance and overhead for every team that came after. That meant moving the platform off EC2 onto serverless, pulling common pieces of the monolith out into core libraries, cookie-cutter templates for new repositories, and formatting and linting standards for backend projects.

Scalable serverless infrastructure

Property management

OfficeRnD’s product had grown up on EC2, with MongoDB self-managed as a replica set on the same kind of machines, while engineering pushed hard on features. Every upgrade was a small project, the on-call rotation covered infrastructure nobody on the team had chosen, and there was no single way for a team to stand up a new service. The replacement is a shared cloud platform on AWS that every team moves its services onto.

The database went first. MongoDB moved onto Atlas without a maintenance window: the private network was built in Terraform before any data moved, and mongomirror kept the new cluster in sync underneath the running application until the cutover was a change of connection string. Engineers chasing bugs that live in the data get a throwaway cluster restored from the nightly backup, with customer data stripped out before anyone can connect, instead of access to production.

Services run on ECS, assembled from reusable Terraform modules for services and workers, Atlas clusters and their networking, and SNS topics and SQS queues. Rather than every repository pinning its own copy of those modules, AWS Proton holds versioned service templates, and a Port catalog in front of it turns them into self-service: a team fills in one form, GitHub Actions creates the repository from a cookie-cutter template, and Proton renders the infrastructure into a pull request. Database clusters and backup policies go through the same catalog.

Background work moved off BullMQ onto EventBridge with SNS and SQS fan-out. Producers publish events instead of naming the job that consumes them, every topic is FIFO, and delivery spans Ireland, Singapore and Sydney. Web servers and job processors run as separate container types, and a shared client library handles batching, draining and graceful shutdown.

  • AWS
  • Amazon ECS
  • Terraform
  • AWS Proton
  • Port
  • GitHub Actions
  • MongoDB Atlas
  • mongomirror
  • AWS PrivateLink
  • Cookiecutter
  • EventBridge
  • Amazon SNS
  • Amazon SQS
  • ElastiCache
  • OpenSearch
  • Elastic Stack
  • Grafana
  • TypeScript
  • Bash

Accounts & billing service

The accounts service is the single answer to who an OfficeRnD customer is: the account with its company details, region, status and subscription, and the locations underneath it. Other services read and change accounts through a shared SDK, and every create, update and delete goes out as an event, so any product can react to an account change without polling for it.

Billing is the interesting part, because OfficeRnD bills its own customers with its own product. Each account is mirrored as a team in OfficeRnD’s internal Flex organisation, with a membership that carries the plan - flexible, fixed or tiered pricing per member, with discounts and minimums - so invoices, VAT and payments run through the same billing engine OfficeRnD sells.

That is what makes self-service checkout possible. A trial customer picks a plan, a monthly or annual term and any allowed add-ons in a standalone React checkout. The service prices the cart with VAT, authorises the card through Stripe, records the signed terms, creates the new memberships in Flex and ends the trial ones, then queues a job that activates the account in its product. Checkout events go through Segment to HubSpot for the sales team, while the rest of the tracking follows each user’s tracking preferences.

A scheduled lifecycle job keeps statuses honest: when a trial’s membership ends the account is suspended, after a configured period it is archived, and after another it is deleted. Company, billing and bank details are edited in an account-settings micro-frontend that loads inside the OfficeRnD app shell through single-spa.

  • TypeScript
  • Node.js
  • NestJS
  • MongoDB
  • Mongoose
  • Redis
  • Bull
  • Amazon SNS
  • Amazon S3
  • Stripe
  • Segment
  • HubSpot
  • Pendo
  • React
  • MUI
  • Vite
  • TanStack Query
  • React Router
  • single-spa
  • Jest
  • Vitest
  • Playwright

Activity log

The activity service is OfficeRnD’s audit log: who changed what, and when, across the products. Whenever a product changes a record, it publishes an activity describing the operation, the record and the location it belongs to, the member and team it concerns, the user who made the change, and the data before and after it.

Products publish through a shared client library onto the platform’s event topic, and the service consumes those events, validates them and stores them in MongoDB. A duplicate delivery is rejected by the database instead of being written twice. Every activity carries an expiry date, so a TTL index clears it after 180 days by default, and compound indexes serve the common views - everything on an account, or for one location, team or member - newest first, with cursor-based pagination. When an account is deleted its activities are purged, and the account is remembered for an hour so late events are dropped instead of bringing it back.

The log itself is a React micro-frontend in account settings, loaded into the OfficeRnD app shell through single-spa. It filters by location, team, member, operation and the kind of record that changed, using friendly names the service keeps for each product. An activity’s detail page shows its metadata next to a git-style diff of the change, with both versions rendered as key-sorted YAML.

  • TypeScript
  • Node.js
  • NestJS
  • MongoDB
  • Mongoose
  • Amazon SNS
  • Amazon SQS
  • React
  • Vite
  • TanStack Query
  • React Router
  • single-spa
  • react-diff-view
  • YAML
  • Jest
  • Vitest
  • Playwright

Identity & OAuth server

The identity server is how everything at OfficeRnD proves who it is. It is the OAuth 2.0 and OpenID Connect provider behind the OfficeRnD APIs, the mobile apps and third-party integrations: people sign in and give consent here, applications receive their tokens here, and every other service asks it whether a bearer token is still valid and what it is allowed to do. It has been running since 2019.

It issues tokens through the authorization-code, client-credentials and refresh-token grants, and runs an OpenID Connect provider on a MongoDB-backed store. Refresh tokens rotate on every use, but a used one is not revoked on the spot: it stays valid for fifteen more minutes, and a second refresh inside that window gets back the access token that was already issued, so two tabs or devices refreshing at the same moment do not sign the user out. Permissions come from a catalog of more than 180 scopes - from officernd.api.read down to ones as specific as flex.billing.payments.create - split into legacy, application and internal groups, and CI publishes the catalog to Port. Clients, with their grants and redirect URIs, are managed through an API of their own, and a sample client app shows third parties how to build against it.

Two flows sit on top of the standard ones. An integration flow lets a signed-in user connect a third-party app to their organisation, and to specific locations within it, through the usual consent screen, with an endpoint to revoke that token later. A mobile flow turns the session a user already has in the OfficeRnD app into an OpenID Connect sign-in. Logins are protected by rate limiting and a Redis-backed brute-force guard, sessions live in Redis, and the server runs as a container on ECS, deployed through staging to production by GitHub Actions.

  • TypeScript
  • Node.js
  • Express
  • InversifyJS
  • OAuth 2.0
  • OpenID Connect
  • oauth2-server
  • oidc-provider
  • Passport
  • MongoDB
  • Mongoose
  • Redis
  • Nunjucks
  • Grunt
  • Jasmine
  • Mocha
  • Docker
  • Amazon ECS
  • GitHub Actions
  • Port
  • New Relic

Notifications service

The notifications service sends the emails and text messages that OfficeRnD’s products send - Flex, Hybrid, Visitor Management and others - and keeps a record of what happened to each one. Products call it through a shared client library, either directly or by queueing a request for a worker to deliver, and customers see the outcome in an email-activity view inside the product.

Email goes out through Mailgun. A customer can send from its own domain: the service registers the domain with Mailgun, hands back the DNS records to add and verifies them, and until the domain checks out, mail goes from the OfficeRnD system domain instead. Each account can override the layouts that wrap its admin, member and system emails, with defaults for each product, and emails are grouped by category and campaign so deliveries and opens can be reported per campaign. Large contents and attachments are kept in private S3 storage. A queued request is retried in-process on timeouts, throttling and server errors, then rescheduled for later if the provider is still failing.

Delivery history comes from Mailgun’s event log. For each message and recipient the service assembles a timeline - sent, delivered, opened, clicked, failed, resent - and exposes the bounce, unsubscribe and complaint suppressions behind a failure, which can be cleared on a customer’s own domain before the original message is sent again. SMS goes out through Amazon SNS, only to valid numbers in supported countries, each with its own sender ID or originator number. The email-activity micro-frontend lists what was sent and, for each email, shows its content and delivery timeline with the resend and suppression controls. The API runs on App Runner and the worker on ECS.

  • TypeScript
  • Node.js
  • NestJS
  • MongoDB
  • Mongoose
  • Redis
  • Mailgun
  • Amazon SNS
  • Amazon SQS
  • Amazon S3
  • AWS App Runner
  • Amazon ECS
  • React
  • Vite
  • TanStack Query
  • React Router
  • single-spa
  • Jest
  • Vitest
  • Playwright

Core libraries & service templates

As the monolith was broken into services, the pieces every service needs were pulled out into shared libraries instead of being copied from one repository to the next. At the centre is server-core-lib, the NestJS library most OfficeRnD backends are built on - accounts, activity, notifications, the public Flex API and the product backends among them - with starter templates and an ESLint plugin that make the same conventions the default for every new service.

The library covers the plumbing: configuration loaders, MongoDB and Redis connections, logging, request and tenant context, caching, health checks and graceful shutdown, error filters and a response-sanitising interceptor, and public and private S3 storage with presigned URLs. Data-access base classes scope every query to the current product and account, so tenant isolation is not left to each query, and add pagination on top. A single auth guard accepts OAuth client tokens, product sessions and JWT integrations, and signed webhooks, and an applications module handles OAuth connect flows and permission sets.

Its event SDK is how the services talk to each other. The publisher batches events to SNS - flushing when a batch fills, on a timer, or on shutdown - moves anything over SNS’s 256 KB limit to S3 and publishes a pointer instead, supports FIFO topics where ordering matters, and schedules one-off future events through EventBridge Scheduler. The consumer polls SQS, dispatches each message to handlers registered with a decorator, and sends failures to a dead-letter queue. Both have an in-memory local mode, so a service can run its whole event flow on a laptop without AWS, and a session manager hands developers temporary AWS credentials when they need the real thing.

New services start from Cookiecutter templates that Port’s create-service action runs: a NestJS backend that boots as an API or a worker with the library already wired in, an example tenant-scoped module, permission sets, Docker and CI/CD; a standalone React app; and a micro-frontend that registers itself with the app shell. The ESLint plugin enforces the conventions the libraries depend on, such as required decorators on controllers, public methods and response DTOs, and the library itself is versioned with release-please and published to GitHub Packages.

  • TypeScript
  • Node.js
  • NestJS
  • MongoDB
  • Mongoose
  • Redis
  • Amazon SNS
  • Amazon SQS
  • Amazon S3
  • EventBridge Scheduler
  • Winston
  • Jest
  • Cookiecutter
  • Port
  • ESLint
  • Prettier
  • React
  • Vite
  • single-spa
  • GitHub Actions
  • GitHub Packages
  • release-please

DIV Motion

Co-Founder and Principal Consultant · 2021 - 2023

At some point, I got tired of the enterprice culture, the many politics there and felt the need for a change. DIV Motion was my own company. We worked with CTOs and technology directors at small and mid-sized companies: leading the discovery phase, architecting the solution that came out of it, then building it.

Ticombo

Entertainment & events

Ticombo is a secondary ticketing marketplace that has been running since 2017. Individual fans and commercial brokers list tickets for events, buyers pay Ticombo, and Ticombo holds the money until the tickets have demonstrably arrived and the event is over - handling delivery, disputes and the seller’s payout in between. Inventory also flows in from partner brokers such as Tixstock and from third parties through a public API, and the site is translated into around thirty languages.

A new listing is approved automatically when the seller is trusted or has direct-approval rights, the event and its category are approved, and the event does not require proof of delivery; everything else waits for an admin. Buyers either pay the listed price or make an offer: the amount is held on their card, the seller accepts or declines, and accepting captures the payment and releases competing offers that can no longer be filled, while unanswered offers expire. At checkout the tickets are reserved - partner inventory gets a twelve-minute hold on the partner’s side - the card is charged through Stripe or Checkout.com, or the order is paid by wire transfer, the listing is split so the sold tickets become their own record, and the seller’s pending balance is credited.

After the sale, the seller’s money moves through wallets on a ledger: pending, available, processing, paid, on hold and refunded. The seller uploads proof of delivery or the buyer confirms receipt, and a missing or unapproved proof blocks the balance. Five days after the event, unblocked balances become available and are paid out through Stripe transfers, wire transfer or Revolut Business, with Sumsub handling identity verification. When a buyer disputes a charge, an admin decides who pays: if Ticombo absorbs it, the seller’s funds are released from hold; if the seller pays, a transfer reversal claws the money back, refunds the order and updates the partner broker. Dispute updates and outcomes from the payment gateway are reconciled with the internal claim automatically.

The platform is a Rush monorepo: an Angular marketplace site, an admin back office and a commercial seller panel for brokers, on API services running on AWS Lambda through the Serverless Framework, over MongoDB with Elasticsearch for search. Partners use the External API to manage listings, upload tickets, place holds and fulfil orders, and webhooks tell them what changed, retried on a schedule. Anything that does not need to happen inside a request goes through event buses on SNS and SQS - email and SMS notifications, search sync, partner webhooks, and scheduled jobs for expirations, exchange rates, reminders and balances. Long-running admin work, such as order reports over long periods, runs as named tasks on a separate bus with fifteen minutes to finish, and Ably pushes the result to the admin panel as soon as it is ready; the same real-time channel shows admins which listings their colleagues are reviewing. Shared AWS infrastructure is defined with CDK for Terraform and deployed through Jenkins.

  • TypeScript
  • JavaScript
  • Node.js
  • AWS Lambda
  • Serverless Framework
  • Express
  • tsoa
  • Amazon Cognito
  • Amazon SNS
  • Amazon SQS
  • MongoDB
  • Mongoose
  • Elasticsearch
  • Stripe
  • Checkout.com
  • Revolut Business
  • Sumsub
  • Ably
  • Postmark
  • MessageBird
  • Angular
  • Storybook
  • Bootstrap
  • Webpack
  • Gulp
  • Rush
  • CDKTF
  • Docker
  • Jenkins
  • GitHub Actions

Reaxys

Science & healthcare

Reaxys is Elsevier’s chemistry search product. It retrieves chemical reactions and substances, their properties, and the articles, journals and patents that report them, so a chemist can search by structure, substructure, similarity or plain text and land on the experimental data behind a compound, or plan a synthesis from reactions that have already been run.

The data is pulled out of the literature. Natural-language processing recognises chemical names in articles and patents, works out which compounds a document is actually about, and links each reaction or substance to its conditions, properties and source. Every record ties back to a chemical structure, an experimental fact - a measured property, a preparation or a reaction - and the document it came from.

The data lives in an RDF triple store - Virtuoso, queried with SPARQL - and in Lucene search indexes, served by .NET services on ASP.NET Framework, WCF and ASP.NET Core alongside Java Spring and Python components. Builds and validation run through Jenkins pipelines written in Groovy, with jMeter for load testing, on AWS infrastructure described in Terraform and configured with Puppet.

  • ASP.NET Framework
  • ASP.NET Core
  • WCF
  • Java Spring
  • Python
  • Lucene
  • RDF
  • SPARQL
  • Virtuoso
  • SQLite
  • Jenkins
  • Groovy
  • jMeter
  • Docker
  • Puppet
  • Terraform
  • AWS
  • PowerShell
  • Bash

VMware (now Broadcom)

Software Engineer → Technical Team Lead · 2017 - 2021

I joined VMware as a software engineer on custom automation projects - setting up complex VMware environments and leading small to mid-sized teams through them.

From 2018 I was a technical team lead: collecting client requirements, turning them into migration and automation plans, and implementing the patterns and business-critical use cases those plans depended on.

Cosmos

Cloud & infrastructure

Cosmos is an internal VMware tool for moving customers between key VMware products that are not compatible with each other - a migration that had been analysed, planned and carried out largely by hand.

It analyses the source environment, reports on what will and will not carry over, and then automates the migration steps. The backend is Node.js and Express with SQLite through Sequelize, packaged in Docker, and the front end is Angular on VMware’s Clarity design system.

  • Node.js
  • Express.js
  • Angular
  • Clarity UI
  • SQLite
  • Sequelize
  • Docker

Telefonica

Cloud & infrastructure

A hybrid cloud for Telefonica, built on VMware. Customers order virtual machines and ready-made services from a catalog, the way they would from a public cloud, and the platform provisions them on Telefonica’s own infrastructure. VMware products provide the base, and custom automation on top turns it into infrastructure as a service, platform as a service and a set of automated services.

Underneath everything is vCenter, VMware’s server for running virtual machines on physical hosts. Two VMware products sit on top of it and manage the same machines. vCloud Director (vCD) is the cloud layer: it creates and manages the virtual machines behind the infrastructure and platform services. vRealize Automation (vRA) is the self-service catalog, and vRealize Orchestrator (vRO) runs the workflows that hook into each stage of a machine’s life, from provisioning to the changes made once it is running.

The hard part was keeping the two in agreement. A machine created or changed through one product has to show up correctly in the other, or the catalog and the cloud drift apart. The sync is built on data collection and messages over AMQP on RabbitMQ. Telefonica’s existing systems connect to the platform over REST, SOAP and plugins. The integration code is C#, ASP.NET and JavaScript, with PowerShell and Bash for the automation itself.

  • vCenter
  • vCloud Director
  • vRealize Automation
  • vRealize Orchestrator
  • JavaScript
  • PowerShell
  • C#
  • ASP.NET
  • Bash
  • RabbitMQ

DHL

Cloud & infrastructure

DHL provisioned its virtual machines through vRealize Automation (vRA) 7 and wanted to move to vRA 8, which is a rebuild of the product rather than an upgrade. This was a consultancy engagement. DHL’s own team was strong and already ran its automation and its Bash and PowerShell scripts for the virtual machines, so the engagement covered the migration and what DHL wanted to build on vRA 8.

Functionality from DHL’s own custom portal is embedded in vRA 8 through an iFrame. Requests go through vRA 8’s custom forms, styled with DHL’s branded CSS. And the provisioning lifecycle is extended with vRealize Orchestrator (vRO) workflows, written in TypeScript, that run at set points while a machine is being created in vCenter.

  • vRealize Automation 8
  • vRealize Automation 7
  • vRealize Orchestrator
  • vCenter
  • TypeScript

Bank of America

Cloud & infrastructure

At the height of COVID, Bank of America needed its people to work from home, and it needed that straight away. The way there was virtual desktops through VMware Horizon: each desktop runs as a virtual machine, managed through vCenter on servers the bank controls, and is streamed to whatever device the employee has at home, so the work and the data never leave the bank’s infrastructure.

The automation around Horizon is vRealize Orchestrator (vRO) workflows written in TypeScript. The project had to ship as soon as possible, and the team worked on it around the clock until it did.

  • VMware Horizon
  • vCenter
  • vRealize Orchestrator
  • TypeScript

Accedia

Software Consultant · 2015 - 2017

Accedia is where my career started. I joined while I was still a student, as a C# developer building business applications for both the desktop and the web: Windows Forms and WPF on one side, ASP.NET Web Forms and MVC on the other.

Alexander Forbes

Financial services

A Windows Forms desktop application for Alexander Forbes. By the time I joined, it was in maintenance and support: the work was fixing the UI and building small UI features, while the backend services and the database behind it stayed untouched.

A desktop application turned out to be the wrong choice for the business. Even with a smooth CI/CD pipeline, getting each new version onto people’s machines was hard, so the team started rebuilding the whole application on ASP.NET MVC.

  • C#
  • .NET Framework
  • Windows Forms
  • ASP.NET MVC

OmniCapital

Financial services

OmniCapital is a UK point-of-sale finance lender: retailers offer its loans to their customers at checkout, online or in store, so a purchase can be paid off in instalments. The system we worked on is the loan processor behind it. It takes a customer’s application and checks their credit history with Experian to decide whether to lend.

Accedia was OmniCapital’s second software vendor. The first had worked on the system for a year, and OmniCapital was very unhappy with what it had delivered. The project was new, but it was built on ASP.NET Web Forms and on Q#, the previous vendor’s own technology, and that came with major limitations.

Working around them meant recreating many of the building blocks ourselves, including a major rewrite from Web Forms to ASP.NET MVC. The application is C# on .NET with SQL Server, and jQuery and Bootstrap on the front end, running on Windows Server and Azure.

  • C#
  • .NET
  • ASP.NET MVC
  • ASP.NET Web Forms
  • SQL Server
  • jQuery
  • Bootstrap
  • Windows Server
  • Azure
  • Experian

Working on something interesting?

I am always happy to talk about systems, architecture, or a problem you are stuck on.

Deyan Peev

Founding Engineer in Sofia, Bulgaria. Currently at 1club.

© 2026 Deyan Peev