[{"data":1,"prerenderedAt":325},["ShallowReactive",2],{"project-case-fly-post":3},[4,13,19,69,85,102,138,259,291,312,318],{"component":5,"data":6},"ProjectCaseHeader",{"index":7,"subtitle":8,"name":9,"headline":10,"url":11,"tone":12},"06","PIGGYBACK DELIVERY · INTERNAL SERVICE","Fly Post","An MVP in ~5 weeks with a team of three developers",null,"secondary",{"component":14,"data":15},"ProjectCaseCover",{"image":16,"imageMobile":17,"imageAlt":18},"fly-post\u002Fmedia\u002Fpreview.webp","fly-post\u002Fmedia\u002Fpreview-mobile.webp","Profile in Fly Post: a table of shipments with statuses",{"component":20,"data":21},"ProjectCaseMeta",{"tags":22,"stack":25},[23,24],"Full-Stack","Accounts",[26,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67],{"label":27,"own":28},"Vue 3",true,{"label":30,"own":28},"TypeScript",{"label":32,"own":28},"Vite",{"label":34,"own":28},"Pinia",{"label":36,"own":28},"Vue Router",{"label":38,"own":28},"Tailwind CSS",{"label":40,"own":28},"daisyUI",{"label":42,"own":28},"vue-i18n",{"label":44,"own":28},"Leaflet",{"label":46,"own":28},"Leaflet.markercluster",{"label":48,"own":28},"vee-validate",{"label":50,"own":28},"yup",{"label":52,"own":28},"date-fns",{"label":54,"own":28},"MSW",{"label":56,"own":28},"ky",{"label":58,"own":28},"maska",{"label":60,"own":28},"NestJS",{"label":62,"own":28},"Swagger",{"label":64,"own":28},"PostgreSQL",{"label":66,"own":28},"Docker",{"label":68,"own":28},"GitLab CI",{"component":70,"data":71},"ProjectCaseFacts",{"facts":72},[73,76,79,82],{"key":74,"value":75},"role","Fullstack Developer, project lead",{"key":77,"value":78},"team","Me and 2 frontend developers",{"key":80,"value":81},"duration","About 5 weeks, March – April 2025",{"key":83,"value":84},"status","MVP handed over to the client",{"component":86,"data":87},"ProjectCaseRole",{"id":74,"name":88,"items":89},"What I did",[90,93,96,99],{"title":91,"text":92},"Design and the UI kit","Figma mockups of the key desktop and mobile screens and a UI kit: buttons, fields, badges, cards. The developers built the screens from my components.",{"title":94,"text":95},"Architecture and backend","Chose the stack, described the API contract in Swagger and wrote the REST API in NestJS. Set up the infrastructure: Docker, a VPS and GitLab CI.",{"title":97,"text":98},"Frontend","The Vite and TypeScript skeleton, an MSW mock server, localization, sign-in and sign-up forms with a confirmation code, the Leaflet map.",{"title":100,"text":101},"Team","Brought in two frontend developers, set their tasks in git issues, defined the project structure and the code style, reviewed their code.",{"component":103,"data":104},"ProjectCaseChapter",{"id":105,"name":106,"blocks":107},"context","Context and starting point",[108,120,128],{"component":103,"data":109},{"id":110,"name":111,"blocks":112},"business","Business",[113,117],{"component":114,"data":115},"ProjectCaseText",{"text":116},"A private client was launching piggyback delivery of documents and small parcels to another city or country. The flight crew, flight attendants and pilots, is flying that route anyway: they pick up the mail at the airport, drop it off at the destination and get paid for it.",{"component":114,"data":118},{"text":119},"For the sender it is faster than courier services, for the crew it is a side income. The service is internal, access to it is granted.",{"component":103,"data":121},{"id":122,"name":123,"blocks":124},"situation","The situation",[125],{"component":114,"data":126},{"text":127},"Before the service, such deliveries were arranged by hand, through acquaintances and chats. The project started from scratch.",{"component":103,"data":129},{"id":130,"name":131,"blocks":132},"goal","The goal",[133],{"component":134,"data":135},"ProjectCaseGoal",{"value":136,"text":137},"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":103,"data":139},{"id":140,"name":141,"blocks":142},"process","Process, architecture and decisions",[143,167,194,208,224,232],{"component":103,"data":144},{"id":145,"name":146,"blocks":147},"flow","The parcel flow",[148,151],{"component":114,"data":149},{"text":150},"The service has two roles: the sender creates a parcel, a courier from the crew takes it on their flight.",{"component":152,"data":153},"ProjectCaseSteps",{"steps":154},[155,158,161,164],{"title":156,"text":157},"Shipment","The sender creates a parcel for shipment",{"title":159,"text":160},"Flight","The courier enters their flight, for example from Moscow to Paris",{"title":162,"text":163},"Airport","The courier picks up the mail at the departure airport",{"title":165,"text":166},"Delivery","Drops the parcel off at the destination and gets paid for the delivery",{"component":103,"data":168},{"id":169,"name":170,"blocks":171},"architecture","Architecture",[172,175],{"component":114,"data":173},{"text":174},"I chose NestJS to have one language with the frontend: TypeScript and shared contract types on both sides. For a team of three on a short deadline this is faster.",{"component":176,"data":177},"ProjectCaseArchitecture",{"items":178},[179,183,187,190],{"label":180,"title":181,"text":182},"CLIENT","SPA","Vue 3 on Vite, Pinia, Vue Router, Tailwind and daisyUI. Localization is laid in with vue-i18n, ru\u002Fen.",{"label":184,"title":185,"text":186},"API","REST in NestJS","The contract is described in Swagger, the sender and courier roles are checked by guards.",{"label":188,"title":64,"text":189},"DATA","The main database with backups.",{"label":191,"title":192,"text":193},"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":103,"data":195},{"id":196,"name":197,"blocks":198},"interface","The map and the interface",[199,202,205],{"component":114,"data":200},{"text":201},"Points on the map stay readable even when there are many of them. I built the map on Leaflet with custom SVG markers, clusters, zoom and geolocation buttons, and the address search with suggestions works through Nominatim from OpenStreetMap.",{"component":114,"data":203},{"text":204},"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":114,"data":206},{"text":207},"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":103,"data":209},{"id":210,"name":211,"blocks":212},"integrations","Integrations",[213,221],{"component":214,"data":215},"ProjectCaseIntegrations",{"items":216},[217,219],{"label":218,"own":28},"Telegram bot · sign-in code",{"label":220,"own":28,"term":28},"Nominatim · OpenStreetMap",{"component":114,"data":222},{"text":223},"The courier enters flights by hand, no schedule API was connected. Courier payouts through a payment service were planned, the MVP does not have them.",{"component":103,"data":225},{"id":226,"name":227,"blocks":228},"security","Data security",[229],{"component":114,"data":230},{"text":231},"The service stores users' personal data. Sensitive fields are encrypted in the application before they are written to the database, the connection goes over HTTPS, the database is backed up. Sign-in is by a code from the Telegram bot, access is split by the sender and courier roles.",{"component":103,"data":233},{"id":234,"name":235,"blocks":236},"quality","Quality and releases",[237,240,243,246,256],{"component":114,"data":238},{"text":239},"Three developers were writing one frontend in a few weeks, so I set the rules right away: the folder structure is described in the README, ESLint checks the code style, all code goes through cross-review.",{"component":114,"data":241},{"text":242},"The backend got unit tests, the frontend got component snapshot tests on Vitest and @vue\u002Ftest-utils, which were written by AI agents.",{"component":114,"data":244},{"text":245},"After the repository moved to GitLab we set up GitLab CI: it builds the Docker images and rolls them out to the VPS automatically.",{"component":247,"data":248},"ProjectCasePipeline",{"stages":249},[250,253],{"name":251,"next":252},"build","Docker images",{"name":254,"next":255},"deploy","VPS",{"component":114,"data":257},{"text":258},"There was no formal process: calls and messages with the client directly, tasks for the developers in git issues, no sprints.",{"component":103,"data":260},{"id":261,"name":262,"blocks":263},"challenges","Challenges",[264],{"component":265,"data":266},"ProjectCaseChallenges",{"items":267},[268,281],{"id":269,"name":270,"rows":271},"parallel","A frontend without a backend",[272,275,278],{"key":273,"text":274},"problem","The deadline was tight, the frontend had to start right away, and there was no backend yet.",{"key":276,"text":277},"solution","I gave the team the API contract in Swagger, and in the first week set up the frontend skeleton and an MSW mock server.",{"key":279,"text":280},"result","Two developers built the screens against the contract without waiting for the API, the backend went in parallel. The MVP was built in about 5 weeks.",{"id":282,"name":283,"rows":284},"login","Phone sign-in without SMS",[285,287,289],{"key":273,"text":286},"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":276,"text":288},"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":279,"text":290},"The bot is free, and the crews are on Telegram anyway, so the code arrives where it is convenient for them.",{"component":103,"data":292},{"id":293,"name":294,"blocks":295},"results","Metrics and results",[296,309],{"component":297,"data":298},"ProjectCaseResults",{"stats":299},[300,303,306],{"value":301,"label":302},"~5 wk","to the MVP, frontend and backend in parallel",{"value":304,"label":305},"3","developers on the team",{"value":307,"label":308},"2","roles: sender and courier",{"component":114,"data":310},{"text":311},"The client accepted the MVP. The project never reached launch, so there are no usage metrics.",{"component":313,"data":314},"ProjectCaseCta",{"id":315,"title":316,"text":317,"url":11},"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":319,"data":320},"ProjectCaseNext",{"slug":321,"name":322,"index":323,"tone":324},"tamriel-codex","Tamriel Codex","07","accent",1792947429804]