[{"data":1,"prerenderedAt":420},["ShallowReactive",2],{"project-case-fly-post":3},{"blocks":4,"person":361},[5,15,50,102,118,135,171,295,327,348,354],{"component":6,"data":7},"ProjectCaseHeader",{"index":8,"subtitle":9,"name":10,"headline":11,"description":12,"url":13,"tone":14},"09","PIGGYBACK DELIVERY · INTERNAL SERVICE","Fly Post","An MVP of a crew-courier air delivery service in ~5 weeks","An internal service where flight crews pick up mail for their own flight and get paid for delivering it.",null,"secondary",{"component":16,"data":17},"ProjectCaseCover",{"image":18,"imageMobile":36,"imageAlt":48,"frame":49},{"src":19,"width":20,"height":21,"variants":22},"\u002F_nuxt\u002Fpreview.C8X6Y8G4.webp",1440,900,[23,26,29,32,35],{"src":24,"width":25},"\u002F_nuxt\u002Fpreview.0QkMr1Qz.webp",480,{"src":27,"width":28},"\u002F_nuxt\u002Fpreview.DA4fYd-7.webp",720,{"src":30,"width":31},"\u002F_nuxt\u002Fpreview.CB8ezYGX.webp",960,{"src":33,"width":34},"\u002F_nuxt\u002Fpreview.BXQsk5g8.webp",1200,{"src":19,"width":20},{"src":37,"width":38,"height":39,"variants":40},"\u002F_nuxt\u002Fpreview-mobile.B_t0i6d9.webp",600,1298,[41,44,47],{"src":42,"width":43},"\u002F_nuxt\u002Fpreview-mobile.UVy05DQ5.webp",160,{"src":45,"width":46},"\u002F_nuxt\u002Fpreview-mobile.CreLp-bD.webp",300,{"src":37,"width":38},"Profile in Fly Post: a table of shipments with statuses and the selected shipment's card","devices",{"component":51,"data":52},"ProjectCaseMeta",{"tags":53,"stack":56},[54,55],"Full-Stack","Accounts",[57,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],{"label":58,"own":59},"Vue 3",true,{"label":61,"own":59},"TypeScript",{"label":63,"own":59},"Vite",{"label":65,"own":59},"Pinia",{"label":67,"own":59},"Vue Router",{"label":69,"own":59},"Tailwind CSS",{"label":71,"own":59},"daisyUI",{"label":73,"own":59},"vue-i18n",{"label":75,"own":59},"Leaflet",{"label":77,"own":59},"Leaflet.markercluster",{"label":79,"own":59},"vee-validate",{"label":81,"own":59},"yup",{"label":83,"own":59},"date-fns",{"label":85,"own":59},"MSW",{"label":87,"own":59},"ky",{"label":89,"own":59},"maska",{"label":91,"own":59},"Vitest",{"label":93,"own":59},"NestJS",{"label":95,"own":59},"Swagger",{"label":97,"own":59},"PostgreSQL",{"label":99,"own":59},"Docker",{"label":101,"own":59},"GitLab CI",{"component":103,"data":104},"ProjectCaseFacts",{"facts":105},[106,109,112,115],{"key":107,"value":108},"role","Fullstack Developer, project lead",{"key":110,"value":111},"team","Me and 2 frontend developers",{"key":113,"value":114},"duration","About 5 weeks, March – April 2025",{"key":116,"value":117},"status","MVP handed over to the client",{"component":119,"data":120},"ProjectCaseRole",{"id":107,"name":121,"items":122},"What I did",[123,126,129,132],{"title":124,"text":125},"Design and the UI kit","Designed the key desktop and mobile screens and a UI kit in Figma. The team built the screens from ready-made components without approving every element.",{"title":127,"text":128},"Architecture and backend","Defined the stack and the architecture and fixed the API contract in Swagger as the single source of truth for the team. Wrote the REST API in NestJS and set up the infrastructure: Docker, a VPS, GitLab CI.",{"title":130,"text":131},"Frontend","Prepared the Vite and TypeScript skeleton and an MSW mock server so the team could start on day one. Took on the complex parts: localization, sign-in with a confirmation code and the Leaflet map.",{"title":133,"text":134},"Team","Brought in two frontend developers, broke the work down into tasks in git issues, defined the project structure and the code style, and reviewed the code.",{"component":136,"data":137},"ProjectCaseChapter",{"id":138,"name":139,"blocks":140},"context","Context",[141,153,161],{"component":136,"data":142},{"id":143,"name":144,"blocks":145},"business","Business",[146,150],{"component":147,"data":148},"ProjectCaseText",{"text":149},"A private client was launching a service for piggyback delivery of documents and small parcels to other cities and countries. Deliveries are made by crew members — flight attendants and pilots who are flying that route anyway. They pick up a shipment at the departure airport, hand it over at the destination and get paid for the delivery.",{"component":147,"data":151},{"text":152},"For the sender it is faster than courier services, for the crew it is extra income. The service is closed: access to it is granted separately.",{"component":136,"data":154},{"id":155,"name":156,"blocks":157},"situation","The situation",[158],{"component":147,"data":159},{"text":160},"Before the service, such deliveries were arranged by hand, through acquaintances and chats. The sender had to find a crew member on the right flight, agree on the price and the meeting, and check where the parcel was in the chat. Neither side had any guarantees: it was unclear who would pick up the shipment and when, and how the delivery would be paid. The service was meant to replace all of this with one process and clear statuses. The project started from scratch.",{"component":136,"data":162},{"id":163,"name":164,"blocks":165},"goal","The goal",[166],{"component":167,"data":168},"ProjectCaseGoal",{"value":169,"text":170},"MVP","in the shortest possible time and on a small budget. The load is low, so the stack is simple: Vue 3, NestJS and PostgreSQL.",{"component":136,"data":172},{"id":173,"name":174,"blocks":175},"process","Process, architecture and decisions",[176,200,227,244,260,268],{"component":136,"data":177},{"id":178,"name":179,"blocks":180},"flow","The parcel flow",[181,184],{"component":147,"data":182},{"text":183},"The service has two roles: the sender creates a parcel, a courier from the crew takes it on their flight.",{"component":185,"data":186},"ProjectCaseSteps",{"steps":187},[188,191,194,197],{"title":189,"text":190},"Shipment","The sender creates a parcel for shipment. In development: a form with masks and validation, address search with suggestions through Nominatim",{"title":192,"text":193},"Flight","The courier enters their flight, for example from Moscow to Paris. In development: a flight form with manual entry, a schedule API is planned in the architecture",{"title":195,"text":196},"Airport","The courier picks up the mail at the departure airport. In development: a Leaflet map with markers, clusters and geolocation, shipment status changes through actions in the table",{"title":198,"text":199},"Delivery","Drops the parcel off at the destination and gets paid for the delivery. In development: an order status timeline, role checks by guards in the API, payouts are planned",{"component":136,"data":201},{"id":202,"name":203,"blocks":204},"architecture","Architecture",[205,208],{"component":147,"data":206},{"text":207},"The team was three people, two of them frontend developers, on a tight deadline. So the stack was chosen to keep the whole team on one language and let it start fast. NestJS on TypeScript gave shared contract types on the frontend and the backend, with no second language to learn. The team already knew Vue 3 with Vite, Pinia and Tailwind, and the ready-made daisyUI components sped up the layout work. PostgreSQL is a reliable relational database for orders and users. Docker on a VPS with GitLab CI gives low-cost infrastructure with automatic rollouts.",{"component":209,"data":210},"ProjectCaseArchitecture",{"items":211},[212,216,220,223],{"label":213,"title":214,"text":215},"CLIENT","SPA","Vue 3 on Vite, Pinia, Vue Router, Tailwind and daisyUI. Localization is laid in with vue-i18n, ru\u002Fen.",{"label":217,"title":218,"text":219},"API","REST in NestJS","The contract is described in Swagger, the sender and courier roles are checked by guards.",{"label":221,"title":97,"text":222},"DATA","The main database with backups.",{"label":224,"title":225,"text":226},"INFRA","Docker on a VPS","The frontend, NestJS and PostgreSQL run in containers, GitLab CI builds the images and rolls them out to the server.",{"component":136,"data":228},{"id":229,"name":230,"blocks":231},"interface","The map and the interface",[232,235,238,241],{"component":147,"data":233},{"text":234},"The UI kit in Figma and on the frontend set one interface language. Screens were built from ready-made components, so new pages came faster and the interface stayed consistent without a separate design review. For the client this means cheaper changes and faster delivery of new features.",{"component":147,"data":236},{"text":237},"Points on the map stay readable even when there are many of them: custom SVG markers, clusters, zoom and geolocation on Leaflet, and address search with suggestions through Nominatim from OpenStreetMap. Users find the right place faster, and the business does not pay for commercial map APIs.",{"component":147,"data":239},{"text":240},"The order's path is shown step by step in a nested collapsible status timeline. Forms have masks and validation, the confirmation code is entered in 4 cells with a resend timer.",{"component":147,"data":242},{"text":243},"On the phone the navigation moves to a bottom tab bar, the shipments table scrolls sideways with a pinned actions column, the details panel and the map open full screen.",{"component":136,"data":245},{"id":246,"name":247,"blocks":248},"integrations","Integrations",[249,257],{"component":250,"data":251},"ProjectCaseIntegrations",{"items":252},[253,255],{"label":254,"own":59},"Telegram bot · sign-in code",{"label":256,"own":59,"term":59},"Nominatim · OpenStreetMap",{"component":147,"data":258},{"text":259},"Two more integrations are planned in the architecture for the next stages: a flight schedule API, so the courier picks a flight from a list instead of typing it in, and a payment service for courier payouts. They were not connected in the MVP, and the architecture is designed to add them.",{"component":136,"data":261},{"id":262,"name":263,"blocks":264},"security","Data security",[265],{"component":147,"data":266},{"text":267},"The service stores the personal data of senders and couriers: full names, phone numbers, addresses and flight details. A leak of such data undermines trust in the service and breaks the personal data law (152-FZ), so protection was built in from day one. Sensitive fields are encrypted in the application with AES-256-GCM before they are written to the database: even if someone gets access to the database or a backup, the data stays unreadable, and GCM prevents it from being tampered with unnoticed. The connection goes over HTTPS only, the database is backed up regularly, sign-in uses a one-time code through a Telegram bot, and access is split between the sender and courier roles at the API level.",{"component":136,"data":269},{"id":270,"name":271,"blocks":272},"quality","Project and process setup",[273,276,279,282,292],{"component":147,"data":274},{"text":275},"Before development started, I set the project up so three developers could work on one frontend in parallel from the first week: the folder structure is described in the README, ESLint checks the code style, and all code goes through cross-review.",{"component":147,"data":277},{"text":278},"Tests are part of the process: unit tests for the backend and component snapshot tests on Vitest and @vue\u002Ftest-utils for the frontend. Writing them was handed to AI agents to save the team's time.",{"component":147,"data":280},{"text":281},"I automated code delivery in GitLab CI: the pipeline builds the Docker images and rolls them out to the VPS.",{"component":283,"data":284},"ProjectCasePipeline",{"stages":285},[286,289],{"name":287,"next":288},"build","Docker images",{"name":290,"next":291},"deploy","VPS",{"component":147,"data":293},{"text":294},"I kept the process light, sized to the team and the deadline: tasks in git issues, direct contact with the client, no sprints.",{"component":136,"data":296},{"id":297,"name":298,"blocks":299},"challenges","Challenges",[300],{"component":301,"data":302},"ProjectCaseChallenges",{"items":303},[304,317],{"id":305,"name":306,"rows":307},"parallel","A frontend without a backend",[308,311,314],{"key":309,"text":310},"problem","The deadline was tight, the frontend had to start right away, and there was no backend yet.",{"key":312,"text":313},"solution","I gave the team the API contract in Swagger and set up a basic backend on frontend tooling: NestJS on TypeScript with the same contract types as the frontend. While the API grew, the frontend worked through an MSW mock server.",{"key":315,"text":316},"result","Two developers built the screens against the contract without waiting for the API, and the backend on a stack the team already knew went in parallel. The MVP was built in about 5 weeks.",{"id":318,"name":319,"rows":320},"login","Phone sign-in without SMS",[321,323,325],{"key":309,"text":322},"The phone number has to be confirmed, and an SMS provider charges for every message. For an MVP on a small budget this matters.",{"key":312,"text":324},"The confirmation code comes through a Telegram bot. On the frontend there is a phone mask, a role choice, sender or courier, and code entry in 4 cells with a resend timer.",{"key":315,"text":326},"The bot is free, and the crews are on Telegram anyway, so the code arrives where it is convenient for them.",{"component":136,"data":328},{"id":329,"name":330,"blocks":331},"results","Metrics and results",[332,345],{"component":333,"data":334},"ProjectCaseResults",{"stats":335},[336,339,342],{"value":337,"label":338},"~5 wk","to the MVP, frontend and backend in parallel",{"value":340,"label":341},"3","developers on the team",{"value":343,"label":344},"2","roles: sender and courier",{"component":147,"data":346},{"text":347},"The client accepted the MVP. The project never reached launch, so there are no usage metrics.",{"component":349,"data":350},"ProjectCaseCta",{"id":351,"title":352,"text":353,"url":13},"cta","Need an MVP in a few weeks or a service with a map and roles?","I will do the design, the frontend and the backend, bring in a team if needed and take the MVP through to handover.",{"component":355,"data":356},"ProjectCaseNext",{"slug":357,"name":358,"index":359,"tone":360},"someday","Someday","10","accent",{"name":362,"alternateNames":363,"role":366,"skills":367},"Michael Malakhov",[364,365],"Михаил Малахов","Mikhail Malakhov","Fullstack Developer",[368,61,369,370,371,372,373,374,375,67,69,376,214,377,378,379,380,381,93,382,383,384,97,385,386,99,387,388,389,63,390,391,392,393,394,395,91,396,397,398,399,400,401,402,403,404,405,406,407,408,65,409,410,411,412,413,414,415,416,417,418,419],"JavaScript (ES6+)","HTML5","CSS3","Sass","Vue.js (Vue 2\u002FVue 3, Composition API)","Nuxt 3\u002F4","React","Next.js","Bootstrap","SSR","MPA","SEO","Chrome DevTools","Node.js","Express","Laravel","Rust","Redis","REST API","Nginx","CI\u002FCD","GitHub Actions","Webpack","Git","GitHub","GitLab","Figma","Redux","Jest","Tauri","Prometheus","Storybook","Jira","Claude Code","Codex","Gemini","MCP","Atlassian Rovo","Zustand","SWR","TanStack Query","Vuex","React Hook Form","Zod","Capacitor","GraphQL","WebSocket","Clinic.js","autocannon","Lighthouse","GitHub Copilot","AI agents",1793309471866]